소개
Deref 를 사용하여 스마트 포인터를 일반 참조처럼 취급하기에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
이 랩에서는 Deref 트레이트를 구현하여 스마트 포인터를 일반 참조처럼 취급하는 방법과 Rust 의 deref 강제 변환 기능을 통해 참조 또는 스마트 포인터로 작업하는 방법을 살펴봅니다.
Deref 를 사용하여 스마트 포인터를 일반 참조처럼 취급하기에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
이 랩에서는 Deref 트레이트를 구현하여 스마트 포인터를 일반 참조처럼 취급하는 방법과 Rust 의 deref 강제 변환 기능을 통해 참조 또는 스마트 포인터로 작업하는 방법을 살펴봅니다.
Deref 트레이트를 구현하면 역참조 연산자 * (곱셈 또는 glob 연산자와 혼동하지 마십시오) 의 동작을 사용자 정의할 수 있습니다. 스마트 포인터를 일반 참조처럼 취급할 수 있도록 Deref를 구현하면 참조에서 작동하는 코드를 작성하고 해당 코드를 스마트 포인터와 함께 사용할 수도 있습니다.
먼저 역참조 연산자가 일반 참조에서 어떻게 작동하는지 살펴보겠습니다. 그런 다음 Box<T>처럼 동작하는 사용자 정의 타입을 정의하고, 역참조 연산자가 새로 정의한 타입에서 참조처럼 작동하지 않는 이유를 살펴보겠습니다. Deref 트레이트를 구현하면 스마트 포인터가 참조와 유사한 방식으로 작동할 수 있게 되는 방법을 탐구할 것입니다. 그런 다음 Rust 의 deref 강제 변환 기능과 이를 통해 참조 또는 스마트 포인터로 작업할 수 있는 방법을 살펴보겠습니다.
참고: 곧 만들
MyBox<T>타입과 실제Box<T>사이에는 한 가지 큰 차이점이 있습니다. 즉, 저희 버전은 데이터를 힙에 저장하지 않습니다. 이 예제는Deref에 초점을 맞추고 있으므로 데이터가 실제로 저장되는 위치는 포인터와 유사한 동작보다 덜 중요합니다.
일반 참조는 일종의 포인터이며, 포인터를 생각하는 한 가지 방법은 어딘가에 저장된 값을 가리키는 화살표로 생각하는 것입니다. Listing 15-6 에서 i32 값에 대한 참조를 생성한 다음 역참조 연산자를 사용하여 참조를 따라 값을 찾습니다.
파일 이름: src/main.rs
fn main() {
1 let x = 5;
2 let y = &x;
3 assert_eq!(5, x);
4 assert_eq!(5, *y);
}
Listing 15-6: 역참조 연산자를 사용하여 i32 값에 대한 참조를 따라가기
변수 x는 i32 값 5를 저장합니다 [1]. y를 x에 대한 참조와 같게 설정합니다 [2]. x가 5와 같다고 단언할 수 있습니다 [3]. 그러나 y의 값에 대해 단언하려면 *y를 사용하여 참조가 가리키는 값을 따라가야 합니다 (따라서 역참조) 컴파일러가 실제 값을 비교할 수 있도록 합니다 [4]. y를 역참조하면 y가 가리키는 정수 값에 접근하여 5와 비교할 수 있습니다.
대신 assert_eq!(5, y);를 작성하려고 하면 다음과 같은 컴파일 오류가 발생합니다.
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} ==
&{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented
for `{integer}`
숫자와 숫자에 대한 참조를 비교하는 것은 허용되지 않습니다. 왜냐하면 서로 다른 타입이기 때문입니다. 역참조 연산자를 사용하여 참조가 가리키는 값을 따라가야 합니다.
<T>를 참조처럼 사용하기Listing 15-6 의 코드를 참조 대신 Box<T>를 사용하도록 다시 작성할 수 있습니다. Listing 15-7 에서 Box<T>에 사용된 역참조 연산자는 Listing 15-6 에서 참조에 사용된 역참조 연산자와 동일한 방식으로 작동합니다.
파일 이름: src/main.rs
fn main() {
let x = 5;
1 let y = Box::new(x);
assert_eq!(5, x);
2 assert_eq!(5, *y);
}
Listing 15-7: Box<i32>에서 역참조 연산자 사용하기
Listing 15-7 과 Listing 15-6 의 주요 차이점은 여기서는 y를 x의 값을 가리키는 참조가 아닌 x의 복사된 값을 가리키는 박스의 인스턴스로 설정한다는 것입니다 [1]. 마지막 단언 [2]에서 y가 참조였을 때와 마찬가지로 역참조 연산자를 사용하여 박스의 포인터를 따라갈 수 있습니다. 다음으로, 자체 박스 타입을 정의하여 역참조 연산자를 사용할 수 있게 해주는 Box<T>의 특별한 점을 살펴보겠습니다.
기본적으로 스마트 포인터가 참조와 어떻게 다르게 동작하는지 경험하기 위해 표준 라이브러리에서 제공하는 Box<T> 타입과 유사한 스마트 포인터를 만들어 보겠습니다. 그런 다음 역참조 연산자를 사용할 수 있는 기능을 추가하는 방법을 살펴보겠습니다.
Box<T> 타입은 궁극적으로 하나의 요소를 가진 튜플 구조체로 정의되므로 Listing 15-8 은 동일한 방식으로 MyBox<T> 타입을 정의합니다. 또한 Box<T>에 정의된 new 함수와 일치하는 new 함수도 정의합니다.
파일 이름: src/main.rs
1 struct MyBox<T>(T);
impl<T> MyBox<T> {
2 fn new(x: T) -> MyBox<T> {
3 MyBox(x)
}
}
Listing 15-8: MyBox<T> 타입 정의하기
MyBox라는 구조체를 정의하고 제네릭 매개변수 T를 선언합니다 [1]. 이는 모든 타입의 값을 저장하려는 것이기 때문입니다. MyBox 타입은 T 타입의 단일 요소를 가진 튜플 구조체입니다. MyBox::new 함수는 T 타입의 매개변수 하나를 받아서 [2] 전달된 값을 저장하는 MyBox 인스턴스를 반환합니다 [3].
Listing 15-7 의 main 함수를 Listing 15-8 에 추가하고 Box<T> 대신 정의한 MyBox<T> 타입을 사용하도록 변경해 보겠습니다. Listing 15-9 의 코드는 Rust 가 MyBox를 역참조하는 방법을 모르기 때문에 컴파일되지 않습니다.
파일 이름: src/main.rs
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Listing 15-9: 참조 및 Box<T>를 사용했던 방식과 동일하게 MyBox<T>를 사용하려는 시도
다음은 결과 컴파일 오류입니다.
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
MyBox<T> 타입은 해당 기능을 타입에 구현하지 않았기 때문에 역참조할 수 없습니다. * 연산자를 사용하여 역참조를 활성화하려면 Deref 트레이트를 구현합니다.
"트레이트를 타입에 구현하기"에서 논의했듯이, 트레이트를 구현하려면 트레이트의 필수 메서드에 대한 구현을 제공해야 합니다. 표준 라이브러리에서 제공하는 Deref 트레이트는 self를 빌리고 내부 데이터에 대한 참조를 반환하는 deref라는 메서드 하나를 구현하도록 요구합니다. Listing 15-10 은 MyBox``<T> 정의에 추가할 Deref의 구현을 포함합니다.
파일 이름: src/main.rs
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
1 type Target = T;
fn deref(&self) -> &Self::Target {
2 &self.0
}
}
Listing 15-10: MyBox<T>에 Deref 구현하기
type Target = T; 구문 [1]은 Deref 트레이트가 사용할 연관 타입을 정의합니다. 연관 타입은 제네릭 매개변수를 선언하는 약간 다른 방법이지만, 지금은 걱정할 필요가 없습니다. 19 장에서 자세히 다루겠습니다.
deref 메서드의 본문을 &self.0으로 채우면 deref는 * 연산자로 접근하려는 값에 대한 참조를 반환합니다 [2]. "이름 없는 필드를 가진 튜플 구조체를 사용하여 다른 타입 만들기"에서 .0이 튜플 구조체의 첫 번째 값에 접근한다는 것을 기억하십시오. MyBox<T> 값에 *를 호출하는 Listing 15-9 의 main 함수는 이제 컴파일되고 단언문이 통과합니다!
Deref 트레이트가 없으면 컴파일러는 & 참조만 역참조할 수 있습니다. deref 메서드는 컴파일러에게 Deref를 구현하는 모든 타입의 값을 가져와 deref 메서드를 호출하여 역참조하는 방법을 알고 있는 & 참조를 얻을 수 있는 기능을 제공합니다.
Listing 15-9 에서 *y를 입력했을 때, Rust 는 실제로 다음과 같은 코드를 실행했습니다.
*(y.deref())
Rust 는 * 연산자를 deref 메서드 호출과 일반 역참조로 대체하므로 deref 메서드를 호출해야 하는지 여부에 대해 생각할 필요가 없습니다. 이 Rust 기능을 사용하면 일반 참조 또는 Deref를 구현하는 타입이 있는지 여부에 관계없이 동일하게 작동하는 코드를 작성할 수 있습니다.
deref 메서드가 값에 대한 참조를 반환하고, *(y.deref())의 괄호 밖에서 일반 역참조가 여전히 필요한 이유는 소유권 시스템과 관련이 있습니다. deref 메서드가 값에 대한 참조 대신 값을 직접 반환하면 값이 self에서 이동됩니다. 이 경우 또는 역참조 연산자를 사용하는 대부분의 경우 MyBox<T> 내부의 내부 값을 소유하고 싶지 않습니다.
* 연산자는 deref 메서드 호출과 * 연산자 호출로 한 번만 대체됩니다. 코드에서 *를 사용할 때마다. * 연산자의 대체는 무한히 재귀되지 않으므로 Listing 15-9 의 assert_eq!에서 5와 일치하는 i32 타입의 데이터로 끝나게 됩니다.
Deref 강제 변환은 Deref 트레이트를 구현하는 타입에 대한 참조를 다른 타입에 대한 참조로 변환합니다. 예를 들어, String이 &str을 반환하도록 Deref 트레이트를 구현하므로, Deref 강제 변환은 &String을 &str로 변환할 수 있습니다. Deref 강제 변환은 Rust 가 함수 및 메서드의 인수에 대해 수행하는 편의 기능이며, Deref 트레이트를 구현하는 타입에서만 작동합니다. 특정 타입의 값에 대한 참조를 함수 또는 메서드 정의의 매개변수 타입과 일치하지 않는 함수 또는 메서드에 대한 인수로 전달할 때 자동으로 발생합니다. deref 메서드에 대한 일련의 호출은 제공된 타입을 매개변수에 필요한 타입으로 변환합니다.
Deref 강제 변환은 프로그래머가 함수 및 메서드 호출을 작성할 때 & 및 *를 사용하여 명시적인 참조 및 역참조를 많이 추가할 필요가 없도록 Rust 에 추가되었습니다. Deref 강제 변환 기능을 사용하면 참조 또는 스마트 포인터 모두에 대해 작동할 수 있는 더 많은 코드를 작성할 수도 있습니다.
Deref 강제 변환을 실제로 확인하려면 Listing 15-8 에서 정의한 MyBox<T> 타입과 Listing 15-10 에서 추가한 Deref의 구현을 사용해 보겠습니다. Listing 15-11 은 문자열 슬라이스 매개변수를 갖는 함수의 정의를 보여줍니다.
파일 이름: src/main.rs
fn hello(name: &str) {
println!("Hello, {name}!");
}
Listing 15-11: &str 타입의 매개변수 name을 갖는 hello 함수
예를 들어, hello("Rust");와 같이 문자열 슬라이스를 인수로 사용하여 hello 함수를 호출할 수 있습니다. Deref 강제 변환을 사용하면 Listing 15-12 에 표시된 것처럼 MyBox<String> 타입의 값에 대한 참조로 hello를 호출할 수 있습니다.
파일 이름: src/main.rs
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
Listing 15-12: Deref 강제 변환으로 인해 작동하는 MyBox<String> 값에 대한 참조로 hello 호출하기
여기서는 MyBox<String> 값에 대한 참조인 인수 &m으로 hello 함수를 호출하고 있습니다. Listing 15-10 에서 MyBox<T>에 Deref 트레이트를 구현했으므로 Rust 는 deref를 호출하여 &MyBox<String>을 &String으로 변환할 수 있습니다. 표준 라이브러리는 문자열 슬라이스를 반환하는 String에 대한 Deref의 구현을 제공하며, 이는 Deref에 대한 API 문서에 있습니다. Rust 는 deref를 다시 호출하여 &String을 &str로 변환하며, 이는 hello 함수의 정의와 일치합니다.
Rust 가 Deref 강제 변환을 구현하지 않았다면, Listing 15-12 의 코드 대신 Listing 15-13 의 코드를 작성하여 &MyBox<String> 타입의 값으로 hello를 호출해야 합니다.
파일 이름: src/main.rs
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
Listing 15-13: Rust 에 Deref 강제 변환이 없는 경우 작성해야 하는 코드
(*m)은 MyBox<String>을 String으로 역참조합니다. 그런 다음 &와 [..]는 hello의 시그니처와 일치하도록 전체 문자열과 동일한 String의 문자열 슬라이스를 가져옵니다. Deref 강제 변환이 없는 이 코드는 관련된 모든 기호로 인해 읽고, 쓰고, 이해하기가 더 어렵습니다. Deref 강제 변환을 사용하면 Rust 가 이러한 변환을 자동으로 처리할 수 있습니다.
관련 타입에 대해 Deref 트레이트가 정의되면 Rust 는 타입을 분석하고 매개변수의 타입과 일치하는 참조를 얻기 위해 필요한 만큼 Deref::deref를 사용합니다. Deref::deref를 삽입해야 하는 횟수는 컴파일 시간에 결정되므로 Deref 강제 변환을 활용하는 데 런타임 페널티가 없습니다!
Deref 트레이트를 사용하여 불변 참조에 대한 * 연산자를 재정의하는 것과 유사하게, DerefMut 트레이트를 사용하여 가변 참조에 대한 * 연산자를 재정의할 수 있습니다.
Rust 는 세 가지 경우에 타입과 트레이트 구현을 발견하면 Deref 강제 변환을 수행합니다.
T: Deref<Target=U>인 경우 &T에서 &U로T: DerefMut<Target=U>인 경우 &mut T에서 &mut U로T: Deref<Target=U>인 경우 &mut T에서 &U로처음 두 경우는 두 번째가 가변성을 구현한다는 점을 제외하고 동일합니다. 첫 번째 경우는 &T가 있고 T가 어떤 타입 U로 Deref를 구현하는 경우, 투명하게 &U를 얻을 수 있다고 명시합니다. 두 번째 경우는 가변 참조에 대해 동일한 Deref 강제 변환이 발생한다고 명시합니다.
세 번째 경우는 더 까다롭습니다. Rust 는 가변 참조를 불변 참조로 강제 변환할 수도 있습니다. 그러나 그 반대는 불가능합니다. 불변 참조는 가변 참조로 절대 강제 변환되지 않습니다. 빌림 규칙 때문에, 가변 참조가 있는 경우 해당 가변 참조는 해당 데이터에 대한 유일한 참조여야 합니다 (그렇지 않으면 프로그램이 컴파일되지 않습니다). 하나의 가변 참조를 하나의 불변 참조로 변환하는 것은 빌림 규칙을 절대 위반하지 않습니다. 불변 참조를 가변 참조로 변환하려면 초기 불변 참조가 해당 데이터에 대한 유일한 불변 참조여야 하지만, 빌림 규칙은 이를 보장하지 않습니다. 따라서 Rust 는 불변 참조를 가변 참조로 변환하는 것이 가능하다고 가정할 수 없습니다.
축하합니다! Deref 를 사용하여 스마트 포인터를 일반 참조처럼 다루는 Lab 을 완료했습니다. LabEx 에서 더 많은 Lab 을 연습하여 실력을 향상시킬 수 있습니다.