소소한 개발 공부

[러스트] 불변성 본문

프로그래밍/Rust

[러스트] 불변성

이내내 2023. 7. 17. 22:49

모든 프로그래밍 언어 대부분이 가진 개념을 러스트가 어떻게 다루는지 알아본다.

- 변수, 기본 타입들, 함수, 주석, 제어문 등

 

변수와 가변성

기본 변수는 불변성 즉, 변하지 않는다. => 안전성과 손쉬운 동시성을 가질 수 있음

변수가 불변인 경우, 값이 이름에 bind 되면 해당 값을 변경할 수 없다.

아래 처럼 variables 디렉터리를 생성해 확인해보자.

cargo new --bin variables

src/main.rs

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}


//
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
3 |     println!("The value of x is: {}", x);
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to previous error

에러가 나는데, 이는 불변성 변수 x에 재할당을 했기 때문이다.

러스트는 변수의 불변성을 보장함으로써 변수의 값이 변경되어 버그가 일어나는 경우(고통의 트래킹!)를 방지함으로써 코드를 더 합리적으로 만들어준다.

 

그러나 러스트는 가변성의 유용성 역시 놓치지 않았다.

바로, mut 을 사용함으로써 변수를 가변적으로 만드는 것이다.

mut 은 x에 bind된 값을 변경할 수 있게 만든다.

fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

// 
$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30 secs
     Running `target/debug/variables`
The value of x is: 5
The value of x is: 6

가변 변수가 유용한 경우는 

대규모 데이터 구조체를 다루는 경우이다. -> 가변 인스턴스를 사용하는 것이 새로 인스턴스를 할당하고 반환하는 것보다 빠를 수 있음

그러나, 데이터 규모가 작을수록 새 인스턴스를 생성하고 함수적 프로그래밍 스타일로 작성하는 것이 더 합리적이고, 약간의 성능 하락을 통해 가독성을 확보할 수 있다면 그것이 더 가치있을 수 있다.

 

언뜻 불변성의 변수는 상수(constant)와 비슷해보이지만 상수는 mut 을 허용하지 않는다.

상수는 let 대신 const 키워드를 사용하고 값의 타입을 함께 선언해야한다.

상수는 오직 상수 표현식만 설정될 수 있으며 함수 호출의 결과값이나 그 외 런타임에 결정되는 값으로 설정될 수 없다.

다음의 예제처럼 상수 (MAX_POINTS)는 값의 타입과 함께 선언한다.

const MAX_POINTS: u32 = 100_000;

상수는 선언된 영역 내에서 항상 유효하므로 유용하다. (한 게임에서 획득할 수 있는 최대 포인트 등)

 

Shadowing

let으로 이전에 선언한 변수와 동명의 새 변수를 선언할 수 있고,

새 변수는 이전 변수를 shadow 한다. 이를 첫 변수가 두번째 변수에 의해 shadowed 되었다고 표현한다.

-> 해당 변수명이 두번째 변수의 값을 갖게 된다는 의미

아래의 예제와 같이 활용할 수 있다.

fn main() {
    let x = 5; // x <- 5

    let x = x + 1; // x <- 6 

    let x = x * 2; // x <- 12

    println!("The value of x is: {}", x);
}

이는 mut 으로 선언한 것과는 차이가 있는데, let 없이 변수에 새 값을 대입하려고 하면 컴파일 에러가 발생하기 때문.

mut 과 let 의 차이는 let 은 효과적으로 동명의 새 변수를 선언하고 값 타입을 변경할 수 있다는 것이다. (새롭게 정의된 변수이기 때문)

아래 예제에서 spaces 는 처음에 string 값을 가지지만 두번째 라인에서 정수 값을 가진다.

let spaces = "   ";
let spaces = spaces.len();

이를 아래처럼 mut을 사용하면, 가변 변수의 값의 타입을 변경하게 되어 컴파일 에러가 발생한다.

let mut spaces = "   ";
spaces = spaces.len();