소유권

  • 소유권은 각 값에 대해 해당 값을 관리하는 고유한 소유자를 배정한다
  • 컴파일러는 컴파일 타임에 소유권을 검증해 메모리 안전성을 보장한다
  • 값은 소유자를 통해 대여할 수 있으며, 이를 빌림이라고 한다
  • 빌림을 통해 한 소유권에 대한 여러 참조자를 만들 수 있다

소유권 이관

fn main() {
    let s1 = String::from("Hello world");
    let s2 = s1;
    println!("{}", s1); // value borrowed here after move
}
  • 첫 번째 줄에서, "Hello world"라는 문자열을 담은 메모리를 s1이 소유한다
  • s1이 가지고 있던 소유권을 s2로 이관했다
  • s1은 더이상 사용할 수 없다

clone을 사용한 복제

use std::clone;

fn main() {
    let s1 = String::from("Hello world");
    let s2 = s1.clone();
    println!("{}", s1);
}
  • clone을 통해 값을 복제한다
  • "Hello world"라는 문자열을 담은 메모리가 2개가 되었으므로 각 변수가 문자열을 하나씩 소유한다

빌림

함수 파라미터로 소유권을 전달하는 경우

fn main() {
    let s = String::from("Hello world");
    push_str(s);
    println!("{}", s); // value borrowed here after move
}

fn push_str(mut s: String) {
    s.push_str("!!");
}
  • 함수 파라미터로 값을 전달하는 경우, 소유권도 함께 이전된다
  • 이전의 변수 s를 재사용할 수 없다
  • 이를 해결하는 방법은 2가지가 있다

섀도잉으로 소유권을 다시 획득

fn main() {
    let s = String::from("Hello world");
    let s = push_str(s);
    println!("{}", s); // value borrowed here after move
}

fn push_str(mut s: String) -> String {
    s.push_str("!!");
    s
}
  • 섀도잉 방식으로 소유권을 다시 받을 수 있다

빌림을 사용해 소유권 공유

fn main() {
    let mut s = String::from("Hello world");
    push_str(&mut s);
    println!("{}", s); // value borrowed here after move
}

fn push_str(s: &mut String) {
    s.push_str("!!");
}
  • 빌림은 & 키워드를 사용한다
  • 문자열 s의 소유권을 이전하지 않고 참조를 전달해 문자열의 내용을 변경한다
  • 빌림은 값은 전달하되 소유권을 유지하고 싶은 경우 사용할 수 있다

데이터 복제

fn main() {
    let mut x = 10;
    let y = x;
    println!("x: {}, y: {}", x, y);

    x = 20;
    println!("x: {}, y: {}", x, y);
}
  • 위 코드는 clone 함수를 사용하지 않고도 데이터를 복제한다
  • 이는 i32 타입에 copy trait이 구현되어 있기 때문이다

copy trait

#[derive(Copy, Clone)]
struct Point {
    x: i32,
    y: i32
}

fn main() {
    let mut p1 = Point {x: 1, y: 2};
    let p2 = p1;
    p1.x = 5;
}
  • Point 구조체에 copy trait을 적용하면 소유권을 이관하는 대신 값을 복제한다
  • copy trait이 구현되지 않은 경우 clone함수를 사용할 수 있다

동적 메모리 할당

  • 동적 메모리 할당은 런타임에 메모리를 할당하는 것이다

Box

fn main() {
    let mut x = Box::new(10);
    *x = 20
}
  • Box를 사용해 메모리를 동적으로 할당받을 수 있다
  • 일종의 스마트 포인터 역할을 한다
  • 할당받은 메모리에 접근할 때는 * 키워드를 사용해야 한다
  • 동적으로 생성한 메모리는 해제해주지 않아도 데이터가 더이상 사용되지 않을 때 알아서 해제된다

Rc

use std::rc::Rc;

fn main() {
    let num0 = Rc::new(100);

    let num1 = num0.clone();

    let num2 = num1.clone();

    println!("{}", Rc::strong_count(&num0)); // 3
}
  • Rc는 Reference Counting Pointer의 약자이다
  • Rc는 불변성을 가진 참조형으로 공유 데이터를 변경할 수 없다
  • Rc로 관리되는 데이터는 공유가 가능해 여러 변수가 동일한 값을 참조할 수 있도록 한다
    • 즉, clone을 해도 실제 값이 복사되는게 아니라 ref count만 늘어난다
  • 이때 Rc는 참조하는 변수들이 존재하는 동안 값을 해제하지 않는다
  • Box는 빌림 방식 외에는 공유가 불가능하지만, Rc는 공유가 가능하다

RefCell

use std::{cell::RefCell, rc::Rc};

struct Node {
    next: RefCell<Option<Rc<Node>>>
}

fn main() {
    let head = Rc::new(Node { next: RefCell::new(None) });
    let new_node = Rc::new(Node { next: RefCell::new(None) });

    let mut next = head.next.borrow_mut(); // head의 next에 대한 가변 참조를 얻음
    *next = Some(new_node.clone());
}
  • Rc가 불변성이므로 Rc만으로는 공유 데이터를 수정할 수 없다
  • RefCell을 사용하면 변경 불가능한 변수를 임시로 변경 가능하게 할 수 있다
use std::{cell::RefCell};

fn main() {
    let num = RefCell::new(0);
    {
        let mut mut_num = num.borrow_mut();
        *mut_num = 10;
    } // mut_num의 scope가 끝나고 mut_num이 더이상 소유권을 빌리지 않아 값을 읽을 수 있음
    println!("{}", num.borrow()); // 내부 값을 불변 참조
}

약한 참조

use std::{cell::RefCell, rc::Rc};

struct Node {
    id: i32,
    next: RefCell<Option<Rc<Node>>>,
}

impl Drop for Node {
    fn drop(&mut self) {
        println!("{} is drop", self.id);
    }
}

fn main() {
    let p1 = Rc::new(Node {id: 1, next: RefCell::new(None)});
    let p2 = Rc::new(Node {id: 2, next: RefCell::new(None)});
    let p3 = Rc::new(Node {id: 3, next: RefCell::new(None)});
    
    let mut next = p1.next.borrow_mut();
    *next = Some(p2.clone());

    let mut next = p2.next.borrow_mut();
    *next = Some(p1.clone());
}
  • Rc는 ref count가 0이 되면 데이터를 삭제한다
  • p1과 p2는 서로 순환참조 관계이다
  • main 함수가 끝나고 p3는 drop이 호출되지만 p1과 p2는 drop이 호출되지 않고 메모리 누수가 발생한다
use std::{cell::RefCell, rc::{Rc, Weak}};

struct Node {
    id: i32,
    next: RefCell<Option<Weak<Node>>>,
}

impl Drop for Node {
    fn drop(&mut self) {
        println!("{} is drop", self.id);
    }
}

fn main() {
    let p1 = Rc::new(Node {id: 1, next: RefCell::new(None)});
    let p2 = Rc::new(Node {id: 2, next: RefCell::new(None)});
    let p3 = Rc::new(Node {id: 3, next: RefCell::new(None)});
    
    let mut next = p1.next.borrow_mut();
    *next = Some(Rc::downgrade(&p2));

    let mut next = p2.next.borrow_mut();
    *next = Some(Rc::downgrade(&p1));
}
  • Weak을 사용하면 순환참조에 의한 메모리 누수를 해결할 수 있다

lifetime 지시자

fn func<'a>(arg: &'a str) -> &'a str {
    // ...
}

struct MyStruct<'a> {
    name: &'a str,
}
  • lifetime 지시자는 변수를 대여할 때 대여기간을 명시적으로 지정하는데 사용한다
  • lifetime은 '를 사용해서 정의한다
  • lifetime 지시자는 관례적으로 a, b, c같은 알파벳을 사용한다
fn longest(x: &str, y: &str) -> &str { // missing lifetime specifier
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
  • 위 함수에서는 lifetime 매개변수가 없다는 컴파일 오류가 발생한다
  • 함수가 반환하는 변수의 생명주기가 매개변수와 동일하므로 같은 lifetime 매개변수를 사용해야 한다
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
  • 위와 같이 'a lifetime 매개변수를 추가하면 정상적으로 동작한다

정적변수

static GLOBAL_CONST: i32 = 10;

fn main() {
    let str: &'static str = "Hello";
}
  • 정적변수는 프로그램이 종료될 때 까지 메모리에 유지되는 변수이다
  • 특정 변수를 &'static로 선언하거나
  • 전역변수를 static으로 선언해서 만들 수 있다

DoubleLinkedList 예시

use std::{cell::RefCell, rc::Rc};

type NodeType = Option<Rc<RefCell<Node>>>;
struct Node {
    item: i32,
    prev: NodeType,
    next: NodeType,
}

impl Node {
    fn new(item: i32) -> Self {
        Self {item, prev: None, next: None}
    }
}

struct DoubleLinkedList {
    head: NodeType,
    tail: NodeType
}

impl DoubleLinkedList {
    fn new() -> Self {
        Self {head: None, tail: None}
    }

    fn push_back(&mut self, item: i32) {
        let node = Rc::new(RefCell::new(Node::new(item)));

        if let Some(tail) = self.tail.take() {
            tail.borrow_mut().next = Some(Rc::clone(&node));
            node.borrow_mut().prev = Some(tail);
            self.tail = Some(node);
        } else {
            self.head = Some(Rc::clone(&node));
            self.tail = Some(Rc::clone(&node));
        }
    }

    fn push_front(&mut self, item: i32) {
        let node = Rc::new(RefCell::new(Node::new(item)));

        if let Some(head) = self.head.take() {
            head.borrow_mut().prev = Some(Rc::clone(&node));
            node.borrow_mut().next = Some(head);
            self.head = Some(node);
        } else {
            self.head = Some(Rc::clone(&node));
            self.tail = Some(Rc::clone(&node));
        }
    }

    fn print_all(&self) {
        let mut cur = match &self.head {
            Some(node) => {
                node.clone()
            },
            None => return
        };

        loop {
            let borrowed_cur = cur.borrow();
            println!("item: {}", borrowed_cur.item);
            let next = match &borrowed_cur.next {
                Some(next) => next.clone(),
                None => break
            };
            drop(borrowed_cur);
            cur = next;
        }
    }
}

fn main() {
    let mut list = DoubleLinkedList::new();

    list.push_back(1);
    list.push_back(2);
    list.push_back(3);
    list.print_all();

    list.push_front(0);
    list.print_all();
}
  • DoubleLinkedList의 print_all 함수의 loop부분을 보자
    • 우선 loop 바깥의 cur의 소유권을 borrowed_cur로 빌려온다
    • borrowed_cur를 사용하다가 cur을 next로 바꾸기 위해 borrowed_cur가 빌려온 소유권을 drop으로 해제한다
    • 그럼 소유권은 다시 cur이 갖게 되고, cur의 값을 next로 바꿀 수 있다

0개의 댓글