이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.
스마트 포인터는 Deref
트레이트와 Drop
트레이트를 구현하여 정의된다.
이 트레이트들의 특징을 각각 알아보도록 하자.
Deref
트레이트Deref
는 역참조 연산자 *
를 통해 대상을 참조할 수 있도록 한다.
역참조 연산자는 포인터가 가리키는 값을 참조하기 위해 사용하는 연산자다.
예를 들어, 다음과 같이 &
를 통해 참조한 값을 *
를 통해 역참조 할 수 있다.
peter@hp-laptop:~/rust-practice/chapter15$ cargo new dereference_example Created binary (application) `dereference_example` package
peter@hp-laptop:~/rust-practice/chapter15$ cd dereference_example/
peter@hp-laptop:~/rust-practice/chapter15/dereference_example$ vi src/main.rs
src/main.rs
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
peter@hp-laptop:~/rust-practice/chapter15/dereference_example$ cargo run Compiling dereference_example v0.1.0 (/home/peter/rust-practice/chapter15/dereference_example)
Finished dev [unoptimized + debuginfo] target(s) in 0.23s
Running `target/debug/dereference_example`
peter@hp-laptop:~/rust-practice/chapter15/dereference_example$
만약 우리가 assert_eq!(5, *y);
이 아니라 assert_eq!(5, y);
를 사용한다면
Rust 컴파일러는 비교할 수 없는 자료형끼리 비교했다며 패닉을 띄울 것이다.
우리는 &
연산자가 아닌 Box<T>
를 통해서도 대상을 참조할 수 있다.
peter@hp-laptop:~/rust-practice/chapter15/dereference_example$ vi src/main.rs
src/main.rs
fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
peter@hp-laptop:~/rust-practice/chapter15/dereference_example$ cargo run Compiling dereference_example v0.1.0 (/home/peter/rust-practice/chapter15/dereference_example)
Finished dev [unoptimized + debuginfo] target(s) in 0.19s
Running `target/debug/dereference_example`
peter@hp-laptop:~/rust-practice/chapter15/dereference_example$
이것을 가능하게 하는 것이 Deref
트레이트다.
Box<T>
와 유사한 기능을 하는 스마트 포인터를 구현해보며 Deref
트레이트에 대해 알아보자.
Box<T>
Box<T>
는 하나의 원소를 가지는 튜플 구조체다.
튜플 구조체에 대한 건 구조체를 다룰 때 설명한 바 있는데
각각의 필드가 식별자를 가지지 않는 구조체를 의미한다.
우리도 이와 같은 형태를 가진 유사 Box<T>
자료형인 MyBox<T>
를 만들어보자.
peter@hp-laptop:~/rust-practice/chapter15/dereference_example$ cd ..
peter@hp-laptop:~/rust-practice/chapter15$ cargo new my_box
Created binary (application) `my_box` package
peter@hp-laptop:~/rust-practice/chapter15$ cd my_box/
peter@hp-laptop:~/rust-practice/chapter15/my_box$ vi src/main.rs
src/main.rs
struct MyBox<T> (T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
물론 우린 아직 이 구조체에 Deref
트레이트를 구현하지 않았기 때문에
역참조를 하려고 하면 어떻게 해야 할지 몰라 오류가 나며 컴파일되지 않는다.
역참조를 사용하기 위해서는 Deref
트레이트의 deref
메서드를 구현해야 한다.
peter@hp-laptop:~/rust-practice/chapter15/my_box$ vi src/main.rs
src/main.rs
use std::ops::Deref; struct MyBox<T> (T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
peter@hp-laptop:~/rust-practice/chapter15/my_box$ cargo run
Compiling my_box v0.1.0 (/home/peter/rust-practice/chapter15/my_box)
Finished dev [unoptimized + debuginfo] target(s) in 0.19s
Running `target/debug/my_box`
peter@hp-laptop:~/rust-practice/chapter15/my_box$
여기서 *y
는 내부적으로 *(y.deref())
로 동작한다.
이로서 참조와 Deref
를 동일한 방법으로 사용할 수 있다.
역참조 강제는 Deref
트레이트를 구현하는 자료형을 보다 편하게 사용하도록
Rust에서 제공하는 기능이다.
이것은 Deref
트레이트를 구현하는 자료형의 참조를 연관 자료형 Target
의 참조로 변환한다.
이로써 Rust 개발자는 Deref
트레이트를 구현하는 자료형을 함수나 메서드에 전달할 때
명시적인 참조/역참조를 하지 않고 그대로 사용할 수 있다.
예를 들어, 다음과 같이 &str
의 자리에 MyBox<String>
을 넣을 수 있다.
peter@hp-laptop:~/rust-practice/chapter15/my_box$ cd ..
peter@hp-laptop:~/rust-practice/chapter15$ cargo new deref_coercion
Created binary (application) `deref_coercion` package
peter@hp-laptop:~/rust-practice/chapter15$ cp my_box/src/main.rs deref_coercion/src/main.rs
peter@hp-laptop:~/rust-practice/chapter15$ cd deref_coercion/
peter@hp-laptop:~/rust-practice/chapter15/deref_coercion$ vi src/main.rs
src/main.rs
use std::ops::Deref; struct MyBox<T> (T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } fn main() { let m = MyBox::new(String::from("Rust")); hello(&m); } fn hello(name: &str) { println!("Hello, {}", name); }
peter@hp-laptop:~/rust-practice/chapter15/deref_coercion$ cargo run
Compiling deref_coercion v0.1.0 (/home/peter/rust-practice/chapter15/deref_coercion)
Finished dev [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/deref_coercion`
Hello, Rust
peter@hp-laptop:~/rust-practice/chapter15/deref_coercion$
Rust 표준 라이브러리에String
을 문자열 슬라이스로 변환하는 Deref
트레이트가 구현되어 있다.
따라서 hello
함수에 전달된 &MyBox<String>
변수는 &String
변수로 변환되고
그것이 다시 &String
변수에서 &str
변수로 변환되어
이 코드는 아무 문제 없이 실행되는 것이다.
이러한 변환에 대한 분석은 컴파일 시간에 진행되어 실행 시간에 영향을 주지 않는다.
만약 Rust가 이런 기능을 제공하지 않았다면
스마트 포인터는 지금보다 매우 복잡하고 가독성이 떨어졌을 것이다.
Deref
는 불변 참조에 대한 역참조 연산자를 다루는 트레이트다.
가변 참조를 원한다면 DerefMut
트레이트를 사용할 수 있다.
역참조 강제는 다음과 같은 상황에 발생한다.
T: Deref<Target=U>
이며 &T
를 &U
로 변환하는 상황T: DerefMut<Target=U>
이며 &mut T
를 &mut U
로 변환하는 상황T: Deref<Target=U>
이며 &mut T
를 &U
로 변환하는 상황가변 참조를 불변 참조로 변환하는 것은 가능하지만 역은 성립하지 않음을 유의하자.
Drop
트레이트스마트 포인터가 구현해야 할 또 다른 트레이트인 Drop
은
값이 범위를 벗어날 때의 동작을 재정의하는 데 사용된다.
Rust는 값이 범위를 벗어나 소유권을 잃을 때 따로 명시해주거나 가비지 컬렉터를 돌릴 필요 없이
Drop
트레이트의 drop
메소드를 자동 호출해줌으로써 이에 대한 처리를 하게 해준다.
이 트레이트는 Prelude에 포함되어 있어 따로 가져올 필요는 없다.
값이 범위를 벗어나면 drop
메소드가 자동 호출됨을
간단한 예제를 통해 알아보도록 하자.
이 때, 자원의 해제는 의존성을 가진 데이터가 있을 수 있으므로 생성의 역순으로 진행된다.
peter@hp-laptop:~/rust-practice/chapter15/deref_coercion$ cd ..
peter@hp-laptop:~/rust-practice/chapter15$ cargo new drop_example
Created binary (application) `drop_example` package
peter@hp-laptop:~/rust-practice/chapter15$ cd drop_example/
peter@hp-laptop:~/rust-practice/chapter15/drop_example$ vi src/main.rs
src/main.rs
data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomStartPointer with data '{}'!", self.data); } } fn main() { let c = CustomSmartPointer { data: String::from("my stuff"), }; let d = CustomSmartPointer { data: String::from("other stuff"), }; println!("CustomSmartPointers created."); }
peter@hp-laptop:~/rust-practice/chapter15/drop_example$ cargo run
Compiling drop_example v0.1.0 (/home/peter/rust-practice/chapter15/drop_example)
# snip warnings
Finished dev [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/drop_example`
CustomSmartPointers created.
Dropping CustomStartPointer with data 'other stuff'!
Dropping CustomStartPointer with data 'my stuff'!
peter@hp-laptop:~/rust-practice/chapter15/drop_example$
때로는 어떤 값이 범위를 벗어나기 전에 해제하고 싶을 수 있다.
가령, 스마트 포인터를 통해 lock을 관리하는 상황이라던가 말이다.
이 때, Drop
트레이트의 drop
메서드를 호출하고자 한다면 컴파일 오류가 발생한다.
drop
메서드와 같은 소멸자는 직접 호출할 수 없다는 것이 오류 메시지의 내용이다.
drop
메서드는 범위를 벗어날 때 자동 호출되기 때문에
그것이 여러 번 호출어 같은 값을 여러 번 해제하려고 하는 것을 방지하기 위한 것이다.
우리는 이런 상황에서 Drop
트레이트의 drop
메서드가 아니라
std::mem::drop
함수를 사용할 수 있다.
이 함수의 인자로 해제할 값을 넣으면 되는데, Prelude에 존재하므로 drop
으로 충분하다.
peter@hp-laptop:~/rust-practice/chapter15/drop_example$ vi src/main.rs
src/main.rs
struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomStartPointer with data '{}'!", self.data); } } fn main() { let c = CustomSmartPointer { data: String::from("my stuff"), }; let d = CustomSmartPointer { data: String::from("other stuff"), }; println!("CustomSmartPointers created."); drop(c); println!("CustomSmartPointer dropped before the end of main."); }
peter@hp-laptop:~/rust-practice/chapter15/drop_example$ cargo run
Compiling drop_example v0.1.0 (/home/peter/rust-practice/chapter15/drop_example)
# snip warnings
Finished dev [unoptimized + debuginfo] target(s) in 0.23s
Running `target/debug/drop_example`
CustomSmartPointers created.
Dropping CustomStartPointer with data 'my stuff'!
CustomSmartPointer dropped before the end of main.
Dropping CustomStartPointer with data 'other stuff'!
peter@hp-laptop:~/rust-practice/chapter15/drop_example$
d
는 여전히 main
을 벗어나며 해제되지만 c
는 미리 해제된 것을 확인할 수 있다.
이 포스트의 내용은 공식문서의 15장 2절 Treating Smart Pointers Like Regular References with the Deref Trait & 15장 3절 Running Code on Cleanup with the Drop Trait에 해당합니다.