Rust 공식 문서를 참고한 글입니다.
변수
Rust에서 변수는 immutable, 즉 불변성을 가진다. 변수를 안전하게 관리할 수 있게 해주며 동시성(Concurrency)의 이점도 존재한다. 명시적으로 변수를 가변적으로 만들 수도 있는데, 기본적으로는 불변성이라는 특징을 가지고 있다. 이제 Rust에서 불변성을 권장하는 이유와 변수를 가변적으로 변경할 수 있는지 살펴보자.
variables라는 새로운 프로젝트를 만들고, main.rs에 아래와 같은 함수를 작성한다.
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
이 프로그램을 실행하면 오류가 발생한다. 불변성을 가진 변수 x에 값을 두 번 할당할 수 없다는 말이다. 친절하게도 x를 선언할 때 mut이라는 것을 사용하면 된다는 힌트를 제공한다. Rust는 변수의 불변성이라는 특성을 이용해서 의도적인 경우에만 값이 변경되도록 한다.
그래서 변수 선언부를 mut과 함께 사용하고 실행하면 오류가 발생하지 않는다.
let mut x = 5;
그렇다면, 이렇게 불변성을 가지는 변수를 상수와 같은 개념으로 볼 수 있을까? 정답은 아니다. Rust는 상수라는 개념을 따로 가진다. 상수와 변수는 3가지 차이점이 존재한다. 상수는 mut과 함께 사용할 수 없고 항상 불변이고, let이 아니라 const로 선언하며 값의 타입을 명시해야 한다. 두번째로 상수는 전역 범위를 포함한 모든 범위에서 선언할 수 있어서 여러 코드에서 필요한 값을 관리하는데 편리하다. 마지막으로 상수는 상수 표현식으로만 설정할 수 있고 런타임에만 계산할 수 있는 값으로 설정할 수 없다.
변수의 또 다른 특징은 shadowing이다. 숫자 맞추기 게임에서 봤다시피, 이전 변수와 같은 이름으로 새 변수를 선언할 수 있다. 처음 선언한 변수가 이후에 선언한 변수에 의해 가려진다고 말하는데, 두 번째 변수가 컴파일러에 첫 번째 변수를 참조하라는 의미이다. let을 사용하면 이전 변수를 가릴 수 있다. 실제로 예제를 통해 알아보자.
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
println!("The value of x is: {x}");
}
이 프로그램은 x에 5를 먼저 바인딩하고, 그 이후에 let x = x + 1;을 선언함으로써, 기존에 선언된 x + 1한 값이 새로운 변수 x에 바인딩되어 6이 된다. 중괄호 안의 새로운 x가 두번째 선언된 x에 2를 곱한 12로 바인딩된다. 이 범위가 끝나면 다시 x는 6으로 돌아온다.
Shadowing은 mut을 사용해 변수에 가변성을 부여하는 것과 다르다. let을 사용하지 않고 새로운 값을 바인딩하려고 하면 오류가 발생한다. let을 사용하면 값을 여러 번 변환할 수 있지만, 그 이후에는 변수를 불변으로 유지할 수 있다. 또, let을 사용하면 새로운 변수를 생성하는 것이기 때문에 같은 이름을 사용하면서 값의 타입을 변경할 수 있다는 것이다.
예를 들어서, spaces라는 공백 문자열을 선언하고, 이후 공백 문자열의 길이로 바인딩한다고 하자.
let spaces = " ";
let spaces = spaces.len();
처음 선언된 spaces는 문자 타입이고, 두 번째 선언된 spaces는 숫자 타입이다. 이렇게 하면, spaces_string, spaces_num이라는 새로운 변수를 선언할 필요가 없다.
그러나 아래와 같은 경우, 가변성을 갖도록 설정한 String 타입의 spaces 를 선언했다. 이후 문자 타입에 숫자 타입을 바인딩하려고 하면 오류가 발생한다.
let mut spaces = " ";
spaces = spaces.len();
자료형
Rust의 모든 값은 특정 데이터 타입을 가지고 있다. 정적 타입 언어이기 때문에 컴파일 시에 모든 변수의 타입을 알아야 한다. 자료형이 명시되어 있지 않다면, 컴파일러가 알아서 자료형을 지정해주지만 그렇지 않은 경우에는 자료형을 지정해주어야 한다. Rust의 자료형은 Scalar과 Compound 타입이 있다.
Scalar
Scalar 자료형은 정수형, 부동소수점, 불리언, 문자 이렇게 네 가지가 있다.
정수
정수형(integer)은 소수 부분이 없는 숫자로, 부호가 있을 수 있고 없을 수 있다. 부호가 있는 정수 자료형은 -2^(n-1) 이상 2^(n-1) 미만까지의 숫자를 가질 수 있다. 부호가 없는 정수 자료형은 0에서 2^n - 1까지의 숫자를 저장할 수 있다. 참고로 arch는 프로그램이 실행되는 아키텍처에 따라 결정되는 것으로 64bit 아키텍처는 64bit, 32bit 아키텍처는 32bit 자료형으로 지정된다.
정수 리터럴은 아래 표를 참고해서 사용할 수 있다. Decimal, Hex, Octal, Binary 형태로 나타낼 수 있는데, 일반 숫자를 쓰면 Decimal, 숫자 앞에 0x를 붙이면 hex, 앞에 0o를 붙이면 octal, ob를 붙이면 Binary 타입이다. 그리고 Byte는 b와 '' 사이에 문자를 넣어 표현한다. 자료형을 명시하지 않으면, 컴파일러는 기본적으로 i32로 선언한다.
부동 소수점
부동 소수점 자료형에는 f32와 f64, 2가지가 있다. 각각 32bit, 64bit 크기를 가지며, 선언을 하지 않으면 기본적으로 f64이다. 그 이유는 현대 CPU에서 f32와 속도가 유사하지만 정밀도가 훨씬 높기 때문이다.
불리언
Rust에서 불리언 타입은 조건을 나타낼 때 사용하는 1byte 자료형이다. 아래 예시처럼 bool을 보통 사용한다.
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
문자
Rust의 Char 타입은 4byte 크기를 가지고 ascii보다 많은 문자를 가질 수 있다. 문자 리터럴은 문자열 리터럴과 달리 작은 따옴표 안에서 표현된다.
Compound
Compound 자료형은 여러 개의 값을 하나로 그룹화한 것으로 튜플과 배열 두 가지가 있다.
Tuple
서로 다른 자료형의 값들을 하나의 compound 그룹으로 표현할 때 사용한다. 괄호 안의 값 목록을 쉼표로 구분한다. 자료형을 명시할 때도 자료형의 목록을 쉼표로 구분해서 나타낸다.
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
tup 내용을 해체할 수도 있다.
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
이렇게 하면, x, y, z 각각에 tup의 숫자가 바인딩된다.
아래 예시처럼 .을 사용해 값을 바인딩할 수도 있다.
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
Array
배열은 튜플과 달리 같은 자료형의 값들을 하나의 자료형으로 그룹화할 때 사용된다. 다른 언어와 달리, Rust에서는 배열 길이가 고정되어 있다. heap 영역보다 Stack 영역에 데이터가 할당되기를 원할 때 배열은 유용하게 사용된다. 혹은 배열의 원소 수를 고정하고 싶을 때도 유용하다.
let a: [i32; 5] = [1, 2, 3, 4, 5];
배열은 []를 사용해 정의할 수 있다. 위 예시는 a 라는 배열의 모든 원소는 i32타입이고, 총 5개의 원소가 존재한다는 의미이다. 배열의 길이가 변화하지 않을 때 배열을 사용하면 된다.
let a = [3; 5];
또한 배열을 같은 숫자로 초기화할 수 있다. 위 예시는 a = [3, 3, 3, 3, 3]과 같은 의미이다.
만약에 배열에 접근할 때 범위를 넘어버리면, 오류가 발생한다. C언어 같은 경우, 인덱스가 범위를 넘어가도 의도치 않은 값에 접근하며 실행은 되지만 Rust는 오류가 발생하여 프로그램이 종료된다. 이렇게 Rust는 메모리 안정성을 보장하고 있어서 메모리에 접근해서 인덱스를 확인하는 작업을 통해서 오류를 처리한다.
'언어 > Rust' 카테고리의 다른 글
[Rust] 4-1. 소유권(Ownership) (0) | 2024.09.28 |
---|---|
[Rust] 3-3. 제어문(Control Flow) (0) | 2024.09.26 |
[Rust] 3-2. 함수와 주석 (0) | 2024.09.25 |
[Rust] 2. 숫자 맞히기 게임 구현(feat.사용자 입출력과 에러 핸들링) (1) | 2024.09.25 |
[Rust] 1. 환경 구축하기 (0) | 2024.09.23 |