제네릭 타입, 트레이트, 라이프타임
모든 프로그래밍 언어는 중복되는 개념을 효율적으로 처리하기 위한 도구를 가지고 있습니다. 러스트에서는 제네릭 (generic) 이 그 역할을 맡습니다: 제네릭은 구체 (concrete) 타입 혹은 기타 속성에 대한 추상화된 대역입니다. 컴파일과 실행 시점에 제네릭들이 실제로 무슨 타입으로 채워지는지 알 필요 없이 제네릭의 동작이나 다른 제네릭과의 관계를 표현할 수 있습니다.
함수가 어떤 값이 들어있을지 모르는 매개변수를 전달받아서 동일한 코드를
다양한 구체적 값으로 실행하는 것처럼, 함수는 i32
, String
같은
구체 타입 대신 제네릭 타입의 매개변수를 전달받을 수 있습니다. 사실은
이미 여러 제네릭을 사용해 봤었습니다. 6장에서는 Option<T>
, 8장에서는
Vec<T>
와 HashMap<K, V>
, 9장에서는 Result<T, E>
제네릭을 사용했죠.
이번 장에서는 제네릭을 사용해 자체 타입, 함수, 메서드를 정의하는 방법을 살펴보겠습니다.
우선, 함수를 추출하여 중복되는 코드를 제거하는 방법을 살펴볼 겁니다. 그다음 매개변수의 타입만 다른 두 함수가 생기면 제네릭 함수를 사용해 코드 중복을 한 번 더 줄여보겠습니다. 또한, 제네릭 타입을 구조체 및 열거형 정의에 사용하는 방법도 살펴보겠습니다.
다음으로는 트레이트 (trait) 를 이용해 동작을 제네릭한 방식으로 정의하는 법을 배워보겠습니다. 트레이트를 제네릭 타입과 함께 사용하면, 아무 타입이나 허용하는 것이 아니라 특정 동작을 하는 타입만 허용할 수 있습니다.
마지막으로는 라이프타임 (lifetime) 을 살펴보겠습니다: 라이프타임은 제네릭의 일종이며, 컴파일러에게 참조자들이 서로 어떤 관계에 있는지를 알려주는 데에 사용합니다. 라이프타임은 빌린 값들에 대한 정보를 컴파일러에게 충분히 제공하여 작성자의 추가적인 도움 없이도 참조자의 여러 가지 상황에 대한 유효성 검증을 할 수 있게 해 줍니다.
함수를 추출하여 중복 없애기
제네릭은 여러 가지 타입을 나타내는 자리표시자의 위치에 특정 타입을 집어넣는 것으로 코드 중복을 제거할 수 있게 해 줍니다. 제네릭 문법을 배우기 전에, 먼저 제네릭 타입을 이용하지 않고 여러 가지 값을 나타내는 자리표시자로 특정 값을 대체하는 함수를 추출하는 방식으로 중복되는 코드를 없애는 요령을 알아보겠습니다. 그다음 동일한 기법을 이용하여 제네릭 함수를 추출해 보겠습니다! 함수로 추출할 수 있는 중복되는 코드를 알아내는 방법을 보는 것으로 제네릭을 사용할 수 있는 중복되는 코드들이 인식되기 시작할 것입니다.
예제 10-1과 같이 리스트에서 가장 큰 숫자를 찾아내는 간단한 프로그램부터 시작하겠습니다.
파일명: 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); assert_eq!(*largest, 100); }
number_list
변수에는 정수 리스트를 저장하고, largest
변수에 리스트의 첫 번째 숫자에 대한 참조자를 집어넣습니다.
그리고 리스트 내 모든 숫자를 순회하는데,
만약 현재 값이 largest
에 저장된 값보다 크다면
largest
의 값을 현재 값으로 변경합니다.
현재 값이 여태까지 본 가장 큰 값보다 작다면 largest
의 값은 바뀌지 않습니다.
리스트 내 모든 숫자를 돌아보고 나면 largest
는 가장 큰 값을 갖게 되며,
위의 경우에는 100이 됩니다.
이번에는 두 개의 다른 숫자 리스트에서 가장 큰 숫자를 찾으라는 일감을 받았습니다. 그렇게 하려면 예제 10-2처럼 예제 10-1의 코드를 프로그램 내 다른 곳에 복사하여 동일한 로직을 이용할 수도 있습니다.
파일명: 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); }
이 코드는 잘 동작하지만, 중복된 코드를 생성하는 일은 지루하고 에러가 발생할 가능성도 커집니다. 또한, 로직을 바꾸고 싶을 때 수정해야 할 부분이 여러 군데임을 기억해야 한다는 의미이기도 합니다.
이러한 중복을 제거하기 위해서, 정수 리스트를 매개변수로 전달받아 동작하는 함수를 정의하여 추상화할 것입니다. 이렇게 하면 코드가 더 명확해지고 목록에서 가장 큰 숫자를 찾는다는 개념을 추상적으로 표현할 수 있습니다.
예제 10-3에서는 가장 큰 수를 찾는 코드를 largest
라는 이름의
함수로 추출합니다. 그다음 예제 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); 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
값 슬라이스를 나타냅니다.
실제로 largest
함수가 호출될 때는 전달받은 구체적인 값으로
실행됩니다.
예제 10-2에서부터 예제 10-3까지 거친 과정을 요약하면 다음과 같습니다:
- 중복된 코드를 식별합니다.
- 중복된 코드를 함수의 본문으로 분리하고, 함수의 시그니처 내에 해당 코드의 입력값 및 반환 값을 명시합니다.
- 중복됐었던 두 지점의 코드를 함수 호출로 변경합니다.
다음에는 제네릭으로 이 과정을 그대로 진행하여 중복된 코드를 제거해 보겠습니다.
함수 본문이 특정한 값 대신 추상화된 list
로 동작하는 것처럼, 제네릭을
이용한 코드는 추상화된 타입으로 동작합니다.
만약 i32
슬라이스에서 최댓값을 찾는 함수와 char
슬라이스에서 최댓값을 찾는 함수를 따로 가지고 있다면 어떨까요?
이런 중복은 어떻게 제거해야 할지 한번 알아봅시다!