보편적인 프로그래밍 개념 - 데이터 타입
개발 환경
이 문서에서 진행한 필자의 개발 환경은 다음과 같다.
- desktop: macbook pro 13 2020
- cpu: Intel Core i7 4core
- memory: 32GB
- rustup v1.24.3
- cargo v1.58.0
이 문서는 여러분이 cargo가 설치되어 있다고 가정한다. 만약 cargo를 설치하지 않았다면, 이 문서를 참고하여 설치 및 설정을 진행하길 바란다.
cargo 설치가 되었다면 이번 장을 위한 프로젝트를 생성한다.
# 프로젝트 생성
$ cargo new --bin datatypes
# 프로젝트 디렉토리로 이동
$ cd datatypes
데이터 타입
rust
에서의 모든 값들은 고정된 특정 "타입"을 갖는다. 이것은 rust
는 컴파일 시에 모든 변수의 타입이 정해져야 함을 의미한다. 예를 들어서 main.rs
를 다음과 같이 작성해보자.
datatypes/src/main.rs
fn main() {
let guess = "42".parse().expect("Not a number!");
println!("guess: {}", guess);
}
이제 컴파일을 해보자.
$ cargo run
Compiling datatypes v0.1.0 (/Users/gurumee/Workspace/today-i-learned/getting-started-rust-programming/ch03/datatypes)
error[E0282]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ consider giving `guess` a type
For more information about this error, try `rustc --explain E0282`.
error: could not compile `datatypes` due to previous error
이 경우, 변수 guess
의 타입이 확정되지 않아서 에러가 발생한다. 때문에 아래 코드처럼 guess
의 타입을 확정하게끔 main.rs
를 수정해야 한다.
datatypes/src/main.rs
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
println!("guess: {}", guess);
}
컴파일을 실행해보면, 이전과 달리 정상적으로 수행되는 것을 확인할 수 있다.
$ cargo run
Compiling datatypes v0.1.0 (/Users/gurumee/Workspace/today-i-learned/getting-started-rust-programming/ch03/datatypes)
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running `target/debug/datatypes`
guess: 42
rust
에서 데이터 타입은 크게 2가지로 아래와 같이 나눌 수 있다.
- 스칼라 타입
- 복합 타입
이 문서에서는 rust
에서 지원하는 스칼라 타입과 간단한 복합 타입 2가지를 소개한다.
스칼라 타입
스칼라 타입이란 하나의 값으로 표현되는 타입을 의미한다. rust
에서 지원하는 스칼라 타입은 크게 다음과 같이 4가지로 분류할 수 있다.
- 정수형
- 문자형
- 실수형
- bool
정수형
정수형 데이터 타입은 소수점이 없는 값들을 표현한다. 대표적으로 u32
, i32
등이 있다. 이들은 32비트 정수형을 의미한다. u
혹은 i
뒤의 숫자는 데이터의 크기를 의미하며 8
, 16
, 32
, 64
가 있다. 정수형은 크게 다음과 같이 분류할 수 있다.
Length | Signed | Unsigned |
---|---|---|
8 | i8 | u8 |
16 | i16 | u16 |
32 | i32 | u32 |
64 | i64 | u64 |
arch | isize | usize |
isize
, usize
는 컴퓨터 아키텍처에 알맞는 정수형 타입을 할당한다. 예를 들어 32bit 아키텍처라면 u32
, i32
를 64bit 아키텍처라면 u64
, i64
를 사용한다.
i
와 u
의 차이는 부호를 갖는 정수 여부를 의미한다. i
는 부호를 갖는 정수값을, u
는 부호를 갖지 않는 정수값 즉 0 이상의 정수를 표현한다. 각 타입이 표현할 수 있는 값의 범위는 다음과 같다.
Data Type | Range |
---|---|
i8 | -(2^7) ~ (2^7 - 1) |
i16 | -(2^15) ~ (2^15 - 1) |
i32 | -(2^31) ~ (2^31 - 1) |
i64 | -(2^63) ~ (2^63 - 1) |
u8 | 0 ~ (2^8 - 1) |
u16 | 0 ~ (2^16 - 1) |
u32 | 0 ~ (2^32 - 1) |
u64 | 0 ~ (2^64 - 1) |
기본적으로 다음과 같이 사용할 수 있다.
let i: i32 = -16;
let u: u32 = 16;
정수형은 사칙 연산자를 사용할 수 있다. main.rs
를 다음과 같이 수정해보자.
datatypes/src/main.rs
fn main() {
let sum = 5 + 10;
let sub = 5 - 10;
let mul = 5 * 10;
let div = 5 / 10;
println!("sum: {}, sub: {}, mul: {}, div: {}", sum, sub, mul, div);
}
이제 컴파일 및 실행을 해보자. 결과 값을 예상할 수 있는가?
$ cargo run
Compiling datatypes v0.1.0 (/Users/gurumee/Workspace/today-i-learned/getting-started-rust-programming/ch03/datatypes)
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running `target/debug/datatypes`
sum: 15, sub: -5, mul: 50, div: 0
참고! div는 왜 0이 나오나요?
일반적으로 5 / 10 = 0.5 이다. 근데 왜 div는 0이 나왔을까? 프로그래밍 세계에서는 정수형 / 정수형 = 정수형 이기 때문이다. 실수를 표현하고 싶다면 다음 절 "실수형"을 살펴보면 된다.
실수형
실수형 데이터 타입은 소수점이 있는 실수를 표현한다. IEEE-754
표준에 맞게 데이터 타입이 설계되었는데 복잡한 이야기는 빼도록 하자. rust
에서 실수형 데이터 타입은 f32
, f64
가 있다. 정수형 데이터 타입과 동일하기 각각 32bit, 64bit 크기만큼의 실수를 표현해낼 수 있다. 우리는 거의 모든 상황에서 f64
를 쓰면 된다. 다음과 같이 사용할 수 있다.
let f: f64 = 2.5;
정수형과 마찬가지로 사칙 연산자를 사용할 수 있다. 이제 main.rs
를 다음과 같이 수정해보자.
datatypes/src/main.rs
fn main() {
let sum = 5.0 + 10.0;
let sub = 5.0 - 10.0;
let mul = 5.0 * 10.0;
let div = 5.0 / 10.0;
println!("sum: {}, sub: {}, mul: {}, div: {}", sum, sub, mul, div);
}
이제 컴파일 및 실행해보자.
$ cargo run
Compiling datatypes v0.1.0 (/Users/gurumee/Workspace/today-i-learned/getting-started-rust-programming/ch03/datatypes)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/datatypes`
sum: 15, sub: -5, mul: 50, div: 0.5
참고! sum, sub, mul은 왜 정수형으로 표현되었나요?
기본적으로 rust에서는 실수형일지라도 5.0 처럼 정수로 표현할 수 있는 실수는 정수로 표현되기 때문이다.
여기서 한 가지 주의할 점은 실수형과 실수형, 정수형과 정수형은 서로 사칙연산이 되지만 정수형과 실수형은 사칙연산이 불가하다. main.rs
를 다음과 같이 수정해보라.
datatypes/src/main.rs
fn main() {
let sum = 5 + 10.0;
let sub = 5 - 10.0;
let mul = 5 * 10.0;
let div = 5 / 10.0;
println!("sum: {}, sub: {}, mul: {}, div: {}", sum, sub, mul, div);
}
이제 정말 실행이 안되는지 컴파일 및 실행해보자.
$ cargo run
Compiling datatypes v0.1.0 (/Users/gurumee/Workspace/today-i-learned/getting-started-rust-programming/ch03/datatypes)
error[E0277]: cannot add a float to an integer
--> src/main.rs:2:18
|
2 | let sum = 5 + 10.0;
| ^ no implementation for `{integer} + {float}`
|
= help: the trait `Add<{float}>` is not implemented for `{integer}`
error[E0277]: cannot subtract `{float}` from `{integer}`
--> src/main.rs:3:18
|
3 | let sub = 5 - 10.0;
| ^ no implementation for `{integer} - {float}`
|
= help: the trait `Sub<{float}>` is not implemented for `{integer}`
error[E0277]: cannot multiply `{integer}` by `{float}`
--> src/main.rs:4:18
|
4 | let mul = 5 * 10.0;
| ^ no implementation for `{integer} * {float}`
|
= help: the trait `Mul<{float}>` is not implemented for `{integer}`
error[E0277]: cannot divide `{integer}` by `{float}`
--> src/main.rs:5:18
|
5 | let div = 5 / 10.0;
| ^ no implementation for `{integer} / {float}`
|
= help: the trait `Div<{float}>` is not implemented for `{integer}`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `datatypes` due to 4 previous errors
에러를 발생시키는 모든 구문을 확인할 수 있다. 모두 정수, 실수를 섞어서 사칙연산을 시도할 수 없다고 에러 문구를 출력하고 있다. 이럴 경우, 모두 실수, 정수로 맞춰주거나 강제로 타입 캐스팅을 해야 한다. 타입 캐스팅이란 컴파일 시 실제 타입 말고 개발자가 원하는 다른 타입으로 바꾸는 것을 의미한다. 다음과 같이 사용할 수 있다.
datatypes/src/main.rs
fn main() {
let sum = 5 + 10.0 as u32;
let sub = 5 - 10.0 as i64;
let mul = 5 as f32 * 10.0;
let div = 5 as f64 / 10.0;
println!("sum: {}, sub: {}, mul: {}, div: {}", sum, sub, mul, div);
}
이제 다시 컴파일 및 실행을 해보자. 정상적으로 실행되는 것을 확인할 수 있다.
$ cargo run
Compiling datatypes v0.1.0 (/Users/gurumee/Workspace/today-i-learned/getting-started-rust-programming/ch03/datatypes)
Finished dev [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/datatypes`
sum: 15, sub: -5, mul: 50, div: 0.5
bool
bool
은 참, 거짓을 나타내는 스칼라 타입이다. 일반적으로 혼자 사용되는 것은 드물고 조건문 및 반복문과 함께 사용된다. 다음과 같이 사용할 수 있다.
let b: bool = true; //false도 사용할 수 있다.
문자형
문자형 데이터 타입은 쉽게 생각해서 문자 한 개를 표현한다고 보면 된다. rust
에서는 ASCII
가 아닌 Unicode Scalar
를 사용하는데, 문자형 타입인 char
는 알파벳뿐 아니라 한글, 중국어, 이모티콘 등 여러 문자를 표현해낼 수 있다.
기본적으로 다음과 같이 사용할 수 있다.
let c: char = 'Z';
우리가 이전에 썼던 "...."
은 &str
이라는 데이터 타입이다. 이는 문자 타입인 char
와 다르다는 것을 알아두자. 추후 다른 문서에서 "문자열 타입(&str
)"을 다루도록 하겠다.
복합 타입
복합 타입은 다른 타입의 다양한 값들을 하나의 타입으로 묶는 타입을 의미한다. rust
에서 기본적으로 제공되는 타입은 다음과 같다.
- 튜플
- 배열
- 구조체
구조체는 다른 문서에서 다룰 것이며, 이 문서에서는 튜플과 배열만 다루도록 하겠다.
튜플
튜플은 다양한 데이터 타입을 나열한 것과 같다. 예를 들어 이렇게 사용될 수 있다.
let tup: (i32, f64, bool) = (500, 6.4, true);
한 번 튜플을 써보자. main.rs
를 다음과 같이 수정한다.
datatypes/src/main.rs
fn main() {
let tup: (i32, f64, bool) = (500, 6.4, true);
let x = tup.0;
let y = tup.1;
let z = tup.2;
println!("x: {}, y: {}, z: {}", x, y, z);
}
위 프로그램은 정수형, 실수형, bool 데이터 타입의 값을 갖는 튜플을 생성한 후 x, y, z에 각 값을 할당하여 출력하는 프로그램이다. 이제 컴파일 및 실행해보자.
$ cargo run
Compiling datatypes v0.1.0 (/Users/gurumee/Workspace/today-i-learned/getting-started-rust-programming/ch03/datatypes)
Finished dev [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/datatypes`
x: 500, y: 6.4, z: true
튜플의 장점은 이런 식으로 "구조 분해"문법을 사용해서 간단하게 초기화가 가능하다는 것이다.
datatypes/src/main.rs
fn main() {
let tup: (i32, f64, bool) = (500, 6.4, true);
let (x, y, z) = tup; // 구조 분해 문법!
println!("x: {}, y: {}, z: {}", x, y, z);
}
결과는 위와 동일하다.
배열
튜플이 다른 타입의 값들을 하나로 묶어주는 타입이라면, 배열은 같은 타입의 값들을 하나로 묶어주는 타입이다. 이런 식으로 사용이 가능하다.
let arr = [1, 2, 3];
이런 식으로 접근이 가능하다.
let arr = [1, 2, 3];
let x = a[0];
let y = a[1];
let z = a[2];
첫 원소가 0으로 접근하는 것에 유의하자. 프로그래밍에서는 "인덱스로 접근한다"라고 표현하는데 첫 원소를 0부터 센다고 보면 된다. 배열 사용시 주의할 점은 배열 크기를 넘는 인덱스를 사용하면 에러가 발생한다는 것이다. main.rs
를 다음과 같이 수정한다.
datatypes/src/main.rs
fn main() {
let arr = [1, 2, 3];
println!("index: 3, arr[3]: {}", arr[3]);
}
그 후 컴파일 및 실행해보자.
$ cargo run
Compiling datatypes v0.1.0 (/Users/gurumee/Workspace/today-i-learned/getting-started-rust-programming/ch03/datatypes)
error: this operation will panic at runtime
--> src/main.rs:3:38
|
3 | println!("index: 3, arr[3]: {}", arr[3]);
| ^^^^^^ index out of bounds: the length is 3 but the index is 3
|
= note: `#[deny(unconditional_panic)]` on by default
error: could not compile `datatypes` due to previous error
그럼 에러 문구에서 배열 길이는 3인데 인덱스는 3으로 접근하면 안된다는 에러가 출력된다. 위에서 언급했던것 처럼 인덱스는 0부터 세므로 길이가 3이면, 접근할 수 있덱스는 0, 1, 2 중 하나여야 한다. 이 중 하나의 값으로 수정하면 정상적으로 컴파일 및 실행되는 것을 확인할 수 있다.
또한 배열은 강제로 런타임에러를 발생시킬 수 있다는 점을 기억하자. 런타임 에러란 컴파일에서는 문제가 없지만 프로그램 실행 시 발생하는 에러를 의미한다. main.rs
를 다음과 같이 수정한다.
datatypes/src/main.rs
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Please enter an array index.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not number");
let elem = a[index];
println!("The value of the elem at index {} is: {}", index, elem);
}
위 프로그램은 배열을 초기화한 후 사용자의 숫자 입력을 받는다. 해당 숫자의 인덱스로 배열을 접근하여 elem
을 초기화한 후 출력하는 프로그램이다. 컴파일 및 실행하여, 10을 입력해보자.
$ cargo run
Compiling datatypes v0.1.0 (/Users/gurumee/Workspace/today-i-learned/getting-started-rust-programming/ch03/datatypes)
Finished dev [unoptimized + debuginfo] target(s) in 0.40s
Running `target/debug/datatypes`
Please enter an array index.
10 # 10입력 후 엔터
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:17:16
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
컴파일은 정상적으로 수행되어 프로그램이 시작된다. 그러나 10을 입력 받으면 배열 길이를 초과하여 인덱스를 접근했다고 에러가 발생하며 프로그램이 강제 종료되는 것을 확인할 수 있다. 0 ~ 4 이내에 값을 입력하면 정상적으로 종료되는 것을 확인할 수 있다.
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/datatypes`
Please enter an array index.
3
The value of the elem at index 3 is: 4