오늘은 Rust의 슬라이스(slice)에 대해 알아보고자 합니다.
슬라이스
슬라이스(slice)는 컬렉션(collection)을 통째로 참조하는 것이 아닌, 컬렉션의 연속된 일련의 요소를 참조하도록 해줍니다.
슬라이스는 참조자의 일종으로서 소요권을 갖지 않습니다.
만약 단어의 공백 부분의 index를 출력하는 함수를 작성하게 된다면 다음과 같이 작성이 가능합니다.
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()
}
String을 하나하나 쪼개서 해당 요소가 공백값인지 확인해야 하므로 as_bytes 메서드를 이용해 바이트 배열로 반환하였습니다.
let bytes = s.as_bytes();
그 다음, 반복자(iterator)를 iter 메서드로 생성했습니다.
for (i, &item) in bytes.iter().enumerate()
iter 메서드는 컬렉션의 각 요소를 반환하고, enumerate 메서드는 iter의 각 결과값을 튜플로 감싸서 반환합니다.
여기서 반환하는 튜플은 (인덱스, 해당 요소의 참조자) 입니다.
for 반복문 내에서 바이트 리터럴 문법으로 공백 문자를 나타내는 바이트를 찾고, 해당 위치를 반환합니다.
만약, 찾지 못했을 때는 s.len()으로 문자열 길이를 반환합니다.
첫 번째 단어 끝의 인덱스를 찾는 방법이 생겼지만, usize를 반환하고 있어, &String의 컨텍스트에서만 의미 있는 숫자를 반환하고 있습니다.
String과는 별개의 값이기 때문에, 향후에도 유효하다는 보장이 없습니다.
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear();
}
만약 다음과 같이 작성한 경우, s.clear() 이후에도 word는 사용하고 있지만, word에 담긴 값은 본래 목적대로 s에서 첫 단어를 추출하는 데 사용하는 경우, s의 내용물은 변경되어, 버그를 유발할 수 있습니다.
만약 word값이 s의 데이터 변경을 제때 반영하지 못했을까 전전긍긍할 순 없습니다.
심지어 second_word 함수를 추가로 만들어야 하는 경우에는 관리할 인덱스가 한둘이 아니게 될겁니다.
두 번째 단어이니, 시작과 끝 두 개의 인덱스가 필요할 것이고, 앞선 예제의 word처럼 어떠한 데이터의 특정 상태에만 의존하는 값들이 늘어나게 됩니다.
다음과 같은 상황에서 rust는 문자열 슬라이스라는 적절한 대안이 존재합니다.
문자열 슬라이스
문자열 슬라이스(string slice)는 String의 일부를 가리키는 참조자를 말합니다.
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
[starting_index..ending_index]는 starting_index부터 시작해 ending_index 직전, 즉 ending_index에서 1을 뺀 위치까지 슬라이스를 생성한다는 의미입니다.
JAVA의 subString()을 생각하시면 편합니다.
.. 범위 표현법의 경우 인덱스 0부터 시작하는 경우 생략할 수 있습니다.
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];
마찬가지로, String의 맨 마지막 바이트까지 포함하는 경우에도 생략할 수 있습니다.
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[..];
전체 문자열을 슬라이스로 생성하기 보다는 불변 참조자 형태로 만드는 방법이 더 간단하고 편리해보입니다.
앞선 예제에서 설명한 예제를 지금 배운 문자열 슬라이스로 만들게 된다면 다음과 같이 가능합니다.
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[..]
}
기존 코드에서는 s.clear()를 통해 s의 값이 변경되었음에도, word값이 유지됨으로 인하여 논리적으로 맞지 않음에도 불구하고 에러가 나타나지 않았습니다.
하지만 슬라이스를 사용하면 이런 버그를 미연에 방지하고, 발생할지도 모를 문제도 사전에 알 수 있습니다.
다음과 같이 작성한 경우, s.clear() 부분에서 에러가 발생하게 됩니다.
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear();
println!("the first word is: {}", word);
}
에러는 다음과 같습니다.
|
16 | let word = first_word(&s);
| -- immutable borrow occurs here
17 |
18 | s.clear(); // error!
| ^^^^^^^^^ mutable borrow occurs here
19 |
20 | println!("the first word is: {}", word);
| ---- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error
이전 절에서 배운 규칙 중, 불변 참조자가 존재할 떄, 가변 참조자를 만들 수 없다는 규칙이 적용된 것입니다.
s.clear()는 String의 길이를 변경해야 하니 가변참조자가 필요합니다.
그러나, clear 호출 이후 불변 참조자인 word를 println!에서 사용하므로, 가변 참조자와 불변 참조자가 같은 시점에 존재하게 되어 컴파일 에러가 발생합니다.
슬라이스로써의 문자열 리터럴
let s = "Hello, world!";
문자열 리터럴의 경우, 값이 변경되지 않고, 해당 값이 있는 주소를 가리키는 형태로 만들어져 불변 참조자이므로 &str 타입이 됩니다.
문자열 슬라이스를 매개변수로 사용하기
이전에는 다음과 같이 함수를 작성했었습니다.
fn first_word(s: &String) -> &str
그러나, &String 값과 &str 값 모두 사용한 함수를 작성하기 위해서는 다음과 같이 작성해야합니다.
fn first_word(s: &str) -> &str
그 이유는, 기본적으로 String의 경우는 읽기와 쓰기가 모두 가능하지만 &str의 경우에는 읽기만 가능합니다.
또한 &String이 &str을 인자로 받는 함수에 전달되게 되면, as_str()메서드를 호출하여 &str 형식으로 변환되어 사용되게 됩니다.
(참조 : https://doc.rust-lang.org/std/string/struct.String.html#deref)
그 외 슬라이스
문자열 슬라이스는 문자열에만 특정되어져 있습니다.
하지만, 더 범용적인 슬라이스 타입도 존재합니다.
다음과 같이 배열의 일부분을 참조하고 싶으면 다음과 같이 할 수 있습니다.
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
정리
- JAVA에서의 substring은 rust에서 python과 같이 사용이 가능합니다.
- 문자열 슬라이스의 경우, 불변 참조자 형태로 생성이 됩니다.
- 문자열 슬라이스한 값 이후에, 원본 문자열의 값이나 길이가 변경된 경우, 불변 참조자와 가변 참조자가 모두 존재하게 되어 에러가 발생합니다.
- 문자열 슬라이스 뿐만 아니라, 특정 배열의 부분 배열을 가져오는 방식으로 슬라이스가 사용 가능합니다.
'Rust' 카테고리의 다른 글
Rust 설치부터 실행까지 (메서드) - 12 (0) | 2025.01.09 |
---|---|
Rust 설치부터 실행까지 (구조체, 디버깅) - 11 (0) | 2025.01.07 |
Rust 설치부터 실행까지 (참조자) - 9 (0) | 2025.01.05 |
Rust 설치부터 실행까지 (소유권) - 8 (0) | 2025.01.02 |
Rust 설치부터 실행까지 (주석, 조건 반복문) - 7 (0) | 2024.12.30 |
댓글