320x100
320x100

제네릭 (generic)

러스트에서 중복되는 개념을 효율적으로 처리하기 위한 도구로, 구체(concrete) 타입 혹은 기타 속성에 대한 추상화된 대역

fn largest<T>(list: &[T]) -> &T {}

 

 

 

 

 

트레이트 (trait)

러스트에서 공통 기능을 정의하는 인터페이스와 비슷한 기능으로, 특정한 타입이 가지고 있으면서 다른 타입과 공유할 수 있는 기능을 정의

트레이트를 제네릭 타입과 함께 사용하면, 아무 타입이나 허용하는 것이 아니라 특정 동작을 하는 타입만 허용할 수 있음

즉, "이 타입은 이런 기능을 반드시 가지고 있어야 한다"는 규칙을 정의한 것

pub trait Summary {
    fn summariaze(&self) -> String;
}

 

 

 

 

 

라이프타임 (life time)

제네릭의 일종으로 컴파일러에게 참조자들이 서로 어떤 관계에 있는지를 알려주는데 사용

라이프타임은 빌린 값들에 대한 정보를 컴파일러에 충분히 제공하여 작성자의 추가적인 도움없이도 참조자의 여러 가지 상황에 대해 유효성 검증이 가능

 

 

 

 

 

 

제네릭 메서드 정의 (구조체 정의와 다른 제네릭 타입을 사용하는 메서드)

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    // 제네릭 매개변수 X2와 Y2는 mixup 메서드에만 연관되어 있음
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };
    
    let p3 = p1.mixup(p2);
    
    println!("p3.x = {}, p3.y = {}", p3.x, p3.y); // 5, c
}

 

 

 

 

 

제네릭 코드의 성능

러스트는 컴파일 타임에 제네릭을 사용하는 코드를 단형성화 하기 때문에 구체적인 타입을 사용했을 떄와 비교해서 전혀 느리지 않다.

 

- 단형성화 (monomorphization)

제네릭 코드를 실제 구체 타입으로 채워진 특정한 코드로 바꾸는 과정을 의미. 이 과정에서 컴파일러는 제네릭 코드가 호출된 곳을 전부 찾고, 제네릭 코드가 호출할 때 사용된 구체 타입으로 코드를 생성

 

 

 

 

 

트레이트 구현

// src/lib.rs
pub trait Summary {
    // 타입에서 구현하라는 의미로 뒤에 세미콜론이 붙음
    fn summariaze(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

// Summary 트레이트를 NewArticle 타입에 대해 구현
impl Summary for NewArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub usernae: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

// Summary 트레이트를 Tweet 타입에 대해 구현
impl Summary for Tweet {
    fn summarize(&self) -> String {
        format("{}: {}", self.username, self.content)
    }
}
// src/main.rs
// 크레이트 사용자는 타입뿐만 아니라 트레이트도 스코프로 가져와야함
use aggregator::{Summary, Tweet}; 

fn main() {
    let tweet = Tweet {
        username: String::from("2mukee"),
        content: String::from("hello"),
        reply: false,
        retweet: false,
    };
    
    // 트레이트 메서드 호출
    println!("1 new tweet: {}", tweet.summarize()); // 2mukee: hello
}

 

- 트레이트 구현의 제약사항 (일관성 > 고아 규칙)

내가 만든 타입에 외부 트레이트를 구현 > 가능

외부 타입에 내가 만든 트레이트를 구현 > 가능

외부 타입에 외부 트레이트를 구현 > 불가능

 

- 일관성 (coherence)

러스트 프로그램 전체에서 어떤 타입에 대해 특정 트레이트의 구현은 딱 하나만 있어야 한다

 

- 고아 규칙 (orphan rule)

트레이트 구현을 작성할 때, 그 조합의 ‘트레이트’ 또는 ‘타입’ 중 하나는 반드시 내가 정의한 것이어야 한다

 

 

 

 

 

 

트레이트의 기본 동작 구현

타입에 트레이트를 구현할 때마다 모든 메서드를 구현할 필요는 없도록 트레이트의 메서드에 기본 동작을 제공할 수 있음

이 경우 타입에 트레이트를 구현할 때 기본 동작을 유지할지 오버라이딩(overriding)할 지 선택할 수 있음

// src/liv.rs
pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

 

 

 

 

 

매개변수로서의 트레이트

// 지정된 트레이트를 구현하는 타입이라면 어떤 타입이든 전달 받을 수 있음
// 그러나 Summary 트레이트를 구현하지 않는 타입으로 notify 함수를 호출할 경우 오류 발생
pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

 

 

- 트레이트 바운드 (trait bound)

단순한 상황에서 사용하는 `impl Trait` 문법과 달리 더 복잡한 상황을 표현할 수 있는 문법

// 트레이트 바운드는 제네릭타입 매개변수 선언에 붙은 : 뒤에 위치 
pub fn notify<T: Summay>(item: &T) {
    println!("Breaking new! {}", item.summarize());
}

 

 

- 트레이트 바운드와 impl Trait 문법 비교

// impl Trait
pub fn notify(item1: &impl Summary, item2: &impl Summary) {}

// trait bound
pub fn notify<T: Summary>(item1: &T, item2: &T) {}

// impl Trait
pub fn notify(item: &(impl Summary + Display)) {}

// trait bound
pub fn notify<T: Summary + Display>(item: &T) {}

 

 

- where 절로 트레이트 바운드 정리하기 (가독성 개선)

// AS-IS
fn some_func<T: Display + Clone, U: Clone + Debug>(t: &T, u: &u) -> i32 {}

// TO-BE
fn some_func<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
}

 

 

 

 

 

트레이트를 구현하는 타입 반환

// 가능
fn return_summ() -> impl Summary {
    Tweet {
        username: String::from("2mukee"),
        content: String::from("Hello"),
        reply: false,
        retweet: false,
    }
}

// 불가능
fn return_summ() -> impl Summary {
    if switch {
        NewsArticle { ... }
    } else {
        Tweet: { ... }
    }
}

 

 

 

 

 

포괄 구현 (blanket implementation)

트레이트 바운드를 만족하는 모든 타입에 대해 트레이트를 구현하는 것

트레이트를 특정 타입 하나하나에 직접 구현하지 않고, 어떤 조건을 만족하는 모든 타입에 한꺼번에 구현하는 방법

// Display 트레이트를 구현한 모든 타입은
// 자동으로 ToString 트레이트도 구현된 것으로 취급됨
// 그래서 i32, f64, String 같은 타입에 대해 to_string()을 쓸 수 있는 것
impl<T: Display> ToString for T {
    fn to_string(&self) -> String {
        format!("{}", self)
    }
}

 

 

 

 

 

라이프타임 (참조자의 유효성 검증)

라이프타임은 어떤 타입이 원하는 동작이 구현되어 있음을 보장하기 보다는, 어떤 참조자가 필요한 기간 동안 유효함을 보장하도록 함

러스트의 모든 참조자는 라이프타임이라는 참조자의 유효성을 보장하는 범위를 가짐

라이프타임은 암묵적으로 추론되나 참조자의 수명이 여러 방식으로 서로 연관될 수 있는 경우에는 라이프타임을 명시해주어야 한다

라이프타임의 주 목적은 댕글링 참조 (프로그램이 참조하려고 한 데이터가 아닌 엉뚱한 데이터를 참조하는 원인) 방지임

 

- 대여 검사기(borrow checker)

러스트는 대여 검사기로 스코프를 비교하여 대여의 유효성을 판단

 

// 댕글링 참조가 발생하는 코드
fn main() {
    let r;
    {
        let x = 5;
        r = &x; // borrowed value does not live long enough 오류 발생
    }
    println!("r: {}", r); // x가 스코프를 벗어날 때 r은 할당 해제된 메모리를 참고하면서 오류발생
}

// 데이터의 라이프타임이 참조자의 라이프타임보다 길어서 문제없는 코드
fn main() {
    let x = 5;
    let r = &x;
    println!("r: {}", r); // x의 수명이 r의 수명보다 길다
    // r 소멸
} // x 소멸

 

 

 

 

 

 

라이프타임 명시

라이프타임을 명시한다고해서 참조자의 수명이 바뀌지 않음

그보다는 여러 참조자에 대한 수명에 영향을 주지 않으면서 서로 간 수명의 관계가 어떻게 되는지에 대해 기술하는 것임

&i32 // 참조자
&'a i32 // 명시적인 라이프타임이 있는 참조자
&'a mut i32 // 명시적인 라이프타임이 있는 가변 참조자

 

라이프타임 매개변수의 이름은 아스트로피(')로 시작해야하고 보통은 제네릭 타입처럼 매우 짧은 소문자로 정함

대부분의 사람들은 라이프타임을 명시할 떄 'a를 사용

라이프타임 매개변수는 참조자의 &뒤에 위치하며 공백을 한 칸 입력하여 참조자의 타입과 분리

 

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";
    
    let result = longest(string1.as_str(), string2);
    println!("longest: {}", result);
}

// 문제가 발생하는 코드
// 반환할 참조자가 x인지 y인지 러스트가 알 수 없기 때문
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

// 정상코드
// x와 y가 동일한 라이프타임을 가짐을 명시
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

// 비정상 코드
// 반환타입에 'a를 지정했지만 반환 값의 라이프타임이 매개변수와 관련 없으므로 오류 발생
fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("real");
    result.as_str();
}

 

라이프타임을 함수에 명시할 떄는 함수 본문이 아닌, 함수 시그니처에 적음

라이프타임 명시는 함수 시그니처의 타입들과 마찬가지로 함수에 대한 계약서의 일부가 됨

이는 러스트 컴파일러가 수행하는 분석이 좀 더 단순해질 수 있음을 의미

만일 함수에 문제가 있으면 컴파일러 에러가 해당 코드의 위치와 제약을 좀 더 정밀하게 짚어낼 수 있음

이는 대여 검사기가 잠재적으로 유효하지 않은 참조자를 가질 수 있다고 판단하는데 단서가됨

 

 

 

 

 

구조체 정의에서 라이프타임 명시하기

구조체가 참조자를 들고 있도록 할 수 있지만, 이 경우 구조체 정의 내 모든 참조자에 라이프타임을 명시해야함

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    // ImportantExcerpt 인스턴스가 생성되기 전부터 존재하며,
    // ImportantExcerpt 인스턴스가 스코프를 벗어나기 전에는
    // novel이 스코프를 벗어나지 않으니 ImportantExcerpt 인스턴스는 유효
    let novel = String::from("call me Ishmael. Some years ago...");
    
    let first_sentence = novel.split('.').next().expect("could not find a '.'");
    
    let i = ImportantExcerpt {
        part: first_senetence,
    };
}

 

 

 

 

 

라이프타임 생략 규칙 (lifetime elision rules)

러스트 초기 버전 (1.0 이전)에는 모든 참조자에 명시적인 라이프타임이 필요했었음

그러나 예측 가능한 상황이면서 결정론적 패턴임에도 똑같은 라이프타임을 작성을 해야했기 떄문에

러스트 개발팀은 이 패턴들을 프로그래밍하여 이러한 상황에서는 라이프타임을 명시하지 않아도 대여 검사기가 추론할 수 있도록 함

러스트 참조자 분석 기능에 프로그래밍된 이 패턴들을 라이프타임 생략 규칙이라고함

이 규칙들은 프로그래머가 따라야하는 규칙이 아니며, 컴파일러가 고려하는 특정한 사례의 모음임

생략 규칙이 완전한 추론 기능을 제공하는 것은 아니고, 라이프타임이 모호한 참조자가 있으면 컴파일러는 이 참조자의 라이프타임을 추측하지 않고 에러를 발생시킴

 

- 입력 라이프타임 (input lifetime)

함수나 메서드 매개변수의 라이프타임

 

- 출력 라이프타임 (output lifetime)

반환 값의 라이프타임

 

- 라이프타임 명시가 없을 때 컴파일러가 사용하는 규칙

이 세가지 규칙을 모두 적용했음에도 라이프타임을 알 수 없는 참조자가 있을 때 컴파일러는 에러를 발생시킴

 

1. [입력 라이프타임] 컴파일러가 참조자인 매개변수 각각에게 라이프타임 매개변수를 할당해야함

매개 변수가 하나인 함수는 하나의 라이프타임 매개변수를 갖고 매개변수가 두 개인 함수는 두 개의 개별 라이프타임을 갖는 식

 

2. [출력 라이프타임] 입력 라이프타임 매개변수가 딱 하나라면 해당 라이프타임이 모든 출력 라이프타임에 대입됨

 

3. [출력 라이프타임] 입력 라이프타임 매개변수가 여러 개인데 그중 하나가 &self나 &mut self (즉, 메서드)라면 self의 라이프타임이 모든 출력 라이프타임 매개변수에 대입됨

 

// 예시
fn first_word(s: &Str) -> &Str {}

// 첫 번쨰 규칙 적용 시 시그니처
fn first_word<'a>(s: &'a str) -> &str {}

// 두 번째 규칙 적용 시 시그니처`
fn first_word<'a>(s: &'a str) -> &'a str {}
// 예시
fn longest(x: &str, y: &str) -> &str {}

// 첫 번쨰 규칙 적용 시 시그니처
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {}

// 입력 라이프타임이 하나가 아니므로 두 번째 규칙 적용 불가
// longest 함수는 메서드가 아니니 세 번째 규칙도 적용 불가

 

 

 

 

 

메서드 정의에서 라이프타임 명시하기

라이프타임 매개변수의 선언 및 사용 위치는 구조체 필드나 메서드 매개변수 및 반환 값과 연관이 있느냐 없느냐에 따라 달라짐

라이프타임이 구조체 타입의 일부가 되기 때문에 구조체 필드의 라이프타임 이름은 impl 키워드 뒤에 선언한 다음 구조체 명 뒤에 사용해야함

// 예시
impl<'a> ImportantExcerpt<'a> {
  fn level(&self) -> i32 {
      4
  }
}

// 세번 쨰 라이프타임 생략 규칙 적용
impl<'a> ImportantExcerpt<'a> {
    fn return_part(&self, announcement: &str) -> &str {
        println!("a: {}", announcement);
        self.part
    }
 }

 

 

 

 

 

정적 라이프타임 (static lifetime)

해당 참조자가 프로그램의 전체 생애주기 동안 살아있음을 의미

// 모든 문자열 리터럴은 'static 라이프타임을 가짐을 명시
let s: &'static str = "I have static lifetime.";

 

이 문자열의 텍스트는 프로그램의 바이너리 내에 직접 저장되기 때문에 언제나 이용 가능

하지만 어떤 참조자를 ;static으로 지정하기 전에 해당 참조자가 반드시 프로그램의 전체 라이프타임 동안 유지되어야 하는지 고민 필요

'static 라이프타임을 제안하는 에러 메시지는 대부분의 경우 댕글링 참조를 만들다가 발생하거나 사용할 수 있는 라이프타임이 잘못 짝지어져서 발생

 

 

 

 

 

제네릭 타입 매개변수, 트레이트 바운트, 라이프타임 전체 사용 예시

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("announce: {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
300x250
728x90