참조 다시 보기원시 포인터역참조* 연산자. 연산자스마트 포인터위험한 스마트 코드익숙한 친구들힙에 할당된 메모리실패할 수 있는 메인 다시 보기참조 카운팅접근 공유하기쓰레드 간에 공유하기스마트 포인터 조합하기The Rust Programming Language 링크의문점
참조 다시 보기
- 참조는 메모리 상의 어떤 바이트들의 시작 위치 가리키는 것
- 특정 자료형의 데이터가 어디에 존재하는지에 대한 개념
- Rust에서 참조가 가리키는 값보다 더 오래 살지 않도록 lifetime을 검증
원시 포인터
- Rust는 raw pointer가 가리키는 메모리 위치의 유효성을 보증하지 않음
- raw pointer 두 종류 (
const
ormut
) *const T
- 자료형 T의 데이터를 가리키는 immutable raw pointer*mut T
- 자료형 T의 데이터를 가리키는 mutable raw pointer
- raw pointer는 숫자와 상호 변환이 가능 (ex>
usize
)
- raw pointer는 unsafe한 코드의 데이터에 접근 가능
fn main() { let a = 42; let memory_location = &a as *const i32 as usize; println!("데이터는 여기 있습니다: {}", memory_location); }
메모리 상세
참조는 C의 pointer와 매우 유사하나, 저장되는 방식이나 다른 함수에 전달되는 부분에 있어 훨씬 많은 컴파일 타임의 제약을 받음.
raw pointer는 C의 pointer와 유사
역참조
- 역참조: 참조 (i.e.
&i32
)를 통해 참조되는 데이터를 접근/변경하는 과정
- 참조로 데이터에 접근/변경하는 두 가지 방법
- 변수 할당 중에 참조되는 데이터에 접근
- 참조되는 데이터의 필드나 메소드에 접근
* 연산자
*
연산자는 참조를 역참조 하는 명시적인 방법
fn main() { let a: i32 = 42; let ref_ref_ref_a: &&&i32 = &&&a; let ref_a: &i32 = **ref_ref_ref_a; let b: i32 = *ref_a; println!("{}", b); }
메모리 상세
stack에 있는 변수
a
의 바이트들은 변수 b
의 바이트들로 복사. 연산자
.
연산자는 참조의 필드와 메소드에 접근하는 데에 사용let f = Foo { value: 42 }; let ref_ref_ref_f = &&&f; println!("{}", ref_ref_ref_f.value);
ref_ref_ref_f
앞에 ***
을 안넣어도 되는 이유? →
.
연산자가 참조 열을 자동으로 역참조컴파일러에 의해 자동적으로 다음과 같이 바뀜
println!("{}", (***ref_ref_ref_f).value);
struct Foo { value: i32 } fn main() { let f = Foo { value: 42 }; let ref_ref_ref_f = &&&f; println!("{}", ref_ref_ref_f.value); // . 연산자로 자동 역참조 }
스마트 포인터
- 스마트 포인터
- 포인터처럼 작동할 뿐만 아니라 추가적인 메타데이터와 기능도 가지고 있는 데이터 구조
- smart pointer라 불리는 참조 같은 struct를 생성하는 기능을 제공
- smart pointer가 일반적인 참조와 다른 점은, 프로그래머가 작성하는 로직에 기반해 작동한다는 것
- struct의
*
와.
연산자로 발생하는 역참조 동작을Deref
,DerefMut
, 그리고Drop
trait을 구현
use std::ops::Deref; struct TattleTell<T> { value: T, } impl<T> Deref for TattleTell<T> { type Target = T; fn deref(&self) -> &T { // 역참조시 동작 정의 println!("{} was used!", std::any::type_name::<T>()); &self.value } } fn main() { let foo = TattleTell { value: "secret message", }; let bar = TattleTell { value: [1, 2, 3, 4], }; // foo가 `len` 함수를 위해 자동 참조된 후 여기서 역참조가 즉시 일어납니다 println!("{}", foo.len()); println!("{}", bar.len()); }
위험한 스마트 코드
- smart pointer는 unsafe한 코드를 꽤 자주 쓰는 경향이 있음
- unsafe → Rust 컴파일러가 보증할 수 없음
- ex> raw pointer를 역참조 (i.e.
*const u8
→u8
) - Rust는 raw pointer로 쓰이는 임의의 숫자에 무엇이 존재하는지 보증할 수 없음
- → 역참조를
unsafe { ... }
블록 안에 넣음
- smart pointer는 raw pointer를 역참조하는데 널리 쓰임
fn main() { let a: [u8; 4] = [86, 14, 73, 64]; // 이게 원시 pointer입니다. // 무언가의 메모리 주소를 숫자로 가져오는 것은 완전히 안전한 일입니다 let pointer_a = &a as *const u8 as usize; println!("a의 메모리 주소: {}", pointer_a); // 숫자를 raw pointer로 변환하는 것 역시 안전한 일입니다. let pointer_b = pointer_a as *const f32; let b = unsafe { // 이건 unsafe한데, // 컴파일러에게 우리의 pointer가 유효한 f32라고 가정하고 // 그 값을 변수 b로 역참조 하라고 하고 있기 때문입니다. // Rust는 이런 가정이 참인지 검증할 방법이 없습니다. *pointer_b }; println!("맹세하건대 이건 파이다! {}", b); }
익숙한 친구들
Vec<T>
나String
smart pointer 고찰
Vec<T>
는 바이트들의 메모리 영역을 소유하는 smart pointer- Rust 컴파일러는 이 바이트에 뭐가 존재하는지 모름
- Vec smart pointer에서 데이터 자료형 해석, 시작점 추적, 인터페이스 역참조 (ex>
my_vec[3]
) 제공
String
은 언제나 유효한utf-8
이 되도록 프로그램적으로 제한&str
로 역참조 가능하게 지원
Vec<T>
와String
둘 다 raw pointer에 대한 unsafe한 역참조 사용
use std::alloc::{alloc, Layout}; use std::ops::Deref; struct Pie { secret_recipe: usize, } impl Pie { fn new() -> Self { // 4 바이트를 요청해 봅시다 let layout = Layout::from_size_align(16, 4).unwrap(); unsafe { // 메모리 위치를 숫자로 할당하고 저장합니다 let ptr = alloc(layout) as *mut u8; // pointer 연산을 사용해 u8 값 몇 개를 메모리에 써봅시다 ptr.write(86); ptr.add(1).write(14); ptr.add(2).write(73); ptr.add(3).write(64); Pie { secret_recipe: ptr as usize, } } } } impl Deref for Pie { type Target = f32; fn deref(&self) -> &f32 { // secret_recipe pointer를 f32 raw pointer로 변환합니다 let pointer = self.secret_recipe as *const f32; // 역참조 하여 &f32 값으로 리턴합니다 unsafe { &*pointer } } } fn main() { let p = Pie::new(); // Pie struct의 smart pointer를 역참조 하여 // "파이를 만듭니다" println!("{:?}", *p); }
힙에 할당된 메모리
Box
는 데이터를 stack에서 heap으로 옮길 수 있게 해주는 smart pointer
struct Pie; impl Pie { fn eat(&self) { println!("heap에 있으니 더 맛있습니다!") } } fn main() { let heap_pie = Box::new(Pie); heap_pie.eat(); }
실패할 수 있는 메인 다시 보기
std::error::Error
→ 오류를 설명하는 범용 trait
Box<dyn std::error::Error>
→ Box의 Error 자료형
Error
trait을 구현하는 한, 프로그램에서 발생할 수 있는 거의 모든 종류의 오류를 리턴할 수 있음
fn main() -> Result<(), Box<dyn std::error:Error>>
use std::fmt::Display; use std::error::Error; struct Pie; #[derive(Debug)] struct NotFreshError; impl Display for NotFreshError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "이 파이는 신선하지 않군요!") } } impl Error for NotFreshError {} impl Pie { fn eat(&self) -> Result<(), Box<dyn Error>> { Err(Box::new(NotFreshError)) } } fn main() -> Result<(), Box<dyn Error>> { let heap_pie = Box::new(Pie); heap_pie.eat()?; Ok(()) }
참조 카운팅
Rc
는 stack에 있는 데이터를 heap으로 옮겨주는 smart pointer (Box?)
- heap에 놓인 데이터를 immutable하게 대여하는 기능을 가진 다른
Rc
smart pointer들을 복제할 수 있게 해줌
- 마지막 smart pointer가 drop 될 때, heap에 있는 데이터가 해제
use std::rc::Rc; struct Pie; impl Pie { fn eat(&self) { println!("heap에 있으니 더 맛있습니다!") } } fn main() { let heap_pie = Rc::new(Pie); let heap_pie2 = heap_pie.clone(); let heap_pie3 = heap_pie2.clone(); heap_pie3.eat(); heap_pie2.eat(); heap_pie.eat(); // 모든 참조 카운트 smart pointer가 여기서 drop 됩니다 // heap 데이터인 Pie가 드디어 할당 해제됩니다 }
접근 공유하기
RefCell
: smart pointer가 보유하는 컨테이너 데이터 구조
- Rust는 런타임에 다음의 메모리 안전 규칙을 적용하여 남용을 방지
- 단 하나의 mutable한 참조 또는 여러개의 immutable한 참조만 허용하며, 둘 다는 안됨!.
- 이 규칙을 어기면
RefCell
은 panic
use std::cell::RefCell; struct Pie { slices: u8, } impl Pie { fn eat(&mut self) { println!("heap에 있으니 더 맛있습니다!"); self.slices -= 1; } } fn main() { // RefCell은 런타임에 메모리 안전성을 검증합니다 // 주의: pie_cell은 mut가 아닙니다! let pie_cell = RefCell::new(Pie { slices: 8 }); { // 그렇지만 mutable 참조를 대여할 수 있습니다! let mut mut_ref_pie = pie_cell.borrow_mut(); // let mut mut_ref_pie2 = pie_cell.borrow_mut(); -> panic! mut_ref_pie.eat(); mut_ref_pie.eat(); // mut_ref_pie는 scope의 마지막에 drop 됩니다 } // 이제 mutable 참조가 drop 되고 나면 immutable하게 대여할 수 있습니다 let ref_pie = pie_cell.borrow(); // ref_pie.eat(); -> immutable 대여이므로 Error! println!("{} 조각 남았습니다", ref_pie.slices); }
쓰레드 간에 공유하기
Mutex
스마트 포인터
- lock()으로 동시에 오직 하나의 CPU만 데이터에 접근 가능하도록 해줌
- 멀티 쓰레드 환경에서 공유 자원 보호에 사용
Arc
: Thread-safety한 참조 카운트 증가 방식을 사용- 기능은
Rc
와 동일 - 동일한
Mutex
에 다수의 참조를 가질 때 종종 사용
use std::sync::Mutex; struct Pie; impl Pie { fn eat(&self) { println!("지금은 오직 나만이 파이를 먹는다!"); } } fn main() { let mutex_pie = Mutex::new(Pie); // 파이에 대한 잠겨있는 immutable한 참조를 빌려봅시다 // lock은 실패할 수도 있기 때문에 그 결과는 unwrap 해야합니다 { let ref_pie = mutex_pie.lock().unwrap(); ref_pie.eat(); } // 잠긴 참조는 여기서 drop 되며, mutex로 보호되는 값은 다른 이에 의해 쓰일 수 있습니다 }
스마트 포인터 조합하기
- smart pointer 조합해서 사용하면 매우 강력함
Rc<Vec<Foo>>
- heap에 있는 immutable한 데이터 구조와 동일한 vector를 대여할 수 있는 복수의 smart pointer를 복제할 수 있게 해줌
Rc<RefCell<Foo>>
- 복수의 smart pointer가 동일한Foo
struct를 mutable/immutable하게 대여할 수 있게 해줌
Arc<Mutex<Foo>>
- 복수의 smart pointer가 임시의 mutable/immutable한 대여를 CPU 쓰레드 독점 방식으로 잠글 수 있게 해줌
use std::cell::RefCell; use std::rc::Rc; struct Pie { slices: u8, } impl Pie { fn eat_slice(&mut self, name: &str) { println!("{}가 한 조각 먹었습니다!", name); self.slices -= 1; } } struct SeaCreature { name: String, pie: Rc<RefCell<Pie>>, } impl SeaCreature { fn eat(&self) { // mutable 대여를 위해 파이에 대한 smart pointer를 사용 let mut p = self.pie.borrow_mut(); // 한 입 먹자! p.eat_slice(&self.name); } } fn main() { let pie = Rc::new(RefCell::new(Pie { slices: 8 })); // ferris와 sarah에겐 파이에 대한 smart pointer의 복제가 주어집니다 let ferris = SeaCreature { name: String::from("ferris"), pie: pie.clone(), }; let sarah = SeaCreature { name: String::from("sarah"), pie: pie.clone(), }; ferris.eat(); sarah.eat(); let p = pie.borrow(); println!("{} 조각 남았습니다", p.slices); }
메모리 상세
내부 데이터를 변경하기 위해 immutable한 데이터 유형(복수의 smart pointer가 소유할 수 있음)을 사용하는 것 → Rust에서는 “내부 가변성(Interior Mutability Pattern)” 패턴이라고 함.
Rust의 컴파일 타임 체크와 동일 수준의 안전성으로 런타임의 메모리 사용 규칙을 변경할 수 있는 패턴
The Rust Programming Language 링크
- 15. 스마트 포인터
의문점
- 실패할 수 있는 메인 다시 보기 예제에서
core::fmt::Display
루틴은 왜 넣었을까? - → Error 객체를 그대로 출력했을때, 에러 메세지가 출력되도록 정의한 것
core::fmt::Display
=std::fmt:Display
- fmt::Display 예제
use std::fmt; struct Point { x: i32, y: i32, } impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "fmt::Display -> ({}, {})", self.x, self.y) } } fn main() { let origin = Point { x: 0, y: 0 }; println!("{}", origin); }