[RUST] 러스트 프로그래밍 공식 가이드(제2판) 3장 요약
CHAPTER 3 일반적인 프로그래밍 개념
변수와 가변성
러스트는 안정성과 쉬운 동시성을 활용하는 방식으로 코드를 작성할 수 있도록 변수는 기본적으로 불변(immutable)
- 불변 변수 (Immutable Variable)
- 한 번 값이 할당되면 변경 불가
- 변수에 다시 값을 할당하려 하는 경우 컴파일타임에 에러가 발생
- 에러메시지 :
cannot assign twice to immutable variable '변수명'
- 에러메시지 :
- 가변 변수 (Mutable Variable)
- 값 변경이 가능한 변수
mut
키워드를 이용- 해당 변수의 값이 변할 것 이라는 의도를 전달
구분 | 키워드 | 값 변경 가능 여부 |
---|---|---|
불변 변수 | let | ❌ |
가변 변수 | let mut | ✅ |
상수
- 항상 불변 (가변 상수라는 개념은 없음)
let
대신const
키워드를 사용- 값의 타입이 반드시 명시 되어야 함
- 전역 스코프를 포함한 어떤 스코프에서도 선언 가능 (코드의 많은 부분에서 알 필요가 잇는 값에 유용)
- 컴파일 타임에 값이 확정 되어야 함 (런타임에서만 계산될 수 있는 결괏값은 안됨)
- 컴파일러는 컴파일 타임에 제한된 연산을 평가할 수 있음 (상숫값 평가 참고)
const THREE_HHOURS_IN_SECONDS: u32 = 60 * 60 * 3;
- 이름 짓는 관례로 상수는 단어 사이에 밑줄을 사용하고 모든 글자를 대문자로 사용
섀도잉
- 새 변수를 이전 변수명과 같은 이름으로 선언할 수 있음
- 첫 번째 변수가 두 번째 변수에 의해 가려졌다(shadowed) 고 표현
- 두번째 변수는 첫번째 것을 가려서, 스스로를 다시 가리거나 스코프가 끝날 때 까지 변수명의 사용을 가져감
let
키워드를 통해 새로운 값을 만드는 것 (let
을 사용하지 않으면 컴파일 에러 발생)mut(가변 변수)
와 섀도잉의 차이구분 mut
(가변 변수)섀도잉 (Shadowing) 의미 같은 변수의 값을 변경 같은 이름의 새 변수를 선언하여 이전 변수를 가림 메모리 주소 같음 다름 (새 변수는 새 메모리에 저장) 타입 변경 ❌ 불가 ✅ 가능 가변성 변경 ❌ 불가 (처음 정한 mut
여부 고정)✅ 가능 (불변 ↔ 가변 자유롭게 변경 가능) 사용 목적 변수 값을 업데이트 변수의 의미를 단계별로 변경, 타입 변환, 불변성 유지하면서 값 변화 표현
데이터 타입
러스트의 모든 값은 특정한 데이터 타입(data type)을 가지며, 이는 러스트가 데이터로 작업하는 방법을 알 수 있도록 어떤 종류의 데이터가 지정되고 있는지 알려줌
- 러스트는 정적 타입(statically typed) 언어로, 모든 변수의 타입이 컴파일 시점에 반드시 정해져야 함
- 컴파일러는 우리가 값을 어떻게 사용하는지에 따라 타입을 추측 할 수 있음
- 여러 가지 타입이 가능하여 타입 추측이 안되는 경우 타입 명시를 추가해야 함
스칼라 타입
스칼라(scalar) 타입은 하나의 값을 표현
정수형
정수형(integer)은 소수점이 없는 숫자이며 부호 있는(signed) 또는 부호 없는(unsigned) 타입이며 명시된 크기를 가짐
길이 | 부호 있음(signed) | 부호 없음(unsigned) |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
- 부호 있는 정수(2의 보수 형태를 사용)
- 범위 =
-2^(n-1)
~2^(n-1) - 1
- 범위 =
- 부호 없는 정수
- 범위 =
0
~2^n - 1
- 범위 =
isize
와usize
타입은 프로그램이 작동하는 컴퓨터 환경에 따라 결정- 64-bit 아키텍처이면 64비트
- 32-bit 아키텍처이면 32비트
- 주로 어떤 컬렉션 종류의 인덱스에 사용됨
- 러스트의 정수형 리터럴
숫자 리터럴 예 10진 98_222 16진 0xff 8진 0o77 2진 0b1111_0000 바이트(u8) b’A’ 타입 접미사 10i32 밑줄 사용 10_000_000
- 정수 오버플로
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
0~255 사이의 값을 담을 수 있는 u8 타입의 변수를 갖고 있다고 해봅시다. 만약에 이 변수에 256처럼 범위 밖의 값으로 변경하려고 하면 정수 오버플로(iteger overflow)가 일어나는데, 이는 다음과 같은 동작을 일으킵니다. - 코드를 디버그 모드에서 컴파일 하는 경우 > 러스트는 런타임에 정수 오버플로가 발생했을때 패닉(panic)을 발생시키는 검사를 포함 - --release 플래그를 사용하여 코드를 리리스 모드로 컴파일 하는 경우 > 패닉을 발생시키는 정수 오버플로 검사를 실행 파일에 포함시키지 않고 > 2의 보수 감싸기(tow's complement wrapping)을 수행 > (즉, 해당 타입이 가질 수 있는 최댓값보다 더 큰 값은 허용되는 최솟값으로 돌아감(wrap around)) > * 위 경우 패닉은 발생하지 않으나, 해당 변수는 예상치 못했던 값을 갖게됨을 주의 명시적으로 오버플로의 가능성을 다루기 위하여 표준 라이브러리에서 기본 수치 타입에 대해 제공하는 메서드 종류들을 사용할 수 있음 - wrapping_add와 같은 wrapping_* 메서드로 감싸기 동작 실행 - checked_* 메서드를 사용하여 오버플로가 발생하면 None값 반환 - overflowing_* 메서드를 사용하여 값과 함께 오버플로 발생이 있었는지를 알려주는 불리언값 반환 - saturating_* 메서드를 사용하여 값의 최댓값 혹은 최솟값 사이로 제한
부동소수점
부동소수점(floating-point)는 소수점을 갖는 숫자를 의미하며 기본 타입은 f64
이고 모든 부동소수점 타입은 부호가 있으며 IEEE-754 표준을 따름
f32 | f64 | |
---|---|---|
크기 | 32-bit | 64-bit |
정밀도 | 1배수 정밀도(단정밀도) | 2배수 정밀도(배정밀도) |
부호 지원 | 있음 | 있음 |
수치 연산
연산자 | 의미 |
---|---|
+ | 덧셈 |
- | 뺄셈 |
* | 곱셈 |
/ | 나눗셈 |
% | 나머지 |
- 모든 숫자 타입에는 기본 수학 연산 기능을 제공
- 정수 나눗셈은 가장 가까운 정숫값으로 버림 처리
불리언 (:boolean
)
true
와 false
값을 가지며 1바이트의 크기이고 주로 if
표현식과 조건문에서 주로 사용
문자 (:char
)
러스트의 char
는 이 언어의 가장 기본적인 알파벳 타입
let c = 'z';
와 같이 작은따옴표를 사용- 4바이트의 크기를 가지며 유니코드 스칼라 값을 표현 (이는 ASCII보다 훨씬 더 많은 값을 표현할 수 있다는 의미)
- 억양 표시가 있는 문자, 한국어/중국어/일본어 문자, 이모지, 넓이가 0인 공백문자 등
- 유니코드 스칼라의 범위
- U+0000 ~ U+D7FF, U+E000 ~ U+10FFFF
복합 타입
복합 타입(compound type)은 여러 값을 하나의 타입으로 묶을 수 있으며 러스트에는 튜플과 배열 이라는 두가지 기본 복합타입이 존재
튜플
튜플(tuple)은 다양한 타입의 여러 값을 묶어 하나의 복합 타입으로 만드는 일반적인 방법
- 고정된 길이를 가짐
- 한번 선언되면 크기를 늘리거나 줄일 수 없음
- 괄호 안에 값을 쉼표로 구분하여 목록을 작성
- 튜플 내의 각 위치는 타입을 갖고, 이 튜플 내의 타입들은 서로 달라도 됨
1 2 3
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); }
- 튜플은 하나의 복합 요소로 취급되므로 변수 tup은 튜플 전체가 바인딩 됨
- 튜플로부터 개별 값을 얻어오려면 패턴 매칭을 하여 튜플값을 해체해서 사용
1 2 3 4 5
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); let (x, y, z) = tup; println!("The value of y is: {y}"); }
tup
을 세 개의 분리된 변수x
,y
,z
로 바꾸는데 이것을 구조 해체(destructuring) 라고 부름
- 점(.) 뒤에 접근하고자 하는 값의 인덱스를 쓰는 방식으로도 튜플 요소에 접근할 수 있음
1 2 3 4 5 6
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; }
- 튜플의 첫번쨰 인덱스는 0 부터 시작
- 아무 값도 없는 튜플은 유닛(unit) 이라는 특별한 이름을 갖음
- 값과 타입은 모두
()
로 작성되고 빈 값이나 비어있는 반환 타입을 나타냄
- 값과 타입은 모두
- 표현식이 어떠한 값도 반환하지 않는다면 암묵적으로 유닛 값을 반환
배열
여러 값의 집합체를 만드는 다른 방법
- 요소들은 모두 같은 타입 이어야 함
- 고정된 길이를 가짐
- 배열은 스택에 할당될 수 있는 계산 가능한 고정된 크기의 단일 메모리 뭉치
- 항상 고정된 개수의 요소로 이루어진 경우에 유용
- 초기화
- 일반적인 배열 작성
1
let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
- 대괄호 안에 요소의 타입을 쓰고 세미콜론을 쓴 뒤 요소의 개수를 적는 방식으로 배열의 타입을 작성
1
let a: [i32; 5] = [1, 2, 3, 4, 5];
- 대괄호 안에 초깃값과 세미콜론을 쓴 다음 배열의 길이를 적는 방식을 사용하여 모든 요소가 동일한 값으로 채워진 배열을 초기화
1
let a = [3; 5]; // let a = [3, 3, 3, 3, 3];
- 일반적인 배열 작성
- 배열 요소에 접근하기
1 2 3 4 5 6
fn main() { let a = [1, 2, 3, 4, 5]; let first = a[0]; // 1 let second = a[1]; // 2 }
- 유효하지 않은 배열 요소에 대한 접근
- 배열 끝을 넘어선 요소에 접근하려고 하는 경우
1
thread 'main' panicked at 'index out of bounds: the len is ...
- 프로그램은 인덱스 연산에서 잘못된 값을 사용한 시점에서 런타임 에러를 발생
- 러스트는 인덱스를 이용하여 배열의 요소에 접근을 시도하는 경우 명시한 인덱스가 배열 길이보다 작은지 검사하고, 인덱스가 배열 길이보다 크거나 같을 경우 패닉(panic)을 일으킴
- 러스트는 위와같이 잘못된 메모리 접근을 허용하고 계속 실행하는 대신 즉시 실행을 종료함으로써 이런 종류의 에러로부터 프로그램을 보호함 (Rust는 메모리 안전 보장을 핵심 목표로 함)
- 배열 끝을 넘어선 요소에 접근하려고 하는 경우
함수
러스트에서는 fn
뒤에 함수 이름과 괄호를 붙여 함수를 정의하고 중괄호는 함수 본문의 시작과 끝을 컴파일러에게 알려주는 역할을 함
- 함수의 호출
- 함수의 이름 뒤에 괄호(
()
) 묶음을 쓰면 정의해둔 어떤 함수든 호출할 수 있음
- 함수의 이름 뒤에 괄호(
main
함수 이후에 정의되어있는 함수도 main 함수에서 호출할 수 있음- 즉, 함수 위치를 고려하지 않으며, 호출하는 쪽에서 볼 수 있는 스코프 어딘가에만 정의 되어 있으면 됨
매개변수
함수는 매개변수(parameter)를 갖도록 정의될 수 있으며, 함수 시그니처의 일부인 특별한 변수
- 함수가 매개변수를 갖고 있다면 이 매개변수에 대한 구체적인 값을 전달할 수 있음 (이때 전달하는 값을 인수(argument)라 부름)
1 2 3 4 5 6 7
fn main() { another_function(5); // 5 는 인수 } fn another_function(x: i32) { // x 는 매개변수 println!("The value of x is: {x}"); }
- 함수 시그니처에서는 각 배개변수의 타입을 반드시 명시해야 함
- 함수 정의에 타입 명시를 강제하면 이 함수를 다른 코드에서 사용할 때 의도한 타입을 컴파일러가 추측하지 않아도 되게 됨
- 컴파일러는 함수가 기대한 타입이 무엇인지 알고 있으면 더욱 유용한 메시지를 제공할 수 있음
- 여러 매개변수를 정의하려면 쉼표 기호(
,
)로 매개변수 정의를 구분 (fn test(val: i32, unit: char)
)
구문과 표현식
함수 본문은 필요에 따라 표현식(expression)으로 끝나는 구문(statement)의 나열로 구성됨
- 구문 : 어떤 동작을 수행하고 값을 반환하지 않는 명령
1 2 3
fn main() { let y = 6; // 구문 }
- let 키워드로 변수를 만들고 값을 할당 하는 것
- 함수 정의
- 표현식 : 결괏값을 평가
- 값을 계산해서 반환하는 코드
- 예를들어
5 + 6
은 11 이라는 값을 평가하는 표현식 - 표현식은 구문의 일부일 수 있음
- 함수, 매크로 호출도 표현식
- 중괄호로 만들어진 새로운 스코프 블록도 표현식
- 표현식은 종결을 나타내는 세미콜론(
;
)을 쓰지 않음 (만약;
을 사용하면 표현식은 구문으로 변경되고 값을 반환하지 않게 됨)
반환값을 갖는 함수
함수는 호출한 코드에 값을 반환할 수 있고, 반환되는 값의 타입은 화살표(->
) 뒤에 선언해야함
1
2
3
4
5
6
7
8
9
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");
}
five
함수의 반환값은5
이며 반환 타입은i32
- 반환 값에
;
가 없는 이유는5
가 반환하려는 값에 대한 표현식 이기 때문 let x = five();
- 반환 값을 변수의 초깃값으로 사용
let x = 5;
와 동일
- 만약 함수에서
;
을 사용하여 구문을 반환하는 경우 구문은 값을 평가하지 않기 때문에()
로 표현되는 유닛 타입으로 표현이 됨- 따라서 아무것도 반환되지 않아 함수가 정의된 내용과 상충하게 되어
mismatched types
에러가 발생
- 따라서 아무것도 반환되지 않아 함수가 정의된 내용과 상충하게 되어
주석
프로그램 실행에는 영향을 주지 않지만 개발자가 설명을 적어 넣을 수 있는 부분을 말하며, 컴파일러는 이를 무시하지만 읽는 사람들에게 유용한 정보를 얻을 수 있게끔 함
- 러스트의 주석은
//
로 시작하며 해당 줄의 끝까지 계속됨
제어 흐름
if 표현식
코드가 조건에 따라 분기할 수 있도록 해줌
if
표현식은if
라는 키워드로 시작하고 그 뒤에 조건이 옴if number < 5 {} else {}
else
키워드는if
조건이 거짓일 경우 실행되는 코드 블록을 제공else
표현식이 없고 조건식이 거짓이라면if
블록을 생략하고 다음 코드를 진행
- 조건식은 반드시 bool 이어야 함
1 2 3 4 5
let number = 3; if number { ... }
- if 조건식의 결과가 위와 같이 bool 이 아니라면 에러를 발생
mismatched types
에러 발생- 불리언이 아닌 타입을 자동으로 불리언으로 변환하지 않기 때문에 항상 명시적으로 불리언 타입의 조건식을 제공해 주어야 함
- if 조건식의 결과가 위와 같이 bool 이 아니라면 에러를 발생
else if 로 여러 조건식 다루기
if와 else 사이에 else if 를 조합하면 여러 조건식을 사용할 수 있음
- 각각의
if
표현식을 순차적으로 검사하고, 조건이 참일 때의 첫 번째 본문을 실행(나머지는 검사도 하지 않음) else if
표현식을 너무 많이 사용하면 코드 복잡도가 올라가므로, 표현식이 두개 이상일 경우 코드를 리팩터링 하는 것이 좋음
let 구문에서 if 사용하기
if
는 표현식이기 때문에 변수에 결과를 할당하기 위하여 let
구문의 우변에 사용할 수 있음
1
2
3
4
5
6
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 }; // if 표현식을 계산한 결과값이 바인딩
println!("The value of number is: {number}");
}
반복문을 이용한 반복
loop로 코드 반복하기
loop
키워드는 그만 두라고 명시적으로 알려주기 전까지 영원히 코드 블록을 반복 수행하도록 해줌
- 만약
loop
로 인해 무한 루프에 빠졌다면Ctrl
+c
(실행 중단 시그널) 를 이용하여 프로그램을 중지 loop
안에서break
키워드를 집어 넣으면 루프를 멈춰야 하는 시점을 프로그램에게 알려줄 수 있음loop
안에서continue
키워드를 집어넣으면 루프를 다음 회차로 넘기도록 프로그램에게 알려줄 수 있음
반복문에서 값 반환하기
1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // counter 가 10까지 반복되었을 때 if 조건식이 만족되며 loop는 10 * 2 의 값을 반환
}
};
println!("The result is {result}");
}
- 위와 같이
loop
수행 결과를 이후 코드에 전달하고 싶은 경우break
표현식 뒤에 반환하고자 하는 값을 넣어 줌
루프 라벨로 여러 반복문 사이에 모호함 없애기
만일 루프 안에 루프가 있다면, break
와 continue
는 해당 지점의 바로 바깥쪽 루프에 적용
- 루프에 루프 라벨(loop label) 을 명시하면
break
나contibue
와 함께 이 키워드 들이 바로 바깥쪽 루프 대신 라벨이 적힌 특정한 루프에 적용- 루프 라벨은 반드시 따옴표로 시작해야 함
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
fn main() { let mut count = 0; 'counting_up: loop { let mut remaining = 10; ... loop { if remaining == 8 { break; // 현재 루프에 적용 } if count == 2 { btrak 'counting_up // 바깥의 루프에 적용 } } } }
while을 이용한 조건 반복문
반복문 내에서 조건을 검사하여 조건문이 true인 동안에는 계속 반복하는 반복문
1
2
3
4
5
6
7
8
9
10
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}");
number -= 1;
}
println!("LIFTOFF!!!");
}
for를 이용한 컬렉션에 대한 반복문
컬렉션의 각 아이템에 대하여 임의의 코드를 수행하려 할때 간편한 대안으로 for 반복문을 사용
1
2
3
4
5
6
7
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
while
이나loop
를 사용하지 않고 위와 같이for
문을 사용하여 코드 안정성이 강화- 배열의 끝을 넘어서거나 끝까지 가지 못해서 몇 개의 아이템을 놓쳐서 발생할 수 있는 버그의 가능성을 제거했다는 의미
for
루프를 사용한다면 배열 내 값의 개수가 변경되더라도 수정해야 할 다른 코드를 기억할 필요가 없음while ( idx < arr.length )
와 같이 번거롭게 작업할 필요가 없음
- Range 타입 이용 (표준 라이브러리에서 제공)
- 특정 횟수만큼의 반복문을 구현할 수 있음
- Range는 어떤 숫자에서 시작하여 다른 숫자 종료 전까지의 모든 숫자를 차례로 생성해 줌
1 2 3 4 5 6
fn main() { for number in (1..4).rev() { // rev 메서드는 범윗값을 역순으로 만들어줌 println!("{number}"); } println!("LIFTOFF!!!"); }