함수 추출을 통한 중복 제거

Beginner

This tutorial is from open-source community. Access the source code

소개

함수 추출을 통한 중복 제거에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.

이 랩에서는 함수를 추출하고 제네릭 (generics) 을 사용하여 추상 타입 (abstract types) 에 대해 작업함으로써 코드 중복을 제거하는 방법을 배웁니다.

함수 추출을 통한 중복 제거

제네릭 (generics) 을 사용하면 특정 타입을 여러 타입을 나타내는 자리 표시자로 대체하여 코드 중복을 제거할 수 있습니다. 제네릭 구문에 대해 자세히 알아보기 전에, 먼저 제네릭 타입을 사용하지 않고 특정 값을 여러 값을 나타내는 자리 표시자로 대체하는 함수를 추출하여 중복을 제거하는 방법을 살펴보겠습니다. 그런 다음 동일한 기술을 적용하여 제네릭 함수를 추출할 것입니다! 함수로 추출할 수 있는 중복된 코드를 인식하는 방법을 살펴보면, 제네릭을 사용할 수 있는 중복된 코드를 인식하기 시작할 것입니다.

먼저 목록에서 가장 큰 숫자를 찾는 Listing 10-1 의 짧은 프로그램부터 시작합니다.

파일 이름: src/main.rs

fn main() {
  1 let number_list = vec![34, 50, 25, 100, 65];

  2 let mut largest = &number_list[0];

  3 for number in &number_list {
      4 if number > largest {
          5 largest = number;
        }
    }

    println!("The largest number is {largest}");
}

Listing 10-1: 숫자 목록에서 가장 큰 숫자 찾기

number_list 변수 [1]에 정수 목록을 저장하고, 목록의 첫 번째 숫자에 대한 참조를 largest라는 변수 [2]에 넣습니다. 그런 다음 목록의 모든 숫자를 반복하고 [3], 현재 숫자가 largest에 저장된 숫자보다 크면 [4], 해당 변수의 참조를 대체합니다 [5]. 그러나 현재 숫자가 지금까지 본 가장 큰 숫자보다 작거나 같으면 변수는 변경되지 않고 코드는 목록의 다음 숫자로 이동합니다. 목록의 모든 숫자를 고려한 후, largest는 가장 큰 숫자, 즉 이 경우 100 을 참조해야 합니다.

이제 두 개의 서로 다른 숫자 목록에서 가장 큰 숫자를 찾는 작업이 주어졌습니다. 이를 위해 Listing 10-1 의 코드를 복제하고 프로그램의 두 다른 위치에서 동일한 로직을 사용할 수 있습니다. Listing 10-2 에 나와 있습니다.

파일 이름: src/main.rs

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}");
}

Listing 10-2: 두 개의 숫자 목록에서 가장 큰 숫자를 찾는 코드

이 코드는 작동하지만, 코드를 복제하는 것은 지루하고 오류가 발생하기 쉽습니다. 또한 코드를 변경하려는 경우 여러 위치에서 코드를 업데이트해야 한다는 것을 기억해야 합니다.

이 중복을 제거하기 위해 매개변수로 전달된 모든 정수 목록에 대해 작동하는 함수를 정의하여 추상화를 만들 것입니다. 이 솔루션은 코드를 더 명확하게 만들고 목록에서 가장 큰 숫자를 찾는 개념을 추상적으로 표현할 수 있게 해줍니다.

Listing 10-3 에서 가장 큰 숫자를 찾는 코드를 largest라는 함수로 추출합니다. 그런 다음 함수를 호출하여 Listing 10-2 의 두 목록에서 가장 큰 숫자를 찾습니다. 앞으로 있을 수 있는 다른 i32 값 목록에서도 함수를 사용할 수 있습니다.

파일 이름: src/main.rs

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}");
}

Listing 10-3: 두 목록에서 가장 큰 숫자를 찾기 위한 추상화된 코드

largest 함수에는 list라는 매개변수가 있는데, 이 매개변수는 함수에 전달할 수 있는 모든 구체적인 i32 값 슬라이스를 나타냅니다. 결과적으로 함수를 호출하면 코드는 전달하는 특정 값에 대해 실행됩니다.

요약하면, Listing 10-2 의 코드를 Listing 10-3 으로 변경하기 위해 수행한 단계는 다음과 같습니다.

  1. 중복된 코드를 식별합니다.
  2. 중복된 코드를 함수의 본문으로 추출하고, 해당 코드의 입력 및 반환 값을 함수 시그니처 (signature) 에 지정합니다.
  3. 함수를 호출하도록 중복된 코드의 두 인스턴스를 업데이트합니다.

다음으로, 제네릭을 사용하여 코드 중복을 줄이기 위해 동일한 단계를 사용합니다. 함수 본문이 특정 값 대신 추상적인 list에 대해 작동할 수 있는 방식과 마찬가지로, 제네릭을 사용하면 코드가 추상 타입에 대해 작동할 수 있습니다.

예를 들어, i32 값 슬라이스에서 가장 큰 항목을 찾는 함수와 char 값 슬라이스에서 가장 큰 항목을 찾는 함수가 있다고 가정해 보겠습니다. 어떻게 그 중복을 제거할 수 있을까요? 알아보겠습니다!

제네릭 데이터 타입 (Generic Data Types)

제네릭을 사용하여 함수 시그니처 (function signatures) 또는 구조체 (structs) 와 같은 항목에 대한 정의를 생성한 다음, 다양한 구체적인 데이터 타입 (concrete data types) 과 함께 사용할 수 있습니다. 먼저 제네릭을 사용하여 함수, 구조체, 열거형 (enums) 및 메서드를 정의하는 방법을 살펴보겠습니다. 그런 다음 제네릭이 코드 성능에 미치는 영향에 대해 논의할 것입니다.

함수 정의에서

제네릭을 사용하는 함수를 정의할 때, 일반적으로 매개변수와 반환 값의 데이터 타입을 지정하는 함수 시그니처 (signature) 에 제네릭을 배치합니다. 이렇게 하면 코드가 더 유연해지고 함수 호출자에게 더 많은 기능을 제공하는 동시에 코드 중복을 방지할 수 있습니다.

largest 함수를 계속 사용하면서, Listing 10-4 는 슬라이스에서 가장 큰 값을 찾는 두 개의 함수를 보여줍니다. 그런 다음 이들을 제네릭을 사용하는 단일 함수로 결합할 것입니다.

파일 이름: src/main.rs

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}");
}

Listing 10-4: 이름과 시그니처의 타입만 다른 두 개의 함수

largest_i32 함수는 Listing 10-3 에서 추출한 함수로, 슬라이스에서 가장 큰 i32를 찾습니다. largest_char 함수는 슬라이스에서 가장 큰 char를 찾습니다. 함수 본문은 동일한 코드를 가지고 있으므로, 단일 함수에 제네릭 타입 매개변수를 도입하여 중복을 제거해 보겠습니다.

새로운 단일 함수에서 타입을 매개변수화하려면, 함수에 대한 값 매개변수와 마찬가지로 타입 매개변수의 이름을 지정해야 합니다. 타입 매개변수 이름으로 어떤 식별자 (identifier) 든 사용할 수 있습니다. 하지만 관례적으로 Rust 의 타입 매개변수 이름은 짧고, 종종 한 글자이며, Rust 의 타입 명명 규칙은 CamelCase 이므로 T를 사용합니다. type의 약자인 T는 대부분의 Rust 프로그래머가 선택하는 기본값입니다.

함수 본문에서 매개변수를 사용할 때는 컴파일러가 해당 이름의 의미를 알 수 있도록 시그니처에 매개변수 이름을 선언해야 합니다. 마찬가지로, 함수 시그니처에서 타입 매개변수 이름을 사용할 때는 사용하기 전에 타입 매개변수 이름을 선언해야 합니다. 제네릭 largest 함수를 정의하려면, 다음과 같이 함수 이름과 매개변수 목록 사이에 꺾쇠 괄호 <> 안에 타입 이름 선언을 넣습니다.

fn largest<T>(list: &[T]) -> &T {

이 정의는 다음과 같이 읽습니다: largest 함수는 어떤 타입 T에 대해 제네릭입니다. 이 함수에는 list라는 매개변수가 하나 있는데, 이는 타입 T의 값 슬라이스입니다. largest 함수는 동일한 타입 T의 값에 대한 참조를 반환합니다.

Listing 10-5 는 시그니처에 제네릭 데이터 타입을 사용하는 결합된 largest 함수 정의를 보여줍니다. 이 Listing 은 또한 i32 값 슬라이스 또는 char 값으로 함수를 호출하는 방법을 보여줍니다. 이 코드는 아직 컴파일되지 않지만, 이 챕터의 뒷부분에서 수정할 것입니다.

파일 이름: src/main.rs

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}");
}

Listing 10-5: 제네릭 타입 매개변수를 사용하는 largest 함수; 아직 컴파일되지 않음

이 코드를 지금 컴파일하면 다음과 같은 오류가 발생합니다.

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 {
  |             ++++++++++++++++++++++

도움말 텍스트는 std::cmp::PartialOrd를 언급하는데, 이는 *트레이트 (trait)*이며, 다음 섹션에서 트레이트에 대해 이야기할 것입니다. 지금은 이 오류가 largest의 본문이 T가 될 수 있는 모든 가능한 타입에 대해 작동하지 않는다는 것을 나타낸다는 것을 알아두세요. 본문에서 타입 T의 값을 비교하려는 경우, 값을 정렬할 수 있는 타입만 사용할 수 있습니다. 비교를 활성화하기 위해 표준 라이브러리에는 타입에 구현할 수 있는 std::cmp::PartialOrd 트레이트가 있습니다 (이 트레이트에 대한 자세한 내용은 부록 C 참조). 도움말 텍스트의 제안을 따르면, T에 유효한 타입을 PartialOrd를 구현하는 타입으로만 제한하고, 이 예제는 컴파일됩니다. 왜냐하면 표준 라이브러리는 i32char 모두에 대해 PartialOrd를 구현하기 때문입니다.

구조체 정의에서

<> 구문을 사용하여 하나 이상의 필드에서 제네릭 타입 매개변수를 사용하도록 구조체를 정의할 수도 있습니다. Listing 10-6 은 모든 타입의 xy 좌표 값을 저장하기 위해 Point<T> 구조체를 정의합니다.

파일 이름: src/main.rs

1 struct Point<T> {
  2 x: T,
  3 y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

Listing 10-6: 타입 Txy 값을 저장하는 Point<T> 구조체

구조체 정의에서 제네릭을 사용하는 구문은 함수 정의에서 사용되는 구문과 유사합니다. 먼저 구조체 이름 바로 뒤에 꺾쇠 괄호 안에 타입 매개변수의 이름을 선언합니다 [1]. 그런 다음 구체적인 데이터 타입을 지정하는 대신 구조체 정의에서 제네릭 타입을 사용합니다 [23].

Point<T>를 정의하기 위해 하나의 제네릭 타입만 사용했기 때문에, 이 정의는 Point<T> 구조체가 어떤 타입 T에 대해 제네릭이며, xy 필드는 모두 동일한 타입, 즉 어떤 타입이든 될 수 있음을 의미합니다. Listing 10-7 과 같이 서로 다른 타입의 값을 갖는 Point<T>의 인스턴스를 생성하면 코드가 컴파일되지 않습니다.

파일 이름: src/main.rs

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

fn main() {
    let wont_work = Point { x: 5, y: 4.0 };
}

Listing 10-7: xy는 모두 동일한 제네릭 데이터 타입 T를 가지므로 동일한 타입이어야 합니다.

이 예제에서 정수 값 5x에 할당하면, 컴파일러는 이 Point<T> 인스턴스에 대해 제네릭 타입 T가 정수가 될 것임을 알게 됩니다. 그런 다음 y에 대해 4.0을 지정하면, x와 동일한 타입을 갖도록 정의했으므로 다음과 같은 타입 불일치 오류가 발생합니다.

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

xy가 모두 제네릭이지만 서로 다른 타입을 가질 수 있는 Point 구조체를 정의하려면, 여러 제네릭 타입 매개변수를 사용할 수 있습니다. 예를 들어, Listing 10-8 에서 Point의 정의를 타입 TU에 대해 제네릭하도록 변경하여 x는 타입 T이고 y는 타입 U가 되도록 합니다.

파일 이름: src/main.rs

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 };
}

Listing 10-8: xy가 서로 다른 값의 타입이 될 수 있도록 두 개의 타입에 대해 제네릭인 Point<T, U>

이제 표시된 모든 Point 인스턴스가 허용됩니다! 정의에서 원하는 만큼 많은 제네릭 타입 매개변수를 사용할 수 있지만, 몇 개 이상 사용하면 코드를 읽기 어려워집니다. 코드에 많은 제네릭 타입이 필요하다는 것을 알게 되면, 코드를 더 작은 조각으로 재구성해야 함을 나타낼 수 있습니다.

열거형 정의에서

구조체와 마찬가지로, 열거형 (enum) 을 정의하여 변형 (variant) 에서 제네릭 데이터 타입을 저장할 수 있습니다. 6 장에서 사용했던 표준 라이브러리가 제공하는 Option<T> 열거형을 다시 살펴보겠습니다.

enum Option<T> {
    Some(T),
    None,
}

이제 이 정의가 더 이해가 될 것입니다. 보시다시피, Option<T> 열거형은 타입 T에 대해 제네릭이며 두 개의 변형을 갖습니다: 타입 T의 값을 하나 갖는 Some과 어떤 값도 갖지 않는 None 변형입니다. Option<T> 열거형을 사용함으로써, 선택적 값 (optional value) 의 추상적인 개념을 표현할 수 있으며, Option<T>가 제네릭이기 때문에 선택적 값의 타입이 무엇이든 이 추상화를 사용할 수 있습니다.

열거형은 여러 제네릭 타입을 사용할 수도 있습니다. 9 장에서 사용했던 Result 열거형의 정의가 그 예입니다.

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

Result 열거형은 두 개의 타입, TE에 대해 제네릭이며 두 개의 변형을 갖습니다: 타입 T의 값을 갖는 Ok와 타입 E의 값을 갖는 Err입니다. 이 정의는 성공할 수 있거나 (어떤 타입 T의 값을 반환) 실패할 수 있는 (어떤 타입 E의 오류를 반환) 연산이 있는 곳에서 Result 열거형을 편리하게 사용할 수 있도록 합니다. 실제로, 이것은 9-3 Listing 에서 파일을 열 때 사용했던 것으로, 파일이 성공적으로 열렸을 때 Tstd::fs::File 타입으로 채워지고, 파일을 여는 데 문제가 있을 때는 Estd::io::Error 타입으로 채워졌습니다.

코드에서 여러 구조체 또는 열거형 정의가 있고, 그들이 저장하는 값의 타입만 다른 상황을 인식하면, 제네릭 타입을 사용하여 중복을 피할 수 있습니다.

메서드 정의에서

5 장에서 했던 것처럼, 구조체와 열거형에 메서드를 구현하고 정의에서 제네릭 타입을 사용할 수도 있습니다. Listing 10-9 는 Listing 10-6 에서 정의한 Point<T> 구조체에 x라는 메서드를 구현한 것을 보여줍니다.

파일 이름: src/main.rs

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());
}

Listing 10-9: Point<T> 구조체에 x라는 메서드를 구현하여 타입 Tx 필드에 대한 참조를 반환합니다.

여기서, x 필드의 데이터에 대한 참조를 반환하는 Point<T>x라는 메서드를 정의했습니다.

impl 바로 뒤에 T를 선언해야 T를 사용하여 Point<T> 타입에 메서드를 구현하고 있음을 지정할 수 있습니다. impl 뒤에 제네릭 타입으로 T를 선언함으로써, Rust 는 Point의 꺾쇠 괄호 안의 타입이 구체적인 타입이 아닌 제네릭 타입임을 식별할 수 있습니다. 구조체 정의에서 선언된 제네릭 매개변수와 다른 이름을 이 제네릭 매개변수에 대해 선택할 수도 있지만, 동일한 이름을 사용하는 것이 일반적입니다. 제네릭 타입을 선언하는 impl 내에서 작성된 메서드는 제네릭 타입을 대체하는 구체적인 타입에 관계없이 해당 타입의 모든 인스턴스에 대해 정의됩니다.

타입에 메서드를 정의할 때 제네릭 타입에 대한 제약 조건을 지정할 수도 있습니다. 예를 들어, 모든 제네릭 타입의 Point<T> 인스턴스가 아닌 Point<f32> 인스턴스에 대해서만 메서드를 구현할 수 있습니다. Listing 10-10 에서는 구체적인 타입 f32를 사용하므로, impl 뒤에 어떤 타입도 선언하지 않습니다.

파일 이름: src/main.rs

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

Listing 10-10: 제네릭 타입 매개변수 T에 특정 구체적인 타입을 가진 구조체에만 적용되는 impl 블록

이 코드는 Point<f32> 타입이 distance_from_origin 메서드를 갖는다는 것을 의미합니다; Tf32 타입이 아닌 다른 Point<T>의 인스턴스는 이 메서드가 정의되지 않습니다. 이 메서드는 좌표 (0.0, 0.0) 에서 얼마나 멀리 떨어져 있는지 측정하며, 부동 소수점 타입에만 사용할 수 있는 수학적 연산을 사용합니다.

구조체 정의의 제네릭 타입 매개변수는 항상 해당 구조체의 메서드 시그니처에서 사용하는 것과 동일하지는 않습니다. Listing 10-11 은 Point 구조체에 대해 제네릭 타입 X1Y1을 사용하고, mixup 메서드 시그니처에 대해 X2Y2를 사용하여 예제를 더 명확하게 만듭니다. 이 메서드는 self Point (타입 X1) 의 x 값과 전달된 Point (타입 Y2) 의 y 값을 사용하여 새로운 Point 인스턴스를 생성합니다.

파일 이름: src/main.rs

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

1 impl<X1, Y1> Point<X1, Y1> {
  2 fn mixup<X2, Y2>(
        self,
        other: Point<X2, Y2>,
    ) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
  3 let p1 = Point { x: 5, y: 10.4 };
  4 let p2 = Point { x: "Hello", y: 'c' };

  5 let p3 = p1.mixup(p2);

  6 println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

Listing 10-11: 구조체의 정의와 다른 제네릭 타입을 사용하는 메서드

main에서, xi32 (값 5) 와 yf64 (값 10.4 [3]) 를 갖는 Point를 정의했습니다. p2 변수는 x에 문자열 슬라이스 (값 "Hello") 와 ychar (값 c [4]) 를 갖는 Point 구조체입니다. p2 인수를 사용하여 p1에 대해 mixup을 호출하면 p3 [5]을 얻게 되며, xp1에서 왔기 때문에 i32가 됩니다. p3 변수는 yp2에서 왔기 때문에 char가 됩니다. println! 매크로 호출 [6]은 p3.x = 5, p3.y = c를 출력합니다.

이 예제의 목적은 일부 제네릭 매개변수가 impl로 선언되고 일부는 메서드 정의로 선언되는 상황을 보여주는 것입니다. 여기서, 제네릭 매개변수 X1Y1은 구조체 정의와 함께 사용되므로 impl 뒤에 선언됩니다 [1]. 제네릭 매개변수 X2Y2는 메서드에만 관련되므로 fn mixup 뒤에 선언됩니다 [2].

제네릭을 사용하는 코드의 성능

제네릭 타입 매개변수를 사용할 때 런타임 비용이 있는지 궁금할 수 있습니다. 다행히 제네릭 타입을 사용해도 프로그램이 구체적인 타입을 사용할 때보다 더 느리게 실행되지 않습니다.

Rust 는 컴파일 시간에 제네릭을 사용하는 코드의 단형화 (monomorphization) 를 수행하여 이를 달성합니다. 단형화는 컴파일 시 사용되는 구체적인 타입을 채워 넣음으로써 제네릭 코드를 특정 코드로 변환하는 과정입니다. 이 과정에서 컴파일러는 Listing 10-5 에서 제네릭 함수를 생성하기 위해 사용했던 단계의 반대 작업을 수행합니다: 컴파일러는 제네릭 코드가 호출되는 모든 위치를 살펴보고 제네릭 코드가 호출되는 구체적인 타입에 대한 코드를 생성합니다.

표준 라이브러리의 제네릭 Option<T> 열거형을 사용하여 이것이 어떻게 작동하는지 살펴보겠습니다.

let integer = Some(5);
let float = Some(5.0);

Rust 가 이 코드를 컴파일할 때, 단형화를 수행합니다. 그 과정에서 컴파일러는 Option<T> 인스턴스에서 사용된 값을 읽고 두 종류의 Option<T>를 식별합니다: 하나는 i32이고 다른 하나는 f64입니다. 따라서 Option<T>의 제네릭 정의를 i32f64에 특화된 두 개의 정의로 확장하여 제네릭 정의를 특정 정의로 대체합니다.

코드의 단형화된 버전은 다음과 유사합니다 (컴파일러는 설명을 위해 여기서 사용하는 것과 다른 이름을 사용합니다).

파일 이름: src/main.rs

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>는 컴파일러가 생성한 특정 정의로 대체됩니다. Rust 는 제네릭 코드를 각 인스턴스에서 타입을 지정하는 코드로 컴파일하기 때문에 제네릭을 사용하는 데 런타임 비용이 들지 않습니다. 코드가 실행될 때, 각 정의를 수동으로 복제한 경우와 동일하게 수행됩니다. 단형화 과정은 Rust 의 제네릭을 런타임에서 매우 효율적으로 만듭니다.

요약

축하합니다! 함수 추출을 통한 중복 제거 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.