대여(Borrowing)란?

  • 어떤 변수가 가진 값에 대한 소유권을 다른 변수에게 빌려줌으로써 소유권의 이동없이 다른 스코프에서도 해당 값을 사용 가능하도록 하는 것
  • 즉, 값에 대한 레퍼런스를 넘기는 것
  • 함수의 매개변수로 특정 변수를 전달하고, 그 변수가 참조하는 값을 다시 사용하기 위해서는 함수로부터 소유권을 반환 받아야 한다
  • 그러나 레퍼런스로 전달, 다시 말해 소유권을 빌려주면 함수가 소유권을 반환 할 필요가 없다.
// 1. 매개변수로 값의 소유권까지 전달하는 경우
fn main() {
    let str_1 = String::from("yjk");
    let (str_2, length) = calculate_length(str_1); // 변수 str_1이 참조하는 값("yjk")의 소유권은 변수 str_2로 이동한다

    println!("str_2 : {str_2}, length : {length}"); // 출력 -> str_2 : yjk, length : 3
}

fn calculate_length(str: String) -> (String, usize) { // 매개변수로 전달된 String값을 다시 사용하기 위해 소유권을 함수에서 반환한다
    let length = str.len();

    (str, length)
}

// 2. 매개변수로 레퍼런스를 전달하는 경우 (소유권 빌림)
fn main() {
    let str_1 = String::from("yjk");
    let length = calculate_length(&str_1); // 매개변수로 레퍼런스를 전달한다. 즉, 소유권을 빌려줬기 때문에 이후로도 변수 str_1은 유효하다.

    println!("str_1 : {str_1}, length : {length}"); // 출력 -> str_1 : yjk, length : 3
}

fn calculate_length(str: &String) -> usize {
    str.len()
}

대여한 소유권으로 값을 수정할 수 있을까?

  • 불가능함. 변수가 기본적으로 불변성을 지니듯 레퍼런스가 참조하는 것도 수정할 수 없다.
  • 그러나 가변 참조자(mutable reference)를 사용해서 수정할 수 있다.
fn main() {
    let name = String::from("hello");
    add_string(&name);
}

fn add_string(target: &String) {
    target.push_str(", world"); // 컴파일 에러 발생
}

가변 참조자(mutable reference)

  • 원본을 수정할 수 있는 레퍼런스. &mut키워드를 붙여 가변 참조자를 생성한다.
    • let a_ref = &mut a : 변수 a에 대한 가변 참조자를 a_ref에 저장. 즉, 값을 수정할 수 있는 소유권을 빌려준다.
  • 가변 참조자를 생성하려면 소유권을 가진 변수가 가변(mut)이어야 한다.
  • 가변 참조자는 단 하나만 존재할 수 있다. (불변 참조자는 여러 개 존재 가능)
    • 단, 가변 참조자가 여러 개 존재해도 사용되지 않는다면 컴파일은 가능하다
  • 불변 참조자가 있다면 가변 참조자를 생성할 수 없다
fn main() {
    let mut name = String::from("YJK");
    add_string_by_mutref(&mut name);
    println!("name : {name}"); // name : YJK490 출력

    let a = &mut name;
    let b = &mut name;
    // println!("x : {a}, y : {b}"); // 주석 해제 시 컴파일 에러 발생, 가변 참조자는 단 하나만 존재해야하기 때문
}

fn add_string_by_mutref(target: &mut String) {
    target.push_str("490");
}

불변 참조자와 가변 참조자가 같은 스코프에 있을 때

  • 기본적으로 같은 스코프 내에 불변 참조자가 있으면 가변 참조자를 생성할 수 없다
  • 그러나 불변 참조자의 소유권이 이동하여 해당 변수가 유효하지 않게 되면 가변 참조자를 사용할 수 있다
let mut s = String::from("hello"); 
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); // r1, r2가 println!()의 매개변수로 전달됨으로써 소유권이 이동하여 더 이상 유효하지 않게됨

let r3 = &mut s;
println!("{}", r3); // r3 사용 가능
  • println!()함수에서 r1, r2의 소유권을 따로 반환하지 않으므로 r1, r2의 소유권은 해제된다.
  • 따라서 가변 참조자인 r3를 생성할 수 있다.

가변 참조자를 단 하나로 제한함으로써 얻는 이점

  • 컴파일 타임에 다음 세 가지 상황에서 발생하는 데이터 경합(data race)을 방지함
    • 둘 이상의 포인터가 동시에 같은 데이터에 접근
    • 포인터 중 하나 이상이 데이터 쓰기 작업 실행
    • 데이터 접근 동기화 메커니즘이 없음
  • 데이터 경합은 예기치 못한 동작을 일으키며 런타임에 추적하기가 어려움
  • 그러나, 러스트에서는 데이터 경합이 발생할 가능성이 있는 코드의 컴파일을 거부함으로써 이 문제를 방지
  • 즉, 데이터 경합 문제를 문법적 수준에서 막음

Rust의 댕글링 참조 관리

  • 댕글링 포인터(dangling pointer)란 이미 해제된 메모리를 가리키고 있는 포인터다.
  • 댕글링 포인터가 남아있으면 다른 객체가 할당받은 메모리 주소를 참조하게 되어 프로그램의 의도치 않은 동작을 일으킴
  • Rust에서는 어떤 데이터의 참조자를 생성하면, 참조자가 스코프를 벗어나기 전에 해당 데이터의 소유권을 가진 변수가 스코프를 벗어나는지 컴파일러에서 검사함으로써 댕글링 포인터가 생성되는 것을 방지한다.
fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let str = String::from("dangle");

    // &str // 주석 해제 시 컴파일에러 발생, 함수가 종료되는 시점에 원본을 가리키는 변수 str은 해제되기 때문. 따라서 str을 그대로 반환해야 함
}

'Rust' 카테고리의 다른 글

[Rust] Slice  (0) 2024.05.02
[Rust] &str vs String : 문자열을 표현하는 두 타입 비교  (0) 2024.04.25
[Rust] 소유권  (0) 2024.04.24
[Rust] 제어문 : 분기 처리와 반복  (0) 2024.04.16
[Rust] 함수  (0) 2024.04.15

+ Recent posts