슬라이스
문자열과 배열의 연속 구간을 소유권 없이 참조하는 슬라이스를 사용해 원본 컬렉션과 안전하게 연결된 API를 설계합니다.
슬라이스(slice)는 연속된 요소 시퀀스의 일부 또는 전체 구간을 나타냅니다. 이 절에서는 &str, &[T]처럼 원본 데이터를 소유하지 않고 구간을 빌리는 슬라이스 참조를 다룹니다. 전체 문자열을 빌린 &s[..]도 슬라이스입니다.
슬라이스는 참조자의 일종으로서 소유권을 갖지 않습니다.
한번 간단한 함수를 만들어 봅시다.
공백문자로 구분된 단어들의 문자열을 입력받아서 해당 문자열의 첫 번째 단어를 반환하는 함수를요.
공백문자를 찾을 수 없으면 문자열 전체가 하나의 단어라는 뜻이니 전체 문자열을 반환하도록 합시다.
이 문제를 슬라이스로 해결할 수 있음을 이해해 보기 위해서, 먼저 슬라이스 없이 이 함수의 시그니처를 어떻게 작성할지부터 생각해봅시다.
fn first_word(s: &String) -> ?이 물음표는 반환 타입을 고르는 미완성 구상이며, 그대로 컴파일할 코드는 아닙니다. 이후에도 함수 첫 줄만 보여주는 조각은 설명용입니다. 실행할 때는 한 버전의 완성된 함수와 main을 함께 사용해야 합니다.
first_word 함수는 소유권을 가질 필요가 없으니 &String을 매개변수로 갖게 했습니다.
그런데, 뭘 반환해야 할까요?
문자열 일부분을 나타내는 법을 모르겠네요.
일단 예제 3-7처럼 공백문자를 가리키는 단어 끝부분의 인덱스를 반환하도록 만들어 보겠습니다.
예제 3-7:String 매개변수의 바이트 인덱스 값을 반환하는
first_word 함수
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 메서드로 문자열의 UTF-8 내용을 빌리는 바이트 슬라이스 &[u8]를 얻었습니다. 새 배열을 만들거나 문자열 바이트를 복사하지는 않습니다.
let bytes = s.as_bytes();
그다음, 이 바이트 슬라이스를 읽을 반복자(iterator)를 iter 메서드로 생성했습니다.
for (i, &item) in bytes.iter().enumerate() {
반복자(iterator)는 12장에서 자세히 알아볼 예정이니, 일단 iter 메서드는 컬렉션의 각 요소를 반환하고, enumerate 메서드는 iter의 각 결괏값을 튜플로 감싸 반환한다는 것만 알아두도록 합시다.
이때 반환하는 튜플은 첫 번째 요소가 인덱스, 두 번째 요소가 해당 요소의 참조자로 이루어져 있습니다.
enumerate 메서드가 반환한 튜플은 패턴을 이용해 해체하였습니다.
패턴에 대해서는 5장에서 더 자세히 다루겠습니다.
for 루프 내에서 i는 튜플 요소 중 인덱스에 대응하고, &item은 바이트에 대응됩니다.
iter().enumerate()가 &u8을 내놓으므로, 패턴의 &는 그 참조 패턴을 해체해 Copy인 u8 값을 item에 바인딩합니다.
for 반복문 내에서는 바이트 리터럴 문법으로 공백문자를 나타내는 바이트를 찾고, 찾으면 해당 위치를 반환합니다.
찾지 못했을 땐 s.len()으로 문자열의 길이를 반환합니다.
if item == b' ' {
return i;
}
}
s.len()
이제 문자열에서 첫 번째 단어 끝의 인덱스를 찾는 방법이 생겼지만, 문제가 있습니다.
usize를 반환하고 있는데, 이는 &String의 컨텍스트에서만 의미 있는 숫자일 뿐입니다.
바꿔 말하면, String과는 별개의 값이기 때문에 향후에도 유효하다는 보장이 없습니다.
예제 3-7의 first_word 함수를 사용하는 예제 3-8의 프로그램을 살펴봅시다.
first_word 함수의 결과를 저장했으나,
이후에 String의 내용물이 변경된 상황
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word는 값 5를 받습니다
s.clear(); // 이 코드는 String을 비워서 ""으로 만듭니다
// 여기서 word에는 여전히 5가 들어있지만, 이 5를 의미있게 쓸 수 있는
// 문자열은 더 이상 없습니다. word는 이제 전혀 유효하지 않습니다!
}
이 코드는 문법적으로 전혀 문제없고, 정상적으로 컴파일됩니다.
s.clear()를 호출한 후에 word를 사용하는 코드를 작성하더라도, word는 s와 분리되어 있으니 결과는 마찬가지죠.
하지만 word에 담긴 값 5를 본래 목적대로 s에서 첫 단어를 추출하는 데 사용할 경우, 버그를 유발할 수도 있습니다.
s의 내용물은 변경되었으니까요.
그렇다고, 혹여나 word 값이 s의 데이터 변경을 제때 반영하지 못했을까 전전긍긍할 순 없는 노릇이죠.
심지어 second_word 함수를 추가로 만들어야 한다면 어떨까요?
다음과 같은 함수를 정의하고 나면, 관리할 인덱스가 한둘이 아닐 겁니다.
fn second_word(s: &String) -> (usize, usize) {두 번째 단어이니 시작, 끝 두 개의 인덱스가 필요할 것이고, 이는 앞선 예제 3-8의 word처럼 어떠한 데이터의 특정 상태에만 의존하는 값들이 늘어남을 의미합니다.
그러면 여러분은 3개의 관련 없는 변수들을 계속 동기화해야겠죠.
다행히도, 러스트에는 문자열 슬라이스라는 적절한 대안이 존재합니다.
문자열 슬라이스
앞의 word는 clear 뒤에도 숫자 5를 담을 수 있습니다. 정수 자체가 깨지는 것이 아니라, 그 5가 더 이상 현재 문자열의 첫 단어 끝을 설명하지 못하는 것입니다. clear만으로 정수 인덱스 버전이 반드시 패닉을 일으키지도 않습니다. 나중에 빈 문자열을 그 범위로 잘라 쓰려 할 때 문제가 드러납니다. 슬라이스 버전은 원본에서 빌리는 관계를 타입에 남깁니다.
인덱스는 분리되지만 슬라이스는 원본과 유효성을 묶는다
hello world의 word=5가 s.clear 뒤에도 5로 남는 실제 전후 상태를 추적할 수 있다. &str 버전은 후속 word 사용을 포함한 별도 코드에서 clear가 컴파일 거부되므로 성공한 실행 뒤 실패를 수행하는 배치를 피한다.
DETACHED INDEX · BORROWED RANGE
독립 인덱스와 슬라이스는 원본 변경을 다르게 다룬다
같은 첫 단어를 나타내더라도 usize는 원본 상태와 분리되고, &str은 원본의 범위와 유효성 관계를 함께 표현합니다.
usize · 원본과 분리
first_word(&s)가 끝 위치5만 반환합니다.정수는
s를 빌리지 않으므로s.clear()가 허용됩니다.word == 5는 남지만 바뀐s와의 관계는 타입에 없습니다.
&str · 원본에 묶인 대여
first_word(&s)가"hello"범위를 공유 대여합니다.word를 뒤에서 쓰면 그 지점까지 대여가 활성입니다.중간의
s.clear()는&mut s가 필요해E0502로 거부됩니다.
word를 더 이상 사용하지 않으면 공유 대여는 마지막 사용 뒤 끝날 수 있고, 그 이후의 clear()는 허용될 수 있습니다.이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 기존 가로 도표와 같은 7노드와 6간선이다. 원본에서 usize 정책과 &str 정책으로 각각 분기하고 각 3노드 경로는 별도 프로그램 사본이다. usize 5→clear 허용→5 남음, borrowed hello→clear의 변경 대여 요구→word 후속 사용 시 E0502. 실패 노드는 실행 후의 결과가 아니라 컴파일 거부다. 전체/부분 슬라이스와 마지막 사용 뒤 변경 허용은 원래 설명을 보존했다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 단어 인덱스 hello world에서 독립 usize를 받는다. |
|
|
| 원본 clear s를 비워도 독립 숫자는 남는다. |
|
|
| 정책 비교 같은 후속 사용을 &str 사본과 대조한다. |
|
|
인덱스는 분리되지만 슬라이스는 원본과 유효성을 묶는다
hello world의 word=5가 s.clear 뒤에도 5로 남는 실제 전후 상태를 추적할 수 있다. &str 버전은 후속 word 사용을 포함한 별도 코드에서 clear가 컴파일 거부되므로 성공한 실행 뒤 실패를 수행하는 배치를 피한다.
DETACHED INDEX · BORROWED RANGE
독립 인덱스와 슬라이스는 원본 변경을 다르게 다룬다
같은 첫 단어를 나타내더라도 usize는 원본 상태와 분리되고, &str은 원본의 범위와 유효성 관계를 함께 표현합니다.
usize · 원본과 분리
first_word(&s)가 끝 위치5만 반환합니다.정수는
s를 빌리지 않으므로s.clear()가 허용됩니다.word == 5는 남지만 바뀐s와의 관계는 타입에 없습니다.
&str · 원본에 묶인 대여
first_word(&s)가"hello"범위를 공유 대여합니다.word를 뒤에서 쓰면 그 지점까지 대여가 활성입니다.중간의
s.clear()는&mut s가 필요해E0502로 거부됩니다.
word를 더 이상 사용하지 않으면 공유 대여는 마지막 사용 뒤 끝날 수 있고, 그 이후의 clear()는 허용될 수 있습니다.이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 기존 가로 도표와 같은 7노드와 6간선이다. 원본에서 usize 정책과 &str 정책으로 각각 분기하고 각 3노드 경로는 별도 프로그램 사본이다. usize 5→clear 허용→5 남음, borrowed hello→clear의 변경 대여 요구→word 후속 사용 시 E0502. 실패 노드는 실행 후의 결과가 아니라 컴파일 거부다. 전체/부분 슬라이스와 마지막 사용 뒤 변경 허용은 원래 설명을 보존했다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 단어 인덱스 hello world에서 독립 usize를 받는다. |
|
|
| 원본 clear s를 비워도 독립 숫자는 남는다. |
|
|
| 정책 비교 같은 후속 사용을 &str 사본과 대조한다. |
|
|
문자열 슬라이스(string slice)는 String의 일부를 가리키는 참조자를 말합니다.
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
만드는 방식은 String 참조자와 유사하지만, hello는 [0..5] 범위로 명시된 String의 일부분을 가리키는 참조자입니다.
[starting_index..ending_index]는 starting_index부터 시작해 ending_index 직전, 즉 ending_index에서 1을 뺀 위치까지 슬라이스를 생성한다는 의미입니다.
슬라이스는 내부적으로 시작 위치, 길이를 데이터 구조에 저장하며, 길이 값은 ending_index 값에서 starting_index 값을 빼서 계산합니다.
따라서 let world = &s[6..11];의 world는 시작 위치로 s의 (1부터 시작하여) 7번째 바이트를 가리키는 포인터와, 길이 값 5를 갖는 슬라이스가 되겠죠.
그림 3-6은 위 내용을 다이어그램으로 그린 것입니다.
String 일부를 참조하는
문자열 슬라이스
.. 범위 표현법은 인덱스 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[..];Note: 본 내용은 문자열 슬라이스를 소개할 목적이기에 ASCII 문자만 사용하여 문제가 발생하지 않았지만, 문자열 범위의 시작과 끝은 바이트 단위이며
start <= end <= len을 만족하는 올바른 UTF-8 문자 경계여야 합니다. 일반 범위 인덱싱은 이 조건을 어기면 panic합니다. 실패를 값으로 처리하려면get계열 메서드를 사용할 수 있습니다. UTF-8 문자를 다루는 방법은 7장 ‘문자열에 UTF-8 텍스트 저장하기’ 절에서 자세히 알아보겠습니다.
여태 다룬 내용을 잘 기억해 두고, first_word 함수가 슬라이스를 반환하도록 재작성해봅시다.
문자열 슬라이스를 나타내는 타입은 &str로 작성합니다.
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 first_word<'a>(s: &'a String) -> &'a str와 같습니다. 반환 슬라이스의 수명이 입력 String 참조의 수명에 묶이므로 원본보다 오래 살아남을 수 없습니다.
예제 3-7과 같은 방법으로 단어의 끝부분 인덱스를 알아내되, 공백문자 발견 시 해당 인덱스를 그대로 반환하는 것이 아니라 문자열 맨 앞부터 알아낸 위치까지 문자열 슬라이스를 생성하여 반환합니다.
이 입문 구현은 바이트 값 b' ' 하나만 구분자로 취급하므로 모든 유니코드 공백을 판별하는 함수는 아닙니다.
이제 first_word가 반환하는 값은 원래 데이터와 분리된 값이 아닙니다.
이 값은 원래 데이터의 슬라이스 시작 위치를 가리키는 포인터와 바이트 길이 메타데이터로 구성되어 있습니다. str의 길이는 UTF-8 문자 개수가 아니라 바이트 수입니다.
second_word 함수도 슬라이스를 반환하도록 바꿔보겠습니다.
fn second_word(s: &String) -> &str {사용법이 훨씬 직관적이지 않나요?
또한 예제 3-8에서 말한 첫 단어의 끝부분 인덱스를 찾은 이후 문자열이 비워지면 찾아낸 인덱스가 쓸모없어지는 문제도 해결했습니다.
이제는 컴파일러가 String을 가리키는 참조자의 유효함을 보증하니까요.
기존 코드는 논리적으로 맞지 않음에도 불구하고 에러가 나타나지 않았습니다.
비어 있는 문자열에서 첫 번째 단어의 인덱스를 이용할 때가 되어서야 에러가 나타났죠.
하지만 슬라이스를 사용하면 이런 버그를 미연에 방지하고, 발생할지도 모를 문제마저 사전에 알 수 있습니다.
이제 슬라이스로 만든 first_word를 잘못된 방법으로 사용했을 때 컴파일 에러가 제대로 나타나는지 확인해 보죠.
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // 에러!
println!("the first word is: {}", word);
}
컴파일러 에러는 다음과 같습니다.
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:18:5
|
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
이전 절에서 배운 대여 규칙 중에서 특정 대상의 불변 참조자가 이미 존재할 경우에는 가변 참조자를 만들 수 없다는 규칙이 있었죠.
clear 함수는 String의 길이를 변경해야 하니 가변 참조자가 필요합니다.
clear 호출 이후 println!은 word의 참조자를 사용하므로, 이 불변 참조자는 이 지점까지 계속 활성화되어 있어야 합니다.
러스트는 clear 내의 가변 참조자와 word의 불변 참조자가 같은 시점에 존재하는 것을 허용하지 않으므로 컴파일 에러가 발생합니다.
이런 식으로 러스트는 우리가 더 간단히 사용할 수 있는 API를 만들도록 도와주고, 다양한 에러를 컴파일 타임에 제거해 줍니다.
슬라이스로서의 문자열 리터럴
앞서, 문자열 리터럴은 바이너리 내에 저장된다는 이야기를 잠깐 언급했는데, 기억하시나요?
슬라이스를 배웠으니, 문자열 리터럴도 제대로 이해해봅시다.
let s = "Hello, world!";여기서 s는 바이너리의 특정 지점을 가리키는 슬라이스입니다.
&str 타입이죠.
&str은 불변 참조자이므로, 문자열 리터럴은 왜 변경할 수 없는지에 대한 의문도 풀립니다.
문자열 슬라이스를 매개변수로 사용하기
리터럴과 String의 슬라이스를 만들 수 있다는 걸 알고 나면
first_word 함수 정의를 다음과 같이 작성할 수 있습니다.
fn first_word(s: &String) -> &str {좀 더 경험이 많은 러스타시안은
다음 예제 3-9처럼 &String 값과 &str 값
모두 사용 가능한 함수를 작성할 것입니다.
s를 문자열 슬라이스 타입으로 변경하여
first_word 함수 개선하기
fn first_word(s: &str) -> &str {
이 시그니처도 같은 생략 규칙에 따라 fn first_word<'a>(s: &'a str) -> &'a str와 같습니다. 매개변수 타입을 더 일반적인 &str로 넓혀도 반환 참조가 입력 참조에 묶인다는 계약은 유지됩니다.
문자열 슬라이스라면 이를 바로 인수로 전달할 수 있습니다.
String이라면 String의 슬라이스 혹은 String에 대한 참조자를 전달할 수 있습니다.
이러한 유연성은 역참조 강제 변환(deref coercions) 기능을 이용하는데, 14장의 ‘함수와 메서드를 이용한 암묵적 역참조 강제 변환’ 절에서 다룰 것입니다.
String에 대한 참조자 대신에 문자열 슬라이스를 매개변수로 하는 함수를 정의하면
기능 면에서 손해보지 않으면서 API를 더 일반적이고 유용하게 만들어 줍니다.
BORROW THE VIEW · KEEP THE OWNER
문자열 뷰를 받는 API는 여러 저장 형태를 빌린다
fn first_word(s: &str)는 저장 형태가 아니라 읽을 문자열 뷰를 요구합니다. 호출 폭은 넓어져도 바이트 범위와 유효성 전제는 사라지지 않습니다.
| 원본·호출 | 함수 경계에서 일어나는 일 | 범위·소유권 결과 |
|---|---|---|
소유한
|
&String이 역참조 강제 변환으로 &str에 맞춰집니다. |
String의 소유권은 호출자에게 남고, 함수는 전체 문자열을 공유 대여합니다. |
|
범위 표현식이 먼저 &str을 만들고 그 값을 그대로 전달합니다. |
바이트 범위가 경계 안에 있고 시작·끝이 UTF-8 문자 경계여야 합니다. 일반 인덱싱은 위반 시 panic합니다. |
문자열 리터럴
|
리터럴 표현식은 이미 프로그램 데이터의 &'static str입니다. |
함수가 소유권을 받지 않으며, 필요하면 더 짧은 참조 수명으로 사용할 수 있습니다. |
다른 연속 시퀀스
|
문자열용 &str이 아니라 요소 타입을 보존하는 &[T] API를 사용합니다. |
원본을 소유하지 않고 데이터 포인터와 요소 수 메타데이터로 구간을 빌리며, 안전한 접근은 범위를 검사합니다. |
&String→&str- 호출
first_word(&my_string) - 계약 역참조 강제 변환으로 전체 문자열을 빌리고 소유권은 호출자에게 남습니다.
String의 부분&str- 호출
first_word(&my_string[0..6]) - 계약 유효한 바이트 범위와 UTF-8 문자 경계를 지켜야 하며, 일반 인덱싱은 위반 시 panic합니다.
- 문자열 리터럴
- 호출
first_word("hello") - 계약 리터럴은 이미
&'static str이고 함수는 소유권을 받지 않습니다. - 일반 슬라이스
&[T] - 예
&a[1..3]: &[i32] - 계약 배열·벡터 같은 연속 시퀀스의 요소 구간을 빌리며, 안전한 접근은 범위를 검사합니다.
&str은 작은 소유 문자열이 아니라 유효한 UTF-8 바이트 구간을 빌리는 참조입니다. 범위가 불확실하면 str::get 계열로 panic 대신 Option을 받을 수 있습니다.예시
아래는 반환 타입까지 바꾼 별도 완전한 프로그램입니다. 원본 코드와 같은 구분 조건인 ASCII 공백 한 바이트 b' '를 유지합니다. 모든 종류의 공백이나 언어별 단어 경계를 나누는 함수는 아닙니다.
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
s
}
fn main() {
let text = String::from("가나 다");
let word = first_word(&text);
println!("{word} {}", word.len());
println!("{:?}", text.get(..1));
}예상 출력은 가나 6, None입니다. 가와 나는 각각 UTF-8 세 바이트이고, 공백은 인덱스 6에서 발견됩니다. 따라서 반환 구간 0..6은 경계가 맞으며 가나를 빌립니다. 반면 끝 인덱스 1은 가의 바이트 중간입니다. &text[..1]로 인덱싱하면 패닉이지만 get의 안전한 반환 계약은 이 조건을 None으로 나타냅니다. {:?}는 Option 값을 디버깅 형태로 표시합니다.
스스로 확인하기
입력만 "봄 날", "가나\t다", " 가나"로 각각 바꾼 사본의 첫 번째 출력 줄은 무엇일까요? 탭을 자동으로 공백과 같은 구분자로 취급하지 말고 if의 실제 비교값을 확인하세요.
구분자와 바이트 길이의 풀이
봄 날은 ASCII 공백이 인덱스 3에 있으므로 봄 3입니다. 가나\t다에서는 탭 바이트가 b' '와 다르고 다른 ASCII 공백도 없어 전체를 반환합니다. 화면에는 가나와 다 사이에 탭이 출력되고, 뒤의 길이는 10입니다(3+3+1+3). 앞에 공백이 있는 경우에는 첫 인덱스 0에서 멈추어 빈 슬라이스를 반환하므로 출력 줄은 공백 한 칸 다음 0입니다. 첫 단어 함수의 정책이 빈 첫 구간을 허용하기 때문입니다.
사용 순서를 바꾸어 확인하기 위의 first_word 함수 정의는 유지하고 main만 아래로 교체합니다.
fn main() {
let mut text = String::from("봄 날");
let word = first_word(&text);
println!("{word}");
text.clear();
println!("{}", text.len());
}예상 출력은 봄 다음 0입니다. word를 출력한 뒤 다시 쓰지 않으므로 clear의 변경 대여 전에 공유 대여를 끝낼 수 있습니다. 마지막에 println!("{word}");를 추가한 사본은 clear 뒤에도 같은 대여를 사용하려 하므로 컴파일 단계에서 거부됩니다. 앞의 인덱스 버전과 달리, 빌린 구간을 이후에 사용하는지 컴파일러가 확인할 수 있습니다.
fn main() {
let my_string = String::from("hello world");
// `first_word`는 `String`의 일부 혹은 전체 슬라이스에 대해 작동합니다
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// 또한 `first_word`는 `String`의 전체 슬라이스와 동일한 `String`의
// 참조자에 대해서도 작동합니다
let word = first_word(&my_string);
let my_string_literal = "hello world";
// `first_word`는 문자열 리터럴의 일부 혹은 전체 슬라이스에 대해 작동합니다
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// 문자열 리터럴은 *곧* 문자열 슬라이스이므로,
// 아래의 코드도 슬라이스 문법 없이 작동합니다!
let word = first_word(my_string_literal);
}
그 외 슬라이스
문자열 슬라이스는 짐작하셨듯 문자열에만 사용됩니다.
하지만 더 범용적인 슬라이스 타입도 존재합니다.
아래 배열을 봅시다.
let a = [1, 2, 3, 4, 5];문자열 일부를 참조할 때처럼 다음과 같이 배열 일부를 참조하고 싶다면 다음과 같이 할 수 있습니다.
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);이 슬라이스는 &[i32] 타입입니다.
동작 방식은 문자열 슬라이스와 마찬가지로 데이터 위치와 길이 메타데이터를 통해 연속 구간을 빌립니다.
배열과 벡터처럼 연속된 요소 시퀀스는 &[T] 또는 &mut [T]로 빌릴 수 있습니다. 모든 컬렉션이 범위 인덱싱이나 슬라이스 표현을 제공하는 것은 아닙니다.
컬렉션은 7장에서 자세히 알아볼 예정입니다.
적용 정리
소유권, 대여, 슬라이스는 러스트가 컴파일 타임에 메모리 안전성을 보장하는 비결입니다.
여타 시스템 프로그래밍 언어처럼 프로그래머에게 메모리 사용 제어 권한을 주면서, 어떠한 데이터의 소유자가 스코프를 벗어날 경우 자동으로 해당 데이터를 정리하는 것이 가능하죠.
소유권과 대여 규칙은 이동된 값 사용이나 충돌하는 대여 같은 메모리 안전 문제를 컴파일 단계에서 줄여줍니다. 그렇더라도 단어를 나누는 정책, 범위·UTF-8 경계, 예상 출력은 직접 확인해야 합니다. 안전한 코드에서도 잘못된 범위 인덱싱은 패닉을 일으킬 수 있으므로 모든 디버깅이 사라지는 것은 아닙니다.
소유권은 수많은 러스트 요소들의 동작 방법에 영향을 미치는 개념인 만큼 이후 내용에서도 계속해서 다룰 예정입니다.
그러면 이제 4장에서 struct로 여러 데이터를 묶는 방법을 알아보죠.
슬라이스를 사용할 때는 원본 컬렉션을 소유하지 않는다는 점과, 시작·끝 범위가 원본 데이터의 유효성에 묶인다는 점을 함께 기억해야 합니다.
슬라이스는 인덱스 값을 따로 들고 다니는 대신 원본 데이터와 유효 범위를 함께 묶어 버그를 컴파일 시점에 줄입니다.