소개
데이터의 형태가 하나로 고정되어 있다면 구체적인 타입이 유용합니다. 하지만 숫자와 텍스트에 거의 동일한 타입을 반복해서 작성하면 프로그램을 유지 관리하기 어려워집니다. 이미 Option<u32>, Vec<String>, HashMap<String, u32>, Result<T, E>와 같은 표준 제네릭 타입을 사용해 보았습니다. 이 실습에서는 직접 제네릭 타입을 정의합니다. Rust의 제네릭을 사용하면 하나의 정의를 여러 구체적인 타입에 적용할 수 있으며, 컴파일러는 각 사용 방식도 계속 검사합니다.
이 실습에서는 준비된 Pair 구조체를 제네릭으로 바꾼 다음, 빌린 값을 사용하는 작은 헬퍼도 일반화합니다. 더 긴 프로그램과 샘플 데이터는 이미 작성되어 있으므로, 재사용을 가능하게 하는 몇 가지 타입 위치에 집중하면 됩니다.
Pair 구조체를 제네릭으로 만들기
이 단계에서는 구조체에 고정된 i32 타입을 하나의 타입 매개변수로 바꿉니다. 그러면 같은 정의로 점수나 경로 이름을 저장할 수 있습니다.
준비된 Cargo 패키지 디렉터리로 이동합니다.
cd /home/labex/project/pair-demo
짧은 소스 파일을 확인합니다. sed -n 명령은 지정한 줄 범위를 출력할 뿐 파일을 변경하지 않습니다.
sed -n '1,120p' src/main.rs
scores 값에는 정수가 들어 있고, routes에는 소유권을 가진 String 값이 들어 있습니다. 현재 Pair 정의는 두 필드를 모두 i32로 고정하므로 두 값을 모두 표현할 수 없습니다. 준비된 #[derive(Debug)] 특성은 이 타입의 디버그 형식 출력을 생성하지만, 현재 화면에 표시되는 프로그램에서는 해당 형식을 사용하지 않습니다. 따라서 이 특성은 이번 제네릭 작업의 일부가 아닙니다.
제네릭 타입에서는 꺾쇠괄호 안에 T와 같은 자리 표시자를 사용합니다. 생성되는 각 Pair는 여전히 하나의 구체적인 타입을 가집니다. 컴파일러는 scores에 대해 Pair<i32>를, routes에 대해 Pair<String>을 추론합니다.
Nano에서 소스 파일을 엽니다.
nano src/main.rs
파일 위쪽에서 다음 세 가지를 수정합니다.
struct Pair {를struct Pair<T> {로 변경합니다.left의 타입을i32에서T로 변경합니다.right의 타입을i32에서T로 변경합니다.
수정이 완료된 구조체는 다음과 같습니다.
struct Pair<T> {
left: T,
right: T,
}
여기서는 두 필드가 같은 T를 사용합니다. 따라서 하나의 pair에 서로 관련 없는 필드 타입을 실수로 섞을 수 없습니다. Ctrl+O로 저장하고 Enter를 누른 다음, Ctrl+X로 종료합니다.
Cargo에 패키지의 타입을 검사하도록 요청합니다.
cargo check
Finished로 끝나는 줄이 표시되면 컴파일러가 Pair<i32>와 Pair<String>을 모두 허용했다는 뜻입니다. 이제 프로그램을 실행합니다.
cargo run --quiet
scores: 82 and 91
routes: north and east
first score: 82
처음 두 줄은 하나의 구조체 정의로 서로 다른 두 구체적인 타입을 저장할 수 있음을 보여 줍니다. 기존 first 헬퍼는 여전히 Pair<i32>만 받습니다. 다음 단계에서 이 헬퍼도 일반화합니다.
빌린 값을 사용하는 헬퍼 일반화하기
이 단계에서는 first 함수에 자체 타입 매개변수를 추가합니다. 그러면 이 함수가 모든 Pair<T>를 빌리고 첫 번째 값에 대한 참조를 반환할 수 있습니다.
파일 위쪽에 있는 현재 헬퍼를 확인합니다.
sed -n '1,30p' src/main.rs
이 함수의 입력 타입인 Pair<i32>와 출력 타입인 &i32는 점수에만 사용할 수 있도록 제한되어 있습니다. 제네릭 함수는 이름 뒤에 <T>를 선언한 다음, 관련된 입력 및 출력 타입이 나타나는 위치에 같은 T를 사용합니다.
파일을 엽니다.
nano src/main.rs
다음 함수 선언만:
fn first(pair: &Pair<i32>) -> &i32 {
아래와 같이 변경합니다.
fn first<T>(pair: &Pair<T>) -> &T {
이 함수는 pair를 빌리고 빌린 필드를 반환하므로 저장된 값을 이동하거나 복사하지 않습니다. Rust는 각 인수에서 T를 추론하므로 호출할 때 타입을 직접 작성하지 않아도 됩니다.
다음으로 // FIRST_ROUTE_REPORT를 텍스트에도 헬퍼가 작동한다는 것을 확인하는 다음 호출 하나로 바꿉니다.
println!("first route: {}", first(&routes));
Nano에서 저장하고 종료합니다. 그런 다음 완성된 프로그램의 타입을 검사하고 실행합니다.
cargo check
cargo run --quiet
scores: 82 and 91
routes: north and east
first score: 82
first route: north
두 first 줄은 같은 제네릭 함수에서 출력됩니다. Rust는 점수 호출에서 T = i32를, 경로 호출에서 T = String을 추론한 다음 각 호출을 해당 구체적인 타입으로 검사합니다. 재사용성을 얻더라도 정적 타입 검사를 포기할 필요는 없습니다.
요약
하나의 구체적인 pair를 Pair<T>로 바꾸고 정수와 문자열 모두에 사용했습니다. 또한 특성을 도입하거나 구현을 복제하지 않고 빌린 값을 사용하는 헬퍼를 일반화했습니다.


