제네릭 타입, 트레이트, 라이프타임
모든 프로그래밍 언어에는 개념 중복을 효과적으로 다루기 위한 도구가 있습니다. 러스트에서는 그중 하나가 제네릭(generics) 입니다. 제네릭은 구체적인 타입이나 다른 속성을 대신하는 추상적인 자리표시자입니다. 우리는 코드를 컴파일하고 실행할 때 그 자리에 무엇이 올지 몰라도, 제네릭의 동작 방식이나 제네릭들 사이 관계를 표현할 수 있습니다.
함수는 i32 나 String 같은 구체 타입 대신 어떤 제네릭 타입의 매개변수를 받을 수
있습니다. 이는 함수가 여러 구체 값에 같은 코드를 적용하기 위해 “값은 아직 모르는”
매개변수를 받는 것과 비슷합니다. 사실 우리는 이미 6장에서 Option<T>, 8장에서
Vec<T> 와 HashMap<K, V>, 9장에서 Result<T, E> 를 통해 제네릭을 사용해
보았습니다. 이 장에서는 여러분 자신의 타입, 함수, 메서드에 제네릭을 정의하는 방법을
살펴보게 됩니다!
먼저, 코드 중복을 줄이기 위해 함수를 추출하는 방법을 다시 훑어보겠습니다. 그런 다음 매개변수 타입만 다른 두 함수를 같은 기법으로 하나의 제네릭 함수로 바꾸겠습니다. 또한 구조체와 enum 정의 안에서 제네릭 타입을 사용하는 방법도 설명합니다.
그다음에는 트레이트를 사용해 동작을 제네릭한 방식으로 정의하는 방법을 배웁니다. 트레이트와 제네릭 타입을 결합하면, “아무 타입이나” 가 아니라 “특정 동작을 가진 타입만” 받아들이도록 제네릭 타입을 제한할 수 있습니다.
마지막으로 라이프타임(lifetimes) 을 다룹니다. 라이프타임은 제네릭의 한 종류로, 참조들이 서로 어떻게 관계되는지를 컴파일러에게 알려 줍니다. 라이프타임을 사용하면 빌린 값에 대해 컴파일러가 더 많은 정보를 알 수 있게 되어, 우리 도움 없이도 보장할 수 있는 것보다 더 많은 상황에서 참조가 유효함을 보장할 수 있습니다.
함수 추출로 중복 제거하기
제네릭은 여러 타입을 나타내는 자리표시자로 특정 타입을 대체함으로써 코드 중복을 없애게 해 줍니다. 제네릭 문법으로 바로 들어가기 전에, 먼저 제네릭 타입을 사용하지 않고도 어떻게 중복을 제거할 수 있는지 살펴봅시다. 구체적인 값을 여러 값을 나타내는 자리표시자로 바꾸는 함수를 추출하는 방식입니다. 그런 다음 같은 기법을 적용해 제네릭 함수를 추출해 보겠습니다. 함수를 추출할 수 있는 중복 코드를 어떻게 찾아내는지 익히면, 제네릭을 사용할 수 있는 중복 코드도 자연스럽게 보이기 시작할 것입니다.
먼저 목록 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 슬라이스에서 가장
큰 값을 찾는 함수 하나가 있다고 해 봅시다. 이 중복을 어떻게 제거할 수 있을까요?
이제 알아봅시다!