트레이트와 제네릭

트레이트

서로 다른 타입 사이의 공통 종작을 정의

다른 언어의 인터페이스와 같지만 제네릭과 연계하여 런타임 오버헤드 없이 재사용할 수 있고 술어논리의 형식으로 선언적 프로그래밍이 가능하다.

마커 트레이트

메서드가 없는 트레이트. 단순히 타입 바운드의 용도로 사용된다.

예를 들어 Copy는 메모리를 비트단위 복사한 것이 올바른 복사본이라는 제약조건을 나타내는데 사용된다.

std::marker에서 확인할 수 있다.

표준 트레이트

주요한 트레이트는 대부분 derive 매크로로 자동 구현이 가능하다.

Clone

어느 타입이 Clone 트레이트를 구현했다는것은clone()메서드를 호출해 복사본을 얻을수 있다는것을 나타낸다.

모든 필드가 Clone을 구현하고 있으면 derive로 간단히 자동 구현이 가능하다.

Copy

비트단위 복사한 것이 올바른 복사본이라는 것을 나타내는 마커 트레이트이다.

Copy를 구현한타입은 이동의미론이 아닌 복사의미론이 적용된다.

i64 등 기본 테이터 타입은 Copy이다.

Clone을 구현한 타입만이 Copy 마커를 달 수 있는데 비트단위복사가 가능한 작은 객체에 사용된다는 점에서 직접구형할 필요 없이 둘을 함께 derive문에 적으면 된다.

책에서는 컴파일러가 Copy 객체에 대해 clone 메서드를 호출하는것이 비트단위 복사로 최적화가 안되는 것처럼 적어놓았지만 실제로는 최적화가 이루어진다. 다만 코딩 컨벤션 등의 이유로 Copy객체에 대해 직접 clone 메서드를 호출하는것은 하지 않는것이 좋다.

Drop

메모리에서 벗어날 때 수행할 작업지정 - 소멸자처럼 메모리 해제될때의 작업을 정의한거

copy랑은 같이 쓸 수 없음, clone과는 같이 사용가능

Default

default()메서드로 디폴트 객체를 생성할 수 있다.

필드가 모두 디폴트 트레이트를 구현하면 derive로 자동구현이 가능하다.

PartialEq/Eq

PartialEq는 부분 동치 비교할 수 있음을 나타낸다.

부분동치란 아래 1,2는 성립하지만 3은 성립하지 않는 동치이다.

  1. a==b이면 b==a이다
  2. a==b이고 b==c이면 a==c이다
  3. 모든 a에 대해 a==a이다

부동소수점 타입에서 NaN==NaN이 성립하지 않기 때문에 3을 만족하지 못한다.

러스트는 이러한 이유로 부분동치와 동치를 구분한다.

대부분의 타입은 PartialEq와 Eq를 모두 만족한다.

PartialOrd/Ord

>, >= 등 크기 비교를 할 수 있음을 나타낸다.

PartialOrd를 구현하려면 PartialEq가 구현된 타입만이 가능하다

Ord를 구현하려면 Eq를 구현해야 한다.

Hash

해당 타입의 객체가 해싱 가능함을 나타낸다.

Eq를 기준으로 동일한 항목은 해시값도 항상 같아야 한다.

이 외에도 Error, Drop, From/Into, Iterator, Send/Sync, Fn* 등이 있다.

위 트레이트는 derive로 자동구현이 가능하다.

연산자 오버로드

러스트에서 연산자는 연산자 트레이트의 메서드를 호출하는 것과 동일하다.

Add(+), AddAssign(+=), BitXor(^), Shr(>>) 등 해당 트레이트를 구현하면 해당 연산자를 그 타입에 사용할 수 있게된다.

제네릭

러스트의 제네릭은 타입시스템이 적용된 C++템플릿으로 이해하면 쉽다.

임의 타입에 대해 코드를 작성하면 컴파일타임에 컴파일러가 구체적인 각 타입에대해 각각의 코드를 만들어 낸다.

러스트의 제네릭은 임의의 타입에대해 타입바운드라는 제약조건을 설정할 수 있다.

fn f<T: Hash>(hashable: T)

여기서 T타입에 올 수 있는 타입은 Hash를 구현한 타입으로 제한된다. 따라서 이 함수를 사용하는 곳에서 Hash를 구현하지 않은 타입을 인자로 넣을경우 컴파일 에러를 낸다.

트레이트 객체

트레이트 객체는 팻포인터의 일종이다.

let a: &dyn A;

여기서 a는 A트레이트를 구현한 임의의 타입 객체를 가리키는 포인터와 해당 그 타입이 구현한 A트레이트의 메서드 테이블의 포인터로 이루어진다.

C++에서는 객체에 vtable을 두는식으로 구현되지만 러스트에서는 객체 또한 포인터로 가리켜진다. 

이러한 방식 덕분에 러스트는 외부 라이브러리 객체에 추가적인 트레이트를 구현하는 방식이 가능해진다.

트레이트 객체를 사용하려면 두 규칙을 만족해야 한다.

  1. 트레이트 메서드는 제네릭이 아니어야한다.
  2. 트레이트 메서드는 Self를 사용하는 타입과 관련 없어야한다.(컴파일 타임에 Self의 크기를 알 수 없으므로)

제네릭 VS 트레이트 객체

제네릭과 트레이트 객체모두 트레이트 바운드를 기반으로 타입을 변수화한다.

둘의 차이는 다음과 같다.

  • 제네릭은 코드가 비대해진다.
  • 트레이트객체는 런타임 오버헤드가 있다.
  • 제네릭은 여러 트레이트 바운드를 조합할 수 있지만 트레이트 객체는 하나의 트레이트 바운드만 가능하다.(트레이트 객체도 해단 바운드들을 적용한 별도의 트레이트를 만드는 식으로 사용하면 가능하기는 하다)
  • 트레이트 객체는 타입 정보가 소실되므로 이종객체 컬렉션이 가능해진다.
    Vec<T: A>로 적어도 A를 구현하는 타입 둘을 같은 벡터에 담을 수 없다. 제네릭은 컴파일타임에 구체적인 타입이 정해지므로 Vec<b>, Vec<c>가 되기 때문이다.
    하지만 Vec<&dyn A>는 그자체로 팻 포인터이다. 따라서 b와c를 같은 벡터에 담을 수 있다. 다만 이 경우 둘을 구분할 수 없으며(런타임 타입 정보를 필드에 두지 않는한) A트레이트의 메서드만 사용 가능하다.