Home [Rust] Generic Types
Post
Cancel
Preview Image

[Rust] Generic Types

오늘은 RustGeneric Types, Traits, and Lifetimes중에서 Generic Types대해 알아보겠다.

사실 이 전에 Trait std::convert::AsRef에 대한 문제를 풀다가, 해당 내용을 글로 정리하던 와중에,

Trait이라는 개념 및 정의가 선행되어야 할 것 같아서, 이 글을 먼저 적게 되었다.

그럼 하나씩 차근차근 가볍게 정리해보고, 추후에 글을 업데이트하던지 아니면 새로운 글로 깊게 들어가보겠다.

Generic Data Types

In Function Definitions

먼저 아래 코드를 보자

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
32
33
34
35
36
37
38
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);
}

// The largest number is 100
// The largest char is y

largest_i32largest_char함수는 각각 i32타입과 char함수에 대해서 가장 큰 값을 알려준다.

그런데 타입 말고는 차이점이 없다.

그러면 하나로 합친 다음에, 타입별로 계산하면 되지않을까?

다음 <>과 같이 함수 이름과 매개 변수 목록 사이의 꺾쇠 괄호 안에 유형 이름 선언을 배치해보자.

아래 코드를 잠시 보자. 아래 코드는 컴파일에러가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
}

를 실행하면 아래와 같은 에러가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ cargo run --bin generics_function
   Compiling workspace-rust v0.1.0 (/Users/matt/gitFolders/workspace-rust)
error[E0369]: binary operation `>` cannot be applied to type `&T`
 --> src/bin/generics_function.rs:4:17
  |
4 |         if item > largest {
  |            ---- ^ ------- &T
  |            |
  |            &T
  |
help: consider restricting type parameter `T`
  |
1 | 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 `workspace-rust`

To learn more, run the command again with --verbose.

에러를 보면, std::cmp::PartialOrd이라는 Trait이 멘션되어있다.

해당 Trait을 정의해주면 잘 실행되는 것을 알 수 있다.

물론 자세한 Trait에 대해서는 다음 섹션에서 자세히 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn largest<T: PartialOrd>(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);
}

In Struct Definitions

<>구문을 사용하여 하나 이상의 필드에서 일반 유형 매개 변수를 사용하도록 구조체를 정의 할 수도 있다.

1
2
3
4
5
6
7
8
9
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 };
}

구조체 정의에서 제네릭을 사용하는 구문은 함수 정의에서 사용되는 구문과 유사하다.

먼저 구조체 이름 바로 뒤에 꺾쇠 괄호 안에 형식 매개 변수의 이름을 선언한다.

그런 다음 구체적인 데이터 유형을 지정하는 구조체 정의에서 제네릭 유형을 사용할 수 있다.

하지만, 아래와 같이 다른 유형의 값을 가진 인스턴스를 생성하면, 코드가 컴파일 되지 않는다.

아래와 같은 불일치 오류가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cargo run --bin generics_struct
   Compiling workspace-rust v0.1.0 (/Users/matt/gitFolders/workspace-rust)
error[E0308]: mismatched types
 --> src/bin/generics_struct.rs:9:34
  |
9 |     let _test = Point { x: 5, y: 4.0 };
  |                                  ^^^ expected integer, found floating-point number

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `workspace-rust`

To learn more, run the command again with --verbose.

이런 경우에는 다른 타입을 받도록 Point 구조체를 정의하면 컴파일이 잘 된다.

1
2
3
4
5
6
7
8
9
10
11
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 };
}

In Enum Definitions

다음과 같이 정의해주면 된다.

1
2
3
4
5
6
7
8
9
enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

두 종류 이상의 제네릭을 보유하고, 각 유형별로 다른 타입을 줄 수 있다.

In Method Definitions

구조체와 열거형에 메서드를 구현할 수 있고 정의에 제네릭 유형도 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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());
}

Rust<>안의 Point유형이 구체적인 타입이 아닌 제네릭 타입임을 식별 할 수 있다.

예를 들어, 제네릭 타입의 Point 인스턴스가 아닌 Point 인스턴스 에서만 메서드를 구현할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
    println!("p's distance = {}", p.distance_from_origin())
}

위의 코드를 실행하면, 다음과 같은 에러가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cargo run --bin generics_method
   Compiling workspace-rust v0.1.0 (/Users/matt/gitFolders/workspace-rust)
error[E0599]: no method named `distance_from_origin` found for struct `Point<{integer}>` in the current scope
  --> src/bin/generics_method.rs:22:37
   |
1  | struct Point<T> {
   | --------------- method `distance_from_origin` not found for this
...
22 |     println!("p's distance = {}", p.distance_from_origin())
   |                                     ^^^^^^^^^^^^^^^^^^^^ method not found in `Point<{integer}>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0599`.
error: could not compile `workspace-rust`

To learn more, run the command again with --verbose.

p는 Integer 타입으로 인스턴스를 생성했지만, method distance_from_origin 는 해당 타입의 인스턴스의 메서드가 없기 때문에 찾을 수 없다는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10 };
    let p2 = Point { x: 5.0, y: 10.0 };

    println!("p1.x = {}", p1.x());
    println!("p2's distance = {}", p2.distance_from_origin())
}

와 같이 Point 인스턴스를 생성해주면 문제없이 실행된다.

구조체 정의의 제네릭 타입 매개변수는 해당 구조체의 메서드 시그니처에서 사용하는 매개 변수와 항상 동일하지는 않다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

위의 코드와 같이 mixup함수는 <T, U>, <V, W> 타입에 대해서 <T, W> 타입을 리턴해준다.

Performance of Code Using Generics

제네릭 타입 매개 변수를 사용할 때 런타임 비용이 있는지 궁금 할 수 있다.

좋은 소식은 Rust가 제네릭을 구현할 때, 구체적인 타입보다 제네릭 타입을 사용한 것을 더 느리게 실행되지 않도록했다는 것이다.

Rust는 컴파일 타임에 제네릭을 사용하는 코드의 monomorphization를 수행(perform)하여 이를 수행(accomplish)한다.

Monomorphization은 컴파일 할 때 사용되는 구체적인 타입을 채워 일반 코드를 특정 코드로 바꾸는 프로세스이다.

다음 예제를 보자.

1
2
3
4
5
#![allow(unused)]
fn main() {
let integer = Some(5);
let float = Some(5.0);
}

Rust가 위의 코드를 컴파일할 때, monomorphization를 수행한다.

이 과정에서 컴파일러는 Option<T>인스턴스에 사용되는 값들을 읽고, 하나는, i32타입의 인스턴스, 다른 하나는 f64타입의 인스턴스의 Option<T>인스턴스들을 명시한다.

즉, Option<T>의 일반 정의를 Option_i32Option_f64로 확장한 것처럼, 일반 정의를 특정한 정의로 치환한다.

코드의 Monomorphization버전은 아래와 같이 실행된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}

Rust는 제네릭 코드를 각 인스턴스의 유형을 지정하는 코드로 컴파일하기 때문에 제네릭 사용에 대한 런타임 비용을 지불하지 않는다.

코드가 실행되면 각 정의를 수동으로 복제한 것처럼 수행된다.

monomorphization 과정은 Rust의 제네릭을 런타임에 매우 효율적으로 만든다.

This post is licensed under CC BY 4.0 by the author.

[Rust] Cargo 프로젝트에서 Author 수정하기

tmux 설치 및 적응기