티스토리 뷰
제네릭, 트레이트, 수명
- Generics - 여러 타입에 대해 공통 코드를 작성하기 위해 사용한다. Struct, Enum, Method 등 어디에나 사용 가능하다.
- Trait - 다양한 타입 중에 제약조건을 만족하는 타입을 한정하기 위해 사용한다. (Java의 interface 와 같은 개념으로 이해하면 편하다. 물론 차이는 있다.)
- Lifetime - 참조 Reference 값의 수명 정보를 제공한다.
Generics
fn smallest_i32(list: &[i32]) -> &i32 {
let mut smallest = &list[0];
for item in list {
if item < smallest {
smallest = item;
}
}
smallest
}
fn smallest_char(list: &[char]) -> &char {
let mut smallest = &list[0];
for item in list {
if item < smallest {
smallest = item;
}
}
smallest
}
fn main() {
let numbers = vec![3, 4, 1, 6, 8, 10];
let result = smallest_i32(&numbers);
println!("가장 작은 수는 {}", result);
let chars = vec!['홍', '길', '동'];
let result = smallest_char(&chars);
println!("가장 작은 글자는 {}", result);
}
smallest_i32() 와 smallest_char() 두 함수는 타입만 다를 뿐 다 동일하다. 아래와 같이 제네릭 T 를 사용해서 다시 작성할 수 있다. fn smallest<T> (list: &[T]) -> &T { 로 작성하면 안되고 PartialOrd 를 받아줘야 하는데 그 이유는 item < smallest처럼 비교할 수 있는 Trait 가 있는 타입만 인자로 받기 위함이다.
fn smallest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
let mut smallest = &list[0];
for item in list {
if item < smallest {
smallest = item;
}
}
smallest
}
fn main() {
let numbers = vec![3, 4, 1, 6, 8, 10];
let result = smallest(&numbers);
println!("가장 작은 수는 {}", result);
let chars = vec!['홍', '길', '동'];
let result = smallest(&chars);
println!("가장 작은 글자는 {}", result);
}
제네릭 구조체 정의
아래 처럼 정수와 실수 타입을 제네릭 인자로 받아서 모두 처리할 수 있다.
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
단, 아래와 같이 타입이 명백히 다른데도 같은 제네릭 문자를 사용한 경우 컴파일 에러가 난다.
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 }; // Error! 둘다 정수거나 실수여야 함.
}
Trait
trait Greet {
// Greet 트레이트를 갖고 있다는 것은
// String 을 반환하는 greeting 함수를 갖고 있다는 의미이다.
fn greeting(&self) -> String;
}
enum Pet {
Dog,
Cat,
Tiger,
}
impl Greet for Pet {
fn greeting(&self) -> String {
match self {
Pet::Dog => String::from("멍멍"),
Pet::Cat => String::from("야옹"),
Pet::Tiger => String::from("어흥"),
}
}
}
struct Person {
name: String,
active: bool,
}
impl Greet for Person {
fn greeting(&self) -> String {
String::from("안녕")
}
}
fn meet(one: &impl Greet, another: &impl Greet) {
println!("첫번째가 인사합니다 = {}", one.greeting());
println!("두번째가 인사합니다 = {}", another.greeting());
}
fn main() {
let cat = Pet::Cat;
println!("{}", cat.greeting());
let gildong = Person {
name: String::from("홍길동"),
active: true,
};
meet(&cat, &gildong); // cat, gildong 은 타입이 다르지만 Greet 라는 Trait 을 동일하게 구현했기때문에 이렇게 사용할 수 있다.
}
/*
야옹
첫번째가 인사합니다 = 야옹
두번째가 인사합니다 = 안녕
*/
Greet 라는 Trait 을 갖고 있으면 meet() 함수의 파라미터로 올 수 있게 작성되있다. JAVA 에서도 이게 된다? 음… 동일한 인터페이스 상속받게 여러 클래스 만든 다음에
void meet( T extends Greet<? super T> one, T extends Greet<? super T> another) 이렇게 하면 되려나?
meet() 의 파라미터로 오는 인자가 Greet을 갖고 있으면서 같은 타입이어야 하는 경우 다음과 같이 표현할 수 있다.
fn meetBounded<T: Greet>(one: &T, another: &T) {
println!("첫번째가 인사합니다 = {}", one.greeting());
println!("두번째가 인사합니다 = {}", another.greeting());
}
fn main() {
let cat = Pet::Cat;
println!("{}", cat.greeting());
let gildong = Person {
name: String::from("홍길동"),
active: true,
};
meetBounded(&cat, &gildong); // ERROR! 같은 타입이 아니라 인사할 수 없다.
meetBounded(&cat, &Pet::Dog); // 인사가능.
}
meet() 의 파라미터로 오는 인자가 같거나 달라도 되는 경우
fn meetBounded<T: Greet, U: Greet>(one: &T, another: &U) {
println!("첫번째가 인사합니다 = {}", one.greeting());
println!("두번째가 인사합니다 = {}", another.greeting());
}
// 즉, 위의 축약표현이 처음에 나왔던 아래 코드와 같다고 볼 수 있다.
fn meet(one: &impl Greet, another: &impl Greet) {
println!("첫번째가 인사합니다 = {}", one.greeting());
println!("두번째가 인사합니다 = {}", another.greeting());
}
여러개의 Trait 를 갖고 있어야 하는 경우 + 로 이어서 제약조건을 추가할 수 있다.
fn multipleTraits<T: Greet + Debug>(one: &T, another: &T) {
println!("{:?} 인사합니다 = {}", one, one.greeting());
println!("{:?} 인사합니다 = {}", another, another.greeting());
}
fn multipleTraits<T: Greet + Debug, U: Greet + Display>(one: &T, another: &U) {
println!("{:?} 인사합니다 = {}", one, one.greeting());
println!("{} 인사합니다 = {}", another, another.greeting());
}
// 위와 같이 + 가 많이 이어져서 보기 어렵다면 아래와 같이 다시 작성할 수 있다.
fn multipleTraits<T, U>(one: &T, another: &U)
where
T: Greet + Debug,
U: Greet + Display,
{
println!("첫번째: {:?} 인사합니다 = {}", one, one.greeting());
println!("두번째: {} 인사합니다 = {}", another, another.greeting());
}
impl Display for Person {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name.as_str())
}
}
Trait 를 반환값으로 사용하는 경우 - 특정 Trait 을 갖는 객체만 반환되게 코드를 짤 수 있다.
fn return_Greet() -> impl Greet {
Person {
name: String::from("pott"),
active: true
}
}
임대값의 수명
- 참조의 수명 Lifetimes
- 수명 : 참조값이 필요한 만큼 유효하게 선언
- 사실 모든 참조값에는 유효 수명이 있다.
- 타입추론으로 타입 선언을 생략할 수 있듯, 대부분의 경우 생략 가능하다.
- 임대검사 Borrow Checker
- 타입이 잘 맞춰졌는지 검사하는 Type Checker 가 있듯이 임대수명이 유효한지 검사하는 Borrow Checker 가 있다.
- 참조의 수명보다, 원래 값의 수명이 같거나 더 길어야 한다.
아래는 참조의 수명보다 원래 값의 수명이 짧아서 에러가 나는 예제다. 이런 경우, 댕글링 참조가 일어났다고 말한다. (dangling : 매달려있는)
fn short_lifetime() {
let x;
{
let y = 5; // y의 수명은 스코프 범위 내
x = &y; // x는 y를 참조하고 있다.
}
println!("x = {}", x); // x가 수명이 끝난 y값의 참조를 들고 반환하려고 한다.
// 참조자보다 참조대상의 수명이 짧다. borrow Checker 가 에러를 반환.
}
수명을 명시해줘야 하는 경우
// borrow checker 에러 발생
fn main() {
let s1 = String::from("가나다");
let s2 = "하나둘셋";
let res = longest(s1.as_str(), s2);
println!("더 긴 문자열은 {} ", res);
}
fn longest(s1: &str, s2:&str) -> &str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
longest() 는 위와 같은 상태에서 컴파일 되지 않는다. 라이프 타임을 명시해줘야 한다.
왜냐하면 위 함수는 참조 타입을 반환하는데, 함수 내부에서 변수를 생성해서 참조를 반환하는 것은 댕글링 포인터 문제가 발생하는 것이기 때문에 인자로 받은 참조 변수를 반환하는 것일 테다. 그런데 러스트 컴파일러는 매개변수로 받은 두 인자 s1 과 s2 중 어느 변수의 수명이 더 긴지 알수 없기 때문에 컴파일에 실패한다.
위와 같은 경우는 런타임 시점이 되야 둘 중 어느 변수의 수명이 더 긴지 알 수 있다. 컴파일 시점에 수명 추론이 안되기 때문에 수명을 명시해줘야 한다.
// 에러 없음
fn main() {
let s1 = String::from("가나다");
let s2 = "하나둘셋";
let res = longest(s1.as_str(), s2);
println!("더 긴 문자열은 {} ", res);
}
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
수명을 명시하는 방법은 제네릭과 비슷하다.
수명을 명시한다는 의미에서 <'> 을 작성하고 아무 알파벳이나 넣어주면 된다. 제네릭에서 관례상 <T> 를 많이 사용하는 것처럼 여기서는 a 를 많이 사용한다.
이 함수 시그니처는 러스트에게, 함수는 두 매개변수를 갖고 둘 다 적어도 라이프타임 'a만큼 살아있는 문자열 슬라이스이며, 반환하는 문자열 슬라이스도 라이프타임 'a만큼 살아있다는 정보를 알려준다. 이것의 실제 의미는, longest 함수가 반환하는 참조자의 라이프타임은 함수 인수로서 참조된 값들의 라이프타임 중 작은 것과 동일하다는 의미이다.
as_str() 과 &은 어떤 차이가 있을까? → 차이가 없는 것 같다.
fn main() {
let s1 = String::from("가나다");
{
let s2 = "하나둘셋";
let res = longest(s1.as_str(), s2);
println!("더 긴 문자열은 {} ", res); // 더 긴 문자열은 하나둘셋
}
}
s2 와 res 의 수명은 둘 다 스코프 범위 내기 때문에 문제 없이 프로그램이 수행된다.
fn main() {
let s1 = String::from("가나다라마바사");
let res;
{
let s2 = String::from("하나둘셋");
res = longest(s1.as_str(), s2.as_str()); // borrowed Error!
}
println!("더 긴 문자열은 {} ", res);
}
longest() 내의 s2 에서 borrow checker 에 의한 에러가 발생한다.
여기서 s2 에서 에러가 발생하는 이유는 s1과 s2 중 s2가 longest() 의 반환값으로 반환 된다면 스코프를 빠져나오면서 s2 가 소멸되는데 res 는 소멸된 s2 의 참조를 들고 있게 되기 때문이다. 둘 중 수명이 더 짧은 s2 를 기준으로 longest 는 반환값의 수명을 할당하기 때문에 이 코드는 에러가 난다.
하지만 아래와 같은 경우에는 에러가 발생하지 않는다. 왜 일까?
위의 코드와 아래의 코드는 s2 를 String 으로 만들었는지, 문자열 리터럴로 만들었는지 그 차이밖에 없다. 왜 문자열 리터럴은 컴파일이 되는 걸까?
fn main() {
let s1 = String::from("가나다라마바사");
let res;
{
let s2 = "하나둘셋";
res = longest(s1.as_str(), s2);
}
println!("더 긴 문자열은 {} ", res);
}
- s2는 &'static str 타입이다.
- 리터럴 문자열은 프로그램이 끝날 때까지 유효한 정적 라이프타임('static)을 가진다.
- 따라서 longest 함수에 넘겨도 res가 main 함수 끝까지 유효하다.
문자열 리터럴은 엄밀하게 말하면 stack 에 저장된다. s2 가 longest 에 할당이 있을때 주소 참조가 일어나는 것이 아니라 참조 copy 가 발생한다. 문자열 리터럴의 값은 어딘가 다른 곳에 저장이 되어 있고, 문자열 리터럴 변수는 그 값의 주소값만 들고 있는데 longest 에 s2 를 넘기게 되면 주소 카피가 발생하게 되는 것이다. 그래서 스코프를 벗어나서 s2 가 소멸되도 컴파일 에러가 발생하지 않는다. 왜냐하면 res 는 참조를 copy 해서 들고 있을 것이고, s2 의 값은 다른 곳에 안전하게 보관 되어 있을테니까!
수명 표기 생략 규칙
- 모든 매개변수에 차례대로 수명을 표기한다. (’a, ‘b, ‘c …)
- 매개변수가 딱 한개라면 모든 반환값에 해당 수명을 부여한다.
- 메소드이고 매개변수 중 하나가 &self 또는 &mut self 라면 모든 반환값에 self 수명을 부여한다.
⇒ 위 3가지 규칙을 적용했음에도 반환값의 수명을 추론할 수 없다면 borrow checker 가 컴파일 단계에서 에러를 발생시킨다.