모두의 코드
Rust 의 레퍼런스들에 관한 규칙들
이 글은 Rust 에서 레퍼런스들에 대해서 공부하면서 머리를 싸메던 저에게 한 줄기 빛과 같았던 글 을 읽고 제 나름대로 다시 정리하여서 쓴 글입니다.
Rust 에선 두 가지 종류의 레퍼런스를 정의할 수 있습니다. 하나는 참조하는 변수를 변경할 수 있는 변경 가능한(mutable) 레퍼런스고, 다른 하나는 참조하는 대상을 변경할 수 없는 변경 불가능(immutable) 한 레퍼런스 입니다. C++ 에서 오신 분들은 레퍼런스와 const
레퍼런스로 생각하시면 됩니다.
하지, Rust 의 레퍼런스와 C++ 의 레퍼런스는 큰 차이가 있는데, C++ 의 경우 아무런 제약 없이 어떤 변수에 대해서 임의로 const/non-const 레퍼런스들을 마음껏 정의하고 사용할 수 있지만, Rust 에선 mutable/immutable 레퍼런스를 사용하기 위해서는 몇 가지 중요한 규칙들을 지켜야 합니다. 이 글에서는 이 규칙들에 대해서 간단히 이야기 하고자 합니다.
Rust 에서 변경 가능한 레퍼런스를 만들기 위해서는 mut
키워드를 레퍼런스를 정의하면 됩니다. 예를 들어서
let mut x = 3; let mrx = &mut x; // x 에 대한 mutable reference. *mrx = 5; println("{}", x); // '5'
위 처럼 &mut
키워드로 정의된 레퍼런스인 mrx
를 통해서 x
의 값을 변경할 수 있습니다.
반면에 아무 키워드 없이 정의된 레퍼런스는 참조하는 값을 변경할 수 없고 읽기 만 가능합니다.
let mut x = 3; let rx = &x; // x 에 대한 immutable reference. // *rx = 5; 불가능 println("{}", *rx); // '3'
각각의 레퍼런스를 언제 사용할 수 있는지는 아래 두 개의 규칙에 따라 작동합니다.
변경 불가능한 레퍼런스에 관한 규칙
어떤 변수의 immutable 레퍼런스의 라이프타임 안에서 해당 변수는 immutable 하고, immutable 한 레퍼런스만 만들 수 있다.
예를 들어서 아래와 같은 코드를 살펴봅시다.
struct S{} fn main() { let mut x = S{}; let rx = &x; // ---- rx 의 라이프타임 시작 &x; // 가능. x 의 immutable 한 레퍼런스 만들 수 있음. // x = S{}; // 불가능. x 는 immutable 하기 때문 // let y = x; // 불가능. 역시 x 는 immutable 하기 때문. // x; // 불가능. 암시적인 move. // let mrx = &mut x; // 불가능. immutable 한 레퍼런스만 만들 수 있음. rx; // ---- rx 의 라이프타임 끝. }
여기서 중요한 점은 위 규칙은 immutable 레퍼런스의 라이프타임 안에서만 적용된다는 점입니다. 예를 들어서 아래 코드를 봅시다.
struct S{} fn main() { let mut x = S{}; let rx = &x; // ---- rx 의 라이프타임 시작 // 만일 밑에서 rx 가 쓰이지 않는다면 rx 의 라이프타임은 여기서 끝난다. let mrx = &mut x; // 가능. rx 의 라이프타임 밖이므로! }
위 경우 mrx
밑으로 rx
가 쓰이지 않기 때문에 rx
의 라이프타임은 mrx
바로 위에서 끝나게 됩니다. 따라서 위 규칙을 위반하지 않고 x
의 mutable 레퍼런스인 mrx
를 정의할 수 있겠죠.
한 가지 중요한 점은 라이프타임 이란 것이 연속적인 영역이 아니라는 점입니다.
fn main() { let mut x = 10; let rx = &x; // rx 의 라이프타임 if rand() > 1 { // (1) // rx 의 라이프타임 아님. let mrx = &mut x; *mrx = 11; } else { // (2) println!("{}", rx); // rx 의 라이프타임 } }
위 경우, x 의 mutable 레퍼런스인 mrx
가 rx
밑에 정의되었지만 위 코드는 성공적으로 컴파일 됩니다. 왜냐하면 컴파일러가 (1) 번 if
문 branch 를 택하게 되면 rx
가 더이상 쓰이지 않는다는 것을 알기 때문에 해당 영역을 rx
의 라이프타임에서 제외할 수 있습니다. 따라서 규칙을 위반하지 않고 x
의 mutable 레퍼런스를 정의할 수 있죠.
변경 가능한 레퍼런스에 관한 규칙
Mutable 레퍼런스를 정의하는 것은 참조하는 변수를 해당 레퍼런스의 라이프타임 동안 임시적으로 이동시킨 것과 같다.
변경 가능한 레퍼런스를 정의하는 것은 해당 변수를 그 레퍼런스로 잠시 동안 이동시킨 (move) 것과 같다고 생각하면 됩니다. 그리고, 레퍼런스의 라이프타임이 끝나게 되면 다시 레퍼런스에서 원래 변수로 돌아가게 됩니다. 예를 들어서
fn main() { let mut x = 1; let rx = &x; let mrx = &mut x; // x 가 *mrx 로 이동했다고 보면 됩니다. // x; // 현재 x 가 mrx 로 이동된 상태이므로 x 를 사용할 수 없습니다. // rx; // 여기서 rx 를 사용하게 되면 rx 의 라이프타임이 여기 까지 연장됩니다. // 그러면 immutable 레퍼런스 규칙에 위배되죠 (mutable 레퍼런스가 정의되어 있으므로) mrx; // 이 다음 부터 mrx 의 라이프타임이 끝납니다. x; // 다시 x 를 사용할 수 있습니다. }
Mutable 레퍼런스 &mut T
는 Copy 를 구현하고 있지 않습니다. 따라서, 아래와 같은 =
는 기본적으로 move 로 구현됩니다.
fn main() { let mut x = 1; let mrx = &mut x; let mry = mrx; // mrx 를 mry 로 이동. // mrx; // 따라서 mrx 를 사용할 수 없다. }
만일 mrx
를 사용하려고 한다면 아래와 예상했던 대로 이동된 값을 참조한다는 오류가 발생하게 됩니다.
다시 빌리기 (Reborrow)
그런데 mutable 레퍼런스가 다른 mutable 레퍼런스로 전달 될 때 항상 이동만 되는 것은 아닙니다. 아래와 같은 코드를 생각해봅시다.
fn take_mut(d: &mut i32) { *d += 1; } fn main() { let mut x = 1; let mrx = &mut x; take_mut(mrx); // mrx 가 d 로 이동이 되나? take_mut(mrx); // 만일 이동이 되었다면 mrx 를 사용할 수 없는데.. println!("{}", x); }
실행 해본다면 놀랍게도 컴파일 오류 없이
실행 결과
3
제대로 take_mut
이 mrx
를 받아서 여러번 호출된 것을 확인할 수 있습니다. 만일 mutable 레퍼런스가 항상 이동만 되었다면 첫 번째 take_mut
호출에서 mrx
가 take_mut
의 인자인 d
로 이동 되었을 것입니다. 그렇다면 take_mut
이후에서 mrx
를 사용할 수 없겠지요. 하지만 그 뒤에도 다시 take_mut
을 호출할 수 있었습니다.
이게 가능한 이유는 mutable 레퍼런스를 전달하는 두 번째 방식인 다시 빌리기 (Reborrow) 때문입니다. 위 경우 d
는 *mrx
를 다시 빌리게 되고, d
의 라이프타임이 끝나게 되면 x
의 소유권이 다시 원래 주인이였던 mrx
로 돌아가게 됩니다.
Rust 컴파일러는 어떤 mutable 레퍼런스가, 다른 mutable 레퍼런스로 정의된 레퍼런스로 전달이 된다면 mutable 레퍼런스를 이동 시키는 대신에 reborrow 를 수행합니다. 여기서 중요한 점은 다른 레퍼런스가 명시적으로 mutable 로 정의 되어 있어야 한다는 것입니다.
예를 들어서, 위 move 예제에서 mry
는 &mut i32
로 명시적으로 타입이 정의되어 있지 않기 때문에 컴파일러가 그냥 이동을 수행하지만, take_mut
함수에서 d
는 &mut i32
로 명시적으로 mutable 레퍼런스로 정의되어 있기 때문에 reborrow 가 수행이 되는 것입니다.
즉, take_mut(mrx)
는
take_mut(&mut *mrx);
로 변환되서 컴파일 됩니다. 기존 레퍼런스 (mrx
) 가 역참조(dereference) 되고, 다시 그 값에 대한 새로운 mutable 레퍼런스가 생성이 되서 함수에 전달됩니다. 그리고 함수가 끝나게 되면 다시 소유권이 mrx
로 넘어오게 되죠.
만일 위 이동 예제에서 mry
가 mrx
를 역참조 하게 된다면
fn main() { let mut x = 1; let mrx = &mut x; let mry = &mut *mrx; // mry 가 mrx 를 reborrow // let mry: &mut i32 = mrx; 도 됩니다. // mry 의 라이프타임 끝. mrx; // 따라서 mrx 를 다시 사용할 수 없다. }
위 처럼 mry
의 라이프타임이 끝났을 때 mrx
를 다시 사용할 수 있게 됩니다.
위 기본적인 규칙들만 잘 기억을 한다면 Rust 에서 레퍼런스를 다룰 때 아마 큰 도움이 될 것이라 생각합니다. 그러면 오늘도 Rust 컴파일러와 고군분투를 벌이고 있는 여러분들을 응원하며 이 글을 마치도록 하겠습니다 :)
참고자료
다음 글들이 큰 도움이 되었습니다.
댓글을 불러오는 중입니다..