제네릭 타입, 트레이트, 라이프타임
제네릭 타입, 트레이트, 라이프타임
모든 프로그래밍 언어에는 개념의 중복을 효과적으로 처리하기 위한 도구가 있어요. Rust에서 그런 도구 중 하나가 바로 **제네릭(generics)**이에요. 구체적인 타입이나 다른 속성 대신 쓰이는 추상적인 대체물(stand-in)이죠. 컴파일하고 실행할 때 그 자리에 무엇이 들어갈지 몰라도, 제네릭의 동작이나 제네릭들이 서로 어떻게 관련되는지를 표현할 수 있어요.
출처: The Rust Book
함수는 i32나 String 같은 구체적인 타입 대신 어떤 제네릭 타입의 매개변수를 받을 수 있어요. 여러 구체적인 값에 같은 코드를 실행하기 위해 값을 알지 못하는 매개변수를 받는 것과 같은 원리죠. 사실 우리는 이미 제네릭을 써 봤어요. 6장에서 Option<T>, 8장에서 Vec<T>와 HashMap<K, V>, 9장에서 Result<T, E>를 다뤘죠. 이번 장에서는 제네릭으로 여러분 자신의 타입, 함수, 메서드를 정의하는 방법을 탐구하게 될 거예요.
먼저 코드 중복을 줄이기 위해 함수를 추출하는 방법을 복습할게요. 그다음에는 같은 기법을 사용해 매개변수의 타입만 다른 두 함수로부터 제네릭 함수를 만들 거예요. 그리고 구조체와 이늄 정의에서 제네릭 타입을 사용하는 방법도 설명할게요.
그다음에는 **트레이트(trait)**를 사용해 동작을 제네릭한 방식으로 정의하는 방법을 배워요. 트레이트를 제네릭 타입과 결합하면, 제네릭 타입이 아무 타입이나 받는 대신 특정 동작을 가진 타입만 받도록 제약할 수 있어요.
마지막으로 **라이프타임(lifetime)**을 다룰 거예요. 참조들이 서로 어떻게 관련되는지에 대한 정보를 컴파일러에게 주는 제네릭의 한 종류죠. 라이프타임은 빌린 값에 대해 컴파일러가 충분한 정보를 갖게 해서, 우리의 도움 없이 보장할 수 있는 것보다 더 많은 상황에서 참조가 유효하다는 것을 보장할 수 있게 해줘요.
함수 추출로 중복 제거하기
제네릭은 여러 타입을 대표하는 자리표시자로 구체적인 타입을 대체해 코드 중복을 없애게 해줘요. 제네릭 문법에 들어가기 전에, 먼저 제네릭 타입 없이 중복을 제거하는 방법을 볼게요. 여러 값을 대표하는 자리표시자로 구체적인 값을 대체하는 함수를 추출하는 거죠. 그런 다음에 같은 기법을 적용해 제네릭 함수를 추출할 거예요. 함수로 추출할 수 있는 중복 코드를 알아보는 방법을 배우면, 제네릭을 사용할 수 있는 중복 코드도 알아보기 시작하게 돼요.
리스트에서 가장 큰 숫자를 찾는 리스팅 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}");
assert_eq!(*largest, 100);
}
정수 리스트를 number_list 변수에 저장하고, 리스트의 첫 번째 숫자에 대한 참조를 largest라는 변수에 넣어요. 그런 다음 리스트의 모든 숫자를 순회하면서, 현재 숫자가 largest에 저장된 숫자보다 크면 그 변수의 참조를 교체해요. 반면 현재 숫자가 지금까지 본 최댓값보다 작거나 같으면 변수는 그대로 두고, 코드는 리스트의 다음 숫자로 넘어가요. 리스트의 모든 숫자를 고려한 뒤에는 largest가 가장 큰 숫자, 여기서는 100을 가리키게 돼요.
이번에는 두 개의 서로 다른 숫자 리스트에서 가장 큰 숫자를 찾는 일이 주어졌다고 해볼게요. 그러려면 리스팅 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}");
assert_eq!(*result, 100);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("The largest number is {result}");
assert_eq!(*result, 6000);
}
largest 함수는 list라는 매개변수를 가지는데, 이 매개변수는 함수에 넘겨줄 수 있는 어떤 구체적인 i32 값의 슬라이스든 나타내요. 그래서 함수를 호출할 때 코드는 우리가 넘긴 특정 값들에 대해 실행돼요.
정리하면, 리스팅 10-2의 코드를 리스팅 10-3으로 바꾸기 위해 우리가 한 단계는 이래요.
- 중복 코드를 식별한다.
- 중복 코드를 함수 본문으로 추출하고, 그 코드의 입력과 반환값을 함수 시그니처에 지정한다.
- 중복된 두 코드 사례를 함수를 호출하도록 업데이트한다.
다음으로, 이 같은 단계를 제네릭과 함께 사용해 코드 중복을 줄일 거예요. 함수 본문이 구체적인 값 대신 추상적인 list에 대해 동작할 수 있는 것과 같은 방식으로, 제네릭은 코드가 추상적인 타입에 대해 동작할 수 있게 해줘요.
예를 들어 두 함수가 있다고 해볼게요. 하나는 i32 값의 슬라이스에서 가장 큰 항목을 찾고, 다른 하나는 char 값의 슬라이스에서 가장 큰 항목을 찾아요. 그 중복을 어떻게 제거할 수 있을까요? 함께 알아볼게요!