트레잇 (trait)

트레잇은 공통으로 사용할 수 있는 타입, 상수, 메서드를 정의합니다.

trait ExTrait {
    type TypeNoDefault;  //공통으로 사용할 수 있는 타입 정의. 구현측에서 타입지정 해야함

    const CONST_DEFAULT: i32 = 100;  //상수값 정의. 구현하는 측에서 재할당 할 수 있음
    const CONST_NO_DEFAULT: i32;  //상수 정의만 수행. 구현하는 측에서 값 할당 해야 함

    fn method_default(&self) {
        println!("default method");
    }  //함수의 본체까지 정의. 구현 측에서 다시 구현 할 수 있음
    fn method_without_default(&self);  //공통 메서드 정의. 구현하는 측에서 함수 바디 구현해야 함
}


트레잇 구현

trait Car {
    fn drive(&self);
}

struct Truck {}

impl Car for Truck{
    fn drive(&self) { println!("Truck is driving."); }
}
  • 사용은 이런 식으로 합니다. 어떻게 보면 인터페이스와 비슷하기도 합니다.


struct Point {
    x:i32, y:i32
}

impl std::ops::Add for Point {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        Self {
            x: self.x + rhs.x,
            y: self.y + rhs.y,
        }
    }
}

impl std::ops::Sub for Point {
    type Output = Self;

    fn sub(self, rhs: Self) -> Self::Output {
        Self {
            x: self.x - rhs.x,
            y: self.y - rhs.y,
        }
    }
}

impl Clone for Point {
    fn clone(&self) -> Self {
        Self { x: self.x.clone(), y: self.y.clone() }
    }
}

fn main(){
    let p1 = Point { x:2, y:3 };  let p2 = Point { x:4, y:5 };

    let p3 = p1.clone() + p2.clone();  
    let p4 = p1 - p2;
    println!("addition=({},{})",p3.x, p3.y);
    println!("subtraction=({},{})",p4.x, p4.y);
}
  • Add, Sub, Clone 트레잇을 구현하는 방법입니다.


트레잇 자동 구현

#[derive(Clone)]
struct Point {
    x:i32, y:i32
}

fn main(){
    let p1 = Point { x:2, y:3 };  let p2 = Point { x:4, y:5 };
    let p3 = p1.clone();
}
  • #derive 어트리뷰트를 이용하면 쉽게 트레잇을 구현할 수 있습니다.


#[derive(Clone, Copy)]
  • 여러 트레잇을 사용하는 경우는 이런 식으로 사용하면 됩니다.


자동 구현이 가능한 트레잇

  • 비교관련 트레잇들: Eq, PartialEq, Ord, PartialOrd.
  • 복제 : Clone
  • 항목값의 복사(이동이 아닌): Copy
  • 해시 계산: Hash
  • 어떤 타입에 대해 비어있는 인스턴스 생성: Default
  • {:?}를 사용한 String 생성: Debug


트레잇 바운드

fn print_distance<T: PointOp>(p:&T)  {
    println!("Distance={}", p.len());
}
  • 제네릭 타입을 정의하는 부분에 <T: PointOp>
  • 제네릭 타입 T인데, PointOp 트레잇을 구현한 것만이라는 의미합니다.
  • 이러한 것을 트레잇 바운드라고 부릅니다.
fn add_sub<T>(a:T, b:T) -> (T,T) 
    where T: std::ops::Add<Output = T> + std::ops::Sub<Output = T> + Copy
{    
    return (a+b, a-b);
}
  • where 키워드를 이용하면 가독성이 더 좋아집니다.


트레잇 연관타입

type Item;
type Output = Self;
  • 트레잇 안에 정의된 타입을 말합니다.
  • 트레이트 정의 시점에는 알 수 없기에 "나중에 구현할 때 정해줘" 라는 의미로 type Item;이라는 자리표시자를 만들어둡니다.


정적 바인딩

struct Human;
struct Dog;

trait Moving {
    fn run(&self);
}

impl Moving for Human{
    fn run(&self) { println!("A human is running."); }
}

impl Moving for Dog {
    fn run(&self) { println!("A dog is running")}
}

fn run(x: impl Moving) { // 정적 바인딩!
    x.run();
}
/* 제네릭으로 구현한다면 이런 형태
fn run<T: Moving>(x: T) {
    x.run();
}
*/

fn main() {
    let tom = Human;
    let hodu = Dog;

    run(tom);
    run(hodu);
}
  • impl Trait 기법으로 정적 바인딩
  • 함수의 파라미터, 함수의 리턴에서 사용 가능합니다.
fn who_moved_there() -> impl Moving {
    let jeff = Human;
    return jeff;
}
  • 이건 함수의 리턴에서 사용하는 모습입니다.


동적 바인딩

fn find_runner(is_human: bool) -> Box<dyn Moving> {
    if is_human {
        Box::new(Human)
    }else {
        Box::new(Dog)
    }
}
  • 함수의 리턴 타입에 대해 어떤 트레잇을 구현한 여러 객체가 가능하도록 하려면 Box<dyn Trait> 형태로 작성해야 합니다.
let x = find_runner(true);
run(x);  // 에러
  • 주의사항이 하나 있습니다.
  • 파라미터로 Moving을 받을 수는 있으나, 현재 x의 타입인 Box<dyn Moving>을 받을 수가 없습니다.
    • x.run() 처럼 Moving 트레잇의 메서드를 직접 호출해야 합니다.




제네릭 (Generic)

구조체, 열거형, 함수, 메서드에서 하나의 코드로 여러 타입을 처리할 수 있게 도와줍니다.


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

제네릭 타입은 꺽쇠 안에 있는 어떤 문자로 표시합니다.

  • 주로 한 개의 대문자 알파벳으로 표현
  • 이 문자는 그냥 동일한 타입임을 나타내는 구분자입니다.


fn main() {
    let p1 = Point { x: 2, y: 3 };
    let p2 = Point { x: 2.5, y: 3.5 };

    println!("p1=({},{})",p1.x,p1.y); //p1=(2,3)
    println!("p2=({},{})",p2.x,p2.y); //p2=(2.5,3.5)
}
  • 예시입니다. 정수만이 아닌 실수도 사용 가능합니다.


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

impl<T> Point<T> {
    fn add(&self, rhs:&Point<T>) -> Point<T> 
        where T: std::ops::Add<Output = T> + Copy
    {
        let x_val = self.x + rhs.x;
        let y_val = self.y + rhs.y;
        return Point {x:x_val, y:y_val};
    }
}
  • 구조체에서의 예시입니다.


use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,   //T 타입
        Err(error) => {   //E 타입
            panic!("There was a problem opening the file: {:?}", error)
        },
    };
}
  • 열거형에서의 예시입니다.


라이프타임

fn main(){
    let r;  // (1)
    {
        let x = 5;  // (2)
        r = &x;  // (3)
    }
    println!("r: {}", r);  // (4)
}
  • (4)에서 에러가 발생하게 됩니다.
    • 수명이 다한 변수를 참조하는 상황!
    • 이를 '댕글링 참조', 혹은 '댕글링 포인터'라고 합니다.


fn main(){
    let x = String::from("123");     // (1)
    let y = String::from("45678");
    let s = longest(x.as_str(), y.as_str());  // (2)
    println!("longest is : {}",s);
}

fn longest(s1:&str, s2:&str) -> &str {    // error: 라이프타임 파라미터가 필요
    if s1.len() > s2.len() { s1 }     
    else { s2 }
}
  • 이런 상황에서 라이프타임을 지정해야합니다.
  • 컴파일 시점에서 s1, s2 중에서 뭐가 리턴될지 모르기 때문에 발생하는 문제입니다.
fn longest<'a>(s1:&'a str, s2:&'a str) -> &'a str {    
    if s1.len() > s2.len() { s1 }     
    else { s2 }
} 
  • 이런 식으로 수동으로 라이프타임을 지정하면 에러가 해결됩니다.


&'a i32
&'a mut i32

&'a str
  • 이프타임 매개변수의 이름을 어퍼스트로피(')로 시작한다는 점만 따릅니다.
    • 일반 제네릭 변수와 구별하기 위함입니다.




스마트 포인터

스마트 포인터의 구조체는 DerefDrop 트레잇을 구현합니다.

  • Deref*x와 같이 스마트 포인터 객체에 *가 붙여졌을 때의 동작을 정의
  • Drop은 스마트 포인터 객체가 스코프를 벗어날 때 자동으로 수행 (스마트 포인터 객체가 사용하고 있는 힙 메모리를 해제하기 위함)


암묵적 "강제 역참조"

함수나 메서드의 파라미터로 참조형 변수가 전달되면 "강제로 deref가 수행"됩니다.

fn print_array(arr:&[i32]) {
    println!("{:?}", arr);
}

fn main(){
    let v = vec![1,2,3];
    print_array(&v);    // [1, 2, 3]
}
  • 강제 역참조가 일어나서 &Vec<i32>&[i32]로 변경됩니다.