콘텐츠로 이동

Rust 슬라이스

슬라이스(slice)를 쓰면 배열이나 벡터의 일부분을 대여할 수 있어요. 데이터의 한 구간을 들여다보는 “창문” 같은 것이라서, 데이터를 복사하지도 않고 소유권을 가져가지도 않아요. 값 하나를 대여해 본 적이 있다면, 슬라이스는 같은 아이디어를 여러 요소의 범위에 적용한 거라고 생각하면 돼요.

fn main() {
let numbers = [10, 20, 30, 40, 50];
let middle = &numbers[1..4]; // 인덱스 1, 2, 3의 요소
println!("{:?}", middle);
}

실행 결과:

[20, 30, 40]
▶ 직접 실행해 보기

1..4는 for 반복문에서 봤던 것과 같은 범위예요. 인덱스 1에서 시작해서 인덱스 4 직전에 멈추기 때문에, 인덱스 1, 2, 3을 얻게 돼요.

자주 쓰이는 줄임 표현이 몇 가지 있어요.

fn main() {
let numbers = [10, 20, 30, 40, 50];
println!("{:?}", &numbers[..3]); // 처음부터 인덱스 3 직전까지 -> [10, 20, 30]
println!("{:?}", &numbers[2..]); // 인덱스 2부터 끝까지 -> [30, 40, 50]
println!("{:?}", &numbers[..]); // 전체 -> [10, 20, 30, 40, 50]
}
▶ 직접 실행해 보기

사실 여러분은 이미 슬라이스를 쓰고 있었어요. &str은 String(또는 다른 문자열)의 슬라이스거든요! 그래서 이 두 타입이 서로 잘 어울리는 거예요.

fn main() {
let sentence = String::from("The quick brown fox");
let first_word = &sentence[0..3];
println!("{first_word}"); // "The"
}
▶ 직접 실행해 보기

여기서는 문자열을 영어 그대로 두었어요. [0..3]은 글자 수가 아니라 바이트 위치로 자르는데, 한글 한 글자는 UTF-8에서 3바이트를 차지해서 결과가 달라지기 때문이에요.

왜 복사하지 않고 슬라이스를 쓸까요?

섹션 제목: “왜 복사하지 않고 슬라이스를 쓸까요?”

원하는 구간만큼 새 Vec이나 String을 복사해서 만드는 방법도 떠올릴 수 있어요. 슬라이스는 그럴 필요가 아예 없어요. 참조자(&)가 값 하나를 가리키듯이, 슬라이스는 기존 데이터의 일부를 가리키기만 하거든요. 그러니 추가 메모리 할당도 없고 복사도 없어요. 그리고 대여 페이지에서 배운 대여 규칙도 그대로 적용돼요.

슬라이스는 함수 매개변수로 특히 유용해요. 배열과 벡터를 모두 받을 수 있으니까요.

fn sum(numbers: &[i32]) -> i32 {
let mut total = 0;
for n in numbers {
total += n;
}
total
}
fn main() {
let array = [1, 2, 3];
let vector = vec![4, 5, 6];
println!("{}", sum(&array));
println!("{}", sum(&vector));
}
▶ 직접 실행해 보기

&[i32]는 배열의 슬라이스든 Vec의 슬라이스든 둘 다 받아요. 함수 하나로 두 가지 호출이 모두 동작해요.

가변 슬라이스로 데이터 바꾸기

섹션 제목: “가변 슬라이스로 데이터 바꾸기”

지금까지의 슬라이스는 데이터를 읽기만 했어요. 가변 슬라이스 &mut [T]를 쓰면 함수가 전달받은 요소를 바꿀 수 있어요. 단, 전달받은 요소만요.

fn double_all(numbers: &mut [i32]) {
for i in 0..numbers.len() {
numbers[i] *= 2;
}
}
fn main() {
let mut scores = vec![1, 2, 3, 4, 5];
double_all(&mut scores[1..3]); // 인덱스 1과 2만
println!("{:?}", scores);
double_all(&mut scores); // 벡터 전체
println!("{:?}", scores);
}

실행 결과:

[1, 4, 6, 4, 5]
[2, 8, 12, 8, 10]
▶ 직접 실행해 보기

첫 번째 호출은 &mut scores[1..3]을 넘기기 때문에, double_all은 인덱스 1과 2만 볼 수 있어요. 그 범위 밖의 요소는 건드릴 수 없으니 안전해요. 함수 안에서는 슬라이스가 다시 인덱스 0부터 시작해요. double_all 입장에서는 그냥 숫자 두 개짜리 목록일 뿐이에요.

이게 안전한 이유는 대여 페이지에서 제대로 배울 규칙 덕분이에요. 함수가 &mut 슬라이스를 들고 있는 동안에는 다른 어떤 코드도 그 요소들을 쓸 수 없어서, 코드의 두 부분이 동시에 같은 요소를 바꾸는 일이 절대 생기지 않아요. 그리고 벡터도 mut로 선언해야 해요. 그렇지 않으면 Rust가 가변으로 대여하는 걸 허락하지 않아요.

벡터 vec![3, 6, 9, 12, 15, 18]을 만들고, 앞쪽 절반과 뒤쪽 절반을 각각 슬라이스로 출력해 보세요. 힌트: let mid = numbers.len() / 2;를 계산한 다음 ..mid와 mid..를 쓰세요.

정답 보기
fn main() {
let numbers = vec![3, 6, 9, 12, 15, 18];
let mid = numbers.len() / 2;
let first_half = &numbers[..mid];
let second_half = &numbers[mid..];
println!("앞쪽 절반: {:?}", first_half);
println!("뒤쪽 절반: {:?}", second_half);
}

실행 결과:

앞쪽 절반: [3, 6, 9]
뒤쪽 절반: [12, 15, 18]
▶ 직접 실행해 보기

mid를 len()으로 계산하기 때문에, 숫자를 더하거나 빼도 계속 잘 동작해요. 길이가 홀수이면 남는 하나는 뒤쪽 절반에 들어가요.

2. 어떤 목록에서든 가장 큰 수 찾기

섹션 제목: “2. 어떤 목록에서든 가장 큰 수 찾기”

가장 큰 수를 돌려주는 함수 largest(numbers: &[i32]) -> i32를 작성해 보세요. 배열로 한 번, 벡터로 한 번, 그리고 벡터의 첫 번째 요소만 담은 슬라이스로 한 번 호출해 보세요. 힌트: numbers[0]에서 시작해서 나머지를 반복문으로 훑어보세요. 배열에서 하던 것처럼요.

정답 보기
fn largest(numbers: &[i32]) -> i32 {
let mut biggest = numbers[0];
for n in numbers {
if *n > biggest {
biggest = *n;
}
}
biggest
}
fn main() {
let array = [7, 2, 9, 4];
let vector = vec![10, 35, 21];
println!("{}", largest(&array));
println!("{}", largest(&vector));
println!("{}", largest(&vector[..1]));
}

실행 결과:

9
35
10
▶ 직접 실행해 보기

슬라이스를 반복하면 숫자 자체가 아니라 각 요소의 참조자(&i32)를 받게 돼요. *n은 “n이 가리키는 값”이라는 뜻이라서, *n > biggest는 평범한 숫자 두 개를 비교해요. 이 코드는 빈 슬라이스를 받으면 패닉이 일어나요. numbers[0]이 존재하지 않으니까요. 이런 경우를 위해 Option을 돌려주는 방법을 나중에 배울 거예요.

  • 슬라이스(&[T])는 배열이나 벡터의 일부를 복사하지 않고 대여해요.
  • 1..4는 인덱스 1부터 3까지를 골라요. ..의 한쪽을 비워 두면 처음부터, 또는 끝까지를 뜻해요.
  • &str 자체가 텍스트의 슬라이스예요. 그래서 문자열과 슬라이스는 동작이 많이 닮았어요.
  • 배열과 벡터를 모두 받고 싶다면 함수 매개변수 타입으로 &[T]를 쓰는 게 좋아요.
  • 가변 슬라이스(&mut [T])를 쓰면 함수가 전달받은 요소만 바꿀 수 있어요.

이해도 점검

1. 슬라이스를 쓰면 배열이나 벡터의 일부로 무엇을 할 수 있나요?

2. &numbers[1..4]에는 어떤 인덱스가 포함되나요?

3. 함수 매개변수 타입으로 &[i32]가 좋은 이유는 무엇인가요?

점수: 0 / 3