이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.
이번 시간에는 트레이트, 자료형, 함수와 클로저에 대한 고급 기술을 살펴보도록 하겠다.
우리는 오래 전 트레이트에 대해 공부한 바가 있다.
그 땐 우리가 아직 Rust의 많은 기능을 다루지 못하는 상태였기에
트레이트에 대한 고급 기술은 설명하지 않고 넘어갔는데 이제 그 이야기를 해도 될 것 같다.
우리가 전에 간단히 설명하고 이후에 더 자세히 알아보자고 한 것 중에
연관 자료형이라는 녀석이 있었다.
연관 자료형이란 트레이트를 구현할 때 어떤 자료형을 사용하여 구현하든 상관 없지만
얘네들은 다 같은 자료형이어야 한다, 하고 적어두는 자료형 자리지정자를 의미한다.
type
이라는 키워드와 함께 사용되며
트레이트를 구현할 때 이 연관 자료형이 실제로는 어떤 구체적인 자료형으로 구현되는지 명시해야 한다.
이로써 트레이트를 작성할 땐
그것을 구현하는 사람이 어떤 자료형을 사용할지는 신경쓰지 않고 작성할 수 있다.
예를 들어, 우리는 트레이트에 대해 다룰 때
Iterator
트레이트는 다음과 같이 선언되어 있다는 것을 배웠다.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// methods with default implementations elided
}
여기서 type Item;
이 연관 자료형을 선언하는 부분이다.
그리고 이것을 구현할 땐 다음과 같이 작성할 수 있는 것을 배웠다.
// snip
impl Iterator for Counter {
type Item = u32;
// snip
}
// snip
만약 우리가 "임의의 자료형이면 제네릭을 써도 되는 거 아냐?" 라며 제네릭을 사용했다면
우리는 next
메서드를 호출할 때마다 그 자료형을 명시해야 했을 것이다.
하지만 연관 자료형은 트레이트를 구현할 때 구체적인 자료형이 정해지기에
자료형을 따로 명시해주지 않고 next
만으로 호출할 수 있다.
우리는 제네릭 자료형을 사용할 때 <자리지정자자료형>
와 같은 표기를 한다.
자리지정자 자료형은 주로 T
와 같은 짧은 이름으로 짓고 말이다.
제네릭을 배울 때 따로 언급하지 않았지만 우리는
제네릭의 실제 자료형을 명시해주지 않았을 경우에 default 자료형을 사용하도록 할 수 있다.
이를 위해 필요한 게 기본 제네릭 자료형 매개변수다.
이것은 <자리지정자자료형=실제자료형>
형태로 사용된다.
기본 제네릭 자료형 매개변수는 연산자 오버로딩에 유용하게 사용된다.
Rust는 기본적으로 사용자 정의 연산자를 지원하지 않지만
std::ops
모듈의 연산자와 관련된 트레이트를 구현하면 연산자를 오버로딩 할 수 있다.
예를 들어, 다음과 같은 코드의 작성이 가능하다.
이것은 두 구조체 변수의 덧셈을 지원하기 위해 Add
트레이트를 구현한 것이다.
peter@hp-laptop:~/rust-practice/chapter19$ cargo new overload_add
Created binary (application) `overload_add` package
peter@hp-laptop:~/rust-practice/chapter19$ cd overload_add/
peter@hp-laptop:~/rust-practice/chapter19/overload_add$ vi src/main.rs
src/main.rs
use std::ops::Add; #[derive(Debug, PartialEq)] struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y, } } } fn main() { assert_eq!( Point { x: 1, y: 0 } + Point { x: 2, y: 3 }, Point { x: 3, y: 3 } ); }
peter@hp-laptop:~/rust-practice/chapter19/overload_add$ cargo run
Compiling overload_add v0.1.0 (/home/peter/rust-practice/chapter19/overload_add)
Finished dev [unoptimized + debuginfo] target(s) in 0.24s
Running `target/debug/overload_add`
peter@hp-laptop:~/rust-practice/chapter19/overload_add$
여기서, 우리가 구현한 Add
트레이트는 다음과 같이 선언되어 있다.
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
Rhs
라는 제네릭 자료형을 사용하고 있으며,
따로 명시해주지 않을 경우 이것을 구현한 자료형이 자동으로 들어간다.
우리는 Point
구조체에서 이 트레이트를 구현했으며 덧셈의 대상 또한 Point
인스턴스이므로
default 자료형을 그대로 사용할 수 있다.
반면, 덧셈의 대상이 다른 자료형일 경우 트레이트를 구현할 때 이를 명시해주어야 한다.
예를 들어 다음과 같은 경우가 그렇다.
peter@hp-laptop:~/rust-practice/chapter19/overload_add$ cd ..
peter@hp-laptop:~/rust-practice/chapter19$ cargo new overload_add_2 --lib
Created library `overload_add_2` package
peter@hp-laptop:~/rust-practice/chapter19$ cd overload_add_2/
peter@hp-laptop:~/rust-practice/chapter19/overload_add_2$ vi src/lib.rs
src/lib.rs
struct Millimeters(u32); struct Meters(u32); impl Add<Meters> for Millimeters { type Output = Millimeters; fn add(self, other: Meters) -> Millimeters { Millimeters(self.0 + other.0 * 1000) } }
기본 제네릭 자료형 매개변수는
기존 코드와의 호환성을 유지하면서 자료형을 확장할 때
기존 코드의 자료형을 default로 두고 다른 자료형은 제네릭으로 명시하게 하는 용도로 사용되거나
대부분의 사용자가 사용하는 방식이 있고 특별한 경우에 커스터마이징이 필요할 때
대부분이 사용하는 방식을 default로 두고 특별한 경우 제네릭으로 명시하게 하는 용도로 사용된다.
위의 Add
예제의 경우 후자에 해당한다.
여러 개의 트레이트를 구현하게 될 경우
때로는 서로 다른 트레이트가 같은 이름의 메서드를 사용하는 것을 마주칠 때가 있다.
구현할 땐 각자 구현했는데 호출하려고 하니
이 녀석이 구현하고 있는 두 개의 트레이트 중 어느 쪽의 메서드인지 알 수 없는 문제가 생긴다.
심지어 트레이트 메서드가 아닌 자료형 자체 메서드 중에도 그 이름이 있다면?
자료형 자체 메서드와 트레이트 메서드의 이름이 겹칠 경우에는
전자가 호출되도록 되어 있다.
예를 들어, 다음과 같이 작성할 경우 Human
의 메서드를 호출한다.
peter@hp-laptop:~/rust-practice/chapter19/overload_add_2$ cd ..
peter@hp-laptop:~/rust-practice/chapter19$ cargo new fly_methods
Created binary (application) `fly_methods` package
peter@hp-laptop:~/rust-practice/chapter19$ cd fly_methods/
peter@hp-laptop:~/rust-practice/chapter19/fly_methods$ vi src/main.rs
src/main.rs
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your catain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() { let person = Human; person.fly(); }
peter@hp-laptop:~/rust-practice/chapter19/fly_methods$ cargo run
Compiling fly_methods v0.1.0 (/home/peter/rust-practice/chapter19/fly_methods)
Finished dev [unoptimized + debuginfo] target(s) in 0.18s
Running `target/debug/fly_methods`
*waving arms furiously*
peter@hp-laptop:~/rust-practice/chapter19/fly_methods$
그런데 만약 트레이트의 메서드를 호출하고 싶다면 어떻게 해야 할까?
우리는 메서드 앞에 트레이트 이름을 명시하고 그 인자로 구조체 인자를 전달함으로써
어떤 메서드를 호출하는지 명확히 할 수 있다.
peter@hp-laptop:~/rust-practice/chapter19/fly_methods$ vi src/main.rs
src/main.rs
// snip fn main() { let person = Human; Pilot::fly(&person); Wizard::fly(&person); person.fly(); }
peter@hp-laptop:~/rust-practice/chapter19/fly_methods$ cargo run
Compiling fly_methods v0.1.0 (/home/peter/rust-practice/chapter19/fly_methods)
Finished dev [unoptimized + debuginfo] target(s) in 0.18s
Running `target/debug/fly_methods`
This is your catain speaking.
Up!
*waving arms furiously*
peter@hp-laptop:~/rust-practice/chapter19/fly_methods$
물론 Human
의 메서드를 호출할 때도 Human::fly(&person);
으로 호출할 수도 있지만
어떤 메서드인지 명백한 상황에서는 굳이 이렇게 작성할 필요는 없다.
그런데 만약 구현할 트레이트의 연관함수와 구조체 자체 연관함수가 이름이 같다면 어떨까?
연관함수의 경우 self
를 인자로 가지고 있지 않아 누가 호출하는지 명시해줄 수 없다.
일단 이 녀석도 따로 명시해주지 않으면 구조체 자체 연관함수가 호출된다는 것을 확인할 수 있다.
peter@hp-laptop:~/rust-practice/chapter19/fly_methods$ cd ..
peter@hp-laptop:~/rust-practice/chapter19$ cargo new assoc_func
Created binary (application) `assoc_func` package
peter@hp-laptop:~/rust-practice/chapter19$ cd assoc_func/
peter@hp-laptop:~/rust-practice/chapter19/assoc_func$ vi src/main.rs
src/main.rs
trait Animal { fn baby_name() -> String; } struct Dog; impl Dog { fn baby_name() -> String { String::from("Spot") } } impl Animal for Dog { fn baby_name() -> String { String::from("puppy") } } fn main() { println!("A baby dog is called a {}", Dog::baby_name()); }
peter@hp-laptop:~/rust-practice/chapter19/assoc_func$ cargo run
Compiling assoc_func v0.1.0 (/home/peter/rust-practice/chapter19/assoc_func)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/assoc_func`
A baby dog is called a Spot
peter@hp-laptop:~/rust-practice/chapter19/assoc_func$
그런데 만약 우리가 원했던 게 Animal
의 메서드였다면 어떻게 해야 할까?
메서드에서처럼 Animal::baby_name();
이라고 하기엔 누가 호출하는지 알 수가 없다.
따라서 이런 경우에는 Dog
를 Animal
처럼 사용하겠다고 명시하여 호출해야 한다.
<자료형 as 트레이트>::연관함수
와 같은 형태로 작성할 수 있다.
우리는 그런 형태를 정규화된 구문fully qualified syntax이라고 한다.
peter@hp-laptop:~/rust-practice/chapter19/assoc_func$ vi src/main.rs
src/main.rs
// snip fn main() { println!("A baby dog is called a {}", <Dog as Animal>::baby_name()); }
peter@hp-laptop:~/rust-practice/chapter19/assoc_func$ cargo run
Compiling assoc_func v0.1.0 (/home/peter/rust-practice/chapter19/assoc_func)
# snip warning
Finished dev [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/assoc_func`
A baby dog is called a puppy
peter@hp-laptop:~/rust-practice/chapter19/assoc_func$
경우에 따라 어떤 트레이트에서 다른 트레이트의 기능을 사용해야 하는 경우도 있다.
이 때 그 의존성을 가진 '다른 트레이트'도 구현해주어야 한다.
우리는 실제 구현하고자 하는 트레이트를 의존성 트레이트의 수퍼 트레이트라고 칭한다.
예를 들어, 우리가 어떤 값을 출력할 때 *
로 테두리를 만들어 꾸며주는 트레이트를 만든다고 하자.
이것은 Outline_Print
트레이트에 outline_print
라는 이름의 메드로 선언될 것이다.
그런데 우리가 이 녀석을 구현할 때 Display
트레이트의 기능을 이용하고자 한다면
우리의 Outline_Print
트레이트가 Display
트레이트의 수퍼 트레이트가 된다.
이로서 우리는 Outline_Print
트레이트는 Display
트레이트가 구현된 경우에만
추가적으로 같이 구현할 수 있다는 것을 나타낼 수 있다.
우리는 트레이트를 선언할 때 trait 수퍼트레이트: 의존성트레이트
와 같이 선언함으로써
이 수퍼 트레이트의 의존성 트레이트를 명시할 수 있다.
peter@hp-laptop:~/rust-practice/chapter19/assoc_func$ cd ..
peter@hp-laptop:~/rust-practice/chapter19$ cargo new outline_print
Created binary (application) `outline_print` package
peter@hp-laptop:~/rust-practice/chapter19$ cd outline_print/
peter@hp-laptop:~/rust-practice/chapter19/outline_print$ vi src/main.rs
src/main.rs
use std::fmt; trait OutlinePrint: fmt::Display { fn outline_print(&self) { let output = self.to_string(); let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {} *", output); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); } } struct Point { x: i32, y: i32, } impl OutlinePrint for Point {} impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } fn main() { let p = Point { x: 1, y: 3, }; p.outline_print(); }
peter@hp-laptop:~/rust-practice/chapter19/outline_print$ cargo run
Compiling outline_print v0.1.0 (/home/peter/rust-practice/chapter19/outline_print)
Finished dev [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/outline_print`
**********
* *
* (1, 3) *
* *
**********
peter@hp-laptop:~/rust-practice/chapter19/outline_print$
만약 위 코드에서 Display
트레이트를 구현하는 부분을 생략한다면
그것이 구현되지 않아 사용할 수 없다는 에러가 발생하며 컴파일을 거부할 것이다.
우리는 트레이트에 대해 배우면서
트레이트 또는 자료형 둘 중 하나는 로컬에 있어야 그것을 구현할 수 있다고 했다.
그런데 이걸 우회하는 방법이 있긴 하다.
newtype 패턴을 이용하면 외부 자료형에 외부 트레이트를 구현할 수 있다.
newtype 패턴은 하나의 필드를 갖는 튜플 구조체에 새로운 자료형을 생성하는 방식이다.
이 하나의 필드에 우리가 사용하려는 외부 자료형을 넣고
우리의 튜플 구조체는 일종의 wrapper로서 작용하도록 한다.
그리고 이 wrapper 자료형은 로컬 자료형이므로 이제 여기에 외부 트레이트를 구현할 수 있다.
외부 자료형과 외부 트레이트를 사용하여 새 로컬 자료형을 생성하는 것이다.
예를 들어, wrapper로 감싼 Vec<String>
에 Display
를 구현하는 예제를 살펴보자.
peter@hp-laptop:~/rust-practice/chapter19/outline_print$ cd ..
peter@hp-laptop:~/rust-practice/chapter19$ cargo new newtype_wrapper
Created binary (application) `newtype_wrapper` package
peter@hp-laptop:~/rust-practice/chapter19$ cd newtype_wrapper/
peter@hp-laptop:~/rust-practice/chapter19/newtype_wrapper$ vi src/main.rs
src/main.rs
use std::fmt; struct Wrapper(Vec<String>); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } } fn main() { let w = Wrapper(vec![String::from("hello"), String::from("world")]); println!("w = {}", w); }
peter@hp-laptop:~/rust-practice/chapter19/newtype_wrapper$ cargo run
Compiling newtype_wrapper v0.1.0 (/home/peter/rust-practice/chapter19/newtype_wrapper)
Finished dev [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/newtype_wrapper`
w = [hello, world]
peter@hp-laptop:~/rust-practice/chapter19/newtype_wrapper$
외부 자료형인 Vector
에 외부 트레이트인 Display
가 구현되어
벡터 안에 있는 자료들이 ,
로 구분되어 출력된 것을 확인할 수 있다.
다만 이 기법의 단점은 내부 자료형의 메서드를 직접 사용할 수 없고
직접 사용하기 위해서는 그것을 외부에서 사용할 수 있도록
wrapper 자료형에 다시 구현해주어야 한다.
이것은 상당히 번거로운 작업이 될 수 있다.
내용이 많이 길어진 것 같으니 자료형, 함수와 클로저에 대한 고급 기술은 다음에 알아보도록 하겠다.
이 포스트의 내용은 공식문서의 19장 2절 Advanced Traits에 해당합니다.