정수(Integers) - 1부
“구문” 섹션에서 보았던 compute 함수의 입력 매개변수 타입은 u32였습니다. 과연 이것이 무엇을 _의미_하는지 자세히 알아봅시다.
원시 타입 (Primitive types)
u32는 Rust의 원시 타입 중 하나입니다. 원시 타입은 언어가 제공하는 가장 기본적인 구성 요소로, 다른 타입을 조합해서 만드는 것이 아니라 언어 자체에 내장되어 있습니다.
이러한 원시 타입들을 조합하면 더 복잡하고 유용한 타입을 만들 수 있습니다. 그 방법은 곧 배우게 될 것입니다.
정수 (Integers)
u32는 정확히 말하면 부호 없는(Unsigned) 32비트 정수입니다. 정수는 소수점 이하 부분이 없는 숫자를 말합니다. 예를 들어 1은 정수이지만, 1.2는 정수가 아닙니다.
부호 있는 정수 vs 부호 없는 정수
정수는 부호가 있을 수도(Signed), 없을 수도(Unsigned) 있습니다. 부호 없는 정수는 0 이상의 양수만 나타낼 수 있습니다. 반면 부호 있는 정수는 -1이나 12처럼 양수와 음수를 모두 나타낼 수 있습니다.
u32의 u는 **unsigned(부호 없는)**를 뜻합니다. 부호 있는 정수 타입은 i32라고 쓰는데, 여기서 i는 **integer(정수)**를 의미하며 양수와 음수 모두 포함할 수 있다는 뜻입니다.
비트 너비 (Bit width)
u32의 32는 메모리에서 숫자를 표현하는 데 사용하는 비트(bit)1 수를 나타냅니다. 비트가 많을수록 더 넓은 범위의 숫자를 담을 수 있습니다.
Rust는 정수에 대해 8, 16, 32, 64, 128비트의 다양한 비트 너비를 지원합니다.
32비트를 사용하면 u32는 0부터 2^32 - 1(u32::MAX라고도 함)까지의 숫자를 나타낼 수 있습니다. 같은 32비트를 사용하는 부호 있는 정수(i32)는 -2^31부터 2^31 - 1(i32::MIN에서 i32::MAX까지)의 숫자를 표현합니다.
i32의 최댓값이 u32보다 작은 이유는 한 비트를 숫자의 부호를 표시하는 데 사용하기 때문입니다. 부호 있는 정수가 메모리에서 어떻게 저장되는지 궁금하다면 2의 보수(Two’s complement) 표현 방식을 찾아보세요.
요약
부호 유무와 비트 너비를 조합하면 다음과 같은 정수 타입들이 만들어집니다.
| 비트 너비 | 부호 있음 (Signed) | 부호 없음 (Unsigned) |
|---|---|---|
| 8비트 | i8 | u8 |
| 16비트 | i16 | u16 |
| 32비트 | i32 | u32 |
| 64비트 | i64 | u64 |
| 128비트 | i128 | u128 |
리터럴 (Literals)
리터럴은 소스 코드에 직접 적은 고정된 값을 말합니다. 예를 들어 42는 숫자 42를 나타내는 Rust 리터럴입니다.
리터럴의 타입 추론
Rust의 모든 값은 반드시 타입을 가져야 합니다. 그렇다면 우리가 적은 42의 타입은 무엇일까요?
Rust 컴파일러는 리터럴이 어떻게 사용되는지 보고 타입을 추론하려고 노력합니다. 만약 별다른 힌트가 없다면, 컴파일러는 정수 리터럴의 기본 타입을 i32로 정합니다. 만약 특정 타입을 지정하고 싶다면 숫자 뒤에 접미사를 붙이면 됩니다. 예를 들어 2u64는 u64 타입으로 명시된 숫자 2를 의미합니다.
가독성을 위한 밑줄
큰 숫자를 적을 때 가독성을 높이기 위해 밑줄(_)을 넣을 수 있습니다. 예를 들어 1_000_000은 1000000과 똑같이 인식되지만 훨씬 읽기 편합니다.
산술 연산자
Rust는 정수 데이터에 대해 다음과 같은 기본적인 산술 연산2을 지원합니다.
+더하기-빼기*곱하기/나누기%나머지
연산의 우선순위와 결합 규칙은 우리가 수학 시간에 배운 것과 같습니다. 괄호를 사용하면 2 * (3 + 4)처럼 계산 순서를 직접 정할 수 있습니다.
⚠️ 주의
정수끼리
/연산자를 사용하면 정수 나눗셈을 수행합니다. 즉, 소수점 이하는 버리고 0에 가까운 쪽으로 결과를 잘라냅니다. 예를 들어5 / 2의 결과는2.5가 아니라2입니다.
자동 타입 변환 없음
앞서 살펴봤듯이 Rust는 타입에 매우 엄격한 언어입니다. 특히 Rust는 값이 손실되지 않는 안전한 경우라도 한 타입을 다른 타입으로 자동으로 변환(Coercion)해주지 않습니다3. 모든 타입 변환은 명시적으로 작성해야 합니다.
예를 들어, 모든 u8 값은 당연히 u32 범위에 포함되지만, 다음과 같이 u8 값을 u32 변수에 그냥 대입할 수는 없습니다.
let b: u8 = 100;
let a: u32 = b;
이 코드를 실행하면 다음과 같은 컴파일 오류가 발생합니다.
error[E0308]: mismatched types
|
3 | let a: u32 = b;
| --- ^ expected `u32`, found `u8`
| |
| expected due to this
|
타입을 안전하게 변환하는 방법은 과정 뒷부분에서 자세히 다룰 예정입니다.
더 읽어보기
- 공식 Rust 가이드의 정수 타입 섹션 (영문)
Exercise
The exercise for this section is located in 02_basic_calculator/01_integers
-
비트는 컴퓨터에서 데이터를 다루는 가장 작은 단위입니다.
0또는1의 두 가지 값만 가질 수 있습니다. ↩ -
Rust에서는 사용자가 임의로 연산자를 새로 정의할 수는 없지만, 내장 연산자가 어떻게 작동할지 제어할 수는 있습니다. 연산자 오버로딩(Operator overloading)에 대해서는 트레이트 섹션에서 설명하겠습니다. ↩
-
이 규칙에도 몇 가지 예외는 있습니다. 주로 참조, 스마트 포인터와 관련된 편의 기능들인데, 이는 나중에 배우게 됩니다. 하지만 지금은 “모든 변환은 명시적이어야 한다“는 원칙을 기억하는 것이 좋습니다. ↩