Rust 스마트 포인터 1

mohadang·2023년 1월 22일
0

Rust

목록 보기
14/30
post-thumbnail

스마트 포인터

Rust에서는 & 연산자로 이미 존재하는 데이터의 참조를 생성하는 기능과 더불어, 스마트 포인터라 불리는 참조 같은 구조체를 생성하는 기능을 제공
스마트 포인터가 일반적인 참조와 다른 점은, 프로그래머가 작성하는 내부 로직에 기반해 동작
일반적으로 스마트 포인터는 구조체가 * 와 . 연산자로 역참조될 때 무슨 일이 발생할 지 지정하기 위해 Deref, DerefMut, 그리고 Drop 트레잇을 구현한다.

use std::ops::Deref;//외부 모듈 사용

struct TattleTell<T> {
    value: T,
}

impl<T> Deref for TattleTell<T> {  // Deref 트레잇 구현 필요
    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",
    };
    println!("{}", foo.len());
}

위험한 스마트 포인터

스마트 포인터는 안전하지 않은 코드를 꽤 자주 쓰는 경향이 있다.
스마트 포인터는 Rust에서 가장 저수준의 메모리를 다루기 위한 일반적인 도구다.
안전하지 않은 코드의 주 기능은 윈시 포인터를 역참조하는 것이다.
이는 원시 포인터를 일반 변수로 변환한뒤 사용하는 것을 의미한다.
원시 포인터로 쓰이는 임의의 숫자에 무엇이 존재하는지 보증할 수 없기 떄문에, 역참조를 unsafe {...} 블록 안에 넣는다.

fn main() {
    let a: [u8; 4] = [86, 14, 73, 64];
    let pointer_a = &a as *const u8 as usize;
    println!("Data memory location: {}", pointer_a);
    let pointer_b = pointer_a as *const f32;
    let b = unsafe { 
        *pointer_b // 위험, 이 숫자는 안정성을 보장하지 않는다.
    };

    println!("I swear this is a pie! {}", b);
}

Vec, String

Vec는 바이트들의 메모리 영역을 소유하는 스마트 포인터다.
스마트 포인터는 데이터 구조가 바이트들 내 어디에서 시작하고 끝나는지 추적, 마지막으로 원시 포인터를 데이터 구조로, 또 쓰기 편한 깔끔한 인터페이스로 역참조(my_vec[3]) 한다.

String은 바이트들의 메모리 영역을 추적하며, 쓰여지는 내용물이 언제나 유효한 UTF-8이 되도록 프로그램적으로 제한
그 메모리 영역을 &str 데이터 타입으로 역참조할 수 있도록 도와준다.

두 데이터 구조 모두 원시 포인터에 대한 안전하지 않은 역참조를 사용한다.

원시 포인터를 이용한 예시

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(4, 1).unwrap();
        unsafe {
            // 메모리 위치를 숫자로 할당
            let ptr = alloc(layout) as *mut u8;//malloc
            // 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 {
        //f32 원시 포인터로 변환
        let pointer = self.secret_recipe as *const f32;
        unsafe { &*pointer }// 값 반환
    }
}

fn main() {
    let p = Pie::new();// 생성
    println!("{:?}", *p);// 역참조
}

힙 메모리(Box)

Box는 데이터를 스택에서 힙으로 옮길 수 있게 해주는 스마트 포인터다.
이를 역참조하면 마치 원래 데이터 타입이었던 것처럼 힙에 할당된 데이터를 편하게 쓸 수 있다.

use std::alloc::{alloc, Layout};
use std::ops::Deref;

struct Pie;
impl Pie {
    fn eat(&self) {
        println!("tastes better on the heap!");
    }
}

fn main() {
    let heap_pie = Box::new(Pie);
    heap_pie.eat(); // 역참조, 원래 데이터 타입인것 마냥 사용 가능
}

힙 메모리 응용 - 사용자 정의 에러 반환

Rust 코드에는 많고도 많은 오류 표현 방법이 있지만, 그 중에도 standard library에는 오류를 설명하기 위한 범용 trait인 std::error::Error가 있다.
smart pointer인 Box를 사용하면 Box를 오류 리턴 시 공통된 자료형으로 사용할 수 있는데, 이는 오류를 heap에 전파하고 특정한 자료형을 몰라도 고수준에서 상호작용할 수 있도록 해준다.

use core::fmt::Display;
use std::error::Error;

struct Pie;

#[derive(Debug)]
struct NotFreshError; // 빈 struct

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)

Rc는 스택에 있는 데이터를 힙으로 옮겨주는 스마트 포인터
마지막 스마트 포인터가 Drop 될때에만 힙에 있는 데이터가 할당 해제

use std::rc::Rc;

struct Pie {
    value: i32,
}
impl Pie {
    fn display(&self) {
        println!("{}", self.value);
    }
}

fn main() {
    let heap_pie = Rc::new(Pie{value: 32_i32});
    let heap_pie2 = heap_pie.clone();//얕은 복사, 참조 카운팅 증가, 컨테이너와 생명 주기가 다르다.
    let heap_pie3 = heap_pie2.clone();//얕은 복사, 참조 카운팅 증가, 컨테이너와 생명 주기가 다르다.
    
    heap_pie.display();
    heap_pie2.display();
    heap_pie3.display();
    heap_pie.value = 32; // error, 변경 불가
}

접근 공유(RefCell)

RefCell은 보통 스마트 포인터가 보유하는 컨테이너 데이터 구조로서, 데이터를 가져오거나 안에 있는 것에 대한 변경 가능한 또는 불가능한 참조를 대여할 수 있게 해준다.

데이터를 대여할 떄, Rust는 런타임에 메모리 안전 규칙을 적용해 남용을 방지
단 하나의 변경 가능한 참조 또는 여러 개의 변경 불가능한 참조만 허용하며, 둘 다는 안됨!
이 규칙을 어기면 RefCell은 패닉을 일으킨다.

use std::cell::RefCell;

struct Pie {
    value: i32,
}
impl Pie {
    fn up(&mut self) {
        self.value += 1;
    }
}

fn main() {
    let heap_pie = RefCell::new(Pie{value: 0});

    {
        let mut ref_pie = heap_pie.borrow_mut(); // 참조(Ref) 반환, 컨테이너와 생명주기 같아야함
        let mut ref_pie2 = heap_pie.borrow_mut(); // error
        ref_pie.up();
        ref_pie.up();
    }
    
    let ref_pie = heap_pie.borrow_mut();
    println!("val : {}", ref_pie.value);
}
profile
mohadang

0개의 댓글