본문 바로가기
dev/rust

Rust - Understading Ownership

by igooo 2024. 6. 2.
728x90

What Is Ownership?

Ownership is a set of rules that govern how a Rust program manages memory. 

소유권은 Rust에 유니크한 특징이며, GC(Garbage Collector)없이 메모리 안정성을 보장하게 해준다.

 

The Stack and the Heap

일반적인 프로그래밍 언어에서는 스택과 힙에 대해서 고민할 필요가 별로 없지만 Rust와 같은 시스템 프로그래밍 언어에서는 값이 스택에 있는지 힙에 있는 여부가 동작 방식에 큰 영향을 준다. 스택과 힙 둘다 코드상에서 런타임에 사용할 수 있는 메모리지만, 스택은 데이터에 접근하는 방식 때문에 힙보다 빠르다. (단 컴파일 탕미에 데이터 사에즈를 알 수 없는 데이터는 스택에 저장 될 수 없다.)

힙에 저장된 데이터를 접근하기 위해서는 포인터가 가리킨 곳을 따라가서 접근해야 하기 때문에 스택 보다 느리다.

 

Owership Rules

  • Rust의 각각의 값은 owner를 가지고 있다. (Eaceh value in Rust has an owner.)
  • 한번에 오직 하나의 owner만 존재한다. (There can only one owner at a time.)
  • owner는 스코프 밖으로 나갈때, 값은 drop된다. (When the owner goes out of scope, the value will be dropped.)

 

Variable Scrope

 {                         // s is not valid here, it’s not yet declared
        let s = "hello";   // s is valid from this point forward
        // do stuff with s
}                      // this scope is now over, and s is no longer valid
  • 스코프 안에서 s가 등장하면 유효하다.
  • 요효성은 스코프 밖으로 나가기 전가지 유지된다.

 

The String Type

Rust가 힙에 저장되는 데이터르 어떻게 저장하고, 조회하며 clean up 하는지 살펴볼 필요가 있다.

String 리터럴은 프로그램 안에 하드코딩 되어있고, 불변(immutable)하다.

그래서 Rust에서는 문자열 타입인 String을 제공한다.

let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String

println!("{}", s); // This will print `hello, world!`

 

 

Memory and Allocation

  • 런타임에 메모리가 요청되야 한다. (The memory must be requested from the memory allocator at runtime.)
  • 사용이 끝난 String은 할당된 메모리를 반납해야 한다. (We need a way of returning thie memory to the allocator when we`re done with our String.)

String Type은 mutable하고, 컴파일 타미에 알 수 없는 크기의 메모리 공간을 할당받아 데이터를 저장할 필요가 있다.

첫번째는 프로그래머가 String::from을 호출하여 직접 수행한다.

두번재는 다르다. GC를 가지고 있는 언어는 GC가 사용하지 않는 메모리를 찾아 지워주고, GC가 없는 경우 프로그래머가 필요 없는 시점에 해제해야한다.

{
  	let s = String::from("hello"); // s is valid from this point forward
	// do stuff with s
}                                  // this scope is now over, and s is no longer valid

s 변수가 스코프 밖으로 나가면, Rust는 drop 함수를 자동으로 호출한다.

 

 

Ways Varialbes and Data Interact: Move

let x = 5;
let y = x;

// print
5
5

x 변수에 5를 할당하고, x 값에 복사본을 만들어 y에 할당한다.

let s1 = String::from("hello");
let s2 = s1;

위 코드와 비슷해 보이지만 String 타입은 어떻게 동작하는지 알아보자.

Representation in memory of a String holing the value "Hello" bound to s1

s1 변수에 hello 값이 저장된 String 메모리 구조 
ptr: 내용물을 담고 있는 메모리의 포인터
len : 길이
capacity : 용량

 

Representation in memory of the variable s2 that has a copy of the pointer, length, and capacity of s1



s2에 s1을 대입하면  String 데이터가 복사되는데, 이는 스택에 있는 포인터, 값, 그리고 용량값이 복사된다는 의미고, 포인터가 가리크고 있는 힙 메모리 상의 데이터는 복제되지 않는다.

 

Rust에서 변수가 스코프 밖으로 벗어날 때, Rust는 자동으로 drop 함수를 호출하여 해당 변수가 사용하는 힙 메모리를 제거한다. 하지만 위 예제에서 s1, s2 두 데이터 포인터가 모두 같은 곳을 가리키고 있는 것이 보이는데 이는 문제가 될 수 있다. s1, s2가 스코프 밖으로 벗어나게 되면 둘다 같은 메모리를 해제하려고 한다. (double free:  메모리 손상, 보안 취약성)

 

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

// error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
// 컴파일러에서 유요하지 않는 참조자 사용을 방지한다.

Rust에서는 메모리 안정성을 보장하기 위해서 s1이 더 이상 유효하지 않다고 간주하고, s1이 스코프 밖으로 벗어났을 때 아무것도 해제하지 않는다. 데이터의 복사 없이 포인터와 길이값, 용량값만 복사하고 벼수를 무효화 시키는것을 move라고 말한다.

 

s1이 무효화된 후 메모리 구조

 

  • shallow copy : 주소를 복사해서 공유
  • deep copy : 실제 값을 메모리 공간에 복사

 

Ways Varialbes and Data Interact: Clone

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

// print
s1 = hello, s2 = hello

clone 함수 호출은 비용이 크다.

 

Stack-Only Data: Copy

let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);

let s1 = "AAA";
let s2 = s1;
println!("{}, {}", s1, s2);

// print
5, 5
AAA, AAA

정수형과 같이 컴파일 타임에 크기가 결정되어있는 타입은 스택에 저장되기 때문에 실제 값의 복사본이 빠르게 만들어 질 수 있다. 이는 변수 y, s2가 생성된 후 x, s1이 더 이상 유효하지 않도록 할 이유가 없다.

Rust는 정수형 같이 스택에 저장할 수 있는 타입에 대하여 달 수 있는  Copy 트레잇(trait)이라고 말하는 어노테이션을 가지고 있다. Copy 트레잇을 가지고 있으면, 대입 과정 후에도 예전 변수를 사용할 수 있고, Drop 트레잇을 구현한 것이 있으면 Copy 트레잇을 사용할 수 없다.

  • All the interger types, such as u32
  • The Boolean type, bool, with values true and false.
  • all the floating porinter types, such as f64
  • The character type, char.
  • Tuples, if they only contain types that also implement Copy. For excample (i32, i32) implements Copy, but(i32, String) does not.

 

Ownership and Functions

함수에 값을 넘기는 것은 값을 변수에 대입하는 것과 유사하다.

fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // x would move into the function,
                                    // but i32 is Copy, so it's okay to still
                                    // use x afterward

} // Here, x goes out of scope, then s. But because s's value was moved, nothing
  // special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.

 

 

Return Values and Scope

fn main() {
    let s1 = gives_ownership();         // gives_ownership moves its return
                                        // value into s1

    let s2 = String::from("hello");     // s2 comes into scope

    let s3 = takes_and_gives_back(s2);  // s2 is moved into
                                        // takes_and_gives_back, which also
                                        // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
  // happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {             // gives_ownership will move its
                                             // return value into the function
                                             // that calls it

    let some_string = String::from("yours"); // some_string comes into scope

    some_string                              // some_string is returned and
                                             // moves out to the calling
                                             // function
}

// This function takes a String and returns one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
                                                      // scope

    a_string  // a_string is returned and moves out to the calling function
}

 

 

References and Borrowing

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
  // it refers to, it is not dropped.
  
  fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

소유권을 넘기지 않고 객체데 대한 references(참조자)를 인자로 사용할 수 있다.

&s1 문법은 s1의 값을 참조하지만 소유하지는 않는 reference를 생성하도록 해준다.

함수 시그니처도 &를 사용하여 파라미터 s의 타입이 reference라는 것을 표시한다.

함수의 파라미터로 reference를 만드는 것을 borrowing(빌림)이라고 한다.

 

fn calculate_length(s: &String) -> usize {
	s.push_str(" World");
    s.len()
}

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

// error
  |
8 |     s.push_str(", World");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
  |

 

 

Mutable References

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

// print
hello, world

&mut s로 mutables reference를 생성하고

함수 식니쳐도 &mut String으로 mutable reference를 받도록 선언한다.

mutable reference는 특정한 스코프 내에서 mutable reference를 딱 하나만 만들 수 있다.

 

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

// error
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

Rust가 컴파일 타임에 data race를 방지할 수 있도록 해준다.

 

아래 세 가지 동작이 발생했을때 나타나는 특정한 race condition

  • 두 개 이상의 포인터가 같은 데이터에 접근
  • 그 중 적어도 하나의 포인터가 데이터를 쓰기
  • 데이터에 접근하는데 공기화를 하는 메커니즘이 없다.

Rust에서는 데이터 레이스가 발생할 수 있는 코드가 컴파일 조차 안되기 때문에 문제의 발생을 방지한다.

 

 

Dangling References

어떤 메모리를 가리키는 포인터를 보존하는 동안, 그 메모리를 해제함으로써 다른 객체에게 사용하도록 전달 했을지도 모를 메모리를 참조하고 있는 포인터.

Rust는 참조자가 Dangling References 참조자가 되지 않도록 보장해준다.

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");
    &s
}

// error
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
5 | fn dangle() -> &'static String {
  |                 +++++++
help: instead, you are more likely to want to return an owned value
  |
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

 

s가 dangle 함수 안에서 만들어졌기 때문에, dangle 함수가 끝나면 s는 할당 해제된다. 하지만 s의 reference를 반환하려고 했고, 이것은 reference가 무효화된 String을 가리키게 될  것이다.

 

The Rules of References

  • 어떠한 경우에도 하나의 mutable reference 또는 몇개의 immutable references를 가질수 있다.
  • Reference는 항상 유효해야한다.

 

 

참고

https://doc.rust-lang.org/stable/book/ch04-00-understanding-ownership.html

'dev > rust' 카테고리의 다른 글

Quick Start WebAssambly (wasm)  (1) 2024.06.05