안녕하세요, 단테입니다.
오늘은 Reference, Borrow, Slice type에 대해 알아보겠습니다.
러스트에서 reference는 값(value)를 한 메모리 위치에서 borrow한 후 새로운 이름을 부여하는 것을 말합니다.
reference 스스로가 값을 가지는게 아니라 값을 참조하기 때문에 스코프를 벗어나더라도 메모리에서 해제되지 않습니다.
다음 함수에서 x
는 5라는 값을, y는 x를 가르키고
있습니다. x, y를 출력해보면 x,y는 같은 메모리 공간을 참조하기 때문에 동일한 값이 출력됩니다.
fn main() {
let x = 5;
let y = &x;
println!("x = {}", x); // 5
println!("y = {}", y); // 5
}
먼저 reference, pointer 모두 값의 주소를 가르킵니다.
reference는 타입 세이프하며 null일 수 없지만 포인터는 세이프 하지 않고 null이거나 dangling pointer일 수도 있으므로 dereferencing 하기 전에 유효한 값을 가지고 있는지 확인해봐야 합니다.
또 다른 차이점으로는 곧 알아볼 borrowing에 대한 부분인데요, 러스트의 reference는 항상 소유권을 가져오는 것이 아닌 값의 주소를 통해 값을 참조할 수 있는 것이라면, 포인터는 소유권또한 양도받을 수 있습니다. reference 사용이 끝나더라도 소유권이 이동되지 않았기 때문에 data race와 같은 문제가 발생하지 않습니다.
또한
fn main() {
// Using a reference
let x = 5;
let y = &x;
println!("x = {}", x);
println!("y = {}", y);
// Using a smart pointer
let x = Box::new(5);
let y = &*x;
println!("x = {}", x);
println!("y = {}", y);
}
위에서 smart pointer가 보이죠? 러스트에서 포인터는 두 가지로 나뉩니다.
원시 포인터는 메모리를 직접 조작하는 데 사용되며 null 또는 매달린 포인터 참조와 같은 메모리 오류로 쉽게 이어질 수 있으므로 사용하기에 안전하지 않습니다. 반면에 스마트 포인터는 더 이상 필요하지 않을 때 메모리를 할당 해제하는 것을 포함하여 가리키는 메모리를 자동으로 관리하기 때문에 사용하기에 더 안전합니다.
위의 예시에서 x
는 5라는 값을 가지고 있는 Box
이며 y는 Box
가 가지고 있는 값에 대한 reference입니다.
x, y를 출력해보면 둘다 동일한 메모리 공간을 참조하기 때문에 동일한 결과를 출력하는 것을 알 수 있습니다.
let x: Box<i32> = Box::new(5);
x는 포인터,
let y: &i32 = &*x;
y는 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()
}
다음의 코드에서 calculate_length
에 전달되는 s가 어떤 것을 가르키는지 보세요.
let s1 = String::from("hello");
let len = calculate_length(&s1);
s1은 reference가 아니라 value
입니다.
&s1
은 value s1에 대한 reference를 만듭니다. 하지만 reference &s1은 s1 값을 가지고 있지는 않습니다.
오너쉽이 없다는 것입니다. 오너쉽이 없기 때문에 스코프를 벗어나도 drop
되지 않습니다.
fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
// it refers to, it is not dropped.
함수 calculate_length의 s 또한 String의 reference이기 때문에 String의 오너쉽을 가지고 있지 않습니다. 따라서 s.len()을 반환한 이후에 reference인 s에 대해 drop
이 발생하지 않습니다.
위와 같이 reference를 생성하는 것을 borrowing
이라고 합니다. 특정 값에 대한 오너쉽을 가지고 있지는 않고 그저 해당 값의 참조만 가지는 것입니다. 빌려온다
는 것이죠.
러스트에서 borrowing을 사용하게 된다면 일시적으로 값을 빌려와 사용하는 것일 뿐 그 과정에서 오너쉽의 교체는 이뤄지지 않습니다.
친구에게 책을 빌려줄 때 소유권을 양도하는 것은 아닌 것과 비슷한 맥락입니다.
borrowing
을 사용해서 원본 값을 변경하고 싶을 때는 어떻게 해야 할까요?아래 코드는 동작하지 않습니다.
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut String`
8 | some_string.push_str(", world");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` due to previous error
variable이 기본적으로 immutable한 것과 동일하게 reference 또한 immutable 합니다. 따라서 다음과 같이 mut 키워드를 사용해야 합니다.
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
특정 값에 대한 mutable reference가 존재할 때 주의해야 할 점은 해당 값을 참조하는 다른 reference는 추가적으로 존재할 수 없다는 것입니다.
그 이유는 특정 값에 대한 data race
발생을 막기 위해서입니다.
data race는 두개 이상의 스레드가 공유 데이터에 대해 동시에 접근할 때 발생합니다.
결과 값의 예상치 못한 변경을 일으켜 버그를 양산하고 디버깅을 어렵게 하기 때문에 이를 조심해야 합니다.
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(0);
let data_ref = Arc::clone(&data);
let t1 = thread::spawn(move || {
*data_ref += 1;
});
let t2 = thread::spawn(move || {
*data_ref += 1;
});
t1.join().unwrap();
t2.join().unwrap();
}
위에서 두개의 스레드에서 동일 data를 참조하고 있는데요,동시에 data_ref가 참조하는 값을 변경하기 때문에 data race를 유발합니다.
dangling pointer라고 불리기도 합니다. 만약 특정 값을 가르키는 reference가 있는데 해당 값이 더 이상 존재하지 않거나 할당해제가 되어 버린다면 이 reference는 dangling reference가 되어 버립니다.
러스트에서 dangling reference는 borrow checker
컴포넌트에 의해 방지됩니다. 이 borrow checker는 reference가 유효한지 항상 확인하고 reference가 참조하고 있는 값이 drop된 이후에 이 reference가 사용되지 않는지 확인합니다.
fn create_dangling_reference() -> & i32 {
let x = 10;
&x
}
fn main() {
let y = create_dangling_reference();
println!("{}", y);
}
위에서 값 10을 가지고 있는 변수 x는 create_dangling_reference
함수 스코프가 끝난 이후 스택에서 메모리 해제됩니다.
따라서 let y가 가르키는 x의 reference는 dangling pointer가 되게 됩니다.
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn create_dangling_reference() -> & i32 {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
|
5 | fn create_dangling_reference() -> &'static i32 {
| ~~~~~~~~
For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` due to previous error
이를 해결하기 위해서는 다음과 같이 값을 그대로 반환시켜야 합니다.
fn create_dangling_reference() -> i32 {
let x = 10;
x
}
fn main() {
let y = create_dangling_reference();
println!("{}", y);
}
다음 함수는 슬라이스를 사용하지 않습니다.
그런데 String의 일부분을 반환하고 싶습니다. 특별히 문자열 중 가장 마지막 문자를 반환해야 합니다.
그런데 파라메터 s는 reference 이기 때문에 String에 대한 오너쉽이 없습니다.
복습) 아, reference는 오너쉽이 없구나
fn first_word(s: &String) -> ?
원하면 해야죠, 한번 코드를 작성해보겠습니다.
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
문자열을 바이트의 배열로 바꾸어 줘야 합니다. 그래서 위에서 as_bytes
메소드를 사용했습니다.
let bytes = s.as_bytes();
그리고 iterator를 사용했습니다.
for (i, &item) in bytes.iter().enumerate() {
...
iter
은 collection의 각 요소(element)를 반환하는 메소드입니다.
그리고 enumerate
는 iter의 반환 값을 감싸고 있죠.
이 메소드는 각 요소의 인덱스와 iter가 반환했던 값의 reference를 tuple 타입으로 반환합니다.
enumerate
가 tuple을 반환하기 때문에 구조분해를 사용해 index와 reference를 조회헀습니다.
복습) 왜 &를 사용했죠? 튜플의 두번째 인덱스의 값이 reference이기 때문입니다.
for
loop에서 우리는 b' '
라는 새로운 문법과 마주합니다.
이 문법은 byte literal
이라고 부릅니다.
여기서 잠시 byte literal
문법이 뭔지 알아보고 오겠습니다.
if item == b' ' {
return i;
}
}
s.len()
if 문을 통해 string에서 빈 공간을 찾은ㅇ 이후에 return i
를 통해 해당 공간의 인덱스를 반환합니다.
만약 공간을 찾지 못한다면 그저 문자열의 길이를 s.len
을 통해 반환합니다.
아, 빈 공간을 byte literal 문법과 조건문을 통해 알아냈구나.
위와 같은 flow를 통해 빈 공간의 index를 알아냈습니다.
이제 first_word
함수를 사용하는 main
함수를 사용하겠습니다.
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word will get the value 5
s.clear(); // this empties the String, making it equal to ""
// word still has the value 5 here, but there's no more string that
// we could meaningfully use the value 5 with. word is now totally invalid!
}
let word
패턴 바인딩을 통해 first_word가 "hello world"에 대한 index를 반환하고 이 값이 word에 바인딩 되었습니다.
하지만 다음 줄에서 s.clear()를 통해 s가 empty String이 되었습니다.
word는 &s ("hello world" 문자열의 레퍼런스)
와 독립적인 값이라 s.clear() 메소드 호출 여부와 관계없이 항상 5라는 값을 가지게 됩니다. s.clear()호출 이후 s의 상태와는 전혀 상관없게 된 것이죠.
따라서 이 index를 통해 문자열의 데이터를 얻는 것은 잠재적인 에러를 가져오는 방법입니다.
러스트는 slice를 제공함으로 인덱스를 직접 관리하는 것과 비교해 더욱 안전하게 문자열의 값을 참조할 수 있게 해줍니다.
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
위의 코드를 그림으로 살펴보면 아래와 같습니다.
자바스크립트의 slice와 유사하죠?
러스트의 slice 문법은 다음처럼 첫번째 인덱스를 생략할 수 있습니다.
이 경우 문자열의 첫번째 인덱스부터 참조하는 것이 보장됩니다.
let s = String::from("hello");
let slice = &s[0..2]; // 아랫줄과 동일한 결과입니다.
let slice = &s[..2];
마지막 인덱스를 생략함으로 문자열의 마지막 바이트 또한 항상 포함됨을 보장할 수 있습니다.
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
시작과 끝 인덱스를 모두 생략할 수도 있습니다.
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
slice를 사용해 다시 first_word 타입을 정의해봅니다.
fn second_word(s: &String) -> &str {
여기서 함수 리턴 타입으로 선언된 str 가 바로 오늘의 주제인 String Slice type입니다.
str 타입을 사용한다면 앞서 봤었던 인덱스를 직접 관리하는 코드보다 더 안전하게 코드를 작성할 수 있습니다.
s.clear() 사용시 컴파일 에러가 발생하거든요.
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);
이제 String reference가 항상 유효함을 보장할 수 있습니다. 앞서 인덱스를 관리하여 잠재적인 오류를 가지고 있던 코드에서는 s.clear()
함수 호출 이후 유효하지 않은 인덱스의 값을 참조하는 것을 막지 못했잖아요?
슬라이스 타입을 사용할 시 에러가 발생할 수 있는 가능성의 코드를 작성 시 가장 빠른 시점에 해당 오류 발생을 예측할 수 있습니다.
러스에서 &str
은 string slice에 대한 타입입니다. 문자열의 일부분에 대한 reference를 의미하며 immutable borrow에 대한 타입입니다.
&string
은 String
객체에 대한 reference입니다.
&str과 마찬가지로 immutable borrow이지만 문자열에 대한 일부분을 참조하는 것이 아니라 String
객체 전체를 참조합니다.
아래 예제 코드에서 두 타입의 차이점에 대해 이야기해보겠습니다.
fn main() {
let s = "hello world".to_string();
let str_slice: &str = &s[..];
let string_ref: &String = &s;
println!("str_slice: {}", str_slice);
println!("string_ref: {}", string_ref);
}
s
는 String
객체이며 "hello world"라는 값을 가지고 있습니다. str_slice
변수는 s
에 대한 slice를 가지고 있으며 slice이지만 "hello world" 문자열 전체를 포함합니다.
string_ref
는 String
객체 전체에 대한 reference입니다.
str_slice, string_ref 모두 "hello world" 값을 가지고 있는 것이 아니기 때문에 해당 값에 대한 오너쉽은 없고 immutable하게 borrow 합니다.
String type은 mutable 합니다.
헷갈리지 마세요. 앞서 봤던 let string_ref: &String = &s
와 같이 string_ref는 String type이 아니라 &String 타입이고 이는 immutable borrow를 사용한 reference이기 때문에 immutable 하지만 String 그 자체는 mutable type 입니다.
이 말은 String 타입을 생성한 직후 변경할 수 있다는 것인데요,
fn main() {
let mut s = "hello".to_string();
s.push_str(" world");
println!("{}", s);
}
String 객체에 push_str
메소드를 사용하여 world 글자를 추가했습니다.
그와 반면에 아래 코드의 s는 &str
타입이며 생성 직후 값을 변경할 수 없습니다. String 객체가 아니므로 push_str과 같은 메소드 또한 사용할 수 없습니다.
fn main() {
let s = "hello world";
let slice = &s[6..];
slice[0] = 'W'; // This line will cause a compile-error
}
따라서 문자열 타입의 값은 변경할 수 있지만 string slice 타입의 실제 내용은 변경할 수 없습니다.
string 값을 변경하고 싶다면 String 타입 사용이 더 나은 선택일지도 모릅니다
앞서 봤던 s.push_str
메소드 외에도 replace_range와 같은 메소드를 사용할 수 있습니다. 아래 코드를 보면
chars
메소드를 통해 String 값에 대한 iterators를 얻고 next
메소드를 통해 index 0에 해당하는 글자부터 마지막 글자까지의 값을 복사하여 새로운 글자 Hello
를 반환합니다.
fn main() {
let mut s = String::from("hello");
let mut chars = s.chars();
if let Some(c) = chars.next() {
let new_c = 'H'; // the new character we want to use
s.replace_range(0..c.len_utf8(), &new_c.to_string());
}
println!("{}", s); // prints "Hello"
}
보너스) 만약 slice type을 변경하고 싶다면 아래와 같이 get_mut
메소드를 이용해 엘리먼트에 대한 mutable reference를 얻어야 합니다.
fn main() {
let mut words = ["hello", "world", "!"];
if let Some(w) = words.get_mut(0) {
*w = "Hi";
}
println!("{:?}", words); // prints ["Hi", "world", "!"]
}
String 타입은 capaciy(수용력)이 있습니다. 여기서 말하는 capaicty란 특정 사이즈 만큼의 메모리에 문자열을 저장할 수 있다는 것인데요, 이 메모리에 공간이 더 없게 되면 명시적으로 메모리 공간을 더 늘릴 필요 없이 자동으로 메몰 ㅣ할당을 해줍니다. String slice type은 capacity가 없고 만들 때 충분한 메모리가 있는지 먼저 확인해야 합니다.
오늘은 reference, data race, dangling reference, borrowing, slice type에 대해 알아보았습니다.
수고 많으셨습니다 :)