러스트의 핵심 기능
모든 프로그램은 실행하는 동안 컴퓨터의 메모리를 사용하는 방법을 관리해야 한다.
몇몇 언어들은 프로그램이 실행될 때 더이상 사용하지 않는 메모리를 끊임없이 찾는 가비지 콜렉션을 갖고있다.
또다른 언어들에서는 프로그래머가 직접 명시적으로 메모리를 할당하고 해제한다.
러스트는 제 3의 접근법을 이용
- 메모리는 컴파일 타임에 컴파일러가 체크할 규칙들로 구성된 소유권 시스템을 통해 관리
- 소유권 기능들의 어떤 것도 런타임 비용이 발생하지 않는다.
스택과 힘 둘다 런타임에 사용할 수 있는 메모리의 부분이지만, 각기 다른 방식으로 구조화 되어 있다.
스코프 - 프로그램 내에서 아이템이 유효함을 표시하기 위한 범위
fn main() {
// s는 유효하지 않습니다. 아직 선언이 안됐거든요.
let s = "hello"; // s는 이 지점부터 유효합니다.
// s를 가지고 뭔가 합니다.
} // 이 스코프는 이제 끝이므로, s는 더이상 유효하지 않습니다.
두가지 중요한 지점
이 지점에서 스코프와 변수가 유효한 시점 간의 관계는 다른 프로그래밍 언어와 비슷하다.
이전에 봐온 모든 데이터 타입들은 스택에 저장되었다가 스코프를 벗어날 때 스택으로부터 팝 된다.
이제 힙에 저장되는 데이터를 관찰하고 어떻게 비워내는지 살펴보자.
fn main() {
let mut s = String::from("hello");
s.push_str(", world!"); // push_str()은 해당 스트링 리터럴을 스트링에 붙여줍니다.
println!("{}", s) // 이 부분이 `hello, world!`를 출력할 겁니다.
}
::
은 String 타입 아래의 from 함수를 특정 지을 수 있도록 해주는 네임스페이스 연산자(메소드 문법)
String
은 변할 수 있지만 스트링 리터럴이 안되는 이유는 두 타입이 메모리를 쓰는 방식에 있다.스트링 리터럴의 경우, 내용물을 컴파일 타임에 알 수 있으므로 텍스트가 최종 실행 파일에 직접 하드 코딩 되었고, 이렇게 하면 스트링 리터럴이 빠르고 효율적이 된다.
그러나 이는 문자열이 변경되지 않는 것을 전재로 하는 특성이다.
컴파일 타임에 크기를 알 수 없는 경우 및 실행 중 크기가 변할 수도 있는 경우의 텍스트 조각을 바이너리 파일에 집어넣을 수 없다.
String 타입은 변경 가능하고 커질 수 있는 텍스트를 지원하기 위해 만들어졌기에, 우리는 힙에서 컴파일 타임에는 알 수 없는 어느 정도 크기의 메모리 공간을 할당 받아 내용물을 저장할 필요가 있다.
런타임에 운영체제로부터 메모리가 요청되어야 한다.
String의 사용이 끝났을 때 운영체제에게 메모리를 반납할 방법이 필요하다.
{
let s = String::from("hello"); // s는 여기서부터 유효합니다
// s를 가지고 뭔가 합니다
} // 이 스코프는 끝났고, s는 더 이상
// 유효하지 않습니다
let x = 5;
let y = x;
5
를 x
에 묶어놓고, x
의 값의 복사본을 만들어 y
에 묶는다.5
라는 값들이 스택에 푸쉬되기 때문에 실제로도 이렇게 된다.let s1 = String::from("hello");
let s2 = s1;
s1
의 복사본을 만들어서 s2
의 묶어놓는 식으로 동작할 것이라 생각하지만 이는 실제 동작과 다른 생각이다.String
은 그림의 왼쪽과 같이 세 개의 부분으로 이루어져 있다.String
의 내용물이 얼마나 많은 메모리를 현재 사용하고 있는지를 말한다.String
이 운영체제로부터 얼마나 많은 양의 메모리를 할당 받았는지를 말한다.s2
에 s1
을 대입하면 String
데이터가 복사되는데, 이는 스택에 있는 포인터, 길이값, 그리고 용량값이 복사된다는 의미이다.메모리 구조는 위의 그림과 같지 않는데, 위 그림은 Rust가 힙 메모리 상의 데이터까지도 복사한다면 벌어질 일이다.
만일 이렇게 동작한다면 힙 안의 데이터가 클 경우 s2 = s1
연산은 런타임 상에서 매우 느려질 가능성이 있다.
변수가 스코프 밖으로 벗어날 때, 러스트는 자동적으로 drop
함수를 호출하여 해당 변수가 사용하는 힙 메모리를 제거한다.
두 데이터 포인터가 모두 같은 곳을 가리키고 있는 Figure 4-4 그림에서는 문제가 된다.
s2
와 s1
이 스코프 밖으로 벗어나게 되면, 둘 다 같은 메모리를 해제하려 할 것이다.메모리 안정성을 보장하기 위해 Rust에서는 이런 경우 할당된 메모리를 복사하는 것을 시도하는 대신, s1
이 더이상 유효하지 않다고 간주한다.
그러므로 s1
이 스코프 밖으로 벗어났을 때 아무것도 해제할 필요가 없어진다.
s1
을 s2
가 만들어진 후에 사용하려고 할 때 어떤 일이 벌어지는지 확인해 보자.
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
error[E0382]: use of moved value: `s1`
--> src/main.rs:4:27
|
3 | let s2 = s1;
| -- value moved here
4 | println!("{}, world!", s1);
| ^^ value used here after move
|
= note: move occurs because `s1` has type `std::string::String`,
which does not implement the `Copy` trait
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
트레잇이라고 불리우는 특별한 어노테이션(annotation)이 있다.Copy
트레잇을 갖고 있다면 대입 과정 후에도 예전 변수를 계속 사용할 수 있다.Copy
가 가능하고 할당이 필요하거나 어떤 자원의 형태인 경우 Copy
를 사용할 수 없다.u32
와 같은 모든 정수형 타입들true
와 false
값을 갖는 불린 타입 bool
f64
와 같은 모든 부동 소수점 타입들Copy
가 가능한 타입만으로 구성된 튜플들, (i32, i32)
는 Copy
가 되지만, (i32, String)
은 안된다.fn main() {
let s = String::from("hello"); // s가 스코프 안으로 들어왔습니다.
takes_ownership(s); // s의 값이 함수 안으로 이동했습니다...
// ... 그리고 이제 더이상 유효하지 않습니다.
let x = 5; // x가 스코프 안으로 들어왔습니다.
makes_copy(x); // x가 함수 안으로 이동했습니다만,
// i32는 Copy가 되므로, x를 이후에 계속
// 사용해도 됩니다.
} // 여기서 x는 스코프 밖으로 나가고, s도 그 후 나갑니다. 하지만 s는 이미 이동되었으므로,
// 별다른 일이 발생하지 않습니다.
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이 호출됩니다.
fn 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
에 의해 제거된다.
만일 함수에게 값을 사용할 수 있도록 하되 소유권은 갖지 않도록 하고 싶다면, 즉 함수의 본체로부터 얻어진 결과와 더불어 우리가 넘겨주고자 하는 어떤 값을 다시 쓰고 싶어서 함께 반환받아야 한다면 튜플을 이용해 여러 값을 돌려받는 식으로 가능하긴 하다.
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len)
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 함수는 문자열의 길이를 반환한다.
(s, length)
}
https://rinthel.github.io/rust-lang-book-ko/ch04-00-understanding-ownership.html