소유권 규칙

  1. 각각의 값은 해당값의 오너owner 라고 불리우는 변수를 갖는다.
  2. 한번에 딱 하나의 오너만 존재한다.
  3. 오너가 스코프 밖으로 벗어날 때, 값은 버려진다.

변수의 스코프

스트링 리터럴의 값은 프로그램의 텍스트 내에 하드코딩 된다.

{
    // s 선언 전
    let s = "hello"; // s 유효
    // ...
} // s는 유효 X

String 타입

  • 힙에 할당되고 컴파일 타임에는 우리가 알 수 없는 양의 텍스트를 저장가능.
  • 스트링 리터럴로부터 from이라는 함수를 이용해서 String을 만들 수 있음.
  • 더블 콜론(::)은 String 타입 아래의 from 함수를 특징하는 네임스페이스 연산자.
let mut s = String::from("hello");
s.push_str(", world!");
println!("{}", s);

Drop

  • 변수가 스코프를 벗어나면 Rust는 drop 함수를 호출.
  • 자원 습득이 곧 초기화 (Resource Acquisition Is Initialization, RAII) 패턴: 아이템의 수명주기의 끝나는 시점에 자원을 해제함.

Copy

let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
// s1
//     ptr: ****
//     len: 5
//     capacity: 5
  • 소유권이 s1이 s2로 이동.
    • 위 코드는 이동 후 s1을 사용하기 때문에 value used here after move 에러가 발생.
  • s2에 s1을 대입하면 String 데이터가 복사되는데, 이는 스택에 있는 포인터, 길이값, 그리고 용량값이 복사된다는 의미.
  • Double Free: s2와 s1이 스코프 밖으로 벗어나게 되면, 둘 다 같은 메모리를 해제하려 한다.
  • 러스트는 결코 자동적으로 데이터에 대한 깊은 복사본을 만들지 않는다.

string_structure.svg

Clone

  • 러스트는 정수형과 같이 스택에 저장할 수 있는 타입에 대해 달수 있는 Copy 트레잇이라고 불리우는 특별한 어노테이션(annotation)을 가지고 있음.
  • 만일 어떤 타입이 Copy 트레잇을 갖고 있다면, 대입 과정 후에도 예전 변수를 계속 사용할 수 있음.
  • Drop 트레잇을 구현한 것이 있다면 Copy 트레잇을 어노테이션 할 수 없게끔 한다.
  • 단순한 스칼라 값들의 묶음은 Copy가 가능하다.
  • Copy가 가능한 타입:
    • u32 같은 정수형 타입
    • bool 타입
    • f64 같은 부동 소수점 타입들
    • Copy 가 가능한 타입만으로 구성된 튜플들
      • (i32, i32)는 가능
      • (i32, String)은 안된다.

clone

let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);

소유권과 함수

  • 함수에게 변수를 넘기면 복사되거나 소유권이 이동된다.
  • 값의 반환은 소유권을 이동시킨다.

ownership_move_before ownership_move

예제 I

fn main() {
    let s = String::from("hello");
    
    takes_ownership(s); // s가 함수로 이동
    // s 는 더이상 유효하지 않음.

    let x = 5;
    makes_copy(x); // i32는 Copy가 되므로 이후 x 유효.
}

// x 스코브 밖으로 나옴. s는 이미 이동되었음으로 아무런 일도 생기지 않음.

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
}

// some_string이 스코프 밖으로 벗어났고 drop이 호출됨. 메모리가 해제된다.

fn makes_copy(some_integer: i32) { // some_integer이 스코프 안으로 이동됨.
    println!("{}", some_integer);
}

// 여기서 some_integer가 스코프 밖으로 벗어나지만 별다른 일이 생기지 않음.

예제 II

fn main() {
    let s1 = gives_ownership(); // 반환값을 s1에게 이동.
    let s2 = String::from("hello");
    let s3 = takes_and_gives_back(s2); // s2가 takes_and_gives_back 안으로 이동. 반환값이 s3으로 이동됨.  
}

// s3는 스코프 밖으로 벗어나며 drop 호출.
// s2는 스코프 밖으로 벗어났지만 이미 이동되었으므로 아무런 일도 생기지 않음.
// s1 스코프 밖으로 벗어나서 drop 호출.
  
fn gives_ownership() -> String {
    let some_string = String::from("hello");
    some_string // some_string 반환, 호출한 쪽으로 이동.
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string // a_string 반환, 호출한 쪽으로 이동.
}

예제 III

값은 사용하되 소유권을 가지지 않기위한 편법: 함수에서 결과를 포함하여 소유권을 리턴.

fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    println!("{} {}", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

힙과 스택

  • 스택
    • 빠르다.
    • 데이터 삽입을 위한 검색이 0: Always Top
    • 삽입될 데이터의 크기가 고정되어 있음
    • 크기가 결정되어있지 않거나 변경될 수 있는 데이터가 존재.
    • 데이터를 힙에 넣을 때, 먼저 저장할 공간이 있는지 물어본다.
    • 힙 데이터에 접근하는 것은 스택보다 느리다. 포인터가 가리킨 곳을 따라가야 하기 때문.
    • 프로세서는 힙보다 붙어있는 스택과 같은 데이터로 작업 하는게 빠르다.

메모리 사용 방식

  1. 직접 수행
    • allocate - free 쌍
  2. 가비지 콜렉터

참고자료