[Chapter4-3] Rust 소유권(Ownership) 03 - 슬라이스(Slices)

hwwwa·2021년 10월 28일
0

🦀 Rust

목록 보기
10/25

슬라이스(Slices)

소유권을 갖지 않는 또 다른 데이터 타입은 슬라이스 입니다. 슬라이스는 컬렉션(collection) 전체가 아닌 컬렉션의 연속된 일련의 요소들을 참조할 수 있게 합니다.

String을 입력 받아 String에서 찾은 첫 번째 단어를 반환하는 함수를 작성해봅시다.

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 메소드를 이용해 바이트 배열로 변환합니다.

iter 메소드를 이용해 바이트 배열의 반복자를 생성하고, enumerate 는 iter 의 결과값을 직접 반환하는 대신 이를 감싸서 튜플의 일부로 만들어 반환합니다. 반환된 튜플의 첫 번째 요소는 인덱스이며, 두 번째 요소는 요소에 대한 참조값입니다. enumerate 메소드가 튜플을 반환하기 때문에 튜플 해체를 위한 패턴을 이용합니다. for 루프 내에서 i 는 튜플 내의 인덱스에 대응하고, &item 은 튜플 내의 한 바이트에 대응하는 패턴입니다. .iter().enumerate() 의 요소에 대한 참조자를 갖는 것이므로 &을 패턴 내에 사용합니다.

for문 안에서 바이트 리터럴 문법을 이요해 공백 문자를 나타내는 바이트를 찾고, 공백 문자의 위치를 반환합니다. 공백이 없다면 String의 길이값을 반환합니다.

이제 first_word 함수를 호출하여 결과를 저장한 뒤 String의 내용물을 바꾸는 main 함수를 작성해봅시다.

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word는 5를 갖게 될 것입니다.

    s.clear(); // 이 코드는 String을 비워서 ""로 만들게 됩니다.

    // word는 여기서 여전히 5를 갖고 있지만, 5라는 값을 의미있게 쓸 수 있는 스트링은 이제 없습니다.
    // word는 이제 완전 유효하지 않습니다!
}

word 는 s 의 상태와 전혀 연결되어 있지 않으므로 s.clear() 호출 뒤에 사용해도 여전히 5를 담고 있습니다. 하지만 s의 값이 바뀌었기 때문에 첫번째 단어를 추출할 수 없게 됩니다.

이처럼 싱크가 맞지 않는 것은 쉽게 발생할 수 있는 오류입니다. 특정 상태에 있는 데이터로부터 계산되었지만 그 상태와 전혀 묶여있지 않은 더 많은 값들을 갖게 될 것입니다.

Rust는 이러한 문제에 대한 해결책을 갖고 있습니다. 바로 스트링 슬라이스(String Slice) 입니다.

String Slice

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

스트링 슬라이스는 String의 일부분에 대한 참조자입니다. [0..5] 코드는 0 이상 5 미만의 연속된 범위를 뜻합니다.

Rust의 .. 범위 문법 사용 시 만약 첫번째 인덱스부터 시작하길 원할 때, 0을 생략할 수 있습니다. 아래의 두 줄은 동일한 표현입니다.

let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];

비슷하게 마지막 바이트까지 포함한다면 끝의 숫자를 생략할 수 있습니다. 아래의 두 줄은 동일한 표현입니다.

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[..];

앞서 작성하던 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[..]
}

이제 first_word 함수 호출 시, 해당 데이터와 묶여있는 하나의 값을 반환받게 됩니다. 값은 슬라이스의 시작 위치에 대한 참조자와 슬라이스의 길이로 이루어져 있습니다.

이제 컴파일러가 String에 대한 참조자들이 유효한 상태로 남아있게끔 보장해줄 것입니다. 앞서 인덱스를 찾은 후 String을 변경해버려서 인덱스가 유효하지 않게 했던 버그는 코드가 논리적으로 맞지 않지만 어떠한 즉각적인 오류도 보여주지 못했습니다. 하지만 스트링 슬라이스는 이러한 버그를 불가능하게 만들고 컴파일 타임 오류를 발생시켜줄 것입니다.

스트링 리터럴은 슬라이스이다.

let s = "Hello, world!";

위 코드에서 s의 타입은 &str 입니다. 이는 바이너리의 특정 지점을 가리키고 있는 슬라이스입니다. 이는 왜 스트링 리터럴이 불변인지도 설명해줍니다. &str 은 불변 참조자이기 때문입니다.

파라미터로서의 스트링 슬라이스

first_word 함수를 한 번 더 개선시킬 수 있습니다.

fn first_word(s: &String) -> &str {

아래의 코드로도 작성할 수 있습니다. &String&str 둘 모두에 대한 같은 함수를 사용할 수 있도록 해주기 때문입니다.

fn first_word(s: &str) -> &str {

스트링 슬라이스를 갖고 있을 때 바로 넘길 수 있고, String을 갖고있을 때에도 String의 전체 슬라이스를 넘길 수 있습니다. 함수가 String 참조자 대신 스트링 슬라이스를 갖도록 정의하는 것은 API를 어떠한 기능적 손실 없이도 더 일반적이고 유용한 코드로 만들어줍니다.

fn main() {
    let my_string = String::from("hello world");

    // first_word가 `String`의 슬라이스로 동작합니다.
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word가 스트링 리터럴의 슬라이스로 동작합니다.
    let word = first_word(&my_string_literal[..]);

    // 스트링 리터럴은 *또한* 스트링 슬라이스이기 때문에,
    // 아래 코드도 슬라이스 문법 없이 동작합니다!
    let word = first_word(my_string_literal);
}

그 밖의 슬라이스들

스트링 슬라이스는 스트링에 특정되어 있습니다. 하지만 더 일반적인 슬라이스 타입들도 존재합니다.

배열의 일부를 참조하고 싶다면 아래의 코드를 사용할 수 있습니다.

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

해당 슬라이스는 &[i32] 타입을 갖습니다. 스트링 슬라이스 동작 방법과 똑같이 슬라이스 첫번째 요소에 대한 참조자와 슬라이스의 길이를 저장합니다. 다른 모든 종류의 컬렉션들에 대해 이러한 슬라이스를 이용할 수 있습니다.

0개의 댓글