[Rust] 12 소유권 3 - 슬라이스

후유카와·2024년 12월 13일

Rust

목록 보기
12/14

슬라이스

슬라이스(Slice)는 컬렉션을 통째로 참조하는 것이 아니라, 컬렉션의 연속된 일련의 요소를 참조하도록 해준다. 슬라이스는 참조자의 일종으로서 소유권을 갖지 않는다.

예제 1 - 공백으로 구분된 문자열을 받아서 첫 번째 단어 반환하기

구현

슬라이스에 대해 이해하기 위해 먼저 슬라이스 없이 함수를 작성해보자.
이 함수는 소유권이 필요 없기 때문에 &String을 받도록 했다.
일단 인덱스를 반환하도록 하자.

fn first_world(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 매서드를 이용해 바이트 배열로 변환했다.

let bytes = s.as_bytes();

그 다음 바이트 배열에 사용할 반복자를 iter 메서드로 작성한 것이다.

이 코드는 추후에 이해할 수 있기 때문에 지금 단계에서는 enumerate가 각 결괏값을 튜플로 감싸 반환한다는 점(첫 번째 값이 인덱스, 두 번째 요소가 해당 요소의 참조자)과 패턴을 이용해 해제했다는 점만 알고 있으면 된다.

for 반복문 내에서는 바이트 리터럴 문법으로 공백문자를 나타내는 바이트를 찾고, 찾으면 해당 위치를 반환한다. 만약 찾지 못할 경우, s.len()으로 문자열 길이를 반환한다.

문제점

문제점 1
String과 별개로 동작하기 때문에 메인 함수에서 문자를 없애버리는 행동을 하면 기껏 구한 인덱스가 쓸모 없어진다.

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

    let word = first_word(&s); // word는 값 5를 받습니다

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

    // 여기서 word에는 여전히 5가 들어있지만, 이 5를 의미있게 쓸 수 있는
    // 문자열은 더 이상 없습니다. word는 이제 전혀 유효하지 않습니다!
}

문제점 2
만약 첫 번째 단어가 아니라 두 번째 혹은 그 이후에 단어를 구하고 싶다면 구해야 하는, 관리해야 하는 인덱스가 매우 많아진다;;

이때 사용하는 것이 문자열 슬라이스다.

문자열 슬라이스

문자열 슬라이스(String Slice)는 String의 일부를 가리키는 참조자를 의미한다.

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

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

만드는 방식은 String의 참조자와 유사하지만, [시작 인덱스...끝 인덱스] 구조를 갖는다.

슬라이스는 내부적으로 시작 위치, 길이를 데이터 구조에 저장하고 길이 값은 ending_index에서 starting_index 값을 빼서 계산한다.

.. 표현법

  1. 인덱스가 0부터 시작하는 경우, 앞의 값을 생략할 수 있다.
let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];
  1. String의 마지막 바이트까지 포함하는 슬라이스는 뒤의 값을 생략할 수 있다.
let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];
  1. 앞뒤 모두 생략할 경우, 전체 문자열이 슬라이스로 생성된다.
let s = String::from("hello");

let len = s.len();

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

이 내용들을 가지고 first_world 함수가 슬라이스를 반환하도록 다시 작성하자.
문자열 슬라이스는 &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[..]
}

슬라이스로써 문자열 리터럴

let s = "Hello, world!";

이 형태는 문자열 리터럴인데, s는 바이너리 특정 지점을 가리키는 슬라이스다. 즉, %str 타입이다.
&str은 불변 참조자이기 때문에 문자열 리터럴은 왜 변경할 수 없는지에 대한 의문도 풀린다.

문자열 슬라이스를 매개변수로 사용하기

만약, 경험이 더 많은 러스타시안이라면 &String&str 값 모두 사용할 수 있는 함수를 작성할 것이다.

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

문자열 슬라이스라면 바로 인수로 전달할 수 있다.
만약 String이라면 String의 슬라이스나 String에 대한 참조자를 전달할 수 있다.

역참조 강제 변환(deref coercions)
유연하게 대응할 수 있는데 역참조 강제 변환 을 사용했기 때문이다. 이는 나중에 자세히 다룰 것이다.

String에 대한 참조자 대신 문자열 슬라이스를 매개변수로 하는 함수를 정의하면 기능 면에서 손해보지 않으면서 API를 더 일반적이고 유용하게 만들어 준다.

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] 타입이다. 동작 방식은 문자열 슬라이스와 동일하다.

슬라이스의 첫 번째 요소를 참조하는 참조자슬라이스의 길이를 저장하여 동작한다.

이런 슬라이스는 모든 컬렉션에 사용할 수 있다.

정리

소유권, 대여, 슬라이스는 러스트가 컴파일할 때 메모리 안정성을 보장하는 비결이다.
기존의 프로그래밍 언어처럼 메모리 사용 제어 권한을 주면서, 어떤 데이터의 소유자가 스코프를 벗어날 경우 자동으로 해당 데이터를 정리해준다.
또한 제어 코드를 추가 작성하고 디버깅할 필요가 없기에 프로그래머의 일이 줄어드는 결과도 가져온다.

소유권의 개념이 러스트에서 많은 영향을 미치는 개념인 만큼 이후 내용에서도 계속해서 다룰 예정이다.

profile
안녕하세요! 저는 전자공학을 전공하며 하드웨어와 소프트웨어 모두를 깊이 있게 공부하고 있는 후유카와입니다. Verilog HDL, C/C++, Java, Python 등 다양한 프로그래밍 언어를 다루고 있으며, 최근에는 알고리즘에 대한 학습에 집중하고 있습니다. 기술적인 내용을 공유하고, 함께 성장할 수 있는 공간이 되기를 바랍니다. 잘못된 내용이나 피드백은 언제나 환영합니다! 함께 소통하며 더 나은 지식을 쌓아가요. 감사합니다!

0개의 댓글