이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.
정수 벡터로부터 가장 큰 정수값을 뽑아내는 프로그램을 작성한다고 하자.
peter@hp-laptop:~/rust-practice$ mkdir chapter10
peter@hp-laptop:~/rust-practice$ cd chapter10/
peter@hp-laptop:~/rust-practice/chapter10$ cargo new largest
Created binary (application) `largest` package
peter@hp-laptop:~/rust-practice/chapter10$ cd largest/
peter@hp-laptop:~/rust-practice/chapter10/largest$ vi src/main.rs
src/main.rs
fn main() { let number_list = vec![34, 50, 35, 100, 65]; let mut largest = number_list[0]; for item in number_list { if item > largest { largest = item; } } println!("The largest number is {}", largest); }
peter@hp-laptop:~/rust-practice/chapter10/largest$ cargo run
Compiling largest v0.1.0 (/home/peter/rust-practice/chapter10/largest)
Finished dev [unoptimized + debuginfo] target(s) in 0.23s
Running `target/debug/largest`
The largest number is 100
peter@hp-laptop:~/rust-practice/chapter10/largest$
만약 우리가 둘 이상의 정수 벡터로부터 각각의 최대값을 구하고자 한다면 코드 중복이 발생한다.
그리고 이를 해결하기 위해 서용하는 것이 지금까지 많이 사용해온 함수다.
함수를 사용하면 동일한 기능을 하는 코드를 여러 번 작성하지 않아도 되고
해당 코드에 수정할 부분이 있을 때 한 번만 수정해도 되며
프로그램의 크기가 불필요하게 커지는 것을 방지할 수 있다.
위 코드를 다음과 같이 함수로 분리하면
둘 이상의 정수 벡터에 대해서 각각의 최대값을 구해도
코드가 많이 길어지지는 않는 것을 확인할 수 있다.
peter@hp-laptop:~/rust-practice/chapter10/largest$ vi src/main.rs
src/main.rs
fn main() { let number_list = vec![34, 50, 35, 100, 65]; println!("The largest number is {}", largest(&number_list)); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; println!("The largest number is {}", largest(&number_list)); } fn largest(list: &[i32]) -> i32 { let mut largest = list[0]; for item in list { if *item > largest { largest = *item; } } largest }
peter@hp-laptop:~/rust-practice/chapter10/largest$ cargo run
Compiling largest v0.1.0 (/home/peter/rust-practice/chapter10/largest)
Finished dev [unoptimized + debuginfo] target(s) in 0.19s
Running `target/debug/largest`
The largest number is 100
The largest number is 6000
peter@hp-laptop:~/rust-practice/chapter10/largest$
그런데 만약 우리가 문자열 슬라이스에서 char 값이 가장 큰 문자를 구하고자 한다면
이것은 방금 구현한 largest
함수와 매우 비슷하겠지만 그것을 사용할 수 없다.
이런 상황에서 코드 중복을 줄이기 위해 사용할 수 있는 것이 제네릭이다.
정수 최대값을 구하는 함수와 문자 최대값을 구하는 함수를
제네릭을 사용하지 않고 구현한다고 하자.
peter@hp-laptop:~/rust-practice/chapter10/largest$ cd ..
peter@hp-laptop:~/rust-practice/chapter10$ cargo new largest_generic
Created binary (application) `largest_generic` package
peter@hp-laptop:~/rust-practice/chapter10$ cd largest_generic/
peter@hp-laptop:~/rust-practice/chapter10/largest_generic$ cp ../largest/src/main.rs src/main.rs
peter@hp-laptop:~/rust-practice/chapter10/largest_generic$ vi src/main.rs
src/main.rs
fn main() { let number_list = vec![34, 50, 35, 100, 65]; println!("The largest number is {}", largest_i32(&number_list)); let character_list = vec!['y', 'm', 'a', 'q']; println!("The largest character is {}", largest_char(&character_list)); } fn largest_i32(list: &[i32]) -> i32 { let mut largest = list[0]; for item in list { if *item > largest { largest = *item; } } largest } fn largest_char(list: &[char]) -> char { let mut largest = list[0]; for item in list { if *item > largest { largest = *item; } } largest }
peter@hp-laptop:~/rust-practice/chapter10/largest_generic$ cargo run
Compiling largest_generic v0.1.0 (/home/peter/rust-practice/chapter10/largest_generic)
Finished dev [unoptimized + debuginfo] target(s) in 0.20s
Running `target/debug/largest_generic`
The largest number is 100
The largest character is y
peter@hp-laptop:~/rust-practice/chapter10/largest_generic$
두 함수는 본문이 정확히 일치하지만 자료형이 다르다는 이유만으로 따로 작성한다.
하지만 제네릭을 이용하여 자료형을 매개변수화 하면 이 함수들의 코드 중복을 최소화할 수 있다.
제네릭은 함수에서 사용하는 자료형을 함수를 정의하는 순간이 아니라
호출하는 순간에 결정할 수 있도록 한다.
제네릭은 단어의 첫 글자를 대문자로 하여 단어를 구분하는 Camel Case로 작성되며
짧게 한 글자 정도로 작성하는 게 일반적이다.
주로 Type의 첫 글자인 T를 많이 사용한다.
제네릭은 함수 이름 옆에 large<T>
와 같이 명시하며
사용할 땐 전달한 인자값의 자료형에 따라 적절히 작동한다.
앞서 작성했던 코드는 제네릭을 통해 다음과 같이 수정할 수 있다.
peter@hp-laptop:~/rust-practice/chapter10/largest_generic$ vi src/main.rs
src/main.rs
fn main() { let number_list = vec![34, 50, 35, 100, 65]; println!("The largest number is {}", largest(&number_list)); let character_list = vec!['y', 'm', 'a', 'q']; println!("The largest character is {}", largest(&character_list)); } fn largest<T>(list: &[T]) -> T { let mut largest = list[0]; for item in list { if *item > largest { largest = *item; } } largest }
그러나 컴파일은 할 수 없다.
peter@hp-laptop:~/rust-practice/chapter10/largest_generic$ cargo run
Compiling largest_generic v0.1.0 (/home/peter/rust-practice/chapter10/largest_generic)
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src/main.rs:13:18
|
13 | if *item > largest {
| ----- ^ ------- T
| |
| T
|
help: consider restricting type parameter `T`
|
9 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
| ^^^^^^^^^^^^^^^^^^^^^^
error: aborting due to previous error
For more information about this error, try `rustc --explain E0369`.
error: could not compile `largest_generic`.
To learn more, run the command again with --verbose.
peter@hp-laptop:~/rust-practice/chapter10/largest_generic$
이는 T
에 해당하는 자료형이 비교연산자를 사용할 수 없는 자료형일 경우 문제가 되기 때문이다.
따라서 std::cmp::PartialOrd
트레이트를 구현하고 있는 자료형으로 T
를 제한해야 하는데
우리는 아직 이것을 배우지 않았으므로 일단 이대로 두고
트레이트에 대한 내용을 다룰 때 작동하는 코드로 수정해보도록 하자.
제네릭은 함수에서뿐만 아니라 구조체에서도 사용할 수 있다.
구조체 이름 옆에 <>
로 명시한 제네릭은 구조체 필드의 자료형으로 사용할 수 있다.
예를 들어, 다음과 같은 식이다.
peter@hp-laptop:~/rust-practice/chapter10/largest_generic$ cd ..
peter@hp-laptop:~/rust-practice/chapter10$ cargo new point
Created binary (application) `point` package
peter@hp-laptop:~/rust-practice/chapter10$ cd point/
peter@hp-laptop:~/rust-practice/chapter10/point$ vi src/main.rs
src/main.rs
#[derive(Debug)] struct Point<T> { x: T, y: T, } fn main() { let integer = Point { x: 5, y: 10, }; let float = Point { x: 1.0, y: 4.0, }; println!("integer point: {:?}", integer); println!("float point: {:?}", float); }
peter@hp-laptop:~/rust-practice/chapter10/point$ cargo run
Compiling point v0.1.0 (/home/peter/rust-practice/chapter10/point)
Finished dev [unoptimized + debuginfo] target(s) in 0.21s
Running `target/debug/point`
integer point: Point { x: 5, y: 10 }
float point: Point { x: 1.0, y: 4.0 }
peter@hp-laptop:~/rust-practice/chapter10/point$
여기서 주의할 건, x
와 y
의 제네릭 자료형이 모두 T
라면
그것의 실제 자료형이 무엇이든 그 둘은 같아야 한다는 것이다.
그들의 자료형이 다를 수 있다면 다음과 같이 정의해야 한다.
struct Point<T, U> {
x: T,
y: U,
}
그렇게 되면 T
와 U
는 서로 다를 수 있고 단지 T
끼리만 같고 U
끼리만 같으면 된다.
물론 T
와 U
가 같다고 해서 문제가 되지도 않는다.
열거형에서도 제네릭을 사용할 수 있다.
당장 우리가 봐왔던 Option<T>
나 Result<T, E>
도 그렇듯이 말이다.
Option<T>
은 다음과 같이 정의된다.
enum Option<T> {
Some(T),
None,
}
정의할 때 T
의 자료형이 특정되지 않기 때문에 어떤 자료형이든 사용 가능하며
T
의 자료형이 무엇인지 정해지고 나면 해당 변수에 대한 T
는 바뀌지 않는다.
마찬가지로 Result<T, E>
의 정의는 다음과 같고
enum Result<T, E> {
Ok(T),
Err(E),
}
어떤 자료형의 성공한 값 또는 어떤 자료형의 오류 값을 가질 수 있다.
구조체나 열거형에 제네릭이 사용되므로 메서드에서도 제네릭을 사용할 수 있다.
메서드에서 제네릭이 사용되었음을 명시해주기 위해
impl<T> Point<T>
와 같이 제네릭을 명시해주어야 한다.
때로는 구조체나 열거형이 특정 자료형을 가진 경우에만 사용 가능한 메서드를 만들 수도 있는데
이 경우 impl Point<f32>
와 같이 자료형을 명시해주며
impl
직후에는 <>
를 작성하지 않는다.
그리고 때로는 메서드에서 구조체나 열거형에서 정의된 것과 다른 제네릭을 사용하기도 하는데
다음이 그 예시다.
peter@hp-laptop:~/rust-practice/chapter10/point$ cd ..
peter@hp-laptop:~/rust-practice/chapter10$ cargo new mixed_generic
Created binary (application) `mixed_generic` package
peter@hp-laptop:~/rust-practice/chapter10$ cd mixed_generic/
peter@hp-laptop:~/rust-practice/chapter10/mixed_generic$ vi src/main.rs
src/main.rs
struct Point<T, U> { x: T, y: U, } impl<T, U> Point<T, U> { fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { Point { x: self.x, y: other.y, } } } fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: "hello", y: 'c' }; let p3 = p1.mixup(p2); println!("x: {}, y: {}", p3.x, p3.y); }
peter@hp-laptop:~/rust-practice/chapter10/mixed_generic$ cargo run
Compiling mixed_generic v0.1.0 (/home/peter/rust-practice/chapter10/mixed_generic)
Finished dev [unoptimized + debuginfo] target(s) in 0.16s
Running `target/debug/mixed_generic`
x: 5, y: c
peter@hp-laptop:~/rust-practice/chapter10/mixed_generic$
제네릭의 자료형을 실행 시간에 판단할 경우 프로그램의 성능 저하가 있을 수 있지만
Rust의 제네릭은 컴파일 시점에 구체적인 자료형으로 변환되므로 실행 상의 성능 저하가 없다.
우리가 작성할 땐 하나의 제네릭이었지만
컴파일 후에는 구체적인 자료형을 가진 서로 다른 함수, 구조체, 열거형, 또는 메서드가 되는 것이다.
우리가 large<T>
라는 함수를 만들어
T
가 i32인 경우와 T
가 char인 경우의 함수 호출이 존재한다면
컴파일 시점에 이를 검사하고 T
가 i32인 함수와 T
가 char인 함수로 각각 교체하는 것이다.
이 포스트의 내용은 공식문서의 10장 1절 Generic Data Types에 해당합니다.