개인적으로 생각하는 rust의 가장 중요한 개념. ownership의 개념이 없더라도 코드 리딩 자체에는 문제가 없을 수 있지만, 디테일한 부분까지 이해하려면 ownership 개념을 먼저 따고 가야한다. 추후에 알아야하는 패키지를 이해하는데에도 많은 도움을 준다
Rust의 Ownership(소유권)은 Rust의 중요한 개념 중 하나로, 메모리 관리와 관련된 부분을 안전하게 다룰 수 있게 해주는 원리입니다. Ownership은 다음과 같은 규칙을 따릅니다:
- 각 값은 변수, 또는 다른 값에 대한 한 가지 "소유자(owner)"만을 가질 수 있습니다.
- 소유자가 스코프를 벗어나면, 값은 자동으로 해제(드롭)됩니다.
- 다른 변수나 함수에 값을 빌리거나 대여(lend)할 수 있지만, 해당 소유자가 그 값을 수정하지 못하도록 제한됩니다.
Ownership은 Rust가 다음과 같은 문제를 방지하도록 도와줍니다:
- Null Pointer 및 댓글(Null-Reference) 예방: Rust의 변수는 항상 유효한 값을 가집니다. 따라서 null 또는 nil과 같은 값이 없으며, 댓글이 발생하지 않습니다.
- Double Free 및 메모리 누수 방지: Rust는 메모리를 정확하게 관리하며, 메모리 누수나 이중 해제(double free)와 같은 문제를 방지합니다.
- Race Condition 예방: Ownership 시스템은 동시성 및 다중 스레드 환경에서도 안전한 코드를 작성할 수 있도록 도와줍니다.
Ownership은 세 가지 규칙으로 구체화됩니다:
- 변수의 소유권(Ownership) 규칙: 값은 항상 단 하나의 변수에만 소유됩니다. 여러 변수가 동일한 값에 대한 소유권을 가질 수 없습니다.
- 빌림(Borrowing) 규칙: 값을 빌릴 수 있으며, 이를 통해 값을 소유하지 않고도 읽거나 수정할 수 있습니다. 빌린 값을 소유하려면, 빌림 규칙에 따라 적절하게 다루어야 합니다.
- 반환(Return) 규칙: 함수가 값을 반환할 때, 해당 값의 소유권을 반환받는 코드에게 이전합니다. 이로써 값의 복사를 피하고 효율적인 코드를 작성할 수 있습니다.
Ownership 규칙은 Rust의 러닝 커브 중 하나이며, Rust 코드를 작성하면서 이를 익히는 데 시간이 걸릴 수 있습니다. 하지만 이러한 규칙과 소유권 시스템은 안전하고 효율적인 코드 작성을 지원하며, Rust의 핵심적인 기능 중 하나입니다. Ownership을 이해하면 메모리 관리와 관련된 다양한 문제를 사전에 방지할 수 있으므로, 안정성과 성능 면에서 이점을 누릴 수 있습니다.
fn main() {
// 문자열 "Hello"를 x에 선언
let x = String::from("Hello");
// 변수 y에 변수 x의 소유권을 move
let y = x;
// 변수 y를 출력합니다. 이 때, "Hello" 문자열이 출력됩니다.
println!("y: {}", y);
// 여기서 변수 x를 사용하려고 하면 컴파일 에러가 발생합니다.
// println!("x: {}", x); // 에러: `x` cannot be used because it was moved into `y`
}
let x = 5;
라고 쓰면, println!("x: {}", x);
이 정상적으로 실행됨 → 는 Copy trait가 구현되어 있으므로, let y = x;
부분이 소유권 이전이 아닌, copy의 역할을 하게 된다. 이런식으로 Copy trait이 구현된 type들은 소유권 이전이 아닌, 복사가 이루어진다.
하지만, String은 Clone trait를 가지고 있기에, clone을 이용해서 Heap 메모리 복사를 만들어 낼 수 있다.
fn main() {
// 문자열 "Original string"를 x에 선언
let original_string = String::from("Original string");
// clone_string 에 original_string을 clone 하여 선언
let clone_string = original_string.clone();
println!("original_string: {}", original_string);
println!("clone_string: {}", clone_string);
}
/* output
original_string: Original string
clone_string: Original string
*/
fn main() {
let original_string = String::from("Original_string");
let mut clone_string = original_string.clone();
clone_string.push_str(& String::from(" + change!!"));
println!("original_string: {}", original_string);
println!("clone_string: {}", clone_string);
}
/* output
original_string: Original_string
clone_string: Original_string + change!!
*/
C++로 치면 포인터, 주소값과 비슷한 개념이다. 사실, 거의 같다고 해도 무방하다. 위와 같이 값들의 Ownership을 그대로 유지하면서, 다른 변수로 접근하는 reference를 만들 수 있다. 이제 이거를 borrow(빌림)이라고 표현하며, 소유권은 그대로 유지된다.
fn main() {
let x = String::from("Hello");
let y = &x; // x를 빌림, immutable reference y를 생성
println!("x: {}", x); // x: Hello
println!("y: {}", y); // y: Hello
}
fn main() {
let mut x = 5;
let y = &mut x; // x를 가변으로 빌림
*y += 1; // 가변 빌림된 데이터 변경
println!("x: {}", x); // 이 코드는 오류 발생
println!("y: {}", y); // 이 코드는 잘 동작
}
immutable borrow는 기능적으로 이해하기 쉽지만, mutable borrow는 몇 가지 제약조건이 발생한다한 scope 안에 mutable borrow는 한 개만 존재할 수 있다.
immutable reference가 존재하는 경우, mutable reference를 만들 수 없다.
이 내용 관련해서(+ lifetime) 뒷 쪽에서 더 할 이야기가 많다.
함수에 파라미터를 전달하는 과정은, 넓은 의미에서 보면 해당 스코프에 현재 스코프의 변수를 넘겨주는 행동이다. 즉, 위에서 언급한 소유권 개념이 들어가지 않을 수 없다.
fn print_length(s: String) -> usize {
s.len()
}
fn main() {
let my_string = String::from("Hello, Rust!");
let len = print_length(my_string);
println!("{}.length -> {}", my_string, len); //ERROR!!!
}
위의 코드는 일반적으로 별 문제 없이 보인다. 하지만, 마지막 줄에서 my_string을 출력할 수 없다고 나온다. my_string이 s를 넘어가면서, 소유권이 print_length
함수의 s로 완전히 넘어가 버렸기 때문이다. 더군다나, String은 Copy trait 또한 없기 때문에, Heap 메모리에 있는 String을 복사하지도 않는다. 즉, 해당 데이터에 소유권이 없는 my_string을 사용하려고 했기 때문에 에러가 났던 것이다. 이런 문제는 아래와 같이 해결 가능하다.fn print_length(s: &String) -> usize {
s.len()
}
fn main() {
let my_string = String::from("Hello, Rust!");
let len = print_length(&my_string);
println!("{}.length -> {}", my_string, len);
}
함수 자체를 immutable borrow로 돌려버려서, my_string이 소유권을 잃지 않도록 설정하였다.fn main() {
let mut x = 5; // i32 타입, Copy trait를 구현함
let y = & mut x;
*y += 1 ;
println!("x: {}", x); // x: 5, x는 여전히 사용 가능
println!("y: {}",y); // doubled: 10, 함수에서 반환한 값
}
//output
error[E0502]: cannot borrow `x` as immutable because it is also borrowed as mutable
--> src\main.rs:7:23
|
3 | let y = & mut x;
| ------- mutable borrow occurs here
...
7 | println!("x: {}", x); // x: 5, x는 여전히 사용 가능
| ^ immutable borrow occurs here
8 | println!("doubled: {}",y); // doubled: 10, 함수에서 반환한 값
| - mutable borrow later used here
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to previous error
fn main() {
let mut x = 5; // i32 타입, Copy trait를 구현함
let y = & mut x;
*y += 1 ;
println!("x: {}", x);
}
//output
x: 6