RustBook - 4.3 The Slice Type

숲사람·2022년 3월 23일
0

Rust

목록 보기
9/15
post-custom-banner

이 시리즈는 Rust Book을 공부하고 정리한 문서입니다. 댓글로 많은 조언 부탁드립니다.
Rust Book: https://doc.rust-lang.org/book/


Slice 타입

소유권을 가지지 않는 또 하나의 타입이다.

먼저 만약 slice가 없을때 우리가 문자열에서 (스페이스로 구분된) 첫번째 단어를 반환하는 함수는 어떻게 만들 수 있을지 살펴보자. 일단 다음과 같이 단어 끝의 인덱스를 리턴하는 함수를 만들 수는 있다.

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()
}
  • .as_bytes() :문자열을 바이트 배열로 변환
  • .iter() : 바이트 배열에 해당하는 Iterator 생성 (13장)
  • .enumerate() : iter() 의 결과를 wraping 하여 튜플로 반환, reference 타입도 반환함에 주의

리턴값은 String 타입인 s가 유효(valid) 할때만 의미가 있다. s 가 메모리에서 drop()되면 해당 값은 아무 의미가 없어진다.

fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s); // word will get the value 5
    s.clear(); // this empties the String, making it equal to ""
    // word값 5는 문자열 s가 없이는 아무 의미가 없다. 
    // we could meaningfully use the value 5 with. word is now totally invalid!
}

String s와 word 값은 어떠한 커넥션도 가지고 있지 않다. 이렇게 string의 인덱스가 해당 String과 싱크가 맞지 않으면 오류가 발생할 여지가 있다. 가령 word를 얻은뒤에 문자열 s가 바뀌었다면 word값은 잘못된 값이 될 것이다.

이런문제를 해결하기 위해 러스트에는 String Slice 가 있다.

String Slice

String Slice 는 String의 일부분에 대한 reference(참조자) 이다.

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

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

&s[0..5]&s 처럼 전체 문자열의 reference가 아니라 부분문자열의 reference를 의미한다. [st..en] 실제 값은 st부터 (en - 1) 까지를 의미. 슬라이스 데이터 구조는 시작위치슬라이스의 길이를 저장한다. 길이는 (en - st) 한 값이다. 아래 데이터 구조를 보면 슬라이스 타입인 world는 시작 위치를 가지고 있다.

첫번째 인덱스부터 시작하는 슬라이스는 [..end] 로 표기 가능.

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

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

슬라이스가 문자열의 특정 위치부터 마지막 바이트를 가리키고자 할때는 [st..] 로 표기 가능.

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

[..] 는 전체 문자열을 의미

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

첫번째 스페이스(b' ')에 해당하는 인덱스를 찾은뒤 바로 슬라이스 타입으로 반환하면 간단하게 해결된다.

fn main() {
    let mut s = String::from("hello world");
  | let word = first_word(&s);
  | s.clear(); // error!
  | println!("the first word is: {}", word);
}

s.clear() 를 시도할때 clear함수는 s의 mutable reference를 갖기 위한 시도를 할것이다. 결과적으로 한 스코프에 mutable reference와 immutable reference 가 동시에 존재하는 것이므로 data race관련 규칙에 따라 컴파일 에러가 발생한다. 다음도 마찬가지.

좀더 부연설명을 하면 여기서 word의 scope 는 | 로 표기된 부분이다. (선언하고 사용할때까지 스코프가 만들어지는듯). 그 동일한 스코프 내에서 &s &mut s 가 동시에 있기 때문에 컴파일 에러가 나는 것이다. 만약 아래와 같이 수정하면 두가지가 동일한 스코프내에 있지 않기 때문에 컴파일 에러가 발생하지 않을것이다.

fn main() {
    let mut s = String::from("hello world");
  | let word = first_word(&s);
  | println!("the first word is: {}", word);
    s.clear(); // error!

}

여기서 word 의 스코프 안은 &s만 존재한다.


fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s);
    s.push_str(", jihun"); // Error!
    println!("{} -> {}", s, word);
}

(질문) 이렇게 반환된 값은 이후에 String이 변경 되었을때도 자동으로 리턴값이 바뀔까?

다시 이 질문으로 돌아와서 답을 해보자면, 애초에 string 자체를 변경하는것이 불가능하다. s 를 변경하는 행위를 하기 위해서 push_str() 같은 함수는 s에 대한 mutable reference 를 얻을 것이고 , 그러면 mutable reference (s.push_str()) 와 immutable reference(first_word(&s)) 가 동시에 한 스코프에 존재할것이기에 규칙에 어긋난다. 그렇다고 first_word(&mut s) 하는건 mutable reference가 두개이기 때문에 또 불가능하다.

String 리터럴은 Slice다

let s = "Hello, world!";

스트링 리터럴 타입은 &str 이다. 따라서 immutable reference 이다. 이것이 스트링 리터럴을 수정하지 못하는 이유이다. 여기서 &s[0..6] 은 "Hello" 이다.

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

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

대신에

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

를 사용하면 first_word() 함수의 인자로 String을 넘길수도 스트링 리터럴을 넘길수도 있기때문에 더 유용한 방법이다.

fn main() {
    // 1. `String`의 슬라이스
    let my_string = String::from("hello world");
    let word = first_word(&my_string[..]);

    // 2. 스트링 리터럴의 슬라이스
    let my_string_literal = "hello world";
    let word = first_word(&my_string_literal[..]);

    // 3. 스트링 리터럴 자체도 가능.
    let word = first_word(my_string_literal);
}

배열 슬라이스

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

println!("{}, {}", slice[0], slice[1]);

2, 3

slice&[i32] 타입을 갖는다. 이것도 스트링 슬라이스와 같이 데이터 구조에 첫번재 요소와 슬라이스 길이가 저장된다. (8장 벡터 참고)

profile
기록 & 정리 아카이브용
post-custom-banner

0개의 댓글