Rust 타입 시스템

변수와 상수에 대해 알아본 지난 포스트에 이어, 이번 포스트는 Rust 타입 시스템을 다루고자 합니다.

Rust는 런타임에서 변수 혹은 상수의 타입 변경을 허용하지 않는 정적 타입 언어이며, 메모리 관리에 개입할 수 있는 시스템 프로그래밍 언어이기도 합니다.

타 언어에 비해 빠르고 안전한 Rust의 이점은 이러한 부분에서 나오기 때문에 타입 시스템을 잘 이해하는 것이 중요합니다.


Rust 타입 시스템은 크게 2가지, 스칼라 타입(Scalar Types)과 컴파운드 타입(Compound Types)으로 나뉩니다.

스칼라 타입(Scalar Types)

Rust 스칼라 타입은 1개의 데이터를 명시하는 타입입니다.

스칼라 타입에는 정수(integers), 실수(floating-point numbers), 불리언(Booleans), 문자(characters) 가 있습니다.

정수(integers)

정수는 소수점 이하가 없는 숫자를 의미합니다.

정수 타입은 데이터가 사용하는 비트 크기와 음수 포함 여부에 따라, 다음과 같이 나뉩니다.

Unsigned 변수가 음수를 포함하지 않은 타입을 의미합니다.

비트SignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

여기서, n비트는 2의 n제곱만큼의 숫자를 나타낼 수 있고, 음수는 2의 보수로 나타낼 수 있기 때문에 signed 변수일 경우, 음수로는 -(2의 (n-1)제곱), 양수로는 (2의 (n-1)제곱) - 1 까지 나타냅니다.

예를 들어, i8 타입은 -128 ~ 127까지, u8 타입은 0 ~ 255 까지의 수를 담을 수 있습니다.

isize, usize 타입은 해당 컴퓨터의 아키텍쳐에 따라 64-bit 혹은 32-bit를 나타낼 수 있습니다.

Rust에서 일반적으로 사용하는 정수 타입은 i32이며, 양수만 있는 값이면 u32를 주로 사용합니다.

만약, u8 타입에 256을 대입하는 등 비트 길이를 넘는 경우가 생긴다면, 개발 빌드 시에는 프로그램을 멈추며 에러의 일종인 panic을 발생시킵니다. 배포 빌드 시에는 2의 보수에 따른 전혀 다른 숫자를 나타내지만 프로그램이 멈추진 않습니다.

정수 리터럴을 나타낼 때는, 1_000_000처럼 언더스코어(_)를 사용해 가독성을 높일 수 있으며, 16진수, 8진수, 2진수, 바이트(u8)는 각각 0x, 0o, 0b, b 접두어를 붙여 나타낼 수 있습니다.

실수(floating-point numbers)

실수의 경우 비트 길이에 따라 f32f64 2가지 타입을 사용할 수 있으며, 각각 싱글 프레시전, 더블 프레시전 실수를 나타냅니다. 즉, 담을 수 있는 자릿수에 차이가 있습니다.

두 타입 모두 signed 타입이며, f64 타입을 주로 기본값으로 사용합니다.

불리언(Booleans)

bool 키워드로 명시할 수 있는 불리언 타입은 각각 true 혹은 false 값을 담을 수 있습니다.

문자(characters)

Rust에서 문자는 char 키워드로 명시하며, 문자 리터럴은 홑따옴표('')를 사용합니다.

하나의 문자는 4바이트 (32비트) 크기를 가지기 때문에 아스키 문자, 이모지 등을 포함한 거의 모든 문자를 나타낼 수 있습니다.

컴파운드 타입(Compound Types)

Rust 컴파운드 타입은 여러 개의 값들을 하나로 가지고 있는 데이터에 대해 명시하는 타입입니다.

컴파운드 타입에는 튜플과 배열 이렇게 2가지 종류가 있는데요. 튜플은 다양한 타입의 값을 한 번에 포함흘 수 있는 반면에 배열에 속한 모든 값들은 동일한 하나의 타입이어야만 합니다.

그럼 하나씩 살펴보겠습니다.

튜플 (Tuple Type)

튜플은 여러 개의 값을 한 번에 관리하는 가장 간단한 방법입니다.

하지만, 그 값들의 정확한 개수를 모를 때는 사용하지 않습니다. 왜냐하면 튜플은 정의했을 때의 길이를 변경할 수 없기 때문입니다.

이 때문에 튜플에는 값을 추가하거나 삭제하는 기능 역시 없습니다.

튜플은 아래와 같이, 소괄호 (())와 쉼표(,)를 이용해서 정의하는데요.

let tup = (200, 4.3, "사과");

위와 같이, 하나의 튜플에는 다양한 타입의 값들이 함께 담길 수 있습니다.

원소값 가져오기

튜플에 저장된 값을 가져오는 방법에는 크게 2가지가 있습니다.

첫번째는 패턴매칭인데 다음과 같이 대입 연산자(=)와 원하는 변수를 튜플과 동일한 형태로 적어주면 됩니다.

만약, 원하지 않는 원소에 대해서는 언더스코어(_)를 사용해서 생략할 수 있습니다.

let tup = (200, 4.3, "사과");

let (two_hund, four_three, apple) = tup;

let (two_hund, _, apple) = tup;

두번째는 마침표(.)와 인덱스를 이용하는 방법입니다.

인덱스는 0부터 시작합니다.

let tup = (200, 4.3, "사과");

let two_hund = tup.0
let four_three = tup.1
let apple = tup.2

배열 (Array Type)

Rust의 배열 역시 튜플과 마찬가지로 고정된 길이를 갖습니다. 다른 언어들과 차이점이라고 할 수 있죠.

나중에 보겠지만, Rust에서 길이가 정해져 있지 않은 배열을 사용할 때는 표준 라이브러리의 vector 타입을 사용합니다.

일상적으로는 주로 vector를 많이 사용하지만, Stack 메모리에 저장할 간단한 데이터들은 배열을 사용하여 효율적으로 관리할 수 있습니다.

또한, 1년의 열두달처럼 정해진 길이의 데이터 집합을 사용할 때도 유용하죠.

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

let b: [u32; 5] = [1, 2, 3, 4, 5];

let c = [3; 5];

위와 같이, 배열을 나타낼 때는 대괄호([])와 쉼표(,)를 사용합니다. 익숙한 형태입니다.

배열의 타입을 명시할 때는 대괄호 안에 타입과 쉼표, 그리고 원소의 개수를 입력합니다.

만약, 같은 값으로 이루어진 배열을 정의한다면 위 3번째 라인과 같이 입력해도 좋습니다.

원소값 가져오기

배열의 값은 대괄호([])와 인덱스로 가져올 수 있습니다.

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

let first = a[0];

배열의 값을 가져오기 위해 메모리 주소를 직접 참조하는 저수준 언어들 중에는 존재하는 인덱스를 벗어나도 에러를 발생시키지 않고 예상치 못한 값들을 불러오기도 합니다.

Rust는 이러한 일을 미연에 방지해주는 똑똑한 low-level 언어라는 점도 알고 넘어갑시다.

지금까지 Rust 타입에 대해 알아보았고, 다음은 함수에 대한 포스팅으로 이어가겠습니다.