#6 참조와 대여

Pt J·2020년 8월 16일
0

[完] Rust Programming

목록 보기
8/41
post-thumbnail

이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.

함수의 인자로 변수를 전달할 때도 값의 복사 또는 이동이 일어난다.
함수의 매개변수에 인자를 bind한다고 생각할 수 있겠다.
따라서 복사가 아니라 이동을 사용하는 값을 함수에 전달할 경우
소유권이 이전되어 함수에서 반환되었을 때 그것에 접근하지 못한다.

함수에서 반환된 후에도 그 값을 다시 사용하고 싶다면
반환값으로 전달해 그 변수의 소유권을 다시 이전시키는 방법도 있긴 하지만
Rust에서는 참조라는 개념을 통해 함수가 소유권을 가져가는 게 아니라 대여할 수 있도록 한다.

참조 Reference

참조를 통해 함수에 인자를 전달하면 그 소유권은 이전되지 않는다.
따라서 함수를 호출하고 반환된 이후에도 인자로 전달했던 값을 사용할 수 있다.
참조를 위해서는 매개변수 자료형 앞과 인자 앞에 &를 붙여 준다.
소유권을 가진 변수가 아닌 소유권을 대여한 변수는 범위를 벗어나도 drop 함수를 호출하지 않는다.

간단한 예제를 통해 확인해보자.

peter@hp-laptop:~/rust-practice$ mkdir chapter04
peter@hp-laptop:~/rust-practice$ cd chapter04
peter@hp-laptop:~/rust-practice/chapter04$ cargo new reference
     Created binary (application) `reference` package
peter@hp-laptop:~/rust-practice/chapter04$ cd reference/
peter@hp-laptop:~/rust-practice/chapter04/reference$ vi src/main.rs

src/main.rs

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

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}
peter@hp-laptop:~/rust-practice/chapter04/reference$ cargo run
   Compiling reference v0.1.0 (/home/peter/rust-practice/chapter04/reference)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/reference`
The length of 'hello' is 5
peter@hp-laptop:~/rust-practice/chapter04/reference$ 

만약 우리가 여기서 참조를 위한 &을 제거한다면
println!에서 s1을 사용할 수 없다는 오류가 발생할 것이다.
그리고 참조 없이 이와 같은 코드를 구현하려면 (usize, String) 튜플을 통해
인자로 전달한 값의 소유권도 다시 돌려 받도록 구현해야 하여 복잡도가 높아질 것이다.

참조를 사용할 때 유의해야 할 점은, 함수 내부의 값에 대한 참조를 반환할 수 없다는 것이다.
함수가 반환된 후에는 그 참조변수가 대여하고 있는 원본 값이 범위를 벗어나 제거되었으니 말이다.

가변 참조 Mutable Reference

변수를 대여하여 사용할 경우 그것은 기본적으로 불변성을 가진다.
대여한 변수를 수정하고 싶다면 &와 함께 mut 키워드도 붙여 주어야 한다.
물론 가변 참조를 하기 위해서는 원본 변수 자체도 가변성을 띄어야 한다.
불변성을 띄는 변수를 가변 참조 하려고 하면 오류가 나는 것을 볼 수 있을 것이다.

peter@hp-laptop:~/rust-practice/chapter04/reference$ cd ..
peter@hp-laptop:~/rust-practice/chapter04$ cargo new mutable_reference
     Created binary (application) `mutable_reference` package
peter@hp-laptop:~/rust-practice/chapter04$ cd mutable_reference/
peter@hp-laptop:~/rust-practice/chapter04/mutable_reference$ vi src/main.rs

src/main.rs

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

    change(&mut s);
    println!("string: {}", s);
}

fn change(s: &mut String) {
    s.push_str(", world");
}
peter@hp-laptop:~/rust-practice/chapter04/mutable_reference$ cargo run
   Compiling mutable_reference v0.1.0 (/home/peter/rust-practice/chapter04/mutable_reference)
    Finished dev [unoptimized + debuginfo] target(s) in 0.22s
     Running `target/debug/mutable_reference`
string: hello, world
peter@hp-laptop:~/rust-practice/chapter04/mutable_reference$ 

가변 참조를 사용할 때 유의해야 할 점은
한 순간에 한 자료에 대한 가변 참조는 최대 하나만 존재할 수 있다는 것이다.
즉, 다음과 같은 코드는 에러를 야기한다.

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

let r1 = &mut s;
let r2 = &mut s;

r1r2가 둘 다 s를 가변 참조한다는 것이 에러의 내용이다.
이러한 제약은 같은 자료가 가변 참조로 의해 서로 다른 맥락에서 수정되면서
데이터 레이스가 발생하는 것을 방지한다.
데이터 레이스는 서로 다른 스레드가 같은 값에 접근을 시도하는 레이스 컨디션과 유사하다.

여담: 데이터 레이스data race
서로 다른 포인터가 같은 자료를 접근하면서 서로 다른 맥락에서 수정하여
결과적으로 예측할 수 없는 결과를 유발하는 경우를 의미한다.
다음과 같은 상황이 겹칠 때 데이터 레이스가 발생하곤 한다.

  • 둘 이상의 포인터가 동시에 같은 자료에 접근하는 경우
    Two or more pointers access the same data at the same time.
  • 하나 이상의 포인터가 자료를 쓰기 위해 사용되는 경우
    At least one of the pointers is being used to write to the data.
  • 자료에 대한 접근을 동기화 할 메커니즘이 존재하지 않는 경우
    There’s no mechanism being used to synchronize access to the data.

데이터 레이스 이슈는 둘 이상의 가변 참조를 사용할 때뿐만 아니라
가변 참조와 불변 참조를 섞어 사용할 때도 발생한다.
가변 참조를 통해 값을 변경하게 되면 불변 참조로 참조하고 있는 값이 변하기 때문에
이로 인한 의도치 않은 결과가 발생할 수 있다.
따라서 이러한 상황도 Rust는 컴파일 시점에 사전 차단한다.
물론 불변 참조는 동시에 여러 개 사용할 수 있다.
이로서 데이터 레이스 이슈로 인해 발생할 수 있는 버그 픽스 비용을 절감할 수 있다.

슬라이스 자료형 Slice

조금 특별한 참조 형태로 슬라이스 자료형이라는 것이 있다.
이 녀석은 N개의 요소들로 이루어진 컬렉션에서 연속된 일부 요소들을 참조할 수 있다.
슬라이스 자료형은 참조의 일종이므로 소유권을 갖지 않는다.

슬라이스 자료형이 어떤 상황에서 유용하게 쓰일 수 있는지
문자열에서 첫번째 단어를 찾아내는 함수를 작성해보며 알아보자.

우리가 지금까지 배운 내용을 통해 이 함수를 구현하고자 한다면
첫번째 단어의 마지막 글자 인덱스를 반환하는 방식을 생각해볼 수 있다.
반복문을 통해 문자열을 탐색하며 띄어쓰기를 만나면 인덱스를 반환하는 방식이다.
그런데 이렇게 작성할 경우 몇 가지 문제점이 있다.

만약 어떤 문자열의 첫번째 단어의 마지막 글자 인덱스를 어떤 정수 자료형의 변수에 저장해놨는데
그 문자열이 변경되거나 사라진다면
우리가 가지고 있는 정수 자료형의 변수는 의미 없는 값을 가지게 된다.
그리고 나중에 다루겠지만 Rust는 인덱스를 통해 문자열의 특정 문자에 접근하지 못한다.

이럴 때 사용할 수 있는 게 슬라이스 자료형이다.
문자열 슬라이스를 통해 문자열의 연속된 일부 문자들을 참조할 수 있다.
문자열 슬라이스는 다음과 같이 참조에 범위를 지정하여 사용한다.

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

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

이 때, 범위가 맨 처음부터 시작하거나 맨 마지막으로 끝나면
다음과 같이 그 부분을 생략할 수 있다.

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

같은 맥락에서 전체 범위를 참조하려면 양쪽 모두 생략 가능하다.

let total = &s[..];

이러한 슬라이스 자료형을 사용하면 우리가 원하는 코드를 다음과 같이 작성할 수 있다.

peter@hp-laptop:~/rust-practice/chapter04/mutable_reference$ cd ..
peter@hp-laptop:~/rust-practice/chapter04$ cargo new first_word
     Created binary (application) `first_word` package
peter@hp-laptop:~/rust-practice/chapter04$ cd first_word/
peter@hp-laptop:~/rust-practice/chapter04/first_word$ 

src/main.rs

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

    println!("first word: {}", first_word(&s));
}

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[..i];
        }
    }

    &s[..]
}
peter@hp-laptop:~/rust-practice/chapter04/first_word$ cargo run
   Compiling first_word v0.1.0 (/home/peter/rust-practice/chapter04/first_word)
    Finished dev [unoptimized + debuginfo] target(s) in 0.22s
     Running `target/debug/first_word`
first word: hello
peter@hp-laptop:~/rust-practice/chapter04/first_word$ 

문자열 슬라이스를 사용하면 원본 문자열이 변경되거나 사라졌을 때에 대한 버그를
컴파일러 수준에서 사전 차단해주므로 보다 안전하게 사용할 수 있다.
문자열을 수정하기 위해 push_str와 같은 메서드를 사용하거나
문자열을 제거하기 위해 clear와 같은 메서드를 사용할 경우
해당 문자열에 대한 가변 참조를 하게 되는데
이미 그것에 대한 불변 참조인 슬라이스가 존재하므로 가변 참조를 할 수 없는 것이다.

사실 문자열 슬라이스를 다룰 때 주의해야 할 부분이 있는데
그건 나중에 문자열에 대해 더 자세히 다룰 때 이야기하도록 하겠다.

문자열 리터럴

문자열 리터럴은 String 자료형과는 달리 불변 값이다.
사실 이 문자열 리터럴의 자료형이 문자열 슬라이스였다.
바이너리 파일 중 이 문자열 리터럴의 값에 해당하는 부분만 참조하고 있는 것이다.
그리고 슬라이스 자료형은 불변참조이기에 문자열 리터럴은 항상 불변이다.
따라서 앞서 구현한 함수를 first_word(s: &String) -> &str가 아닌
first_word(s: &str) -> &str으로 작성할 경우
문자열 리터럴과 String 자료형 모두에 사용할 수 있게 된다.
문자열 리터럴은 있는 그대로 전달하고 String 자료형은 전체 참조([..])로 전달해서 말이다.

다른 슬라이스

물론 문자열 외에도 슬라이스 자료형은 존재한다.
예를 들어 다음과 같이 배열의 연속된 일부 요소들을 참조할 수도 있다.

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

slice&[i32] 자료형 변수로, [1, 2]를 참조한다.
그 외에도 컬렉션에 해당하는 녀석들에 대해 사용할 수 있는데
컬렉션에 대한 이야기는 나중에 하도록 하겠다.

이 포스트의 내용은 공식문서의 4장 2절 References and Borrowing & 4장 3절 The Slice Type에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글