JS, TS랑 비교하며 Rust 공부하기 (2)

안 까먹기 위해 러스트로 코테 문제를 꾸준히 풀고 있으면서 블로그에 글을 쓰는 건 계속 미뤄버렸다.
러스트로 코테 문제를 풀면서 느낀 점이 있는데 숫자 타입과 소유권이 참 번거롭다는 것이다. (JS가 참 편한 언어다…)
어쨋든 얼른 정리해서 끝내버리자.


소유권

소유권의 개념을 알기전에 알아야 할 정보가 있다. 러스트에서는 변수가 자신이 소속된 스코프를 벗어나는 순간 자동으로 메모리를 해제하는 방식을 택했으며, 변수가 스코프 밖으로 벗어나면 drop이라는 특별한 함수를 호출한다.

소유권의 규칙은 아래와 같다.

  • 각각의 값은 소유자가 정해져 있다.
  • 한 값의 소유자는 동시에 여럿 존재할 수 없다.
  • 소유자가 스코프 밖으로 벗어날 때, 값은 버려진다.

힙에 저장되는 데이터

힙 영역을 사용하는 변수는 소유권의 이동이 발생하며, 이는 얕은 복사와는 다르게 기존의 변수를 무효화한다.

let s1 = String::from("hello");
let s2 = s1; // s1은 더 이상 유효하지 않기 때문에 스코프를 벗어나도 메모리를 해제할 필요가 없다.

println!("{}, world!", s1); // error

힙 데이터까지 깊은 복사하고 싶을 땐 clone이라는 공용 메서드를 사용할 수 있다.

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

함수로 값을 전달할 때와 함수가 값을 반환할 때도 소유권의 이동이 발생한다.

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

 let s2 = takes_ownership(s1);  // s1의 값이 함수로 이동되며, 더 이상 유효하지 않다.

소유권 이동 없이 값을 사용하고 싶을땐 참조자를 이용하면 된다.

스택에만 저장되는 데이터

스택에만 저장되는 데이터는 컴파일 타임에 크기가 고정되기 때문에 소유권의 이동이 발생하지 않고 복사된다.
이러한 타입에는 Copy트레이트가 구현되어 있다.
하지만 Drop트레이트가 구현된 경우엔 Copy트레이트를 어노테이션 할 수 없다.

Copy 가능한 타입 중 일부는 아래와 같다.

  • 정수형 타입
  • bool 타입
  • 부동 소수점 타입
  • 문자 타입
  • Copy 가능한 타입만으로 구성된 튜플

참조자

참조자는 해당 주소에 저장된 데이터에 접근할 수 있도록 해주는 주소값에 해당하는, 포인터와 비슷한 것이다. 포인터와는 달리, 참조자는 살아있는 동안 특정 타입에 대한 유효한 값을 가리킴을 보장해 준다.
참조자를 만드는 행위를 대여라고 한다.

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()
} // s에는 소유권이 없으므로 s가 더 이상 사용되지 않을 때도 이 참조자가 가리킨 값이 버려지지 않는다.

가변 참조자

참조자는 단 하나의 가변 참조자만 갖거나, 여러 개의 불변 참조자를 가질 수 있다.

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

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

println!("{}, {}", r1, r2);
let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
let r3 = &mut s; // error

println!("{}, {}, and {}", r1, r2, r3);

참조자는 정의된 지점부터 시작하여 해당 참조자가 마지막으로 사용된 부분까지 유효하다.

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

let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);

let r3 = &mut s;
println!("{}", r3);

댕글링 참조

러스트에서는 어떤 데이터의 참조자를 만들면, 해당 참조자가 스코프를 벗어나기 전에 데이터가 먼저 스코프를 벗어나는지 컴파일러에서 확인하여 댕글링 참조가 생성되지 않도록 보장한다.

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
} // error

슬라이스

슬라이스는 컬렉션을 통째로 참조하는 것이 아닌, 컬렉션의 연속된 일련의 요소를 참조하도록 해준다.
슬라이스는 참조자의 일종으로서 소유권을 갖지 않는다.
[starting_index..ending_index]는 starting_index부터 시작해 ending_index 직전, 즉 ending_index 에서 1을 뺀 위치까지 슬라이스를 생성한다는 의미이다.
슬라이스는 내부적으로 시작 위치, 길이를 데이터 구조에 저장하며, 길이 값은 ending_index 값에서 starting_index 값을 빼서 계산한다.

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

let hello = &s[0..5];
let hello = &s[..5]; 

let world = &s[6..11];
let world = &s[6..];

let hello_world = &s[0..11];
let hello_world = &s[..];

문자열 슬라이스를 매개변수로 사용하면 String의 슬라이스 혹은 String에 대한 참조자를 전달할 수 있다.

fn main() {
    let value = String::from("test");

    test_slice(&value);

}

fn test_slice(s: &str) -> &str {
    s
}

모든 컬렉션에서도 슬라이스를 사용 가능하다.

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

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

assert_eq!(slice, &[2, 3]);

익숙하지 않은 개념이다 보니 숙지하는 데 꽤 시간이 걸린다.
안 쓰다 보면 금방 까먹기 때문에 지속적으로 보고 코드를 짜보자.
다음 글의 시작은 구조체다.