출시·고도화 중
Rust 안내서 · 6/6
Rust가 가비지 컬렉터 없이도 메모리 안전을 보장하는 비결은 소유권 체계입니다. 처음에는 컴파일러가 엄격하게 느껴지지만, 이 규칙을 이해하면 대부분의 오류 메시지가 왜 나오는지 자연스럽게 읽힙니다. 마지막으로 코드를 일반화하는 트레이트와 제네릭을 살펴봅니다.
소유권 규칙은 세 가지입니다.
String처럼 힙을 쓰는 값을 다른 변수에 대입하거나 함수에 넘기면 소유권이 이동(move) 하고, 원래 변수는 더 이상 쓸 수 없습니다. 깊은 복사가 필요하면 clone()을 명시합니다. 정수, bool, char처럼 크기가 작고 고정된 타입은 Copy 트레이트를 구현하므로 이동 대신 복사됩니다.
fn take(s: String) {
println!("받음: {s}");
} // s 가 여기서 해제됩니다
fn main() {
let a = String::from("hello");
let b = a; // 소유권이 a 에서 b 로 이동
// println!("{a}"); // 컴파일 오류: 이동된 값을 사용했습니다
let c = b.clone(); // 힙 데이터까지 복사
take(b); // b 의 소유권이 함수로 이동
println!("{c}");
let x = 5;
let y = x; // i32 는 Copy 이므로 복사됩니다
println!("{x} {y}");
}값을 넘길 때마다 소유권이 옮겨 가면 불편하므로, 소유권 없이 값을 잠시 빌려 쓰는 참조가 있습니다. &T는 읽기 전용 참조, &mut T는 수정할 수 있는 참조입니다. 빌림 규칙은 다음과 같습니다.
&T)는 여러 개 있을 수 있습니다.&mut T)는 단 하나만 있을 수 있고, 그동안 다른 참조는 없어야 합니다.fn len_of(s: &str) -> usize {
s.len()
}
fn add_world(s: &mut String) {
s.push_str(", world");
}
fn main() {
let mut s = String::from("hello");
let n = len_of(&s); // 빌려 주기만 하므로 s 는 계속 쓸 수 있습니다
add_world(&mut s);
println!("{s} ({n})");
let r1 = &s;
let r2 = &s;
println!("{r1} {r2}");
// r1, r2 는 위에서 마지막으로 쓰였으므로 이제 가변 참조를 만들 수 있습니다
let r3 = &mut s;
r3.push('!');
println!("{s}");
}이 규칙은 "한쪽이 데이터를 바꾸는 동안 다른 쪽이 읽는" 데이터 경쟁을 컴파일 단계에서 막아 줍니다. 예를 들어 let first = &v[0];로 벡터 요소를 빌린 상태에서 v.push(4)를 호출하면 컴파일 오류가 납니다. push가 메모리를 다시 할당하면 first가 해제된 메모리를 가리키게 되기 때문입니다.
수명은 참조가 유효한 범위를 뜻합니다. 대부분은 컴파일러가 추론하지만(수명 생략 규칙), 함수가 여러 참조를 받아 그중 하나를 돌려줄 때처럼 관계가 모호하면 'a 같은 수명 매개변수로 알려 줘야 합니다. 수명 표기는 값을 더 오래 살게 하는 것이 아니라, 참조들 사이의 관계를 설명할 뿐입니다.
// 반환값은 a 와 b 중 더 짧게 사는 쪽만큼 유효합니다
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
// 참조를 필드로 가진 구조체도 수명을 적어야 합니다
struct Excerpt<'a> {
part: &'a str,
}
fn main() {
let text = String::from("첫 문장입니다. 두 번째 문장입니다.");
let first = text.split('.').next().unwrap_or("");
let excerpt = Excerpt { part: first };
println!("{}", excerpt.part);
let a = String::from("long string");
{
let b = String::from("xyz");
let result = longest(a.as_str(), b.as_str());
println!("{result}"); // b 가 살아 있는 동안에만 사용합니다
}
}&'static str처럼 'static은 프로그램 전체 동안 유효하다는 뜻이며, 문자열 리터럴이 대표적입니다.
트레이트는 타입이 가져야 할 동작(메서드 목록)을 정의합니다. 다른 언어의 인터페이스와 비슷하며, 기본 구현을 줄 수도 있습니다. 제네릭 함수는 T: 트레이트 형태의 트레이트 바운드로 "이 동작을 할 수 있는 타입이면 무엇이든"을 표현합니다. 제네릭은 컴파일할 때 타입마다 코드가 만들어지므로 실행 비용이 없습니다.
trait Summary {
fn author(&self) -> String;
// 기본 구현: 구현하는 타입이 따로 정의하지 않으면 이것을 씁니다
fn summarize(&self) -> String {
format!("({}의 글)", self.author())
}
}
struct Post {
title: String,
user: String,
}
struct Tweet {
user: String,
}
impl Summary for Post {
fn author(&self) -> String {
self.user.clone()
}
fn summarize(&self) -> String {
format!("{} - {}", self.title, self.user)
}
}
impl Summary for Tweet {
fn author(&self) -> String {
format!("@{}", self.user)
}
}
fn notify(item: &impl Summary) {
println!("새 소식: {}", item.summarize());
}
fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
let mut max = items[0];
for &item in items {
if item > max {
max = item;
}
}
max
}
fn main() {
let post = Post { title: "Rust 입문".to_string(), user: "kim".to_string() };
let tweet = Tweet { user: "lee".to_string() };
notify(&post);
notify(&tweet);
println!("{}", largest(&[3, 9, 2])); // 9
// 서로 다른 타입을 한 목록에 담을 때는 트레이트 객체(dyn)를 씁니다
let feed: Vec<Box<dyn Summary>> = vec![Box::new(post), Box::new(tweet)];
for item in &feed {
println!("{}", item.summarize());
}
}#[derive(Debug, Clone, PartialEq)]처럼 표준 트레이트는 자동으로 구현할 수도 있습니다. impl Trait와 제네릭은 컴파일 시점에 타입이 정해지고, dyn Trait는 실행 중에 메서드를 찾는다는 차이가 있습니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.