제네릭 데이터 타입
모든 프로그래밍 언어는 중복되는 개념을 효율적으로 처리하기 위한 도구를 가지고 있습니다. 러스트에서는 제네릭(generic) 이 그 역할을 맡습니다. 제네릭은 구체(concrete) 타입 혹은 기타 속성에 대한 추상화된 대역입니다. 컴파일과 실행 시점에 제네릭들이 실제로 무슨 타입으로 채워지는지 알 필요 없이 제네릭의 동작이나 다른 제네릭과의 관계를 표현할 수 있습니다.
함수가 어떤 값이 들어있을지 모르는 매개변수를 전달받아서 동일한 코드를
다양한 구체적 값으로 실행하는 것처럼, 함수는 i32, String 같은
구체 타입 대신 제네릭 타입의 매개변수를 전달받을 수 있습니다. 사실은
이미 여러 제네릭을 사용해 봤었습니다. 6장에서는 Option<T>, 8장에서는
Vec<T>와 HashMap<K, V>, 9장에서는 Result<T, E> 제네릭을 사용했죠.
이번 장에서는 제네릭을 사용해 자체 타입, 함수, 메서드를 정의하는 방법을 살펴보겠습니다.
우선, 함수를 추출하여 중복되는 코드를 제거하는 방법을 살펴보겠습니다. 그다음 매개변수의 타입만 다른 두 함수가 생기면 제네릭 함수를 사용해 코드 중복을 한 번 더 줄여보겠습니다. 또한, 제네릭 타입을 구조체 및 열거형 정의에 사용하는 방법도 살펴보겠습니다.
다음으로는 트레이트(trait) 를 이용해 동작을 제네릭한 방식으로 정의하는 법을 배워보겠습니다. 트레이트를 제네릭 타입과 함께 사용하면, 아무 타입이나 허용하는 것이 아니라 특정 동작을 하는 타입만 허용할 수 있습니다.
마지막으로는 라이프타임(lifetime) 을 살펴보겠습니다. 라이프타임은 제네릭의 일종이며, 컴파일러에게 참조자들이 서로 어떤 관계에 있는지를 알려주는 데에 사용합니다. 라이프타임은 빌린 값들에 대한 정보를 컴파일러에게 충분히 제공하여 작성자의 추가적인 도움 없이도 참조자의 여러 가지 상황에 대한 유효성 검증을 할 수 있게 해 줍니다.
함수를 추출하여 중복 없애기
제네릭은 여러 가지 타입을 나타내는 자리표시자의 위치에 특정 타입을 집어넣는 것으로 코드 중복을 제거할 수 있게 해 줍니다. 제네릭 문법을 배우기 전에, 먼저 제네릭 타입을 이용하지 않고 여러 가지 값을 나타내는 자리표시자로 특정 값을 대체하는 함수를 추출하는 방식으로 중복되는 코드를 없애는 요령을 알아보겠습니다. 그다음 동일한 기법을 이용하여 제네릭 함수를 추출해보겠습니다! 함수로 추출할 수 있는 중복되는 코드를 알아내는 방법을 보는 것으로 제네릭을 사용할 수 있는 중복되는 코드들이 인식되기 시작할 것입니다.
예제 10-1과 같이 리스트에서 가장 큰 숫자를 찾아내는 간단한 프로그램부터 시작하겠습니다.
예제 10-1: 숫자 리스트에서 가장 큰 수 찾기fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {}", largest);
}number_list 변수에는 정수 리스트를 저장하고, largest
변수에 리스트의 첫 번째 숫자에 대한 참조자를 집어넣습니다.
그리고 리스트 내 모든 숫자를 순회하는데,
만약 현재 값이 largest에 저장된 값보다 크다면
largest의 값을 현재 값으로 변경합니다.
현재 값이 여태까지 본 가장 큰 값보다 작다면 largest의 값은 바뀌지 않습니다.
리스트 내 모든 숫자를 돌아보고 나면 largest는 가장 큰 값을 갖게 되며,
위의 경우에는 100이 됩니다.
이번에는 두 개의 다른 숫자 리스트에서 가장 큰 숫자를 찾으라는 일감을 받았습니다. 그렇게 하려면 예제 10-2처럼 예제 10-1의 코드를 프로그램 내 다른 곳에 복사하여 동일한 로직을 이용할 수도 있습니다.
예제 10-2: 두 개의 숫자 리스트에서 가장 큰 숫자를 찾는 코드fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {}", largest);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {}", largest);
}이 코드는 잘 동작하지만, 중복된 코드를 생성하는 일은 지루하고 에러가 발생할 가능성도 커집니다. 또한, 로직을 바꾸고 싶을 때 수정해야 할 부분이 여러 군데임을 기억해야 한다는 의미이기도 합니다.
이러한 중복을 제거하기 위해서, 정수 리스트를 매개변수로 전달받아 동작하는 함수를 정의하여 추상화할 것입니다. 이렇게 하면 코드가 더 명확해지고 목록에서 가장 큰 숫자를 찾는다는 개념을 추상적으로 표현할 수 있습니다.
예제 10-3에서는 가장 큰 수를 찾는 코드를 largest라는 이름의
함수로 추출합니다. 그다음 예제 10-2에 있는 두 리스트에서 가장 큰
수를 찾기 위해 이 함수를 호출합니다. 나중에 있을지 모를 다른 어떤 i32
값의 리스트에 대해서라도 이 함수를 사용할 수 있습니다.
fn largest(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("The largest number is {}", result);
}largest 함수는 list 매개변수를 갖는데,
이는 함수로 전달될 임의의 i32 값 슬라이스를 나타냅니다.
실제로 largest 함수가 호출될 때는 전달받은 구체적인 값으로
실행됩니다.
예제 10-2에서부터 예제 10-3까지 거친 과정을 요약하면 다음과 같습니다.
- 중복된 코드를 식별합니다.
- 중복된 코드를 함수의 본문으로 분리하고, 함수의 시그니처 내에 해당 코드의 입력값 및 반환 값을 명시합니다.
- 중복됐었던 두 지점의 코드를 함수 호출로 변경합니다.
다음에는 제네릭으로 이 과정을 그대로 진행하여 중복된 코드를 제거해보겠습니다.
함수 본문이 특정한 값 대신 추상화된 list로 동작하는 것처럼, 제네릭을
이용한 코드는 추상화된 타입으로 동작합니다.
만약 i32 슬라이스에서 최댓값을 찾는 함수와 char
슬라이스에서 최댓값을 찾는 함수를 따로 가지고 있다면 어떨까요?
이런 중복은 어떻게 제거해야 할지 한번 알아봅시다!
이제 제네릭, 트레이트, 라이프타임의 연결 구조를 살펴보겠습니다.
제네릭을 사용하면 함수 시그니처나 구조체의 아이템에 다양한 구체적 데이터 타입을 사용할 수 있도록 정의할 수 있습니다. 함수, 구조체, 열거형, 메서드를 제네릭으로 정의하는 방법을 알아보고, 제네릭이 코드 성능에 미치는 영향을 알아보겠습니다.
제네릭 함수 정의
제네릭 함수를 정의할 때는, 함수 시그니처 내 매개변수와 반환 값의 데이터 타입 위치에 제네릭을 사용합니다. 이렇게 작성된 코드는 더 유연해지고, 이 함수를 호출하는 쪽에서 더 많은 기능을 사용할 수 있도록 하며 코드 중복 또한 방지합니다.
largest 함수를 이용해 계속해보겠습니다. 예제 10-4는 슬라이스에서
가장 큰 값을 찾는 두 함수를 보여줍니다. 제네릭 사용하여 이 함수들을
하나의 함수로 묶어보겠습니다.
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
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest_i32(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {}", result);
}함수 largest_i32는 예제 10-3에서 봤던 슬라이스에서 가장 큰 i32를 찾는 함수이고,
largest_char 함수는 슬라이스에서 가장 큰 char를 찾는 함수입니다.
이 두 함수의 본문은 완벽히 동일하니, 제네릭을 이용해
이 두 함수를 하나로 만들어서 코드 중복을 제거해보겠습니다.
새 단일 함수의 시그니처 내 타입을 매개변수화하려면 타입 매개변수의
이름을 지어줄 필요가 있습니다. 방법은 함수 매개변수와 비슷합니다.
타입 매개변수의 이름에는 아무 식별자나 사용할 수 있지만, 여기서는 T를 사용하겠습니다.
러스트에서는 타입 이름을 지어줄 때는 대문자로 시작하는 낙타 표기법(UpperCamelCase)을
따르고, 타입 매개변수의 이름은 짧게(한 글자로만 된 경우도 종종 있습니다) 짓는 것이
관례이기 때문에, 대부분의 러스트 프로그래머는 'type'을 줄인 T를 사용합니다.
함수 본문에서 매개변수를 사용하려면 함수 시그니처에 매개변수의 이름을 선언하여
컴파일러에게 해당 이름이 무엇을 의미하는지 알려주어야 해야 하는 것처럼,
타입 매개변수를 사용하기 전에도 타입 매개변수의 이름을 선언해야 합니다.
예를 들어, 제네릭 largest 함수를 정의하려면 아래와 같이 함수명과
매개변수 목록 사이의 꺾쇠괄호(<>)에 타입 매개변수 이름을 선언해야
합니다.
fn largest<T>(list: &[T]) -> &T {이 정의는 ‘largest 함수는 어떤 타입 T에 대한 제네릭 함수’라고
읽힙니다. 이 함수는 T 타입 값의 슬라이스인 list 매개변수를
가지고 있고, 동일한 T 타입의 값에 대한 참조자를
반환합니다.
예제 10-5는 제네릭 데이터 타입을 사용해 하나로 통합한 largest 함수 정의를 나타냅니다.
코드에서 볼 수 있듯, 이 함수를 i32 값들의 슬라이스로 호출할 수도 있고
char 값들의 슬라이스로도 호출할 수 있습니다. 이 코드는 아직 컴파일되지 않음을
주의해 주시고, 나중에 고치도록 하겠습니다.
largest 함수;
아직 컴파일되지는 않습니다
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}이 코드를 지금 바로 컴파일해 보면 다음과 같은 에러가 발생합니다.
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
--> src/main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- &T
| |
| &T
|
help: consider restricting type parameter `T`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
| ++++++++++++++++++++++
For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` due to previous error도움말에서 트레이트(trait) std::cmp::PartialOrd가 언급되는데, 트레이트는
다음 절에서 살펴볼 것입니다. 지금은 이 에러가 ‘largest의 본문이 T가
될 수 있는 모든 타입에 대해 동작할 수 없음’을 뜻한다는 정도만 알아둡시다.
함수 본문에서 T 타입 값들에 대한 비교가 필요하므로, 여기에는 값을 정렬할
수 있는 타입에 대해서만 동작할 수 있습니다. 비교가 가능하도록 하기 위해,
표준 라이브러리는 임의의 타입에 대해 구현 가능한 std::cmp::PartialOrd
트레이트를 제공합니다. (이 트레이트에 대한 더 자세한 사항은 부록 C를 보세요.)도움말의 제안을 따라서 T가 PartialOrd를 구현한 것일 때만 유효하도록
제한을 두면 이 예제는 컴파일되는데, 이는 표준 라이브러리가 i32와 char
둘 모두에 대한 PartialOrd를 구현하고 있기 때문입니다.
제네릭 구조체 정의
<> 문법으로 구조체 필드에서 제네릭 타입 매개변수를 사용하도록
구조체를 정의할 수도 있습니다. 예제 10-6은 임의의 타입으로 된
x, y를 갖는 Point<T> 구조체를 정의합니다.
T 타입의 값 x, y를 갖는
Point<T> 구조체
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 };
}문법은 함수 정의에서 제네릭을 사용하는 것과 유사합니다. 먼저 구조체 이름 바로 뒤 꺾쇠괄호에 타입 매개변수 이름을 선언하고, 구조체 정의 내 구체적 데이터 타입을 지정하던 곳에 제네릭 타입을 대신 사용합니다.
Point<T> 선언에 하나의 제네릭 타입만 사용했으므로,
이 선언은 Point<T>가 어떤 타입 T에 대한 제네릭이며
x, y 필드는 실제 타입이 무엇이건 간에 둘 다 동일한 타입이라는 것을 의미합니다.
만약 예제 10-7처럼 서로 다른 타입의 값을 갖는 Point<T> 인스턴스를 생성하려고 할 경우,
코드는 컴파일되지 않습니다.
x와 y 필드는 둘 다 동일한
제네릭 데이터 타입 T이므로 같은 타입이어야 합니다
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}컴파일러는 x에 정숫값 5를 대입할 때 Point<T> 인스턴스의
제네릭 타입 T를 정수 타입으로 인지합니다.
그다음에는 y에 4.0을 지정했는데, y는 x와 동일한 타입을 갖도록
정의되었으므로 컴파일러는 타입 불일치 에러를 발생시킵니다.
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integer, found floating-point number
For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` due to previous error제네릭 Point 구조체의 x, y가 서로 다른 타입일 수 있도록
정의하고 싶다면 여러 개의 제네릭 타입 매개변수를 사용해야 합니다.
예제 10-8에서는 x는 T 타입으로, y는 U 타입으로 정의한
제네릭 Point 정의를 나타냅니다.
x와 y가 서로 다른 타입의 값이 될 수 있는 Point<T, U>
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}이제 위와 같이 모든 Point 인스턴스를 생성할 수 있습니다!
제네릭 타입 매개변수는 원하는 만큼 여러 개를 정의할 수 있지만,
많으면 많아질수록 코드 가독성은 떨어집니다. 만약 코드에서 많은 수의
제네릭 타입이 필요함을 알게 되었다면, 코드를 리팩터링해서 작은 부분들로
나누는 것을 고려해야 할 수도 있습니다.
제네릭 열거형 정의
구조체처럼, 열거형도 배리언트에 제네릭 데이터 타입을 갖도록 정의할 수 있습니다.
6장에서 사용했었던 표준 라이브러리의 Option<T> 열거형을
다시 살펴봅시다.
enum Option<T> {
Some(T),
None,
}이제는 이 코드를 이해할 수 있습니다. 보시다시피 Option<T>
열거형은 T 타입에 대한 제네릭이며, T 타입을 들고 있는 Some
배리언트와 아무런 값도 들고 있지 않은 None 배리언트를 갖습니다.
Option<T> 열거형을 사용함으로써 옵션 값에 대한 추상화된 개념을
표현할 수 있고, Option<T> 열거형이 제네릭으로 되어있는 덕분에
옵션 값이 어떤 타입이건 상관없이 추상화하여 사용할 수 있죠.
열거형에서도 여러 개의 제네릭 타입을 이용할 수 있습니다.
9장에서 사용했던 Result 열거형의 정의가 대표적인 예시입니다.
enum Result<T, E> {
Ok(T),
Err(E),
}Result 열거형은 T, E 두 타입을 이용한 제네릭이며,
T 타입 값을 갖는 Ok와 E 타입 값을 갖는 Err 배리언트를
갖습니다. 제네릭으로 정의되어 있는 덕분에, 연산이 성공할지(따라서
T 타입 값을 반환할지) 실패할지(E 타입 값을 반환할지) 알 수
없는 어느 곳이든 Result 열거형을 편리하게 사용할 수 있습니다.
예제 9-3에서 파일을 열 때도 사용했었죠.
이때는 파일을 여는 데 성공하면 T는 std::fs::File 타입이 되고,
파일을 열다가 문제가 생기면 E는 std::io::Error 타입이 됐었습니다.
작성한 코드에서 보유하는 값의 타입만 다른 구조체나 열거형이 여러 개 있음을 발견했을 때는 제네릭 타입을 사용해 코드 중복을 제거할 수 있습니다.
제네릭 메서드 정의
5장에서 했던 것처럼 구조체나 열거형에 메서드를 구현할 때도
제네릭 타입을 이용해 정의할 수 있습니다. 예제 10-9는
예제 10-6에서 정의했던 Point<T> 구조체에 x 메서드를 구현한 모습입니다.
T 타입의 x 필드에 대한
참조자를 반환하는 x 메서드를 Point<T>에
정의
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}x 필드 데이터의 참조자를 반환하는 x 메서드를 Point<T>에
정의해 보았습니다.
impl 바로 뒤에 T를 선언하여 Point<T> 타입에
메서드를 구현한다고 명시했음을 주의하세요.
이렇게 하면 러스트는 Point의 꺾쇠괄호 내 타입이
구체적인 타입이 아닌 제네릭 타입임을 인지합니다. 구조체 정의에
선언된 제네릭 매개변수와는 다른 제네릭 매개변수를 선택할 수도
있었겠지만, 같은 이름을 사용하는 것이 관례입니다. 제네릭 타입이
선언된 impl 안에 작성된 메서드는 이 제네릭 타입에 어떤 구체
타입을 집어넣을지와는 상관없이 어떠한 타입의 인스턴스에라도
정의될 것입니다.
이 타입의 메서드를 정의할 때 제네릭 타입에 대한 제약을 지정할 수도 있습니다.
예를 들면, 임의의 제네릭 타입 Point<T> 인스턴스가 아닌 Point<f32>
인스턴스에 대한 메서드만을 정의할 수 있습니다. 예제 10-10에서는 구체적
타입 f32을 사용하였는데, impl 뒤에는 어떤 타입도 선언하지 않았습니다.
T가
특정 구체적 타입인 경우에만 적용되는 impl 블록
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}이 코드에서 Point<f32> 타입 인스턴스는 distance_from_origin
메서드를 갖게 될 것입니다; T가 f32 타입이 아닌 Point<T>
인스턴스는 이 메서드가 정의되지 않습니다. 이 메서드는 생성된 점이
원점(0.0, 0.0)으로부터 떨어진 거리를 측정하며 부동 소수점 타입에서만
사용 가능한 수학적 연산을 이용합니다.
구조체 정의에서 사용한 제네릭 타입 매개변수와, 구조체의 메서드 시그니처 내에서 사용하는
제네릭 타입 매개변수가 항상 같은 것은 아닙니다. 예제 10-11을 보면 예제를 명료하게
만들기 위해 Point 구조체에 대해서는 X1와 Y1이라는 제네릭 타입을, 그리고 mixup
메서드에 대해서는 X2와 Y2라는 제네릭 타입을 사용했습니다. 이 메서드는 self
Point의 (X1 타입인) x값과 매개변수로 넘겨받은 Point의 (Y2 타입인) y값으로
새로운 Point 인스턴스를 생성합니다.
struct Point<X1, Y1> {
x: X1,
y: Y1,
}
impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
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!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}main에서는 i32 타입 x(5)와
f64 타입 y(10.4)를 갖는 Point를 정의했습니다.
p2는 문자열 슬라이스 타입 x("Hello")와 char 타입 y(c)를 갖는 Point입니다.
p3는 p1에서 mixup 메서드를 p2를 인수로 호출하여 반환된 값입니다.
p3의 x는 p1에서 온 i32 타입이며,
y는 p2에서 온 char 타입입니다.
println! 매크로는 p3.x = 5, p3.y = c를 출력합니다.
이 예제는 제네릭 매개변수 중 일부가 impl 에 선언되고
일부는 메서드 정의에 선언되는 경우를 보여주기 위한 예제입니다.
여기서 제네릭 매개변수 X1, Y1는 구조체 정의와 한 묶음이니
impl 뒤에 선언했지만, 제네릭 매개변수 X2, Y2는 mixup
메서드에만 연관되어 있으므로 fn mixup 뒤에
선언합니다.
제네릭 코드의 성능
제네릭 타입 매개변수를 사용하면 런타임 비용이 발생하는지 궁금해 할지도 모르겠습니다. 좋은 소식은, 제네릭 타입의 사용이 구체적인 타입을 사용했을 때와 비교해서 전혀 느려지지 않는다는 것입니다.
러스트는 컴파일 타임에 제네릭을 사용하는 코드를 단형성화(monomorphization) 합니다. 단형성화란 제네릭 코드를 실제 구체 타입으로 채워진 특정한 코드로 바꾸는 과정을 말합니다. 이 과정에서, 컴파일러는 예제 10-5에서 제네릭 함수를 만들 때 거친 과정을 정반대로 수행합니다. 즉 컴파일러는 제네릭 코드가 호출된 곳을 전부 찾고, 제네릭 코드가 호출될 때 사용된 구체 타입으로 코드를 생성합니다.
표준 라이브러리의 Option 열거형을 사용하는
예제를 통해 알아봅시다.
let integer = Some(5);
let float = Some(5.0);러스트는 이 코드를 컴파일할 때 단형성화를 수행합니다.
이 과정 중 컴파일러는 Option<T> 인스턴스에 사용된 값을 읽고,
i32, f64 두 종류의 Option<T>가 있다는 것을 인지합니다.
그리고 제네릭 정의를 i32와 f64에 대해 특성화시킨
정의로 확장함으로써, 제네릭 정의를 이 구체적인 것들로
대체합니다.
단형성화된 코드는 다음과 비슷합니다. (여기 사용된 이름은 예시를 위한 것이며 컴파일러에 의해 생성되는 이름은 다릅니다).
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}제네릭 Option<T>가 컴파일러에 의해 특정한 정의들로
대체되었습니다. 러스트 컴파일러가 제네릭 코드를 각 인스턴스의
명시적인 타입으로 변경해 주는 덕분에, 굳이 런타임 비용을 줄이기 위해
수동으로 직접 각 타입마다 중복된 코드를 작성할 필요가 없습니다.
단형성화 과정은 러스트 제네릭을 런타임에 극도로 효율적으로
만들어줍니다.