0%

Rust 제네릭 타입

러스트 제네릭 타입에 대해 공부한 내용을 정리합니다.

제네릭 타입이란

보편적으로 함수 코드를 작성할 때는 매개변수와 결과값에 대한 데이터 타입을 지정한다. 그렇기에 해당 함수는 지정한 데이터 타입을 이외의 다른 타입이 매개변수로 들어온다면 타입에러를 발생시킨다. 따라서 만약 정수와 문자 모두를 매개변수로 받는 프로그램을 만들고 싶다면 아래와 같은 코드가 필요하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
fn find_max_i32(list: &[i32]) -> i32 {
let mut max = list[0];

for &item in list.iter() {
if item > max {
max = item;
}
}
max
}

fn find_max_char(list: &[char]) -> char {
let mut max = list[0];

for &item in list.iter() {
if item > max {
max = item;
}
}
max
}

fn main() {
let i32_list = [10, 9, 2, 15, 30];
let i32_result = find_max_i32(&i32_list);
println!("max num: {}", i32_result);

let char_list = ['r','h','j','k','n'];
let char_result = find_max_char(&char_list);
println!("max char: {}", char_result);
}

위에서 정의된 함수 find_max_i32() find_max_char()은 매개변수와 결과값은 데이터 타입만 다를 뿐 동일한 로직을 가지고 있다. 만약 위 예시처럼 두 가지가 아닌 모든 데이터 타입을 받는 함수를 작성하고 싶다면 어떻게 해야할까? 예시처럼 모든 데이터 타입에 맞추어 코드를 짜는 것은 복잡하다.

위와 같은 상황에서 사용할 수 있는 방법이 바로 제네릭 타입이다. 제네릭 타입이란 말 그대로 데이터 타입을 정의해야 하는 부분을 대신하는 추상적인 데이터 타입이다. 이번 글에서는 데이터 타입을 정의해야 하는 상황에 따라 어떻게 제네릭 타입을 사용하는지 알아볼 것이다.

함수 정의

위 예시를 그대로 사용해보자. 위 예시에서는 i32 타입과 char 타입의 데이터를 모두 받아서 처리하는 프로그램을 작성하기 위한 방법 중 하나였다. 이는 각각의 데이터 타입에 맞춰 코드를 작성해야 하기에 코드가 길고 중복된다는 문제점이 있었다. 이를 제네릭 타입을 사용하여 수정해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn find_max(list: &[T]) -> T {
let mut max = list[0];

for &item in list.iter() {
if item > max {
max = item;
}
}
max
}

fn main() {
let i32_list = [10, 9, 2, 15, 30];
let i32_result = find_max(&i32_list);
println!("max num: {}", i32_result);

let char_list = ['r','h','j','k','n'];
let char_result = find_max(&char_list);
println!("max char: {}", char_result);
}

이전의 예시 코드의 위 코드의 차이점은 무엇인가? 일단 max값을 찾는 코드가 하나로 줄었다. 또한 그 함수이름 옆에 <T>가 추가되었고, 매개변수의 데이터 타입을 지정해야 하는 위치에 T로 변경되었다. 이 변경 사항들이 함수 정의에서 제네릭 타입을 사용하는 문법이다. 이 문법은 함수 find_max()가 어떤 타입 T를 일반화한 함수이며, 타입 T로 구성된 리스트를 매개변수로 받아 타입 T를 결과값으로 반환한다는 의미를 나타낸다.
이제 위 예시코드를 컴파일해보자. 그러면 아래의 에러가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
error[E0369]: binary operation `>` cannot be applied to type `T`
--> test.rs:5:17
|
5 | if item > max {
| ---- ^ --- T
| |
| T
|
help: consider restricting type parameter `T`
|
1 | fn find_max<T: std::cmp::PartialOrd>(list: &[T]) -> T {

위 에러는 러스트 트레이트와 관련이 있기 때문에 트레이트 관련 글에서 자세히 다뤄볼 것이다.

구조체 정의

다음으로는 구조체를 정의할 때 제네릭 타입을 사용하는 방법이다. 일반적으로 구조체는 정의 당시에 해당 변수의 데이터 타입을 지정한다. 아래의 예시를 살펴보자.

1
2
3
4
struct Point {
x: i32,
y: i32,
}

위처럼 정의된 Point 구조채에는 무조건 i32 타입만 필드 값으로 사용될 수 있다. 하지만 float도 필드값으로 사용하고 싶다면 어떻게 해야 할까? 답은 예상했다시피 제네릭 타입을 사용하면 된다.

1
2
3
4
5
6
7
8
9
struct Point<T> {
x: T,
y: T,
}

fn main() {
let int = Point {x: 4, y: 7};
let float = Point {x: 4.1, y: 7.1};
}

위 코드는 문제없이 작동된다. 하지만 구조체 정의에서 두 필드값을 동일한 T 타입으로 정의했기 때문에 필드의 데이터 타입은 동일해야 한다. 즉, 둘다 i32아거나 float이어야 한다. 만약 두 필드의 데이터 타입을 다르게 정의하고 싶다면 다중 제네릭 타입을 사용하면 된다.

1
2
3
4
5
6
7
8
9
struct Point<T,U> {
x: T,
y: U,
}

fn main() {
let int = Point {x: 4, y: 7.1};
let float = Point {x: 4.1, y: 7};
}

열거자 정의

열거자를 정의할 때도 제네릭 타입을 사용할 수 있다. 표준 라이브러리인 Option<T>를 통해서 알아보자.

1
2
3
4
enum Option<T> {
some(T),
None,
}

위 코드에서 알 수 있듯이 Option<T>는 타입 T를 일반화한 열거자이며, someT값을 저장하고, None은 아무것도 저장하지 않는다.

구조체와 마찬가지로 열거자 또한 다중 제네릭 타입을 사용할 수 있다.

1
2
3
4
enum verification<T,E> {
success(T),
fail(E),
}

위에서 정의한 verification<T,E>는 성공했을 때와 실패했을 때의 값들을 모두 나타낼 수 있다.