포인터란?
포인터는 메모리에 주소를 포함하는 변수에 대한 일반적인 개념입니다. 이 주소는 다른 데이터를 참조하거나 "가리키고"있습니다. Rust에서 가장 일반적인 종류의 포인터는 4장에서 배운 참조입니다. 참조는 & 기호로 표시되며 참조가 가리키는 값을 차용합니다. 데이터를 참조하는 것 외에는 특별한 기능이 없으며 오버헤드도 없습니다.
반면 스마트 포인터는 포인터처럼 작동하지만 추가적인 메타데이터와 기능을 가진 데이터 구조입니다. 스마트포인터는 C++에서 시작되었으며 다른 언어에도 존재하기 때문에 스마트 포인터의 개념은 Rust에만 있는 것이 아닙니다. Rust에는 표준 라이브러리에 정의된 다양한 스마트 포인터가 있어 레퍼런스가 제공하는 기능 이상의 기능을 제공합니다. 일반적인 개념을 살펴보기 위해 참조 계수 스마트 포인터 유형을 포함하여 스마트 포인터의 몇 가지 다른 예를 살펴보겠습니다. 이 포인터를 사용하면 소유자 수를 추적하고 소유자가 남아 있지 않을 경우 데이터를 정리하여 데이터에 여러 소유자를 허용할 수 있습니다.
소유권과 차용이라는 개념을 가진 Rust는 참조와 스마트 포인터 사이에 또 다른 차이점이 있는데, 참조는 데이터를 차용할 뿐이지만 많은 경우 스마트 포인터는 자신이 가리키는 데이터를 소유합니다.
당시에는 스마트 포인터를 그렇게 부르지 않았지만, 이전 포스팅에서 String과 Vec를 포함해 몇 가지 스마트 포인터를 접한적이 있습니다. 이 두가지 유형은 모두 약간의 메모리를 소유하고 있고 이를 조작할 수 있기 때문에 스마트 포인터로 간주됩니다. 또한 메타데이터와 추가 기능 또는 보증이 있습니다. 예를 들어 문자열은 용량을 메타데이터로 저장하고 데이터가 항상 유효한 UTF-8이 되도록 보장하는 추가 기능을 가지고 있습니다.
스마트 포인터는 일반적으로 구조체를 사용하여 구현됩니다. 일반 구조체와 달리 스마트 포인터는 디레프 및 드롭 특성을 구현합니다. Deref 특성을 사용하면 스마트 포인터 구조체의 인스턴스가 참조처럼 동작할 수 있으므로 참조 또는 스마트 포인터와 함께 작동하도록 코드를 작성할 수 있습니다. Drop 특성을 사용하면 스마트 포인터의 인스턴스가 범위를 벗어날 때 실행되는 코드를 사용자 정의할 수 있습니다. 이 포스팅에서는 두가지 특성에 대해 설명하고 스마트 포인터가 왜 중요한지 설명하겠습니다.
스마트 포인터 패턴은 Rust에서 자주 사용디는 일반적인 디자인 패턴이므로 이 장에서는 기존의 모든 스마트 포인터를 다루지는 않을 것입니다. 많은 라이브러리에는 자체 므타ㅡ 포인터가 있으며 직접 작성할 수 도 있습니다. 여기서는 표준 라이브러리에서 가장 일반적인 스마트 포인터를 다루겠습니다.
Box 타입
앞에서 포스팅한 원시 타입과 구조체 타입들은 모두 크기가 일정했습니다. 그런데 만일 어떤 타입의 크기를 컴파일 타임에 미리 알 수 없다면 어떨까요? 어떤 타입의 크기가 런타임에 정해지는 경우가 있을까요? 다음과 같이 자기 자신을 필드값의 타입으로 갖는 재귀 형태의 구조체(Recursive type)를 정의해 보겠습니다.
Node 타입의 크기를 컴파일 타임에 미리 알 수 없다.
struct Node {
value: i32,
next: Option<Node>,
}
fn main() {
let mut head = Node {
value: 1,
next: None,
};
head.next = Some(Node {
value: 2,
next: None,
});
println!("{}", head.value);
}
error[E0072]: recursive type `Node` has infinite size
--> src/main.rs:1:1
|
1 | struct Node {
| ^^^^^^^^^^^
2 | value: i32,
3 | next: Option<Node>,
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
3 | next: Option<Box<Node>>,
| ++++ +
Box 를 사용하라고 함
struct Node {
value: i32,
next: Option<Box<Node>>,
}
fn main() {
let mut head = Node {
value: 1,
next: None,
};
head.next = Some(Box::new(Node {
value: 2,
next: None,
}));
println!("{}", head.value);
}
문제없이 컴파일
Box<T>
Box가 대체 무엇일까?
Box를 사용하면 스택이 아닌 힙에 데이터를 저장할 수 있습니다. 스택에 남는 것은 힙 데이터에 대한 포인터입니다.
Box 사용하기
아래 예제는 Box를 사용하여 i32 값을 힙에 저장하는 방법을 보여줍니다.
fn main() {
let my_box = Box::new(5);
println!("my_box = {}", my_box);
}
변수 b가 힙에 할당된 값 5를 가리키는 Box의 값을 갖도록 정의합니다. 이 프로그램은 b = 5를 출력합니다. 이 경우 이 데이터가 스택에 있을 때와 유사하게 상자에 있는 데이터에 액세스할 수 있습니다. 다른 소유 값과 마찬가지로 상자가 범위를 벗어나면, 메인 끝에서 b가 그러하듯이, 상자는 할당 해제됩니다. 할당 해제는 상자(스택에 저장됨)와 상자가 가리키는 데이터(힙에 저장됨) 모두에 대해 발생합니다. 여기까지는 스택에 값을 저장하는 것과 크게 다르지 않습니다.
Box는 주로 다음과 같은 상황에 사용됩니다.
- 컴파일 시 크기를 알 수 없는 타입 내부의 값에 접근해야 하는 경우
- 크기가 큰 값의 소유권을 이전하고 싶지만, 메모리 효율성을 위해 전체 값이 복사되지 않도록 해야 하는 경우
- 특정 타입이 아닌, 특정 트레이트를 구현하는 타입의 변수의 소유권을 가져오고 싶은 경우
첫 번째 상황은 위에서 이미 살펴본 Node의 경우입니다. 이제 나머지 각각의 경우를 자세히 살펴보겠습니다.
소유권을 효율적으로 전달하기
레퍼런스 대신
fn transfer_box(_data: Box<Vec<i32>>) {}
fn transfer_vec(_data: Vec<i32>) {}
fn main() {
let data = vec![0; 10_000_000];
transfer_vec(data.clone());
let boxed = Box::new(data);
transfer_box(boxed);
}
dyn 과 Box로 트레이트 타입 표현하기
cargo add rand
struct Dog {}
struct Cat {}
trait Animal {
fn noise(&self) -> &'static str;
}
impl Animal for Dog {
fn noise(&self) -> &'static str {
"🐶멍멍!"
}
}
impl Animal for Cat {
fn noise(&self) -> &'static str {
"🐱야옹!"
}
}
fn random_animal() -> impl Animal {
if rand::random::<f64>() < 0.5 {
Dog {}
} else {
Cat {}
}
}
fn main() {
for _ in 0..10 {
println!("{}", random_animal().noise());
}
}
실행 결과
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:24:9
|
21 | / if rand::random::<f64>() < 0.5 {
22 | | Dog {}
| | ------ expected because of this
23 | | } else {
24 | | Cat {}
| | ^^^^^^ expected struct `Dog`, found struct `Cat`
25 | | }
| |_____- `if` and `else` have incompatible types
|
help: you could change the return type to be a boxed trait object
|
20 | fn random_animal() -> Box<dyn Animal> {
| ~~~~~~~ +
help: if you change the return type to expect trait objects, box the returned expressions
|
22 ~ Box::new(Dog {})
23 | } else {
24 ~ Box::new(Cat {})
|
struct Dog {}
struct Cat {}
trait Animal {
fn noise(&self) -> &'static str;
}
impl Animal for Dog {
fn noise(&self) -> &'static str {
"🐶멍멍!"
}
}
impl Animal for Cat {
fn noise(&self) -> &'static str {
"🐱야옹!"
}
}
fn random_animal() -> Box<dyn Animal> {
if rand::random::<f64>() < 0.5 {
Box::new(Dog {})
} else {
Box::new(Cat {})
}
}
fn main() {
for _ in 0..10 {
println!("{}", random_animal().noise());
}
}
실행 결과
🐱야옹!
🐶멍멍!
🐶멍멍!
🐶멍멍!
🐱야옹!
🐱야옹!
🐶멍멍!
🐶멍멍!
🐶멍멍!
🐶멍멍!
Rc<T>
대부분의 상황에서 어떤 값에 대한 소유권이 어떤 변수에 있는지를 정확하게 알 수 있습니다. 러스트의 소유권 규칙은 하나의 값에 단 하나만의 소유자를 보장합니다. 그런데 만일 하나의 값에 여러 개의 소유자를 정말로 가지고 싶다면 어떻게 할까요? 이럴 때 사용할 수 있는 자료형이 바로 Rc<T>입니다. 레퍼런스 카운팅(Reference counting)의 앞 자를 따서 만든 이름으로, Rc<T> 역시 스마트 포인터입니다.
use std::rc::Rc;
fn main() {
let origin = Rc::new(1);
assert_eq!(1, *origin);
}
마지막 순간까지
프로그램의 여러 부분이 읽을 수 있도록 힙에 일부 데이터를 할당하고 컴파일 시점에 어느 부분이 데이터를 마지막으로 사용할지 결정할 수 없을 때 Rc<T> 타입을 사용합니다. 어떤 부분이 마지막으로 완료될지 알 수 있다면 해당 부분을 데이터의 소유자로 설정하면 컴파일 시점에 적용되는 일반적인 소유권 규칙이 적용될 것입니다.
일반적인 레퍼런스와 Rc<T>가 다른 점은 여기에 있습니다. 다음과 같은 예제를 살펴보겠습니다.
fn main() {
let cloned;
{
let origin = "Rust".to_string();
cloned = &origin; // 🤯
}
println!("{}", cloned);
}
하지만 Rc를 사용하면 원래 값이 스코프를 벗어나더라도 값을 참조하고 있는 Rc가 존재하기 때문에 여전히 값을 사용할 수 있습니다. 여기서 clone을 사용하면, 실제로 값이 복사되는 것이 아니라 Rc의 레퍼런스 카운트가 1 증가합니다.
use std::rc::Rc;
fn main() {
let cloned;
{
let origin = Rc::new(1);
cloned = origin.clone();
}
println!("{}", cloned);
}
레퍼런스 카운팅
Rc<T>는 값의 소유권을 가지고 있는 변수가 몇 개인지를 계속 확인하고 있다가, 값을 소유하고 있는 변수가 전부 사라지면 값을 메모리에서 삭제합니다.
use std::rc::Rc;
fn main() {
let origin = Rc::new(0);
println!("Reference count: {}", Rc::strong_count(&origin));
{
let _dup1 = Rc::clone(&origin);
println!("Reference count: {}", Rc::strong_count(&origin));
{
let _dup2 = &origin.clone();
println!("Reference count: {}", Rc::strong_count(&origin));
}
println!("Reference count: {}", Rc::strong_count(&origin));
}
println!("Reference count: {}", Rc::strong_count(&origin));
// origin drops here
}
실행 결과
Reference count: 1
Reference count: 2
Reference count: 3
Reference count: 2
Reference count: 1
Rc<T>는 멀티스레드 환경에서 동작하지 않습니다. 멀티스레드 환경에서는 Arc<T>를 사용해야 하며, 자세한 내용은 나중에 다루겠습니다.
Quiz
struct Node {
value: i32,
next: Option<Box<Node>>,
}
fn main() {
let mut head1 = Node {
value: 1,
next: None,
};
let node1 = Node {
value: 2,
next: None,
};
head1.next = Some(Box::new(node1));
let mut head2 = Node {
value: 3,
next: None,
};
head2.next = Some(Box::new(node1)); // 🤯
println!("{} {}", head1.value, head1.next.unwrap().value);
println!("{} {}", head2.value, head2.next.unwrap().value);
}
정답
use std::rc::Rc;
struct Node {
value: i32,
next: Option<Rc<Node>>,
}
fn main() {
let mut head1 = Node {
value: 1,
next: None,
};
let node1 = Rc::new(Node {
value: 2,
next: None,
});
head1.next = Some(Rc::clone(&node1));
let mut head2 = Node {
value: 3,
next: None,
};
head2.next = Some(Rc::clone(&node1));
println!("{} {}", head1.value, head1.next.unwrap().value);
println!("{} {}", head2.value, head2.next.unwrap().value);
}
RefCell<T>
Rc<T>의 한계
Rc를 사용하면 프로그램의 여러 부분에서 읽기 전용으로 데이터를 공유할 수 있습니다. 하지만 Rc가 불변 레퍼런스를 통해 값을 공유하기 때문에, 공유받은 값을 변경하는 것은 불가능합니다.
use std::rc::Rc;
struct Owner {
name: String,
tools: Rc<Vec<Rc<Tool>>>,
}
struct Tool {
owner: Rc<Owner>,
}
pub fn main() {
let indo = Rc::new(Owner {
name: "indo".to_string(),
tools: Rc::new(vec![]),
});
let pliers = Rc::new(Tool {
owner: Rc::clone(&indo),
});
let wrench = Rc::new(Tool {
owner: indo.clone(),
});
indo.tools.push(Rc::clone(&pliers)); // 🤯
indo.tools.push(Rc::clone(&wrench));
println!("Pliers owner: {}", pliers.owner.name);
for tool in indo.tools.iter() {
println!("Tool's owner: {:?}", tool.owner.name);
}
}
실행 결과
error[E0596]: cannot borrow data in an `Rc` as mutable
--> src/main.rs:24:5
|
24 | brad.tools.push(Rc::clone(&pliers)); // 🤯
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot borrow as mutable
|
...
use std::{cell::RefCell, rc::Rc};
struct Owner {
name: String,
tools: RefCell<Vec<Rc<Tool>>>,
}
struct Tool {
owner: Rc<Owner>,
}
pub fn main() {
let indo = Rc::from(Owner {
name: "indo".to_string(),
tools: RefCell::new(vec![]),
});
let pliers = Rc::from(Tool {
owner: Rc::clone(&indo),
});
let wrench = Rc::from(Tool {
owner: indo.clone(),
});
indo.tools.borrow_mut().push(Rc::clone(&pliers));
indo.tools.borrow_mut().push(Rc::clone(&wrench));
println!("Pliers owner: {}", pliers.owner.name);
for tool in indo.tools.borrow().iter() {
println!("Tool's owner: {:?}", tool.owner.name);
}
}
내부 가변성(Interiror mutability)
RefCell<T>가 불변이어도 내부의 값은 가변으로 사용 가능
indo.tools.borrow_mut().push(Rc::clone(&pliers));
불변 소유권 대여도 가능
indo.tools.borrow().iter()
소유권 규칙
- 여러 번 빌려도 괜찮습니다
- 한 번 빌리는 것도 괜찮습니다
- 하지만 가변과 불변이 대여는 불가능합니다
런타임 시간에 소유권이 확인되기 때문에 컴파일이 되지만 런타임 에러 발생
use std::{cell::RefCell, rc::Rc};
struct Owner {
name: String,
tools: RefCell<Vec<Rc<Tool>>>,
}
struct Tool {
owner: Rc<Owner>,
}
pub fn main() {
let indo = Rc::from(Owner {
name: "indo".to_string(),
tools: RefCell::new(vec![]),
});
let pliers = Rc::from(Tool {
owner: Rc::clone(&indo),
});
let wrench = Rc::from(Tool {
owner: indo.clone(),
});
let mut borrow_mut_tools1 = indo.tools.borrow_mut();
let mut borrow_mut_tools2 = indo.tools.borrow_mut(); // 🤯
borrow_mut_tools1.push(Rc::clone(&pliers));
borrow_mut_tools2.push(Rc::clone(&wrench));
println!("Pliers owner: {}", pliers.owner.name);
for tool in indo.tools.borrow().iter() {
println!("Tool's owner: {:?}", tool.owner.name);
}
}
실행 결과
thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:25:44
Rc<RefCell<T>>
RefCell<T>를 사용하는 일반적인 방법은 Rc<T>와 함께 사용하는 것입니다. Rc<T>를 사용하면 일부 데이터의 소유자를 여러 명 가질 수 있지만, 해당 데이터에 대한 불변 액세스 권한만 부여한다는 점을 기억하세요. RefCell<T>를 보유한 Rc<T>가 있다면, 여러 소유자를 가질 수 있고 변경할 수 있는 값을 얻을 수 있습니다!
요약하자면, Rc는 공유 소유권을 제공합니다. 내부 값에는 여러 소유자가 있으며, 참조 카운팅은 적어도 한 명의 소유자가 데이터를 계속 보유하고 있는 한 데이터가 계속 유지되도록 합니다. 이는 데이터 소유자가 명확하지 않은 경우에 유용합니다. RefCell은 내부 가변성을 제공합니다. 즉, 런타임에 내부 값을 동적으로 빌릴 수 있고 공유 참조를 통해서도 수정할 수 있습니다. Rc<RefCell<...>> 조합은 소유자가 여러 명인 값을 소유자 중 한 명이 가변적으로 빌릴 수 있는 두 가지의 조합을 제공합니다.
언제 무엇을
소유권 | 한 개 | 한 개를 공유 | 한 개 |
소유권 확인 시점 | 불변/가변 소유권을 컴파일 타임에 확인 | 불변 소유권을 컴파일 타임에 확인 | 불변/가변 소유권을 런타임에 확인 |
특징 | 스코프를 벗어나면 레퍼런스도 모두 삭제 | 레퍼런스가 존재한다면 스코프를 벗어나도 값이 유지됨 | RefCell<T>가 불변이어도 내부의 값은 가변으로 사용 가능 |
RefCell는 멀티스레드 코드에서는 작동하지 않는다는 점에 유의하세요! Mutex는 스레드에 안전한 RefCell<T의 버전이며, Mutex<T에 대해서는 나중에 설명하겠습니다.
Quiz
use std::fmt::Display;
use std::vec::Vec;
#[derive(Debug)]
struct Node<T> {
data: T,
children: Vec<Node<T>>,
}
impl<T: Display> Node<T> {
fn new(data: T) -> Node<T> {
Node {
data,
children: Vec::new(),
}
}
fn depth_first(&self) {
println!("{}", self.data);
for child in self.children.iter() {
child.depth_first();
}
}
}
fn main() {
let mut a = Node::new('A');
let mut b = Node::new('B');
let c = Node::new('C');
let d = Node::new('D');
b.children.push(d);
a.children.push(b);
a.children.push(c);
a.depth_first();
}
fn add_child(&mut self, child: Wrapper<Node<T>>) {
self.children.push(child);
}
fn main() {
let a = wrap(Node::new('A'));
let b = wrap(Node::new('B'));
let c = wrap(Node::new('C'));
let d = wrap(Node::new('D'));
a.borrow_mut().add_child(Rc::clone(&b));
a.borrow_mut().add_child(Rc::clone(&c));
b.borrow_mut().add_child(Rc::clone(&d));
a.borrow_mut().depth_first();
}
정답
use std::cell::RefCell;
use std::fmt::Display;
use std::rc::Rc;
use std::vec::Vec;
type Wrapper<T> = Rc<RefCell<T>>;
fn wrap<T>(data: T) -> Wrapper<T> {
Rc::new(RefCell::new(data))
}
#[derive(Debug)]
struct Node<T> {
data: T,
children: Vec<Wrapper<Node<T>>>,
}
impl<T: Display> Node<T> {
fn add_child(&mut self, child: Wrapper<Node<T>>) {
self.children.push(child);
}
fn new(data: T) -> Node<T> {
Node {
data,
children: Vec::new(),
}
}
fn depth_first(&self) {
println!("node {}", self.data);
for child in self.children.iter() {
child.borrow().depth_first();
}
}
}
fn main() {
let a = wrap(Node::new('A'));
let b = wrap(Node::new('B'));
let c = wrap(Node::new('C'));
let d = wrap(Node::new('D'));
a.borrow_mut().add_child(Rc::clone(&b));
a.borrow_mut().add_child(Rc::clone(&c));
b.borrow_mut().add_child(Rc::clone(&d));
a.borrow_mut().depth_first();
}
'Rust' 카테고리의 다른 글
Rust 비동기 프로그래밍 (36) | 2023.05.28 |
---|---|
Rust 멀티스레딩 (28) | 2023.05.27 |
Rust 예외처리 (36) | 2023.05.13 |
Rust 제네릭 (88) | 2023.05.12 |
Rust 모듈과 크레이트 (98) | 2023.05.11 |