Rust

RUST Documentation 따라 공부하기 6

2019.12.30


Ownership

It enables Rust to make memory safety guarantees without needing a garbage collector.

In Rust, memory is managed through a system of ownership with a set of rules that the compiler checks at compile time. None of the ownership features slow down the program while it's running.

The Stack and the Heap

  • The stack stores values in the order it gets them and removes the values in the opposite order. (Last In First Out)

  • Adding data is called pushing onto the stack, and removing data is called popping off the stack.

  • All data stored on the stack must have a known, fixed size.

  • Data with an unknown size at compile time or a size that might change must be stored on the heap instead.

  • When I put data on the heap, the OS finds an empty spot in the heat, marks it as being in use, and returns a pointer, which is the address of that location.

  • This process is called allocating on the heap and is somtimes abbreviated as just allocating. (Pushing values onto the stack is not considered allocating.)

  • Because the pointer is a known, fixed size, I can store the pointer on the stack, but when I want the actual data, I must follow the pointer.

  • Pushing to the stack is faster than allocating on the heap. ( The OS never has to search for a place to store new data; that location is always at the top of the stack. )

  • Accessing data in the heap is slower than accessing data on the stack. ( I have to follow a pointer to get there. )

Keeping track of what parts of code are using what data on the heap, minimizing the amount of duplicate data on the heap, and cleaning up unused data on the heap so you don’t run out of space are all problems that ownership addresses. Once you understand ownership, you won’t need to think about the stack and the heap very often, but knowing that managing heap data is why ownership exists can help explain why it works the way it does.

Ownership Rules

  • Each value in Rust has a variable that's called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

The String Type

let s = "hello"; // string literals

let s = String::from("hello"); // immutable string

let must s = String::from("hello"); // mutable
s.push_str(", world!");
println!("{}", s);
  • The double colon(::) is an operator that allows us to namespace this particular from function under the String type rather than using some sort of name like string_from.
  • String can be mutated but literals cannot. (Why? - Memory and Allocation)

Memory and Allocation

In the case of a string literal, we know the contents at compile time, so the text is hardcoded directly into the final executable. This is why string literals are fast and efficient. But these properties only come from the string literal’s immutability. Unfortunately, we can’t put a blob of memory into the binary for each piece of text whose size is unknown at compile time and whose size might change while running the program.

  • With the String type, in order to support a mutable, growable piece of text, we need to allocate an amount of memory on the heat, unknown at compile time, to hold the contents.
  • The memory must be requested from the OS at runtime. ( String::from )
  • We need a way of returning this memory to the OS when we're done with our String.
    • In languages with a Garbage Collector(GC) keeps track and cleans up memory that isn't being used anymore.
    • Without a GC, it's our responsibility to identify when memory is no longer being used and call code to explicitly return it.
    • Doing this correctly is important.
    • Rust takes a different path: the memory is automatically returned once the variable that owns it goes out of scope.
    • Rust calls drop automatically at the closing curly bracket.

Ways Variables and Data Interact: Move

let x = 5;
let y = x;

let s1 = String::from("hello");
let s2 = s1; // this doesn't work like what we expected.

String is made up of three parts: pointer, length, capacity.

  • length is how much memory, in bytes, the contents of the String is currently using.

  • capacity is the total amount of memory, in bytes, that the String has received from the OS.

  • When we assign s1 to s2, the String data is copied, meaning we copy the pointer, the length, and the capacity that are on the stack. ( We do not copy the data on the heat that the pointer refers to. )

  • When s2 and s1 go out of scope, they will both try to free the same memory. It is called double free error. Freeing memory twice can lead to memory corruption.

  • So, Rust considers s1 to no longer be valid and, therefore, Rust doesn't need to free anything when s1 goes out of scope.

    fn main() {
      let s1 = String::from("hello");
      let s2 = s1;
      
      println!("{}, world!", s1); // error : value used here after move
      println!("{}, world!", s2); // it's okay
    }
    
  • The concept of copying the pointer, length, and capacity without copying the data probably sounds like making a shallow copy.

  • Rust invalidates s1, instead of being called a shallow copy, it's known as a move.

  • In this case, only s2 valid. So, when it goes out of scope, it alone will free the memory.

  • If we do want to deeply copy the heap data of the String, not just the stack data, we can use a common method called clone.

    fn main() {
      let s1 = String::from("hello");
      let s2 = s1.clone();
      
      println!("s1 = {}, s2 = {}", s1, s2);
    }
    
  • How about integer?

    Types such as integers that have a known size at compile time are stored entirely on the stack, so copies of the actual values are quick to make.

  • Rust has a special annotation called the Copy trait.

    • If a type has the Copy trait, an older variable is still usable after assignment.

    • Rust won;t let us annotate a type with the Copy trait if the type, or any of its parts, has implemented the Drop trait.

    • Types that are Copy

      • All the integer types, such as u32.

      • The Boolean type, bool.

      • All the floating point types, such as f64.

      • The character type, char.

      • Tuples, if they only contain types that are also Copy

        ex) (i32, i32) is Copy. (i32, String) is not.

Ownership and Functions

fn main() {
  let s = String::from("hello");
  takes_ownership(s); // s's value moves into the function
  // s is no longer valid here

  let x = 5;
  makes_copy(x); // i32 is Copy
  // it's okay to still use x
}

fn takes_ownership(some_string: String) {
  println!("{}", some_string);
} // some_string goes out of scope and 'drop' is called. -> memory is freed.

fn makes_copy(some_integer: i32) {
  println!("{}", some_integer);
}

Return Values and Scope

Returning values can also transfer ownership.

fn main() {
  let s1 = gives_ownership(); // this function moves its return values into s1
  
  let s2 = String::from("hello"); // s2 comes into scope
  
  let s3 = takes_and_gives_back(s2); // s2 is moved into fn,
  // also moves its return value into s3
} // s3 goes out of scope, -> drop,
// s2 goes out of scope, moved -> nothing happens
// s1 goes out of scope -> dropped


fn gives_ownership() -> String {
  let some_string = String::from("hello");
  some_string
}

fn takes_and_gives_back(a_string: String) -> String {
  a_string
}

Taking ownership and then returning ownership with every function is a bit tedious.

What if we want to let a function use a value but not take ownership?

It's possible to return multiple values using a tuple.

fn main() {
  let s1 = String::from("hello");
  
  let (s2, len) = calculate_length(s1);
  println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s1: String) -> (String, usize) {
  let length = s.len();
  (s, length)
}