Rust, 쉽게 하자!

Rust 입문

[Rust] 참조 (대여)

바로크냥 2022. 10. 9. 05:54

2. 참조 (대여)

소유권 개념을 알아봤습니다. 그러면 Rust에서는 Copy 트레이트가 없는 변수들끼리는 소유권 이동 없이 재할당을 하지 못 할까요?

네, 못 합니다. 

하지만 소유권 이동 없이 값을 사용할 수는 있습니다.

이런 작업을 위해서 참조(reference) 또는 대여(borrow / lending)라는 것이 있습니다.

값에 대한 소유권을 넘기지 않고 주소를 복사해 주어서 그 값을 사용할 수 있게 하는 방법입니다. 

참조를 사용할 때는 & 기호를 사용합니다.

참조한 값을 변경할 수 있게 설정하려면 &mut를 사용합니다.

 

fn main() {
  let s1 = String::from("Hello");
    let s2 = &s1;                  // 1
    println!("s1:{}", s1);
    println!("s2:{}", s2);

    type_of(&s1);
    type_of(&s2);
}
// 2

fn type_of<T>(_: &T){
   let res = std::any::type_name::<T>();
   println!("{:#?}", res);
}
// 출력

s1:Hello
s2:Hello
"alloc::string::String"
"&alloc::string::String"       // 앞에 &이 붙어 있는 것에 주목

// 1: let s2 = &s1; 코드를 보면 &s1이 사용되었습니다. s2는 s1에 대한 참조를 담고 있는 변수입니다.

다르게 말하면, s2는 s1이 갖고 있는 값의 주소를 가지고 있습니다.

// 2: type_of 함수는 타입 이름을 출력하기 위해 만든 함수입니다. T는 임의의 타입을 의미하는 제네릭입니다.

파라미터 이름으로 사용된 “_”은 파라미터를 받기는 하지만 사용하지 않겠다는 의미입니다.

이 함수를 사용할 때 파라미터에 우리가 어떤 타입의 값을 넣으면 데이터 타입 추론을 통해 T가 결정됩니다. std::any::type_name::<T>()는 타입 T의 타입 이름을 반환합니다.

type_of 함수에 대한 더 자세한 설명은 이전 장에 있습니다.

// 출력: s1은 String이고 s2의 타입은 &String임을 알 수 있습니다. &String은 문자열에 대한 참조입니다.


참조가 있으니 역참조도 있습니다.

역참조 기호는 “*”입니다.

참조가 주소를 가리키는 것이라면 역참조는 그 주소에 있는 내용물을 가리킵니다.

 


참조에는 &만 사용하는 불변 참조와 &mut를 사용하는 가변 참조가 있습니다.

참조한 값의 변경이 가능한가의 여부가 이 둘의 가장 큰 차이입니다.

그런데 Rust의 안전성(safety) 차원에서 아주 중요한, 또 다른 차이가 있습니다. 

불변 참조는 여러 번 참조하는 것을 허용하는 반면, 가변 참조는 단 한 번만 허용합니다.

다음 예제를 보겠습니다.

 

fn main() {
    let x = 10;
    let y1 =&x;
    let y2=&x;
    let y3 =&x;
    println!("y1={}, y2={}, y3={}", y1,y2, y3);
  }

/// 출력
y1=10, y2=10, y3=10

 

위 예제에서는 변수 x를 여러 번 불변 참조했습니다.

 

fn main() {
    let mut a = 20;
    let mut b1 = &mut a;
    let mut b2 = &mut a;
    println!("b1={}, b2={}", b1, b2);
}

// 출력
..에러
|     let mut b1 = &mut a;
|                           ------ first mutable borrow occurs here
|     let mut b2 = &mut a;
|                         ^^^^^^ second mutable borrow occurs here
|     println!("b1={}, b2={}", b1, b2);
|                              -- first borrow later used here

위 예제는 변수 a를 여러 변 가변 참조했습니다. 그 결과, 에러가 발생합니다.

여러 변수가 가변 참조를 통해 하나의 값을 참조하고 있을 때, 한 변수에서 값을 변경하게 되면 다른 모든 변수에도 영향을 미치게 됩니다. 이때 상황에 따라 에러가 발생할 수도 있습니다. 이런 상황을 미연에 방지하기 위해 가변 참조를 여러 번 재할당하는 것을 금지하고 있습니다. 


참조형 변수는 C언어의 포인터 변수처럼 주소를 저장합니다.

fn main() {
    let x:i32 = 10;
    let x_p = &x;
    println!("variable x at {:p} has {}",x_p, x);
    println!("{}", *x_p);
}

// 출력
variable x at 0x16b136d94 has 10
10
 

변수 x_p는 변수 x의 주소를 저장하고 있습니다. 

출력에 보면, 0x16b136d94이 있습니다. 

*x_p는 변수 x의 주소를 담은 변수 x_p를 역참조 했습니다.

그래서 변수 x_p에 담겨 있는 메모리 주소로 가서 그곳에 저장되어 있는 값을 가져옵니다.

물론 변수 x와 값이 같습니다. 

 

참조형 변수는 주소를 저장하기 때문에 그 크기가 늘 동일합니다. 

그래서 컴파일 시점에 크기를 알 수 있기 때문에  stack에 저장됩니다.

직접 확인해 보겠습니다.

 

 

use std::mem::size_of_val;    // 변수의 크기를 리턴하는 함수

fn main() {
    let x: i32 = 10;
    let x_p = &x;
    let size_of_x_p = unsafe { size_of_val(&x_p) };
    let size_of_x = unsafe { size_of_val(&x) };
    println!("size of x_p :{}", size_of_x_p);
    println!("size of x :{}\n", size_of_x);

    let y: i64 = 100;
    let y_p = &y;
    let size_of_y_p = unsafe { size_of_val(&y_p) };
    let size_of_y = unsafe { size_of_val(&y) };
    println!("size of y_p :{}", size_of_y_p);
    println!("size of y :{}", size_of_y);
}

// 출력
size of x_p :8    // 주소 크기
size of x :4      // 32비트 정수

size of y_p :8    // 주소 크기
size of y :8      // 64 비트 정수

데이터 타입의 크기가 달라도 주소의 크기는 동일합니다.


참조형 변수가 유효한 범위를 수명(lifetime)이라고 합니다. 

참조형 변수의 수명은 어떤 변수를 참조한 순간에 시작되어서 그 변수가 스코프를 벗어나서 소유권을 잃을 때 끝납니다. 

아래 예시를 보면 쉽게 이해되실겁니다.

{
  let x: i32 = 10;
  let y = &x;   // 참조형 변수 y의 수명 시작
  ...
  ...
}               // 변수 x가 스코프를 벗어나고 y의 수명도 끝
 
 
 

 

lifetime annotation이나 borrow checker 등 수명과 관련된 중요한 내용은 중급 코스에서 다루도록 하겠습니다.

 

반응형

'Rust 입문' 카테고리의 다른 글

[Rust] 모듈 (파일)  (0) 2022.10.11
[Rust] 모듈  (0) 2022.10.11
[Rust] 소유권  (0) 2022.10.09
[Rust] Result와 Rust의 에러 처리  (0) 2022.10.07
[Rust] Option  (0) 2022.10.07