이 시리즈는 Rust Book을 공부하고 정리한 문서입니다. 댓글로 많은 조언 부탁드립니다.
Rust Book: https://doc.rust-lang.org/book/
소유권을 가지지 않는 또 하나의 타입이다.
먼저 만약 slice가 없을때 우리가 문자열에서 (스페이스로 구분된) 첫번째 단어를 반환하는 함수는 어떻게 만들 수 있을지 살펴보자. 일단 다음과 같이 단어 끝의 인덱스를 리턴하는 함수를 만들 수는 있다.
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()
:문자열을 바이트 배열로 변환.iter()
: 바이트 배열에 해당하는 Iterator 생성 (13장).enumerate()
: iter() 의 결과를 wraping 하여 튜플로 반환, reference 타입도 반환함에 주의리턴값은 String 타입인 s가 유효(valid) 할때만 의미가 있다. s 가 메모리에서 drop()되면 해당 값은 아무 의미가 없어진다.
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값 5는 문자열 s가 없이는 아무 의미가 없다.
// we could meaningfully use the value 5 with. word is now totally invalid!
}
String s와 word 값은 어떠한 커넥션도 가지고 있지 않다. 이렇게 string의 인덱스가 해당 String과 싱크가 맞지 않으면 오류가 발생할 여지가 있다. 가령 word를 얻은뒤에 문자열 s가 바뀌었다면 word값은 잘못된 값이 될 것이다.
이런문제를 해결하기 위해 러스트에는 String Slice 가 있다.
String Slice 는 String
의 일부분에 대한 reference(참조자) 이다.
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
&s[0..5]
는 &s
처럼 전체 문자열의 reference가 아니라 부분문자열의 reference를 의미한다. [st..en]
실제 값은 st부터 (en - 1) 까지를 의미. 슬라이스 데이터 구조는 시작위치와 슬라이스의 길이를 저장한다. 길이는 (en - st) 한 값이다. 아래 데이터 구조를 보면 슬라이스 타입인 world는 시작 위치를 가지고 있다.
첫번째 인덱스부터 시작하는 슬라이스는 [..end]
로 표기 가능.
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];
슬라이스가 문자열의 특정 위치부터 마지막 바이트를 가리키고자 할때는 [st..]
로 표기 가능.
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
[..]
는 전체 문자열을 의미
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
이제 슬라이스를 활용해 첫번째 문자를 리턴하는 함수를 다시 작성해보자.
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[..]
}
첫번째 스페이스(b' '
)에 해당하는 인덱스를 찾은뒤 바로 슬라이스 타입으로 반환하면 간단하게 해결된다.
fn main() {
let mut s = String::from("hello world");
| let word = first_word(&s);
| s.clear(); // error!
| println!("the first word is: {}", word);
}
s.clear()
를 시도할때 clear함수는 s의 mutable reference를 갖기 위한 시도를 할것이다. 결과적으로 한 스코프에 mutable reference와 immutable reference 가 동시에 존재하는 것이므로 data race관련 규칙에 따라 컴파일 에러가 발생한다. 다음도 마찬가지.
좀더 부연설명을 하면 여기서 word의 scope 는 |
로 표기된 부분이다. (선언하고 사용할때까지 스코프가 만들어지는듯). 그 동일한 스코프 내에서 &s
&mut s
가 동시에 있기 때문에 컴파일 에러가 나는 것이다. 만약 아래와 같이 수정하면 두가지가 동일한 스코프내에 있지 않기 때문에 컴파일 에러가 발생하지 않을것이다.
fn main() {
let mut s = String::from("hello world");
| let word = first_word(&s);
| println!("the first word is: {}", word);
s.clear(); // error!
}
여기서 word 의 스코프 안은 &s만 존재한다.
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.push_str(", jihun"); // Error!
println!("{} -> {}", s, word);
}
(질문) 이렇게 반환된 값은 이후에 String이 변경 되었을때도 자동으로 리턴값이 바뀔까?
다시 이 질문으로 돌아와서 답을 해보자면, 애초에 string 자체를 변경하는것이 불가능하다. s 를 변경하는 행위를 하기 위해서 push_str() 같은 함수는 s에 대한 mutable reference 를 얻을 것이고 , 그러면 mutable reference (s.push_str()
) 와 immutable reference(first_word(&s)
) 가 동시에 한 스코프에 존재할것이기에 규칙에 어긋난다. 그렇다고 first_word(&mut s)
하는건 mutable reference가 두개이기 때문에 또 불가능하다.
let s = "Hello, world!";
스트링 리터럴 타입은 &str 이다. 따라서 immutable reference 이다. 이것이 스트링 리터럴을 수정하지 못하는 이유이다. 여기서 &s[0..6]
은 "Hello" 이다.
fn first_word(s: &String) -> &str {
대신에
fn first_word(s: &str) -> &str {
를 사용하면 first_word() 함수의 인자로 String
을 넘길수도 스트링 리터럴을 넘길수도 있기때문에 더 유용한 방법이다.
fn main() {
// 1. `String`의 슬라이스
let my_string = String::from("hello world");
let word = first_word(&my_string[..]);
// 2. 스트링 리터럴의 슬라이스
let my_string_literal = "hello world";
let word = first_word(&my_string_literal[..]);
// 3. 스트링 리터럴 자체도 가능.
let word = first_word(my_string_literal);
}
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
println!("{}, {}", slice[0], slice[1]);
2, 3
slice
는 &[i32]
타입을 갖는다. 이것도 스트링 슬라이스와 같이 데이터 구조에 첫번재 요소와 슬라이스 길이가 저장된다. (8장 벡터 참고)