[Rust] Ownership

silver·2023년 4월 13일
0

Ownership 규칙

  1. 러스트의 각각의 값은 해당 값의 owner라고 불리우는 변수를 갖고 있다.
  2. 한번에 딱 하나의 오너만 존재할 수 있다.
  3. 오너가 스코프 밖으로 벗어나는 때, 값은 버려진다.(dropped)

메모리와 할당

let x = 5;
let y = x;

정수 값 5를 x에 묶어놓고, x의 값의 목사본을 만들어 y에 묶는다.
정수 값이 결정되어 있는 고정된 크기의 단순한 값이고, 5라는 값들이 스택에 푸시되어 있다.

string일 경우

let s1 = String::from("hello");
let s2 = s1;

String이 아래 구조와 같이 생겼다.
String은 그림의 왼쪽과 같이 세 개의 부분으로 이루어져 있다.
문자열의 내용물을 담고 있는 포인터, 길이, 그리고 용량이다. 이 데이터의 그룹은 스택에 저장된다.
내용물을 담은 오른쪽의 것은 힙 메모리에 저장된다.

s2에 s1을 대입하면, String 데이터가 복사되는데, 이는 스택에 있는 포인터, 길이값, 그리고 용량값이 복사된다는 의미이다.
포인터가 가르키고 있는 힙 메모리 상의 데이터는 복사되지 않는다.

앞서 우리는 변수가 스코프 밖으로 벗어날 때, 러스트는 자동적으로 drop 함수를 호출하여 해당 번수가 사용하는 힙 메모리를 제거한다고 했다. 하지만 위와 같이 두 데이터 포인터가 모두 한 곳을 가르키고 있는 것을 봤을 때 double free 에러가 발생할 수 있다.

러스트에서는 이런 경우를 방지하기 위해 한가지 더 디테일을 추가하였는데,
할당된 메모리를 복사하는 것을 시도하는 대신, 러스트에서는 s1이 더 이상 유효하지 않다고 간주하는 것이다. 그러므로 러스트는 s1이 스코프 밖으로 벗어났을 때 해제할 필요가 없어지는 것이다.

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

위의 코드처럼 작성할 경우 error[E0382]: use of moved value: 's1' 에러가 발생하는 것을 볼 수 있다. 그 이유는 러스트가 유효하지 않은 참조자를 사용하는 것을 막기 때문이다.

이것을 얕은 복사(shallow copy)로 보기는 힘든 이유는 첫번째 변수를 무효화 시키기 때문에 이를 러스트에서는 이동(move)라고 말한다.
또한 러스트는 결코 자동적으로 깊은 복사(deep copy)를 수행하지 않는다.

변수와 데이터가 상호작용하는 방법: 클론

만일 String의 스택 데이터 만이 아니라, 힙 데이터를 복사하기를 정말 원한다면, clone이라 불리우는 공용 메소드를 사용할 수 있다.

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

이 코드는 에러가 발생하지않고, 즉 힙 데이터가 정말로 복사되는 동작을 명시적으로 만들어 낼 수 있는 방식이다.

스택에만 있는 데이터: 복사

let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);

위의 코드는 정수 값을 이용하는 코드로 잘 동작하며 유효한 코드이다.
하지만 이 코드는 방금 언급 한 것들과 대립되는 것처럼 보인다. clone을 호출하지 않았지만, x도 유효하며 y로 이동하지도 않았다.

그 이유는 정수형과 같이 컴파일 타임에 결정되어 있는 크기의 타입은 스택에 모두 저장되기 때문에, 실제 값의 복사본이 빠르게 만들어질 수 있다. 이는 변수 y가 생성된 후에 x가 더 이상 유효하지 않도록 해야할 이유가 없다는 뜻이다.

러스트는 정수형과 같이 스택에 저장할 수 있는 타입에 대해 달 수 있는 Copy trait이라고 불리우는 특별한 annotation을 가지고 있다. 만일 어떤 타입이 Copy trait을 갖고 있다면, 대입 과정 후에도 예전 변수를 계속 사용할 수 있다. 러스트는 만일 그 타입 혹은 그 타입이 가지고 있는 부분 중에서 Drop trait을 구현한 것이 있다면 Copy trait을 annotation 할 수 없게끔 한다. 만일 어떤 타입이 스코프 밖으로 벗어났을 때 어떤 특수한 동작을 필요로 하고 우리가 그 타입에 대해 Copy annotation을 추가한다면, 컴파일 타임 오류를 보게 된다.

그래서 어떤 타입이 Copy 가 되는가? 일반적인 규칙으로서 단순한 스칼라 값들의 묶음은 copy가 가능하고, 할당이 필요하거나 어떤 자원의 형태인 경우 copy를 사용할 수 없다.

  • u32와 같은 모든 정수형 타입들
  • true와 false 값을 갖는 bool 타입
  • f64와 같은 모든 부동 소수점 타입들
  • Copy가 가능한 타입만으로 구성되 튜플들 (i32, i32)는 copy가 되지만 (i32, String)은 안됨

소유권과 함수

함수에게 값을 넘기는 의미론(sematics)은 값을 변수에 대입하는 것과 유사하다.
함수에게 변수를 넘기는 것은 대입과 마찬가지로 이동하거나 복사될 것이다.

fn main() {
	let s = String::from("hello");	// s가 스코프 안으로 들어왔다.
    
    takes_ownership(s);				// s의 값이 함수 안으로 이동했다.
    								// ... 그리고 이제 더이상 유효하지 않다.
    let x = 5;						// x가 스코프 안으로 들어왔다.
    
    makes_copy(x);					// x가 함수 안으로 이동했다.
    								// i32는 Copy가 되므로, x 이후에 계속 사용해도 된다.
}

fn takes_ownership(some_string: String) {	// some_string이 스코프 안으로 들어왔다.
	println!("{}", some_string);
}	// 여기서 some_string이 스코프 밖으로 벗어났고 'drop'이 호출된다. 메모리는 해제되었다.

fn makes_copy(some_integer: i32) {	//some_integer가 스코프 안으로 들어왔다.
	println!("{}", some_integer);
}	// 여기서 some_integer가 스코프 밖으로 벗어났다. 별다른 일이 발생하지 않는다.

만일 s를 takes_ownership 함수를 호출한 이후에 사용하려고 한다면, 러스트는 컴파일 타임 오류를 낼 것이다. 이러한 정적 확인은 여러 실수를 방지해 준다.

반환 값과 스코프

fn main() {
	let s1 = gives_ownership();			// gives_ownership은 반환값을 s1에게 이동시킨다.
    
    let s2 = String::from("hello");		// s2가 스코프 안에 들어왔다.
    
    let s3 = takes_and_gives_back(s2);	// s2는 takes_and_gives_back 안으로 이동되었고,
    									// 이 함수가 반환 값을 s3으로도 이동시켰다.
}	// 여기서 s3는 스코프 밖으로 벗어났으며 drop이 호출된다.
	// s2는 스코프 밖으로 벗어났지만 이동되었으므로 아무 일도 일어나지 않는다.
    // s1은 스포크 밖으로 벗어나서 drop이 호출된다.
 
fun gives_ownership() -> String {	// gives_ownership 함수가 반환 값을 호출한 쪽으로 이동시킨다.
	let some_string = String::from("hello");	// some_string이 스코프 안에 들어왔다.
    
	some_string									// some_string이 반환되고,
    											// 호출한 쪽의 함수로 이동한다.
}
 
// takes_and_gives_back 함수는 String을 하나 받아서 다른 하나를 반환한다.
fn takes_and_gives_back(a_string: String) -> String { // a_string이 스코프 안으로 들어왔다.
 	a_string	// a_string은 반환되고, 호출한 쪽의 함수로 이동된다.
}

변수의 소유권은 모든 순간 똑같은 패턴을 따른다.: 어떤 값을 다른 변수에 대입하면 값이 이동한다.
힙에 데이터를 갖고 있는 변수가 스코프 밖으로 벗어나면, 해당 값은 데이터가 다른 변수에 의해 소유되도록 이동하지 않는 한 drop에 의해 제거될 것이다.

출처: https://rinthel.github.io/rust-lang-book-ko/ch04-01-what-is-ownership.html

0개의 댓글