이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.
Rust에는 대부분의 프로그래밍 언어에서 찾아볼 수 있는 개념도 있고
Rust만의 고유한 개념도 있다.
먼저 대부분의 프로그래밍 언어에서 찾아볼 수 있는 개념들에 대해 알아보자.
이번 시간에는 그 중 변수와 자료형에 대해 이야기를 하겠다.
지난 시간에도 간단히 언급되었지만 Rust의 변수는 기본적으로 불변성을 갖는다.
이는 변수를 안전하게 관리할 수 있도록 해주며 특히 동시성 측면에서 유리하다.
물론 명시적으로 가변성을 부여할 수도 있지만 기본적으로는 불변성을 가진다는 특성은
일반적인 프로그래밍 언어에서의 변수의 개념과 사뭇 달라 처음엔 좀 혼란스러울 수 있다.
정말 값을 변경할 수 없는지 직접 확인해보도록 하자.
peter@hp-laptop:~/rust-practice$ mkdir chapter03
peter@hp-laptop:~/rust-practice$ cd chapter03
peter@hp-laptop:~/rust-practice/chapter03$ cargo new variables
Created binary (application) `variables` package
peter@hp-laptop:~/rust-practice/chapter03$ cd variables/
peter@hp-laptop:~/rust-practice/chapter03/variables$ vi src/main.rs
src/main.rs
fn main() { let x = 5; println!("The value of x is: {}", x); x = 6; println!("The value of x is: {}", x); }
실행해보면 다음과 같은 컴파일 오류를 만날 수 있다.
peter@hp-laptop:~/rust-practice/chapter03/variables$ cargo run
Compiling variables v0.1.0 (/home/peter/rust-practice/chapter03/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: make this binding mutable: `mut x`
3 | println!("The value of x is: {}", x);
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
error: aborting due to previous error
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables`.
To learn more, run the command again with --verbose.
peter@hp-laptop:~/rust-practice/chapter03/variables$
x
는 불변성을 가진 변수인데 값이 두 번이나 대입되었다는 것이다.
Rust 컴파일러는 이렇게 친절하게 어느 부분이 어떻게 문제가 되는지 알려주며
심지어는 make this binding mutable
이라며 대응책까지 알려준다.
// Rust를 지원하는 IDE나 편집기를 사용할 경우
// 컴파일을 해보기도 전에 밑줄을 치고 경고해줄 수도 있다.
// 예를 들어 vscode에서 rust-analyzer를 사용한다거나...
이러한 오류 메시지 덕분에 Rust 개발자는 버그가 발생할 수 있는 지점을 쉽게 찾을 수 있다.
Rust는 변수의 불변성을 통해 변수의 값이 의도적인 경우에만 변경할 수 있도록 한다.
불변성을 가지고 있는 변수라면 컴파일러가 의도치 않은 값의 변경이 없음을 보장한다.
변수에 가변성을 부여하고자 한다면 앞서 언급된 것과 같이 mut
키워드를 사용하면 된다.
mut
키워드를 통해 가변성을 가진 변수를 선언함과 동시에
이 코드를 읽을 동료 개발자로 하여금
이 변수는 코드의 다른 부분에서 값이 변경될 수 있음을 인지하게 한다.
peter@hp-laptop:~/rust-practice/chapter03/variables$ vi src/main.rs
src/main.rs
fn main() { let mut x = 5; println!("The value of x is: {}", x); x = 6; println!("The value of x is: {}", x); }
peter@hp-laptop:~/rust-practice/chapter03/variables$ cargo run
Compiling variables v0.1.0 (/home/peter/rust-practice/chapter03/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.24s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
peter@hp-laptop:~/rust-practice/chapter03/variables$
mut
키워드를 추가한 것만으로 문제 없이 잘 실행되는 것을 확인할 수 있다.
그런데 불변성을 가진 변수는 다른 언어에서의 상수와 같은 개념이라고 생각해도 될까?
정답은 NO다. Rust의 상수는 따로 존재한다.
그것은 let
대신 const
키워드를 통해 선언되며 mut
키워드를 사용할 수 없다.
그리고 컴파일 시점에 자료형이 명확하면 자료형을 생략할 수 있는 변수와 달리
상수의 경우 항상 자료형을 명시해주어야 한다.
상수는 전역 범위를 포함하여 어디에서나 선언 가능하며, 반드시 상수 리터럴을 bind해야 한다.
상수의 식별자는 대문자만을 사용하며, 여러 단어로 이루어질 경우 _
로 구분한다.
Rust 변수의 특징 중 또 다른 하나는 shadowing이 가능하다는 것이다.
지난 시간에도 사용한 적 있는 개념인데, 새로 선언한 변수로 기존의 변수의 값을 가리는 것이다.
기존에 선언된 변수와 같은 이름의 변수를 let
으로 선언하면 그 변수는 가려진다.
대입 연산은 =
의 우측 연산이 먼저 수행된 후 값을 bind하므로
shadowing을 할 때 기존 변수의 값을 활용할 수 있다.
shadowing 없이 그냥 대입할 땐 기존의 것과 자료형이 같아야 하지만
shadowing을 할 경우 서로 다른 자료형이더라도 상관 없다.
이에 대한 예제를 작성해보자.
peter@hp-laptop:~/rust-practice/chapter03/variables$ cd ..
peter@hp-laptop:~/rust-practice/chapter03$ cargo new shadowing
Created binary (application) `shadowing` package
peter@hp-laptop:~/rust-practice/chapter03$ cd shadowing/
peter@hp-laptop:~/rust-practice/chapter03/shadowing$ vi src/main.rs
src/main.rs
fn main() { let x = 5; let x = x + 1; let x = x * 2; println!("The value of x is {}", x); let spaces = " "; let spaces = spaces.len(); println!("The value of spaces is {}", spaces); }
peter@hp-laptop:~/rust-practice/chapter03/shadowing$ cargo run
Compiling shadowing v0.1.0 (/home/peter/rust-practice/chapter03/shadowing)
Finished dev [unoptimized + debuginfo] target(s) in 0.21s
Running `target/debug/shadowing`
The value of x is 12
The value of spaces is 4
peter@hp-laptop:~/rust-practice/chapter03/shadowing$
위 코드에서 x
는 가변성 변수로 변경 후 shadowing이 아닌 bind로 바꾸어도 문제 없지만
spaces
의 경우 그렇게 수정하면 자료형 차이로 인해 오류가 발생한다.
Rust는 컴파일 시점에 모든 변수의 자료형이 결정되어야 하는 정적 타입 언어다.
따라서 모든 변수의 자료형이 확정되어야 정상적으로 컴파일된다.
자료형이 명시되지 않아도 컴파일 시점에 자료형이 명확한 변수에 대해서는
Rust 컴파일러가 자동으로 자료형을 지정해주지만
그렇지 않은 경우에는 반드시 자료형을 직접 명시해주어야 한다.
Rust의 자료형은 크게 Scalar 자료형과 Compound 자료형으로 구분할 수 있다.
Scalar 자료형은 하나의 값을 표현하는 자료형으로
정수, 부동소수점,불리언, 문자, 이렇게 네 가지가 있다.
이 네 가지 Scalaer 자료형은 또 세부적으로 나눌 수 있다.
Rust의 정수 자료형은 그 크기와 부호의 유무에 따라 구분된다.
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
arch | isize | usize |
부호가 있는 정수 자료형은 비트수 n에 대하여 -2^(n-1) 이상 2^(n-1) 미만의 값을 가질 수 있고
부호가 없는 정수 자료형은 비트수 n에 대하여 0 이상 2^n 미만의 값을 가질 수 있다.
arch는 컴퓨터 아키텍처에 따라 자료의 크기가 정해지는 경우인데
64-bit 아키텍처의 경우 64-bit 자료형이 되며 32-bit 아키텍처의 경우 32-bit 자료형이 되는 식이다.
정수 리터럴은 Decimal, Hex, Octal, Binary, Byte의 형태로 사용할 수 있다.
단순히 숫자만으로 이루어진 정수값을 적으면 그것은 Decimal이 되고
숫자 앞에 0x
를 붙이면 그것은 Hex가 되고
숫자 앞에 0o
를 붙이면 그것은 Octal이 되고
숫자 앞에 0b
를 붙이면 그것은 Binary가 된다.
이들은 숫자 뒤에 정수 자료형을 붙여 자료형을 명시해줄 수 있으며
밑줄을 이용해 자리수를 표현할 수도 있다.
0b1100_1101
, 2878i32
와 같이 작성하면 된다.
그리고 Byte는 b'
와 '
사이에 문자를 넣어 그것을 정수로 나타낸 값을 저장하며
이것은 u8 자료형만 가능하며 밑줄을 통한 자리수 표현이 존재하지 않는다.
만약 우리가 정수를 사용할 때 자료형을 명시해주지 않는다면
Rust 컴파일러는 기본적으로 그것을 i32로 선언한다.
여담: 오버플로우Overflow
어떤 자료형이 나타낼 수 있는 범위를 벗어난 값이 입력되면 그 자료형은 그것을 저장하지 못하는데
이러한 상황을 오버플로우라고 한다.
Rust는 디버그 모드로 컴파일 할 경우 오버플로우가 발생할 때 오류로 취급하고 종료하며
배포 모드로 컴파일 할 경우 오버플로우가 발생할 때 범위의 반대쪽 끝에서 다시 시작하도록 한다.
물론 이렇게 범위의 반대쪽 끝에서 다시 시작된 값은 의도치 않은 결과를 일으킬 수 있다.
Rust의 부동소수점 자료형은 f32와 f64, 두 가지가 있다.
이 녀석들도 정수 자료형과 마찬가지로 자료형의 숫자는 비트수를 의미한다.
이들은 IEEE754 표준을 따르며 f32는 단정도 부동소수점, f64는 배정도 부동소수점을 표현한다.
현재 대부분의 CPU에서는 f64 연산을 f32만큼 빠르게 수행할 수 있는데
정확도는 f64가 더 높으므로 Rust는 부동 소수점 기본 자료형으로 f64를 사용한다.
Rust의 불리언 자료형은 bool으로, 조건을 나타낼 때 사용하는 1byte 자료형이다.
모든 식별자를 소문자로 적는 것과 같이 true와 false도 소문자로 적는다.
Rust의 문자 자료형은 유니코드를 저장하는 4byte 자료형으로 ASCII보다 많은 문자를 표현할 수 있다.
문자 리터럴은 작은 따옴표로 묶어서 사용한다.
문자를 유니코드로 저장함으로써 다양한 언어의 문자와 이모지 등까지 표현 가능하다.
Compound 자료형은 여러 개의 값을 그룹화한 자료형으로
튜플과 배열, 이렇게 두 가지가 있다.
튜플은 서로 다른 자료형의 여러 값을 하나의 자료형으로 그룹화할 때 사용한다.
괄호 안에 값의 목록을 쉼표로 구분하여 표기함으로써 생성할 수 있다.
자료형을 명시할 때도 괄호 안에 자료형의 목록을 쉼표로 구분하여 나타낸다.
그리고 다음과 같이 튜플의 값을 개별 값으로 해체할 수도 있다.
src/main.rs
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); let (x, y, z) = tup; println!("The value of x is: {}", x); println!("The value of y is: {}", y); println!("The value of z is: {}", z); }
peter@hp-laptop:~/rust-practice/chapter03/tuple$ cargo run
Compiling tuple v0.1.0 (/home/peter/rust-practice/chapter03/tuple)
Finished dev [unoptimized + debuginfo] target(s) in 0.66s
Running `target/debug/tuple`
The value of x is: 500
The value of y is: 6.4
The value of z is: 1
peter@hp-laptop:~/rust-practice/chapter03/tuple$
그리고 튜플을 해체하지 않고도 튜플 tup
에 대해 tup.0
와 같이
마침표를 찍고 인덱스를 지정함으로써 튜플의 각 요소에 직접 접근할 수도 있다.
배열은 튜플과 달리 동일한 자료형의 여러 값을 하나의 자료형으로 그룹화할 때 사용한다.
어떤 언어의 배열은 그 길이가 유동적이지만 Rust의 배열은 고정되어 있다.
튜플은 소괄호(()
)를 통해 생성하는 반면 배열은 대괄호([]
)를 통해 생성한다.
배열을 선언할 때 자료형을 명시해줄 경우 let arr: [i32; 5];
와 같이 그 크기와 함께 명시한다.
그리고 배열의 초기값을 설정할 때 모두 동일한 값을 사용하고 싶다면
let arr = [0; 3];
와 같이 작성하면 let arr = [0, 0, 0];
와 동일한 배열이 생성된다.
배열의 원소를 접근할 땐 배열 arr
에 대해 arr[0]
와 같이 []
에 인덱스를 지정하여 접근한다.
C 언어 같은 경우에는 인덱스가 배열의 범위를 넘어가도
의도치 않은 값에 접근하며 실행은 되지만
Rust는 컴파일은 되어도 실행 시점에 오류를 출력하며 프로그램을 종료한다.
이 포스트의 내용은 공식문서의 3장 1절 Variables and Mutability & 3장 2절 Data Types에 해당합니다.