[RUST] 4. 소유권

    소유권이란?

    러스트는 다른 언어들과 달리 가비지 콜렉터를 사용하거나, 명시적 메모리 할당 해제 하는 방법으로 메모리를 관리하지 않는다.

    소유권 규칙

    • 러스트가 다루는 각각의 값은 소유자(owner)라고 부르는 변수를 가지고 있다.
    • 특정 시점에 값의 소유자는 단 하나뿐이다.
    • 소유자가 범위를 벗어나면 그 값은 제거된다.

    변수의 범위

    변수의 범위(scope)는 선언된 지점 부터 현재의 범위를 벗어나기 전까지 유효하다. 범위와 변수의 유효성 사이의 관계는 다른 프로그래밍 언어와 크게 다르지 않다.

    {                       //변수 s를 아직 선언하지 않았다
        let s = "hello";    //변수 s는 이 지점부터 유효
    
        //변수 s를 이용해 필요한 동작을 수행
    }   //범위를 벗어나므로 변수 s는 이제 유효하지 않음

    String 타입

    String타입은 힙에 할당되어 컴파일 시점에 알 수 없는 크기의 문자열을 저장할 수 있다. 다음과 같이 from 함수를 이용하면 문자열 리터럴을 이용해 String 인스턴스를 생성할 수 있다.

    let mut s = String::from("hello");
    
    s.push_str(" , world!");
    
    println!("{}", s);

    메모리와 할당

    변수와 데이터가 상호작용하는 방식: 이동(Move)

    let x = 5;
    let y = x;

    위 코드는 x값의 복사본을 변수 y에 대입하는 코드이다. 고정 크기의 값이기에 5라는 값 두개가 스택에 저장된다.

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

    위 코드는 일반적으로 s1을 s2 변수에 복사한다고 생각이 되지만 러스트에서는 메모리 안정성 확보를 위해 할당된 메모리를 복사하는 대신 변수 s1이 더 이상 유효하지 않다고 판단하여 s1이 범위를 벗어날 때에도 메모리를 해제 하지 않는다. 복사후 s1을 다시 참조하면 코드가 동작하지 않고 에러 메시지가 발생한다.

    다른언어의 얕은 복사와 깊은 복사 개념이 있는데 실제 데이터를 복사하지 않고 포인터와 길이, 용량만을 복사하는 동작이 얕은 복사와 유사하다고 생각할 수 있다. 그러나 러스트는 s1변수를 무효화 해버리므로 얕은 복사라고 하지 않고 이동(move)라고 한다.

    변수와 데이터가 상호작용하는 방식: 복제(Clone)

    만일 힙 메모리에 저장된 String 데이터를 복사하기를 원한다면 clone이라는 공통 메서드를 사용하자.

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

    스택 전용 데이터: 복사(copy)

    러스트는 스택에 저장되는 정수형 같은 타입에 적용할 수 잇는 Copy 트레이트(trait)를 제공한다. Copy 트레이트는 다음과 같은 타입에 적용된다.

    • u32와 같은 모든 정수형 타입
    • true, false 값만 가지는 bool 타입
    • 문자타입, char
    • f64와 같은 모든 부동 소수점 타입
    • Copy 트레이트가 적용된 타입을 포함하는 튜플

    소유권과 함수

    값을 함수에 전달한다는 의미는 값을 변수에 대입하는 것과 유사하다. 변수를 함수에 전달하면 대입과 마찬가지로 값의 이동이나 복사가 이루어진다.

    fn main() {
        let s = String::from("hello");  //변수 s가 범위내 생성
        takes_ownership(s);             //변수 s가 함수내로 이동
                                        //여기서 부터 변수 s는 유효하지 않음
    
        let x = 5;                      //변수 x가 범위내 생성
        makes_copy(x);                  //변수 x의 값이 함수 내로 이동
        //하지만 i32 타입은 복사를 수행하므로 이시점에서도 여전히 유효
    }
    
    fn takes_ownership(some_string: String) {
        println!("{}", some_string);
    }   //some_string 변수는 범위가 벗어나며 메모리가 해제된다.
    
    fn makes_copy(some_integer: i32) {
        println!("{}", some_integer);
    }

    리턴 값과 범위

    리턴값도 소유권을 이전한다.

    fn main() {
        let s1 = gives_ownership();     //gives_ownership 함순의 리턴값이 옮겨짐
    
        let s2 = String::from("hello"); //범위내 생성
    
        let s3 = takes_and_gives_back(s2);  //s2가 함수로 옮겨간 후 리턴값은 변수 s3로 옮겨진다
    }
    
    fn gives_ownership() -> String {
        let some_string = String::from("hello");
        some_string
    }
    
    fn takes_and_gives_back(a_string: String) -> String {
        a_string
    }

    튜플을 이용하여 여러값을 리턴할 수도 있다.

    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)
    }

    참조와 대여

    앞선 예제처럼 값의 소유권을 가져오는 대신, 매개변수로 전달된 객체의 참조를 이용하도록 calculate_length 함수를 다시 작성할 수 있다.

    fn main() {
        let s1 = String::from("hello");
        let len = calculate_length(s1);
    
        println!("`{}` 의 길이는 {} 입니다.", s2, len);
    }
    
    fn calculate_length(s: &String) -> usize {
        s.len()
    }

    참조는 소유권을 갖지 않기 때문에 참조가 가리키는 값은 참조가 범위를 벗어나더라도 drop 함수가 호출되지 않는다. 이처럼 매개변수로 참조를 전달하는 것을 대여(borrowing)라고 한다.

    그리고 대여한 변수를 사용하려고 하면 에러가 발생한다.

    fn main() {
        let s = String::from("hello");
        change(&s);
    }
    
    fn change(some_string: &String) {
        some_string.push_str(", world!");
    }

     

    변수가 기본적으로 불변인 것처럼 참조도 기본적으로 불변이다.

    가변 참조

    위 코드를 조금만 수정하면 에러를 수정할 수 있다.

    fn main() {
        let mut s = String::from("hello");
    
        change(&mut s);
    }
    
    fn change(some_string: &mut String) {
        some_string.push_str(", world");
    }

    가변 참조에도 한가지 제약이 있다. 특정 범위 내의 특정 데이터에 대한 가변 참조는 오직 한 개만 존재해야한다. 따라서 다음 코드는 실행되지 않는다.

    let mut s = String::from("hello");
    
    let r1 = &mut s;
    let r2 = &mut s;
    println!("{}, {}", r1, r2);

     

    데이터 경합(data races)을 컴파일 시점에 방지하는데 다음과 같은 동작으로 발생한다.

    • 둘 이상의 포인터가 동시에 같은 데이터를 읽거나 쓰기 위해 접근할 때
    • 최소한 하나의 포인터가 데이터를 쓰기 위해 사용될 때
    • 데이터에 대한 접근을 동기화할 수 있는 메커니즘이 없을 때

    죽은 참조

    포인터를 사용하는 언어는 죽은 포인터로 인해 에러가 발생한다. 죽은 포인터란 이미 해제되어 다른 정보를 저장하도록 변경된 메모리를 참조하는 포인터를 말한다. 러스트는 죽은 참조가 발생하지 않도록 컴파일러가 보장해 준다.

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

     

    위 코드를 해결하기 위해서는 &s 가 아닌 s 를 리턴하여 소유권을 이동할 수 있도록 하여 해결할 수 있다.

    참조에 대한 규칙

    • 어느 한 시점에 코드는 하나의 가변 첨조 또는 여러개의 불변 참조를 생성할수 있지만 둘 모두를 생성할 수 없다.
    • 참조는 항상 유효해야 한다.

    슬라이스 타입

    슬라이스도 소유권을 갖지 않는 타입이다. 슬라이스를 이용하면 컬렉션 내의 연속된 요소들을 참조할 수 있다.

    문자열 슬라이스

    let s = String::from("hello world");
    
    let hello = &s[0..5];
    let world = &s[6..11];

    대괄호 안에 [starting_index..ending_index]를 지정해서 범위를 이용해 슬라이스를 생성할 수 있다. starting_index항목이 비워져 있으면 첫번째(0)인덱스 부터 탐색을 ending_index가 비워져 있으면 마지막 인덱스까지 탐색한다. &str 타입은 문자열 슬라이스를 표현한다.

    fn main() {
        let mut s = String::from("hello world");
    
        let word = first_word(&s);
    
        s.clear(); //에러
        println!("the first word is: {}", word);
    }
    
    fn first_word(s: &String) -> &str {
        let bytes = s.as_bytes();
    
        for( i, &item) in bytes.iter().enumerate() {
            if item == b' ' {
                return &s[0..i];
            }
        }
    
        &s[..]
    }

     

    문자열 리터럴은 슬라이스다

    let s = "hello world";

    변수 s의 타입은 &str이다. 바이너리의 어느 한 지점을 가리키는 슬라이스라는 뜻이다. 문자열 리터럴은 항상 불변이다.

    문자열 슬라이스 매개변수

    fn first_word(s: &str) -> &str {

    위 경우 문자열 슬라이스는 함수에 직접 전달할 수 있다.

    fn main() {
        let my_string = String::from("hello world");
    
        let word = first_word(&my_string[..]);
    
        let my_string_literal = "hello world";
    
        //둘다 동일 하며 정상적으로 동작한다.
        let word = first_word(&my_string_literal[..]);
        let word = first_word(my_string_literal);
    }

    다른 타입의 슬라이스

    문자열 슬라이스는 문자열에 특화된 것이다. 더 보편적인 슬라이스 타입도 제공이 된다.

    let a = [1, 2, 3, 4, 5];
    let slize = &a[1..3];

    위 슬라이스는 &[i32] 타입이다.

    요약

    소유권, 대여, 슬라이스 개념은 러스트 프로그램의 컴파일 시점에 메모리 안정성을 확보하기 위한 개념들이다. 메모리의 안전한 사용이나 디버깅을 위한 추가 코드를 전혀 작성할 필요가 없다.

    반응형

    댓글

    Designed by JB FACTORY