이번 시간에는 Rust의 보편적인 프로그래밍 개념인
데이터 타입들에 대해 알아보도록 하겠습니다.
Rust의 데이터 타입들
Rust에서 사용되는 모든 값들은 어떠한 타입을
가지고 있습니다. 그러니 어떤 형태의 데이터인지
명시하여 Rust에게 알려주면서 이를 통해 데이터를
어떻게 다룰 것인지 알수 있도록 해야 합니다.
이번 장에서는 언어에 포함되어 있는
여러 타입들을 살펴보고자 합니다.
타입은 크게 스칼라와 컴파운드 둘로 나뉠 수 있습니다.
이번 장에서 전체에 걸쳐 명심해야 함 점은 Rust는
타입이 고정된 언어라는 점 입니다.
이것이 의미하는 바는 모든 변수의 타입이
컴파일 시 반드시 정해져 있어야 한다는 겁니다.
보통 컴파일러는 우리가 값을 사용하는 지에
따라서 타입을 추측할 수 있습니다.
이번 장에서는 String을 parse를 사용해
숫자로 변환했던 경우처럼 타입의 선택 폭이
넒은 경우는 반드시 타입의 명시를 첨가해야 합니다.
let guess: u32 = "42".parse().expect("Not a Number!");
여기서 타입 명시를 첨가하지 않은 경우
Rust에서는 다음과 같은 에러가 발생합니다.
이와 같은 에러는 컴파일러가 우리에게
사용하고 싶은 타입이 무엇인지 추가적인
정보를 요구하는 것 입니다.
warning: unused variable: `guess`
--> src/main.rs:3:9
|
3 | let guess: u32 = "42".parse().expect("Not a Number");
| ^^^^^ help: if this is intentional, prefix it with an underscore: `_guess`
|
우리가 다루고자 하는 다양한 데이터 타입들
각각의 타입 명시를 살펴봅시다.
스칼라 타입들
스칼라는 하나의 값으로 표현되는 타입입니다.
Rust는 정수형, 부동소수점 숫자, boolean 그리고
문자 총 4가지 스칼라 타입들을 보유하고 있습니다.
아마 다른 프로그래밍에서도 본 적이 있겠지만
Rust에서 어떻게 동작하는지를 살펴봅시다.
정수형 타입
정수형은 소수점이 없는 숫자로 우리가 앞서
u32 타입인 정수형을 사용해 보았습니다.
해당 타입의 선언은 부호 없는 32비트
변수임을 나타냅니다.
변수와 가변성 파트에는 Rust에서
사용되는 정수형들을 알아보았습니다.
부호, 미부호로 나뉜 다른 열의 타입을
사용하여(i16처럼) 정수 값의 타입을
선언할 수 있습니다.
각각의 타입은 부호 혹은 미부호이며 명시된 크기를 갖습니다.
부호 혹은 미부호의 의미는 숫자가 양수 혹은 음수를
다룰 수 있는지 없는지를 나타냅니다.
다르게 말하자면 숫자가 부호를 가져야 하는 경우
(부호)혹은 오직 양수만을 가질 것이기에
부호가 없이도 표현할 수 있는가(미부호)를 나타냅니다.
종이에 숫자를 기재하는 것과 같습니다.
부호와 함께 다뤄야 하는 경우 숫자는 더하기
혹은 빼기 기호와 함께 표시합니다.
숫자가 양수라고 가정해도 문제 없는 상황에는
부호 없이 표시하게 됩니다.
부호된 숫자는 2의 보수 형태를 사용해
저장됩니다.
각 부호 변수는 $(2^{n-1})$ 부터 $2^{n-1} -1$
까지의 값을 포과합니다.
여기서 n부분은 사용되는 타입의 비트 수로
즉 i8은 $(2^7)$ 에서 $2^7 - 1$ 까지의 값
즉 -128 에서 127 까지의 값들을
저장할 수 있습니다.
미부호 타입은 0에서 $2^n -1 까지의 값을
저장할 수 있습니다.
즉, u8 타입은 0에서 $2^8 - 1 다시말해
0에서 255 까지의 값을 저장할 수 있습니다.
추가로 isize와 usize 타입은 당신이
프로그램이 동작하는 컴퓨터 환경이 64-bit인지
아닌지에 따라 결정됩니다.
64-bit 아키텍쳐라면 64bit를, 32-bit 아키텍쳐라면
32bit를 가지게 됩니다.
정수형 리터럴도 사용할 수 있는데
byte 리터럴을 제외하고 모든 정수형 리터럴은
57u8과 같은 타입 접미사와 1_000과 같이
시각적인 구분을 위한 ...의 사용을 허용합니다.
위 사진은 Rust의 정수형 리터럴들을 정리한
사진입니다.
그렇다면 어떤 타입의 정수를 사용해야 할까요?
확실하게 정해진 경우가 아닌 이상
Rust의 기본 값인 i32가 일반적으로는 좋은 선택입니다.
이는 일반적으로 가장 빠르기 때문입니다.
심지어 64-bit 시스템에서도 빠릅니다.
isize나 usize는 주로 일부 콜랙션 타입의 색인에 사용됩니다.
부동 소수점 타입
Rust에는 소수점을 갖는 숫자인 부동소수점 숫자를
위한 두 가지 기본 타입도 있습니다.
Rust의 부동소수점 타입은 f32와 f64로 서로 각각
32bit와 64bit의 크기를 갖습니다.
기본 타입은 f64로 그 이유에 대해서는 최신 CPU상
f64가 f32와 대략 비슷한 속도를 내면서도
더 정밀한 표현이 가능하기 때문입니다.
다음은 부동소수점 숫자가 활용되는 예제입니다.
- src/main.rs
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
부동소수점 숫자는 IEEE-754 표준에 따라서 표현됩니다.
f32 타입은 1배수의 정밀도인 부동소수점이고
f64는 2배수의 정밀도인 부동소수점입니다.
수학적 연산들
Rust가 지원하는 일반적인 기본 수학적 연산은
모든 숫자 타입에 적용됩니다.
더하기,빼기,곱하기, 나누기 등등 다음의
코드를 보여주려는 것은 각각 let 문에서
사용할 수 있는 방법입니다.
fn main() {
// addtion
let sum = 5 + 10;
// subtraction
let difference = 95.5 - 4.3;
// multiplication
let product = 4 * 50;
// division
let quotitent = 45.5 / 23.1;
// remainder
let remainer = 50 % 4
}
위 문장에서 각 표현식들은 수학 연산자를 사용해
산출된 값을 변수로 bound 합니다.
Boolean 타입
대부분의 다른 언어들처럼, boolean 타입은 Rust에서
둘 중 하나의 값만 가질 수 있습니다.
true 와 false, boolean 타입은 러스트에서
bool로 명시됩니다.
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
boolean 값을 사용하는 주된 방법은 if문과 같은
조건문에서 조건으로 사용하는 것 입니다.
우리는 if문이 Rust에서 동작하는 방식을
"제어 흐름"장에서 다룹니다.
문자 타입
지금까지 숫자 타입만을 살펴보았고
Rust 문자 또한 지원됩니다.
Rust의 char는 이 언어의 가장 근본적인
알파벳 타입이며, 다음의 코드는 이를
사용하는 한 가지 방법입니다.
스트링이 큰 따옴표를 쓰는 것에 반하여
char 타입은 작은 따옴표로 쓰는 점을
주목해야 합니다.
- Filename: src/main.rs
fn main() {
let c = 'z';
let z = 'Z';
let heart_eyed_cat = '🐈'
}
Rust의 char 타입은 Unicode Scalar를 표현하는
것이고 이는 ASCII 보다 많은 표현이 가능합니다.
억양 표시가 있는 문자, 한국어/중국어/일본어
표의 문자, 이모티콘, 넓이가 0인 공백문자
모두가 Rust에서 char 타입으로 사용할 수
있습니다.
Unicode Scalar 값의 범위는 U+0000에서
U+D7FF 그리고 U+E000에서 U+10FFFF로
포괄합니다.
그럼에도 불구하고 "문자"는 Unicode를
위한 개념이 아니기 때문에
당신의 인간적 직관에 따라 "문자"와
Rust의 char이 동일하지 않을 수 있습니다.
복합 타입들
복합 타입들은 다른 타입의 다양한 값들을
하나의 타입으로 묶을 수 있습니다.
Rust는 두 개의 기본 타입을 가지고 있습니다.
- 튜플
- 배열
값들을 집합시켜 튜플화
튜플은 다양한 타입의 몇 개의 숫자를
집합시켜 하나의 복합 타입으로 만드는
일반적인 방법입니다.
우리는 괄호 안에 콤마로 구분되는 값들의
목록을 작성해 튜플을 만들 것 입니다.
튜플에 포함되는 각 값의 타입이
동일할 필요없이 서로 달라도 됩니다.
다음 예제에 우리는 선택 사항인
타입 명시를 추가합니다.
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
튜플은 단일 요소를 위한 복합계로
고려되었기에 변수 tup에는 튜플 전체가
bind가 됩니다.
개별 값을 튜플의 밖으로 빼내오기 위해서는
패턴 매칭을 사용해 튜플의 값을
구조해체 시키면 됩니다.
fn main() {
let tup = (500, 6.4, 1);
let (x , y, z) = tup;
println!("The value of y is: {}", y);
}
해당 프로그램은 처음에 튜플을 만들고
변수 tup에 bind 시킵니다.
이후 패턴과 let을 통해 tup을 세개의
분리된 변수 x, y, 그리고 z에 이동시킵니다.
이것을 구조해체라고 부르는 이유는
하나의 튜플을 세부분으로 나누기 때문입니다.
최종적으로 프로그램은 y의 값을
출력할 것이고 이는 6.4입니다.
패턴 매칭을 통한 구조해체에 추가로
우리는 마침표( . ) 뒤에 우리가 접근할 값의
색인을 넣는 것을 통해 튜플의 요소에
직접적으로 접근할 수 있습니다.
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_bundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
위 프로그램은 튜플 x를 만들고, 이의 각 요소들을
그들의 색인을 통해 접근해 새 변수를 만듭니다.
대부분의 언어가 그렇듯이 튜플의 첫번째
색인은 0입니다.
배열
여러 값들의 집합체를 만드는 다른 방법은
배열입니다. 튜플과는 다르게 배열의 모든 요소는
모두 같은 타입이어야 합니다.
Rust의 배열이 몇 다른 언어들의 배열과
다른 점은 Rust에서는 배열은 고정된 길이를
갖는다는 점입니다.
한번 선언되면 이들은 크기는 커지거나
작아지지 않습니다.
Rust에서 대괄호 안에 값들을 콤마로
구분하여 나열해 배열을 만듭니다.
fn main() {
let a = [1, 2, 3, 4, 5];
}
배열이 유용할 떄는 데이터를 heap보다
stack에 할당하는 것은 원하거나
항상 고정된 숫자의 요소를 갖는다고
확신하고 싶을때 입니다.
이들은 백터 타입처럼 가변적이지 않고
백터 타입은 유사 집합체로 표준 라이브러리에서
제공되며 확장 혹은 축소가 가능합니다.
배열이나 백터 중에 뭘 선택해야 할지
확실하지 않은 상황이라면 백터를 사용합니다.
백터가 아닌 배열을 선택할 경우 예로
프로그램이 올해의 달 이름을 알고자 할 경우
프로그램은 달을 추가하거나 삭제하는 경우가
거의 없을 것이므로, 고정적으로 12개의 아이템을
가질테니 배열을 사용하면 됩니다.
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
배열 요소에 접근하기
배열은 stack에 단일 메모리 뭉치로 할당됩니다.
우리는 색인을 통해 배열의 요소에 접근할 수 있습니다.
fn main() {
let a = [1,2,3,4,5];
let first = a[0];
let second = a[1];
}
이번 예제에서, first로 명명된 변수는 값 1이 될텐데
이유는 배열 색인 [0]에 들어있는 값이기 때문입니다.
second로 명명된 변수는 배열의 색인
[1]의 값인 2가 됩니다.
유효하지 않는 배열 요소에 대한 접근
만약 우리가 배열의 끝을 넘어선 요소에 접근하려고
하면 어떻게 될까요? 다음 예제에서 살펴봅시다.
fn main() {
let a = [1, 2, 3, 4, 5];
let index = 10;
let element = a[index];
println!("The value of element is: {}", element);
}
이번 코드를 cargo run을 통해 동작하면 다음과
같은 결과가 출력됩니다.
error: this operation will panic at runtime
--> src/main.rs:99:19
|
99 | let element = a[index];
| ^^^^^^^^ index out of bounds: the length is 5 but the index is 10
|
컴파일 시에는 아무런 에러도 발생하지 않았지만
프로그램의 결과는 실행 중에 에러가 발생했고
성공적으로 종료되지 못했다고 뜹니다.
색인을 사용해 요소에 접근하려고 하면 Rust가
지정한 색인이 배열 길이보다 작은지 확인합니다.
색인이 길이보다 길다면 Rust는 프로그램이
오류와 함께 종료 될 때 Rust 가 사용하는 용어인
"패닉(panic)"을 합니다.
이것은 Rust의 안전 원칙이 동작하는 첫번째 예로
많은 저수준 언어에서 이러한 타입의 검사는
수행되지 않으며 잘못된 색인을 제공하면
유효하지 않은 메모리에 엑세스 할 수 있습니다.
Rust는 메모리 접근을 허용하고 계속 진행하는
대신 즉시 종료하여 이러한 종류의 오류로부터
사용자를 보호 합니다.
참고 자료
'백엔드 > Rust' 카테고리의 다른 글
[Rust] Rust 개념 - 1.보편적인 프로그래밍 개념 - 변수와 가변성 (0) | 2023.01.11 |
---|---|
[Rust] Rust 추리 게임 만들기 (0) | 2023.01.11 |
[RUST] RUST 설치하기 (0) | 2023.01.10 |