[Rust] 참조, 대여

MS Choi·2022년 5월 30일
0

Rust Study

목록 보기
2/4

지난 글에 이어서...

최근에 적은 소유권 2탄이라고 할 수 있다 이전에는 소유권이 무엇인지 알아보았다면 이 소유권을 잘 쓰기위한 몇가지 기능들에 대해 알아볼 예정이다.

참조와 대여

소유권을 이용할 경우 소유권이 흐름에 따라 이동을 하면서 여러가지 불편한 점이 많았다. 특히 함수 내부로 소유권이 이동할 경우 이를 다시 받기 위해 무조건 리턴값을 써야만 하는 문제가 있었다.

이를 해결하기 위한 기능이 바로 참조와 대여

먼저 아래의 코드를 보자

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

이 코드에서 이전과 달리 일반적인 String이 아닌 &String을 매개변수로 받고 있다.

&(ampersands, 엠퍼센드)를 우리는 참조라고 부르며 만약 C,C++경험이 있는 독자라면 너무나도 친숙한 표현이라고 생각한다.

이 참조를 사용하는 이유는 소유권을 가져오지 않고도 값을 참조할 수 있기 때문이다.

함수의 호출 부분을 다시 한번 보면 다음과 같다.

 let s1 = String::from("hello");
 let len = calculate_length(&s1);
 
  fn calculate_length(s:&String)->usize{// String에 대한 참조다.
 	s.len(); 
 }// 이시점에 s가 범위를 벗어난다.
 //참조이기때문에 소유권을 가지지 않는다. 따라서 아무일도 일어나지 않는다.

이전에 우리가 소유권에서 보았든 매개변수가 유효한 범위는 달라지지 않는다. 다만 차이점이라면 일반적인 매개변수를 사용했을때는 소유권이 있기때문에 s를 리턴하지 않으면 drop이 발생하며 소유권이 소멸하지만, 참조를 이용하면 매개변수에 소유권을 전해주지 않기때문에 아무일도 일어나지 않는다.

또한 소유권을 전달받지 않았기 때문에 다시 소유권을 돌려주기위해 리턴값을 적을 필요가 없다.

이처럼 함수 매개변수로 참조를 전달 하는것을 '대여(borrowing)'라고 한다.

만약에 이 대여받은 매개변수를 수정하려고 하면 무슨일이 일어날까?

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

이 코드를 실행해보면 에러가 난다. 왜냐하면 기본적으로 변수가 불변이듯 참조도 불변이기 때문이다.

가변참조

위에서 본 예제가 정상적으로 실행되게 하기 위해서는 조금만 수정을 하면된다.

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

우리가 수정가능한 변수를 만들었을때 mut키워드를 사용했듯 참조에도 mut을 붙여서 선언해주면 가변참조를 사용할 수 있다.
이렇게 가변참조가 만능일것 같지만, 한가지 제약이 있는데 특정 범위내에서 특정 데이터에 대한 가변참조는 오직 한개만 존재 할 수 있다는 제약이다. 말이 조금 어려운데 예제를 보면 한번에 이해가 된다.

 fn main(){
	let mut s =  String::from("hello");
    
    let r1 = &mut s;
    let r2 = &mut s;
 }

이렇게 예제를 작성하고 실행하면 에러를 만나게 된다.

이 부분이 다른 언어들과 가장 다른점이라고 할 수 있는데, 다른 언어에서는 참조나 가변참조에 대한 제약이 크게 없지만, rust에서는 우리가 공부했듯 제약이 많기 때문에 이부분을 가장 어려워한다.

이렇게 사람들이 어려워 함에도 Rust가 이러한 제약을 고수하는 이유는 '데이터 경합'을 컴파일 시점에 방지해 안전한 코딩이 가능해진다.

(데이터 경합이 무엇인지는 각자 찾아보기를 바란다. 여기에 이 내용까지 다 적으면 너무 글이 길어져서...)

아마 여기까지 공부를 했다면, 이런 의문이 들 수도 있다.

"그럼 불변참조와 가변참조는 동시에 사용할 수 있는거야?"

 fn main(){
	let mut s =  String::from("hello");
    
    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s;
 }

이런 간단한 예제를 실행해보면 r3에서 에러가 발생하는걸 알 수 있다.

기본적으로 불변참조가 범위내에 존재한다면 그 범위내에서는 그 값이 변경되면 안된다. 그렇기 때문에 같은 범위내에서는 불변과 가변을 동시에 사용할 수 없다. 반대로 불변참조는 여러개를 사용할 수 있는데, 불변 참조는 여러개 사용한다고 데이터가 변경될 가능성이 없기때문에 여러개 선언이 가능하다.

죽은참조

포인터를 사용하는 언어를 주로 이용하다보면 죽은 포인터로 인해 에러가 발생하기 쉽상이다.

이러한 경우를 dangling이라고 부르는데 C나 C++에서 프로그래밍을 하다보면 너무나도 쉽게 만날 수 있다.

문제는 C나 C++언어에서는 프로그램이 실행되고 해당지점에 도달할때까지 이러한 오류가 있는지 모른다는 점에 원인을 찾기가 힘들다.

하지만 rust에서는 이러한 오류를 컴파일 시점에서 찾아내어 사용자에게 알려준다 어떻게 이게 가능한걸까?

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

이 예제를 컴파일하면 오류가 발생한다.

오류가 발생하는 이유는 소멸할 s에 대한 참조를 return값으로 넘겨주었기 때문이다.

우리는 앞서 배웠듯이 한 변수에 소유권이 유지되는건 범위 내에서만이다. dangle()함수에 선언된 s는 함수 내에서만 소유권이 유지가 된다. 지금처럼 s의 참조를 넘기게 되면 곧 소멸할 변수의 참조를 리턴하게 되므로 죽은 참조를 갖게 된다는걸 알 수 있다.

만약 이렇게 사용하고 싶다면 다음과 같이 참조가 아닌 소유권의 이동을 이용하면 원하는 대로 동작하게 할 수 있다.

 fn main(){
	let reference = no_dangle();
 }
 
 fn no_dangle()->String{
 	let s = String::from("hello");
    
    s
 }

이제 우리가 배운 내용인 참조에 대한 간단한 규칙을 정리해보자

  • 어느 한 심저에 코드는 하나의 가변 참조 또는 여러 개의 불변 참조를 생성할 수 는 있지만, 둘 모두를 생성할 수 없다.

  • 참조는 항상 유효해야 한다.

다음에는 slice에 대해 작성을 할것 같은데 잘 모르겠다. 만약 작성하게 된다면 이 글에 이어서 간단하게 작성해보겠다.

profile
다양한 경험을 하는걸 좋아하는 개발자입니다.

0개의 댓글