러스트의 핵심 기능
모든 프로그램은 실행하는 동안 컴퓨터의 메모리를 사용하는 방법을 관리해야 한다.
몇몇 언어들은 프로그램이 실행될 때 더이상 사용하지 않는 메모리를 끊임없이 찾는 가비지 콜렉션을 갖고있다.
또다른 언어들에서는 프로그래머가 직접 명시적으로 메모리를 할당하고 해제한다.
러스트는 제 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값을 갖는 불린 타입 boolf64와 같은 모든 부동 소수점 타입들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