English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Rust 제너릭과 트레이트

제너릭은 프로그래밍 언어에서 필수적인 메커니즘입니다.

C++ 언어는 "템플릿"을 통해 제너릭을 구현하며, C 언어에는 제너릭 메커니즘이 없어 C 언어가 복잡한 타입의 프로젝트를 구축하기 어렵습니다.

제너릭 메커니즘은 프로그래밍 언어가 타입 추상을 표현하는 데 사용되며, 일반적으로 함수 결정이 확정되고 데이터 타입이 불확정한 클래스, 예를 들어 링크드 리스트, 맵 테이블 등에 사용됩니다.

함수에서 제너릭을 정의합니다

이는 정수형 숫자에 대한 선택 정렬 방법입니다:

fn max(array: &[i32]) -> i32 {}}
    let mut max_index = 0;
    let mut i = 1;
    while i < array.len() {
        if array[i] > array[max_index] {
            max_index = i;
        }
        i += 1;
    }
    array[max_index]
}
fn main() {
    let a = [2, 4, 6, 3, 1];
    println!("max = {}", max(&a));
}

실행 결과:

max = 6

이는 간단한 최대값 추출 프로그램으로 i를 처리할 수 있습니다.32 숫자 타입의 데이터지만 f에 사용할 수 없습니다.64 타입의 데이터입니다. 제너릭을 사용하면 이 함수가 여러 타입에 대해 사용될 수 있습니다. 하지만 모든 데이터 타입이 비교할 수 있는 것은 아닙니다. 따라서 다음 코드는 실행되지 않으며, 함수 제너릭의 문법 구조를 설명하는 데 사용됩니다:

fn max<T>(array: &[T])}} -> T {
    let mut max_index = 0;
    let mut i = 1;
    while i < array.len() {
        if array[i] > array[max_index] {
            max_index = i;
        }
        i += 1;
    }
    array[max_index]
}

구조체와 열거형 클래스의 기본 타입

이전에 배운 Option과 Result 열거형 클래스는 기본 타입을 가지고 있습니다.

Rust에서 구조체와 열거형 클래스는 모두 기본 타입의 메커니즘을 구현할 수 있습니다.

struct Point<T> {
    x: T,
    y: T
}

이는 포인트 좌표 구조체입니다. T는 포인트 좌표를 설명하는 수치 타입을 의미합니다. 이렇게 사용할 수 있습니다:

let p1 = Point { x: 1, y: 2};
let p2 = Point { x: 1.0, y: 2.0};

사용할 때 타입을 선언하지 않았습니다. 여기서는 자동 타입 메커니즘을 사용합니다. 하지만 타입 불일치가 발생하지 않도록 합니다:

let p = Point { x: 1, y: 2.0};

x와 1 바인딩할 때 이미 T를 i로 설정했습니다32그래서 f가 더 이상 등장하지 않도록 합니다64 의 타입입니다. 만약 x와 y가 다른 데이터 타입으로 표현하고 싶다면, 두 개의 기본 타입 식별자를 사용할 수 있습니다:

struct Point<T1,2> {
    x: T1,
    y: T2
}

열거형 클래스에서 기본 타입을 표현하는 방법으로 Option과 Result와 같은 것들을 사용합니다:

enum Option<T> {
    Some(T),
    None,
}
enum Result<T, E> {
    Ok(T),
    Err(E),
}

구조체와 열거형 클래스는 모두 메서드를 정의할 수 있기 때문에, 메서드는 기본 타입의 메커니즘을 구현해야 합니다. 그렇지 않으면 기본 타입의 클래스는 효과적으로 메서드로 처리할 수 없습니다.

struct Point<T> {
    x: T,
    y: T,
}
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}
fn main() {
    let p = Point { x: 1, y: 2 };
    println!("p.x = {}", p.x());
}

실행 결과:

p.x = 1

주의하세요, impl 키워드 뒤에는 <T>가 반드시 있어야 합니다. 왜냐하면 그 뒤의 T는 이것을 기준으로 합니다. 하지만 우리는 그 중 하나의 기본 타입에 메서드를 추가할 수도 있습니다:

impl Point<f64> {
    fn x(&self) -> f64 {}}
        self.x
    }
}

impl 블록 자체의 기본 타입은 내부 메서드가 기본 타입을 가지고 있는 것을 방해하지 않습니다:

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

method mixup은 Point<T, U> 포인트의 x와 Point<V, W> 포인트의 y를 합쳐 Point<T, W> 타입의 새로운 포인트로 만듭니다.

이미터프레스

이미터프레스(trait)의 개념은 Java의 인터페이스(Interface)와 유사하지만, 완전히 동일하지 않습니다. 이미터프레스와 인터페이스는 모두 행위 규격으로서 사용되며, 어떤 클래스가 어떤 메서드를 가지고 있는지를 표시할 수 있습니다.

이미터프레스는 Rust에서 trait로 표현됩니다:

trait Descriptive {
    fn describe(&self) -> String;
}

Descriptive는 구현자가 describe(&self)를 구현해야 한다고 지정합니다 -> String 메서드.

그것을 구조체 구현에 사용합니다:

struct Person {
    name: String,
    age: u8
}
impl Descriptive for Person {
    fn describe(&self) -> String {
        format!("{} {}", self.name, self.age)
    }
}

형식은 다음과 같습니다:

impl <이미터프레스 이름> for <구현된 타입 이름>

Rust에서는 하나의 클래스가 여러 이미터프레스를 구현할 수 있으며, 각 impl 블록은 하나의 이미터프레스만 구현할 수 있습니다.

기본 이미터프레스

이미터프레스와 인터페이스의 차이점은 다음과 같습니다: 인터페이스는 메서드를 규격화할 수만 있으며 메서드를 정의할 수 없습니다. 하지만 이미터프레스는 기본 메서드를 정의할 수 있으며, "기본"이기 때문에 객체는 메서드를 재정의할 수도 있고 재정의하지 않고 기본 메서드를 사용할 수도 있습니다:

trait Descriptive {
    fn describe(&self) -> String {
        String::from("[Object]")
    }
}
struct Person {
    name: String,
    age: u8
}
impl Descriptive for Person {
    fn describe(&self) -> String {
        format!("{} {}", self.name, self.age)
    }
}
fn main() {
    let cali = Person {
        이름: String::from("Cali"),
        나이: 24
    };
    println!("{}", cali.describe());
}

실행 결과:

Cali 24

impl Descriptive for Person 블록의 내용을 제거하면 실행 결과는 다음과 같습니다:

[Object]

특성 매개변수

대부분의 경우 함수를 매개변수로 전달해야 합니다. 예를 들어, 콜백 함수, 버튼 이벤트 설정 등입니다. Java에서는 함수는 인터페이스 구현의 클래스 예제로 전달되어야 하며, Rust에서는 특성 매개변수를 통해 전달할 수 있습니다:

fn output(object: impl Descriptive) {
    println!("{}", object.describe());
}

Descriptive 특성을 구현한 어떤 객체도 이 함수의 매개변수로 사용될 수 있습니다. 이 함수는传入된 객체가 다른 속성이나 메서드가 있는지 알 필요가 없으며, Descriptive 특성 규격의 메서드가 확실하게 있다면 충분합니다. 물론, 이 함수 내에서는 다른 속성과 메서드를 사용할 수 없습니다.

특성 파라미터는 이와 같은等效 문법으로도 구현할 수 있습니다:

fn output<T: Descriptive>(object: T) {
    println!("{}", object.describe());
}

이는 일반형과 유사한 스타일의 문법 연구로, 여러 파라미터 타입이 특성인 경우 매우 유용합니다:

fn output_two<T: Descriptive>(arg1: T, arg2: T) {
    println!("{}", arg1.describe());
    println!("{}", arg2.describe());
}

특성을 타입으로 사용할 때 여러 특성이涉及到 경우, + 기호로 표현하면 예를 들어:

fn notify(item: impl Summary + Display)
fn notify<T: Summary + Display>(item: T)

주의:타입을 나타내는 데만 사용되는 것은 impl 블록에서 사용할 수 있다는 것을 의미하지 않습니다.

단순한 구현 관계를 where 키워드로 간소화할 수 있습니다 예를 들어:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U)

다음과 같이 간단히 할 수 있습니다:

fn some_function<T, U>(t: T, u: U) -> i32
    where T: Display + Clone,
          U: Clone + Debug

이 문법을 이해하고 나면, 일반형 장에서의 "최대값 가져오기" 예제를 실제로 구현할 수 있습니다:

trait Comparable {
    fn compare(&self, object: &Self) -> i8;
}
fn max<T: Comparable>(array: &[T]) -> &T {
    let mut max_index = 0;
    let mut i = 1;
    while i < array.len() {
        if array[i].compare(&array[max_index]) > 0 {
            max_index = i;
        }
        i += 1;
    }
    &array[max_index]
}
impl Comparable for f64 {}}
    fn compare(&self, object: &f64) -> i8 {}}
        if &self > &object { 1 }
        else if &self == &object { 0 }
        else { -1 }
    }
}
fn main() {
    let arr = [1.0, 3.0, 5.0, 4.0, 2.0];
    println!("arr의 최대값은 {}", max(&arr));
}

실행 결과:

arr의 최대값은 5

팁: compare 함수의 두 번째 매개변수는 트레이트를 구현한 타입과 같아야 하므로, Self (대소문자를 구분합니다) 키워드는 현재 타입 자체를 나타냅니다 (예제가 아니라).

트레이트가 반환 값으로 사용

트레이트가 반환 값으로 사용할 형식은 다음과 같습니다:

fn person() -> impl Descriptive {
    Person {
        이름: String::from("Cali"),
        나이: 24
    }
}

하지만有一点, 트레이트가 반환 값으로 받을 수 있는 것은 트레이트를 구현한 객체만이며, 동일한 함수에서 모든 가능한 반환 값 타입이 완전히 동일해야 합니다. 예를 들어, 구조체 A와 구조체 B가 모두 트레이트를 구현하면 다음 함수는 잘못됩니다:

fn some_function(bool bl) -> impl Descriptive {
    if bl {
        return A {};
    } else {
        return B {};
    }
}

조건부 메서드 구현

impl 기능은 매우 강력하며, 우리는 그것을 사용하여 클래스 메서드를 구현할 수 있습니다. 하지만 제너릭 클래스에 대해서는, 때로는 그가 이미 구현한 메서드를 구분하여 다음에 구현해야 할 메서드를 결정해야 할 때가 있습니다:

struct A<T> {}
impl<T: B + C> A<T> {
    fn d(&self) {}
}

이 코드는 A<T> 타입이 T가 B와 C 트레이트를 구현한 후에 이 impl 블록을 유효하게 구현할 수 있도록 해야 합니다.