#12 제네릭 자료형

Pt J·2020년 8월 22일
0

[完] Rust Programming

목록 보기
15/41
post-thumbnail

이 시리즈는 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 함수와 매우 비슷하겠지만 그것을 사용할 수 없다.
이런 상황에서 코드 중복을 줄이기 위해 사용할 수 있는 것이 제네릭이다.

제네릭 Generic

함수에서의 제네릭

정수 최대값을 구하는 함수와 문자 최대값을 구하는 함수를
제네릭을 사용하지 않고 구현한다고 하자.

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$ 

여기서 주의할 건, xy의 제네릭 자료형이 모두 T라면
그것의 실제 자료형이 무엇이든 그 둘은 같아야 한다는 것이다.
그들의 자료형이 다를 수 있다면 다음과 같이 정의해야 한다.

struct Point<T, U> {
    x: T,
    y: U,
}

그렇게 되면 TU는 서로 다를 수 있고 단지 T끼리만 같고 U끼리만 같으면 된다.
물론 TU가 같다고 해서 문제가 되지도 않는다.

열거형에서의 제네릭

열거형에서도 제네릭을 사용할 수 있다.
당장 우리가 봐왔던 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에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글