슬라이스(slice, 타입 [T] 및 &[T])는 러스트에서 배열, 벡터 및 기타 요소 시퀀스의 하위 집합에 안전하고 효율적으로 접근하기 위해 도입되었습니다. 이들은 데이터의 할당 및 복사를 피하고, 컬렉션의 일부에 대한 "보기" 또는 창을 제공합니다. 이는 컴파일 단계에서 크기가 고정된 배열과 포인터 및 길이를 저장하지만 메모리를 소유하는 동적 컬렉션과는 다릅니다.
메모리 생명 주기에 대한 엄격한 통제가 없는 언어에서 배열 및 벡터와 작업할 때는 종종 범위를 벗어난 접근(out of bounds) 오류, 메모리 누수 및 잘못된 포인터 사용이 발생합니다. 컬렉션의 하위 집합을 작업할 때 복사가 발생하지 않고 메모리 안전성이 손실되지 않도록 하는 것이 중요합니다. 이는 시스템 수준에서 특히 중요합니다.
러스트에서 슬라이스는 데이터의 부분에 대한 "포인터 + 길이"이며 내용물에 대한 소유권이 없습니다. 슬라이스는 항상 생명 주기를 동반하며, 컴파일러는 슬라이스가 원본(array, Vec, String)보다 오래 지속되지 않도록 보장합니다. 슬라이스와의 모든 작업은 안전한 접근 방법을 통해 이루어지며, 경계 초과 접근 시 런타임에서 panic이 발생합니다.
코드 예:
let arr = [1, 2, 3, 4, 5]; let slice = &arr[1..4]; // [2,3,4] 타입: &[i32] let mut vec = vec![10, 20, 30]; let mut_slice: &mut [i32] = &mut vec[..2]; mut_slice[0] = 99; assert_eq!(vec, [99, 20, 30]);
주요 특징:
원본 배열이나 벡터의 크기를 초과하는 슬라이스를 생성할 수 있습니까?
아니요. 컴파일러와 런타임은 슬라이스가 원본 데이터의 유효한 인덱스 내에서만 생성될 수 있도록 보장합니다. 경계를 넘어서는 시도는 panic을 발생시킵니다.
let arr = [1, 2, 3]; let s = &arr[0..4]; // 실행 시 panic
슬라이스는 독립적인 메모리 소유자인가요?
아니요. 슬라이스는 데이터에 대한 "창"일 뿐이며 메모리를 소유하지 않습니다. 원본이 로컬인 경우 함수에서 슬라이스를 반환하려고 하면 컴파일 시간 오류가 발생합니다.
fn give_slice() -> &[i32] { let arr = [1,2,3]; &arr[1..] } // 오류: arr이 충분히 오래 살아있지 않음
슬라이스와 러스트 배열은 타입 및 연산 수준에서 어떤 차이가 있습니까?
배열은 컴파일 시간에 알려진 고정 길이를 가지며 스택에 완전히 포함됩니다. 슬라이스는 길이가 동적으로 결정될 수 있으며 항상 포인터와 길이를 저장합니다.
let a: [u32; 3] = [1,2,3]; // 고정 길이 배열 let s: &[u32] = &a[..]; // 어떤 크기의 슬라이스
프로그래머가 로컬 배열에서 슬라이스를 반환했습니다. 함수가 종료된 후 원본이 삭제되고 슬라이스가 "느슨한" 포인터가 되었습니다. 이로 인해 버그가 발생하고 심지어 프로그램이 중단되었습니다.
장점:
단점:
슬라이스는 항상 외부 데이터에 대한 참조로 생성되며, 데이터 소유자와 슬라이스는 동일한 시간만큼 살아 있습니다. 컴파일러는 슬라이스와 원본 간의 생명 주기가 밀접하게 연결되어 있음을 보장합니다.
장점:
단점: