
Rust는 가비지 컬렉터 없이도 메모리 안전성을 보장하기 위해 **소유권(ownership)**이라는 개념을 사용합니다. 이는 Rust의 핵심 개념 중 하나로, 값이 언제 생성되고 언제 해제되는지를 명확히 정의합니다.
fn main() {
let s = String::from("hello"); // s가 소유자
let t = s; // 소유권이 t로 이동
// println!("{}", s); // 컴파일 에러: s는 더 이상 유효하지 않음
println!("{}", t);
}
{
let s = "hello"; // 이 시점부터 s는 유효합니다
// s로 작업 가능
} // 스코프가 끝나면 s는 더 이상 유효하지 않습니다
Rust의 String은 힙(heap)에 데이터를 저장합니다.
let s = String::from("hello");
drop 함수가 자동 호출되어 메모리가 해제됩니다.let s1 = String::from("hello");
let s2 = s1; // s1 → s2로 소유권 이동
s1은 더 이상 사용할 수 없습니다. (얕은 복사가 아닌 이동)let s1 = String::from("hello");
let s2 = s1.clone();
s1과 s2 모두 사용 가능. (깊은 복사)함수로 값을 전달하는 것도 이동이 발생할 수 있습니다.
fn main() {
let s = String::from("hello");
takes_ownership(s); // 소유권 이동
let x = 5;
makes_copy(x); // Copy 타입이므로 여전히 x 사용 가능
}
fn takes_ownership(s: String) {
println!("{}", s);
}
fn makes_copy(x: i32) {
println!("{}", x);
}
값을 이동시키지 않고 참조할 수 있습니다.
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()
}
&는 참조를 의미하며, 데이터 소유권을 이동시키지 않습니다.fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(s: &mut String) {
s.push_str(", world");
}
Rust는 해제된 메모리를 참조하는 것을 원천적으로 방지합니다.
fn dangle() -> &String {
let s = String::from("hello");
&s // 컴파일 에러: s는 함수 종료 시 해제됨
}
컬렉션의 일부를 참조하는 방법입니다.
let s = String::from("hello world");
let hello = &s[..5]; // "hello"
let world = &s[6..]; // "world"
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
아래의 함수는 소유권과 대여 개념이 코드에 어떻게 반영되는지를 잘 보여줍니다.
use std::collections::HashSet;
pub fn anagrams_for<'a>(word: &str, possible_anagrams: &[&str]) -> HashSet<&'a str> {
let mut result = HashSet::new();
// 구현 내용은 생략
result
}
입력 인자: word: &str와 possible_anagrams: &[&str]는 모두 참조 타입입니다. 이 말은 함수가 입력 문자열들의 소유권을 가져오지 않고 빌려오기만 한다는 뜻입니다. 따라서 호출자는 이 함수를 실행한 뒤에도 원래 데이터를 자유롭게 사용할 수 있습니다.
반환 값: HashSet<&'a str>는 입력된 문자열들에 대한 참조를 반환합니다. 여기서 'a 라이프타임 파라미터는 반환된 참조들이 여전히 유효한 메모리를 가리킨다는 것을 컴파일러가 보장하도록 합니다. 즉, 함수가 새로운 소유권을 만들지 않고, 기존의 데이터를 안전하게 참조만 한다는 점을 명시합니다.
'a'a는 제네릭 라이프타임 파라미터입니다. 간단히 말하면, "이 참조는 최소한 어떤 입력 값이 살아있는 동안만 유효하다"라는 약속을 컴파일러에게 알려주는 역할을 합니다.
즉, 'a는 함수의 입력 참조와 반환 참조가 같은 생존 기간을 공유한다는 뜻입니다. 반환된 참조가 입력 데이터보다 오래 살 수 없게 보장해 주므로 안전합니다.
fn first<'a>(s: &'a str) -> &'a str {
&s[0..1]
}
fn main() {
let word = String::from("hello");
let ch = first(&word); // ch는 word가 유효한 동안만 유효
println!("{}", ch);
} // 여기서 word와 ch 모두 스코프 종료와 함께 해제
여기서 ch는 word의 일부를 참조하기 때문에 word가 끝나면 같이 끝나야 합니다. 라이프타임 'a가 바로 이 규칙을 표현하는 도구입니다.
만약 함수 내부에서 새로운 String을 만들고 참조를 반환한다면, 그 String은 함수가 끝나는 순간 사라지므로 참조는 더 이상 유효하지 않습니다. Rust는 이런 코드를 컴파일 단계에서 막아주며, 결과적으로 댕글링 포인터 문제를 원천적으로 방지합니다.
anagrams_for와 같은 함수는 소유권을 넘기지 않고 참조와 라이프타임을 통해 안전하게 데이터를 활용한다.