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 |