소유권
은 러스트의 가장 독특한 기능이면서 러스트 언어의 나머지 부분을 깊이 함축하고 있습니다. 소유권을 통해 러스트는 가비지 콜렉터 없이도 메모리 세이프티를 보장할 수 있기 때문에 어떻게 소유권이 동작하는지를 이해하는 것은 중요합니다. 이 챕터에서 소유권과 관련 개념들: 빌리기, 슬라이스, 어떻게 러스트가 데이터를 배치하는지를 다룰 것입니다.
소유권은 러스트 프로그램이 메모리를 어떻게 관리하는 방식을 다룬 규칙입니다. 모든 프로그램들은 실행하는 동안 컴퓨터의 메모리를 어떻게 사용할지를 관리해야 합니다. 몇몇 언어들은 프로그램이 실행될 때 더 이상 사용되지 않는 메모리가 있는지 찾는 가비지 콜렉터를 가지고 있습니다. 다른 언어들에서는 프로그래머가 명시적으로 메모리를 할당하고 해제해야 합니다. 러스트는 새로운 방법을 사용합니다: 메모리는 컴파일러가 확인하는 규칙들과 소유권이라는 시스템을 통해 관리됩니다. 만약 그 규칙들 중 어떤 것이라도 위반된다면, 로그램은 컴파일되지 않습니다. 소유권의 특징 중 어떤 것도 당신의 프로그램이 실행중일 때 속도를 늦추지 않습니다.
많은 프로그래머들에게 소유권이 새로운 개념이다 보니, 익숙해지는 데 시간이 좀 걸립니다. 좋은 소식은, 당신이 러스트와 소유권 시스템의 규칙에 더 익숙해질수록, 안전하고 호율적인 코드를 자연스럽게 개발하는 게 더 쉬워질 것입니다. 열심히 하세요!
소유권을 이해하면, 러스트를 유니크하게 하는 특징에 대한 단단한 기반을 다진 것입니다. 이 챕터에서 아주 흔한 자료구조인 문자열에 대한 몇 가지 예시를 통해 소유권을 배울 것입니다.
스택과 힙
러스트같은 프로그래밍 언어의 시스템에서, 값이 스택에 저장되어있는지 또는 힙에 저장되어있는지에 따라 어떻게 동작하는지가 달라집니다.
스택과 힙 모두 런타임에서 당신의 코드가 사용가능한 메모리입니다. 하지만 다른 구조를 갖고 있습니다.
스택은 값을 들어오는 순서대로 저장하고 가장 나중에 들어온 값부터 제거합니다. 이것이 LIFO(last-in,first-out) 방식입니다. 스택에 저장되는 모든 데이터들은 반드시 확정된 고정 크기여야 합니다.
컴파일 타임에 크기를 모르거나 크기가 바뀔 수 있는 데이터들은 힙에 저장되어야 합니다. 힙에 데이터를 저장할 때, 특정 크기의 공간을 요청합니다. 힙에서 충분히 큰 빈 공간을 메모리 할당자가 찾아서, 사용 중이라고 표시하고, 그 위치의 주소를 나타내는 포인터를 반환합니다. 이것을 할당이라고 합니다.
스택에 저장하는 것이 힙에 할당하는 것보따 빠른데, 할당자가 새 데이터를 저장할 공간을 검색해야 하기 때문입니다. 반면, 스택에서는 항상 제일 탑에 데이터를 둡니다. 데이터에 접근할 때도 포인터를 따라가야 정확한 위치를 찾을 수 있기 때문에 힙이 더 느립니다.
함수를 호출할 때, 함수에 전달되는 인자들과 함수의 로컬 변수들은 스택에 저장되고, 함수 실행이 끝나면 스택에서 제거됩니다. 코드의 어떤 부분이 힙의 어떤 데이터를 사용하는지 추적하고, 힙에 중복된 데이터의 양을 최소화하고, 실행 중 메모리가 모자라지 않게 힙에서 사용되지 않는 데이터를 제거하는 것은 소유권이 다루는 문제들입니다. 일단 소유권을 이해하면 스택과 힙에 대해 생각할 필요가 없지만, 소유권의 주 목적이 힙 데이터를 관리하는 것이란 걸 알면 왜 그렇게 작동하는지 이해하는 데 도움이 될 것입니다.
소유권 규칙
변수 범위
러스트 기초 문법을 배웠으니, 예시에서 모든 부분을 main함수에만 포함시키지 않으려고 합니다. 그러니 따라올 때 다음의 예시들을 main함수에 수동으로 추가해줘야 실행될 것이란 걸 기억하세요. 이렇게 함으로써 우리의 예시들은 더 구체적이고, 전체 템플릿보다 실제 디테일들에 집중할 수 있게 될 것입니다.
첫 번째 예시로 변수들의 범위에 대해 알아볼 것입니다. 범위는 프로그램에서 어떤 항목이 유효한 범위입니다.
fn main() {
{ // s is not valid here, it’s not yet declared
let s = "hello"; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
}
변수 s는 문자열이 하드코딩된 스트링 리터럴을 참조합니다. 이 변수는 선언된 곳부터 현재 범위가 끝나기 저낙지 유ㅛㅎ합니다.
String 타입
이전에 알아본 타입들은 미리 고정 사이즈라 스택에 저장되고 스코프가 끝나면 스택에서 제거되고 빠르고 만약 다른 범위에서 같은 값을 사용해야 한다면 쉽게 새롭고 독립적인 인스턴스로 복사될 수 있습니다. 이제 힙에 저장되는 자료들을 살펴보고 그 데이터를 어떻게 지울지 알아볼텐데, String 타입이 좋은 예시가 될 것입니다.
위에서 살펴본 스트링 리터럴들은 간편하지만 다양한 상황에 두루 사용하긴 적합하지 않습니다. 그 이유 중 하나는 그것들이 변경 불가능하기 때문입니다. 또다른 이유는 모든 문자열 값을 코드를 작성하는 시점에 알 수는 없기 때문입니다. 예를 들어 사용자 입력값을 저장하고 싶다면 어떨까요? 이런 상황에서 러스트는 String 타입을 사용합니다. 이 타입은 힙에 할당된 데이터를 관리하고 컴파일 타임에 우리가 모르는 텍스트들을 저장할 수 있습니다.
fn main() {
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{}", s); // This will print `hello, world!`
}
왜 리터럴은 수정할 수 없는데 String은 수정할 수 있을까요? 그 차이점은 두 타입이 어떻게 메모리를 다루는지에 있습니다.
메모리와 할당
스트링 리터럴의 경우에, 우리는 내용을 컴파일 타임에 알고 있기 때문에 텍스트는 바로 최종 실행가능한 값으로 하드코딩될 수 있습니다. 그래서 스트링 리터럴이 빠르고 효율적입니다. 그러나 이런 특징은 스트링 리터럴이 변경불가능하기 때문에 가능한 것입니다.
스트링 타입은 변경가능하고 크기를 늘릴 수 있게 하기 위해 힙에 컴파일 타임에는 알 수 없는 양의 메모리를 할당해야합니다. 이는
첫번째 부분은 String::from()을 호출하는 부분입니다. 이 함수의 구현은 필요한 메모리를 요청하는 것입니다. 다른 프로그래밍 언어들에서도 비슷합니다.
그러나 두 번째 부분이 다릅니다. 가비지 콜렉터가 있는 언어들에서는 GC가 메모리를 추적하고 제거해줘서 우리가 생각할 필요가 없습니다. GC가 없는 대부분의 언어들에서는 메모리 관리가 우리의 몫입니다. 이것을 올바르게 하는 것은 역사적으로 어려운 프로그래밍 문제였습니다. 만약 우리가 까먹으면, 메모리를 낭비하는 것입니다. 만약 너무 빨리 해제하면 유효하지 않은 변수를 갖게 됩니다. 두 번 해제하는 것도 문제입니다. 우리는 정확히 한 번 할당하고 한 번 제거해야 합니다.
러스트는 다른 길을 택했는데, 메모리는 변수 소유가자 범위를 벗어나면 자동으로 반납됩니다. 범위를 벗어나면 drop이라는 함수를 자동으로 호출합니다.
fn main() {
let s1 = String::from("hello");
let s2 = s1;
}
러스트에서 스트링을 복사하면 위와 같이 데이터 그 자체가 아니라 포인터와 길이, 용량이 복사됩니다.
이 경우, s1과 s2는 모두 같은 공간을 가리키고 있기 때문에, 범위를 벗어났을 때 drop 함수가 같은 공간을 두 번 해제할 수 있습니다. 메모리 안전을 보장하기 위해, s1을 s2에 복사하는 줄 뒤에 러스트는 더 이상 s1을 유효한 것으로 생각하지 않습니다. 이것이 바로 move입니다.
clone : 변수와 데이터가 상호작용하는 방법
스택 데이터가 아니라 힙에 저장된 데이터를 깊은 복사하고 싶다면, clone이라고 불리는 메소드를 사용하면 됩니다.
copy: 스택 온리 데이터
스택에 저장된 데이터들은 빨리 복사됩니다. 러스트는 정수형같이 스택에 저저ㅏㅇ되는 타입들에 copy라는 trait를 가집니다. 만약 어떤 타입이 copy trait를 가지면, 그 변수는 다른 변수로 할당된 이후에도 유효합니다.
함수에 변수를 전달하는 것은 move 또는 copy를 합니다.
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function,
// but i32 is Copy, so it's okay to still
// use x afterward
} // Here, x goes out of scope, then s. But because s's value was moved, nothing
// special happens.
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.
리턴값과 범위
값을 리턴하는 것 또한 소유권을 이동합니다.
참조는 우리가 다른 변수가 소유한 그 주소에 저장된 데이터에 접근할 수 있다는 점에서 포인터같습니다. 포인터와 다르게 레퍼런스는 특정 타입의 유효한 값을 가리키도록 보장되어있습니다. 어떤 값의 소유권을 갖지 않고도 어떤 객체의 레퍼런스를 인자로 사용하는 방법은 다음과 같습니다:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
&를 통해 값을 참조만 하고 소유는 하지 않을 수 있습니다. 소유하지 않기 때문에 그것이 가리키는 값이 사용되지 않더라도 메모리 해제되지 않습니다.
우리는 레퍼런스를 만드는 행동을 borrowing이라고 부릅니다. 잠깐 빌렸다가 다시 돌려준다는 것이죠!
그럼 우리가 빌린 상태에서 수정려고 하면 어떻게 될까요? 안됩니다!!
변수들이 기본적으로 수정불가능한 것처럼, 레퍼런스들도 마찬가지입니다. 우리는 참조하고 있는 무언가를 변경할 수 없습니다.
변경가능한 레퍼런스
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
먼저 참조하고자 하는 변수를 mut로 선언합니다. 그런 다음 mut를 붙인 참조를 만들어 인자로 넘김니다. 이를 통해 빌린 변수의 값을 바꿀 수 있습니다.
변경가능한 참조는 하나의 큰 제한을 갖습니다: 한 데이터의 변경가능한 참조는 한 순간에 한 개만 있어야 합니다.
또한 이미 참조를 하나 만들었으면 변경가능한 참조를 하나 더 만들지 못합니다. 그러나 변경 불가능한 참조 여러 개는 가능합니다.
참조의 범위는 처음 선언된 데에서 마지막으로 사용된 곳까지입니다.
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
// Danger!
메모리가 해제되었는데 밖에서 사용하려고 하니 에러 발생
그냥 스트링을 리턴해서 소유권 move하면 됨
슬라이스는 연속적인 데이터의 전체가 아니라 일부분만을 참조할 수 있게 해줍니다. 참조의 일종이므로 소유권은 없습니다.
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error!
println!("the first word is: {}", word);
}
struct, or structure는 다양한 값들을 의미있는 그룹으로 묶기 위한 커스텀 자료구조입니다.
구조체는 튜플처럼 다른 타입들을 가질 수 있지만, 각 데이터의 이름을 지정할 수 있어서 해당 값이 무엇을 의미하는지 더 명확히 할 수 있습니다. 해당 값에 접근하기 위해 순서를 몰라도 돼서 더 유연한 구조입니다. 구조체를 사용하려면 인스턴스를 만들면 됩니다.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("new@example.com");
}
전체 인스턴스는 수정가능해야 합니다.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
이렇게 user1의 값을 user2로 move한 다음에 user1은 해제되어 접근할 수 없게 됩니다.
그런데 active sign_in_count는 bool, int 는 copy가 있기 때문에 만약 user2에 active랑 sign_in_count만 복사했다면 user1은 그 이후에도 유효합니다
tuple structs라 불리는 구조체도 지원합니다.
전체 이름은 있지만 필드 이름은 없는 것
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}