소유권
러스트가 메모리를 관리하는 방식이다.
- 각각의 값은 소유자(owner)가 정해져 있다.
- 한 값의 소유자는 동시에 여럿 존재할 수 없다.
- 소유자가 스코프 밖으로 벗어날 때 값은 버려진다(dropped).
String 타입
쌍따옴표로 감싸 만든 문자열 리터럴은 immutable이라 변경할 수 없다. 따라서 String
타입이 존재한다. 힙에 할당된 데이터로 컴파일 타임에 크기를 알 수 없는 텍스트도 저장할 수 있다.
let s = String::from("hello");
위 코드는 String 타입에 있는 from 함수를 네임스페이스 연산자(::
)를 사용해 지정했다. 힙에 메모리를 할당했으므로 변경할 수 있다.
메모리를 할당했으면 해제도 해야 한다. 러스트에서는 변수가 소속된 스코프를 벗어나는 순간 메모리가 해제된다.
소유권의 이동
let s1 = String::from("hello");
let s2 = s1;
s1
의 내용을 복사해 s2
에 할당하는 것이 아니다.
힙 영역에 “hello”라는 문자열이 저장되어 있고, s1
은 포인터로 이 힙 영역을 가리킨다. s2
에 s1
이 할당되면 s2
역시 포인터로 s1
과 같은 영역을 가리킨다.
그런데 s1
과 s2
모두 스코프를 벗어난다면, s1
의 메모리도 해제되고, s2
의 메모리도 해제된다. 그런데 두 변수는 같은 값을 가리키고 있었으므로, s2
는 이미 해제된 메모리를 또 해제하게 될 것이다.
따라서 러스트는 이런 문제를 막기 위해 s1
을 s2
로 이동(move)시켜 버린다.
클론
포인터가 같은 영역을 가리키게 만들지 않고, 아예 힙 데이터까지 복사해 새로 만들고 싶다면 clone
메서드를 사용한다.
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
복사
정수형과 같이 컴파일 타임에 크기가 고정되는 타입은 스택에 저장되어 복사본을 빠르게 만들 수 있으므로 깊은 복사를 하든 얕은 복사를 하든 차이가 없다. 즉, clone
을 호출해도 얕은 복사와 차이가 없어서 생략해도 상관없다.
스택에 저장되는 타입에 Copy
트레이트를 붙일 수 있다. Copy
가 구현되어 있다면 이 타입의 변수는 사용되어도 이동되지 않고 자명하게 복사된다.
소유권과 함수
fn main() {
let s = String::from("hello"); // s가 스코프 안으로 들어옴
takes_ownership(s); // s의 값이 함수로 이동
let x = 5; // x가 스코프 안으로 들어옴
makes_copy(x); // x가 함수로 이동되지만
// i32는 Copy이므로 앞으로 계속 x를
// 사용해도 ok
} // 여기서 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가 스코프 밖으로 벗어남. 별다른 일이 발생하지 않음.
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가 스코프 밖으로 벗어나면서 버려짐
// s2는 이동되어서 아무 일도 일어나지 않음
// s1은 스코프 밖으로 벗어나고 버려짐
fn gives_ownership() -> String {
let some_string = String::from("yours"); // some_string이 스코프 안으로 들어옴
some_string // some_string이 반환되고
// 호출자 함수(main) 쪽으로 이동
}
fn takes_and_gives_back(a_string: String) -> String { // a_string이 스코프 안으로
// 들어옴
a_string // a_string이 반환되고 호출자 함수 쪽으로 이동
}
참조와 대여
참조자(reference)는 해당 주소에 저장된 데이터에 접근할 수 있도록 해주는 포인터와 같다.
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)라고 한다.
가변 참조자
&mut s
와 같이 정의하면 빌린 값을 수정할 수 있게 된다. 가변 참조자가 존재하는 값에는 더 이상 참조자를 만들 수 없다. 같은 데이터에 대해 동시에 여러 가변 참조자가 생기면 경합(race)이 발생할 수 있기 때문이다.
슬라이스 타입
슬라이스는 컬렉션의 일부를 참조할 수 있게 해주는 참조자이다.
문자열 슬라이스
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
슬라이스는 내부적으로 시작 위치, 길이를 데이터 구조에 저장한다. hello
는 s
의 첫 번째 바이트를 가리키는 포인터와 길이가 5인 슬라이스로 구성된다.
문자열 리터럴은 바이너리의 특정 지점을 가리키는 슬라이스로 &str
타입이다.
'프로그래밍 > Rust' 카테고리의 다른 글
[Rust] 컬렉션 (0) | 2024.05.18 |
---|---|
[Rust]프로젝트 모듈 관리 (0) | 2024.05.17 |
[Rust] 열거형 (0) | 2024.05.17 |
[Rust] 구조체 (0) | 2024.05.16 |
[Rust] 변수, 함수, 조건문, 반복문 (0) | 2024.05.16 |