0. 들어가기 전에
키워드
- 러스트 언어는 대부분의 다른 언어들과 마찬가지로, 이 언어만 사용 가능한 키워드라는 집합이 있습니다.
- 키워드는 함수명이나 변수명으로 사용할 수 없음을 알아두세요.
- 대부분의 키워드들은 특별한 의미가 있으며, 러스트 프로그램의 다양한 일들을 처리하기 위해 사용할 것
- 몇몇은 아직 아무 기능도 없지만 차후에 추가될 기능들을 위해 예약되어 있습니다. 키워드 목록은 부록 A에서 확인할 수 있습니다.
1. 변수와 가변성
-
변수는 기본적으로 불변 (immutable)
-
이것은 러스트가 제공하는 안정성과 쉬운 동시성을 활용하는 방식으로 코드를 작성할 수 있도록 하는 넛지 (nudge, 슬며시 선택을 유도하기) 중 하나
-
하지만 여러분은 여전히 변수를 가변 (mutable) 으로 만들 수 있습니다.
mut
은 처음에 let으로 설정했을 때 지정한 data type 내에서만, 가변적으로 바꿀 수 있게 허용.
- 어떻게 하는지 살펴보고 왜 러스트가 불변성을 권하는지와 어떨 때 가변성을 써야 하는지 알아봅시다.
-
변수가 불변일 때, 어떤 이름에 한번 값이 묶이면 그 값은 바꿀 수 없습니다.
-
그리고, 새 variables 디렉터리의 src/main.rs 파일을 열어서 다음의 코드로 교체하세요 (아직은 컴파일되지 않습니다):
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
cargo run
하면 에러가 남
- 하지만 가변성은 아주 유용할 수 있고, 코드 작성을 더 편하게 해 줍니다.
- 변수는 기본적으로 불변이더라도, 여러분이 2장에서 했던 것처럼 변수명 앞에 mut을 붙여서 가변으로 만들 수 있습니다.
- mut를 추가하는 것은 또한 미래에 코드를 읽는 이들에게 코드의 다른 부분에서 이 변수의 값이 변할 것이라는 의도를 전달
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
1.1. 상수(constant)
- 상수 (constant) 는
불변 변수와 비슷
한데, 어떤 이름에 묶여 있는 값이고 값을 바꾸는 것이 허용되지 않지만, 변수와는 약간 다른 점들이 있음
- 먼저, 상수는 mut와 함께 사용할 수 없습니다.
- 상수는 기본적으로 불변인 것이 아니고, 항상 불변입니다.
- 상수는 let 키워드 대신 const 키워드로 선언하며, 값의 타입은 반드시 명시되어야 합니다.
- 다음 절 ‘데이터 타입’에서 타입과 타입 명시에 대해 다룰 예정이므로, 자세한 사항은 아직 걱정하지 않아도 됩니다. 항상 타입 명시를 해야 한다는 것만 알아두세요.
- 상수는
(전역 스코프를 포함한) 어떤 스코프
에서도 선언 가능하므로 코드의 많은 부분에서 알 필요가 있는 값에 유용
- 마지막 차이점은, 상수는 반드시 상수 표현식으로만 설정될 수 있고 런타임에서만 계산될 수 있는 결괏값으로는 안된다는 것입니다.
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
- 상수의 이름은 THREE_HOURS_IN_SECONDS이고 값은 60(분당 초의 개수), 60(시간당 분의 개수), 3(이 프로그램에서 알아둘 필요가 있는 시간의 숫자)를 모두 곱한 값
- 러스트의 이름 짓기 관례에서 상수는 단어 사이에 밑줄을 사용하고 모든 글자를 대문자로 쓰는 것
- 컴파일러는 컴파일 타임에 제한된 연산을 수행할 수 있는데, 이런 상숫값을 10,800으로 쓰는 대신 이해하고 검사하기 더 쉽게 작성할 방법을 제공해 줍니다.
- 상수는 선언된 스코프 내에서 프로그램이 동작하는 전체 시간 동안 유효합니다.
- 이러한 특성은, 플레이어가 얻을 수 있는 점수의 최곳값이라던가 빛의 속도 같이,
- 프로그램의 여러 부분에서 알 필요가 있는 값들에 유용합니다.
1.2. shadowing
- 새 변수를 이전 변수명과 같은 이름으로 선언할 수 있습니다.
- 첫 번째 변수가 두 번째 변수에 의해 가려졌다 (shadowed) 라고 표현하며,
- 이는 해당 변수의 이름을 사용할 때 컴파일러가 두 번째 변수를 보게 될 것이라는 의미
- 사실상 두 번째 변수는 첫 번째 것을 가려서, 스스로를 다시 가리거나 스코프가 끝날 때까지 변수명의 사용을 가져가 버립니다.
- 아래처럼 똑같은 변수명과 let 키워드의 반복으로 변수를 가릴 수 있습니다:
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
println!("The value of x is: {x}");
}
The value of x in the inner scope is: 12
The value of x is: 6
- 섀도잉은 변수를 mut로 표시하는 것과는 다릅니다.
- 실수로 let 키워드 없이 변수에 값을 재할당하려고 한다면 컴파일 타임 에러가 발생하기 때문입니다.
- let을 사용하면, 값을 변형하면서 변형이 완료된 후에는 불변으로 유지할 수 있습니다.
- mut과 섀도잉의 또 다른 차이점은
- 다시금 let 키워드를 사용하여 새로운 변수를 만드는 것이기 때문에,
같은 변수명으로 다른 타입의 값을 저장할 수 있다는 것
입니다.
- 예를 들어, 프로그램이 사용자에게 어떤 텍스트 사이에 몇 개의 공백을 넣고 싶은지 공백문자를 입력하도록 요청하고, 이 값을 숫자로 저장하고 싶다 칩시다:
# shadowing: 같은 변수명으로, 다른 타입의 값을 지정할 수 있음.
let spaces = " ";
let spaces = spaces.len();
# 변수를 mut 표시하여, 가변 변수로 만드는 것:
# 그런데 여기에서 mut을 사용하려 한다면, 보시다시피 컴파일 타임 에러가 발생합니다:
# 변수의 타입을 바꿀 수 없다고 뜸.
let mut spaces = " "; # 문자열 타입
spaces = spaces.len(); # 숫자 타입
2. 데이터 타입
- 러스트의 모든 값은 특정한 타입을 가지며, 이는 러스트가 해당 데이터로 작업하는 방법을 알 수 있도록 어떤 종류의 데이터가 지정되고 있는지 알려줍니다.
- 여기서는 타입을
스칼라 타입
과 복합 타입
, 두 가지 부분 집합으로 나누어 보겠습니다.
- 러스트는 정적 타입의 (statically typed) 언어라는 점을 주지
- 이게 의미하는 바는 모든 변수의 타입이 컴파일 시점에 반드시 정해져 있어야 한다는 뜻
- 보통 컴파일러는 우리가 값을 어떻게 사용하는지에 따라 타입을 추측할 수 있습니다. (2장의 ‘비밀번호와 추릿값을 비교하기’에서 String에 parse를 사용하여 숫자로 변환했던 경우처럼) 여러 가지 타입이 가능한 경우에는 다음과 같이 반드시 타입 명시를 추가해야 합니다:
let guess: u32 = "42".parse().expect("Not a number!");
- 여기에
: u32
라는 타입 명시를 하지 않으면 러스트는 아래와 같은 에러를 출력하는데,
- 이는 컴파일러에게 사용하고자 하는 타입이 무엇인지에 대한 추가적인 정보가 필요하다는 뜻
2.1. 스칼라 타입
- 스칼라 (scalar) 타입은 하나의 값을 표현
- 러스트는 4가지 스칼라 타입
정수, 부동 소수점 숫자, 부울린 (boolean), 그리고 문자
2.1.1. 정수형
- 정수형 (integer type) 은 소수점이 없는 숫자
- 2장에서 정수형 중 하나인 u32 타입을 사용했었죠.
- 해당 타입의 선언은 부호 없는 32비트 변수임을 나타냅니다 (부호 있는 타입은 u 대신 i로 시작합니다).
- 표 3-1은 러스트에서 사용되는 정수형들을 보여줍니다. 이 변형 중 어떤 것이라도 정숫값의 타입 선언에 사용할 수 있습니다.
![](https://velog.velcdn.com/images/jk01019/post/689f809f-72c6-4466-adf5-38cc623c53ac/image.png)
- 각각의 타입은 부호 있는 (signed) 혹은 부호 없는 (unsigned) 타입이며 명시된 크기를 갖습니다.
- 그래서 u8 타입은 0에서 2^8 - 1 다시 말해, 0에서 255까지의 값을 저장할 수 있습니다.
- 추가로, isize와 usize 타입은 여러분의 프로그램이 동작하는 컴퓨터 환경에 따라 결정되는데, 위 테이블에는 ‘arch’라고 적시되어 있습니다.
- 64-bit 아키텍처이면 64비트를, 32-bit 아키텍처이면 32비트를 갖게 됩니다.
- 정수형 리터럴은 표 3-2에서 보시는 것과 같은 형태로 작성할 수 있습니다.
- "정수형 리터럴"이란 프로그래밍에서 사용되는
특정한 값을 직접 나타내는 수치를 의미
- Rust에서 정수형 리터럴은 코드 내에서 직접적으로 표현된 정수값을 나타내며, 여러 가지 방식으로 작성될 수 있습니다.
- 이를 통해 개발자는 코드에서 필요한 정수 값을 바로 사용할 수 있습니다.
- 기본적인 정수 리터럴
let x = 42; // 기본적으로 i32 타입으로 추정됩니다.
타입 접미사를 사용한 리터럴
let x = 42u8; // 8비트 부호 없는 정수
let y = 42i32; // 32비트 부호 있는 정수
- 여러 숫자 타입이 될 수 있는 숫자 리터럴에는 57u8과 같은 타입 접미사를 사용하여 타입을 지정할 수 있습니다.
구분자를 사용한 리터럴
: 1000처럼 시각적인 구분으로 읽기 쉽게 하기 위해서 을 사용할 수 있는데, 이는 1000이라고 쓴 것과 똑같은 값이 됩니다.
let long_number = 1_000_000; // 100만을 더 쉽게 읽기 위한 표현
let bin = 0b101010; // 2진수 리터럴
let oct = 0o52; // 8진수 리터럴
let hex = 0x2A; // 16진수 리터럴
![](https://velog.velcdn.com/images/jk01019/post/6c4ac65b-b098-4a66-a9bb-0499a21f46ac/image.png)
- 그러면 어떤 타입의 정수를 사용해야 하는지는 어떻게 알아낼까요?
- 확실히 정해진 경우가 아니라면 러스트의 기본값인, i32가 일반적으로 좋은 시작 지점
- isize나 usize는 주로 어떤 컬렉션 종류의 인덱스에 사용됩니다.
2.1.1.1. 정수 overflow
- 여러분이 0과 255 사이의 값을 담을 수 있는 u8 타입의 변수를 갖고 있다고 해봅시다.
- 만약에 이 변수에 256처럼 범위 밖의 값으로 변경하려고 하면 정수 오버플로우 (integer overflow) 가 일어나는데, 이는 둘 중 한 가지 동작을 일으킵니다.
- 코드를 디버그 모드에서 컴파일하는 경우,
- 러스트는 런타임에 정수 오버플로우가 발생했을 때 패닉 (panic) 을 발생시키는 검사를 포함시킵니다.
- 러스트에서는 에러가 발생하면서 프로그램이 종료되는 경우 패닉이라는 용어를 사용합니다;
- 9장의 ‘panic!으로 복구 불가능한 에러 처리하기’절에서 패닉에 대해 좀 더 자세히 다루겠습니다.
- --release 플래그를 사용하여 코드를 릴리즈 모드로 컴파일하는 경우
- 패닉을 발생시키는 정수 오버플로우 검사를 실행파일에 포함시키지 않습니다.
- 대신 오버플로우가 발생하면 러스트는 2의 보수 감싸기 (two's complement wrapping) 을 수행합니다.
- 짧게 설명하자면, 해당 타입이 가질 수 있는 최댓값보다 더 큰 값은 허용되는 최솟값으로 ‘돌아갑니다 (wrap around)’.
- u8의 경우 256은 0이, 257은 1이 되는 식입니다.
- 프로그램은 패닉을 발생시키지 않으나, 해당 변수는 아마도 여러분이 예상치 못했던 값을 갖게 될 겁니다. 정수 오버플로우의 감싸기 동작에 의존하는 것은 에러로 간주됩니다.
- 명시적으로 오버플로우의 가능성을 다루기 위해서는, 표준 라이브러리에서 기본 수치 타입에 대해 제공하는 아래 메서드 종류들을 사용할 수 있습니다:
- wrappingadd와 같은 wrapping* 메서드로 감싸기 동작 실행하기
- checked_* 메서드를 사용하여 오버플로우가 발생하면 None 값 반환하기
- overflowing_* 메서드를 사용하여 값과 함께 오버플로우 발생이 있었는지를 알려주는 부울린 값 반환하기
- saturating_* 메서드를 사용하여 값의 최대 혹은 최솟값 사이로 제한하기
- TODO: 위 내용들 공부하기
2.1.2. 부동 소수점 타입
- 러스트에도 소수점을 갖는 숫자인 부동 소수점 (floating-point) 숫자 기본 타입이 두 가지 있음
- 러스트의 부동 소수점 타입은 f32와 f64로, 각각 32bit와 64bit의 크기를 갖습니다.
- 기본 타입은 f64인데, 그 이유는 현대의 CPU 상에서 f64가 f32와 대략 비슷한 속도를 내면서도 더 정밀하기 때문
- 모든 부동 소수점 타입은 부호가 있습니다.
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
2.1.3. 수치 연산
fn main() {
// 덧셈
let sum = 5 + 10;
// 뺄셈
let difference = 95.5 - 4.3;
// 곱셈
let product = 4 * 30;
// 나눗셈
let quotient = 56.7 / 32.2;
let truncated = -5 / 3; // 결괏값은 -1입니다
// 나머지 연산
let remainder = 43 % 5;
}
2.1.4. boolean type
- 대부분의 다른 언어들처럼, 러스트에서의 부울린 (boolean) 타입도 true와 false 두 값을 가질 수 있습니다.
부울린 값은 1바이트 크기
fn main() {
let t = true;
let f: bool = false; // 명시적인 타입 어노테이션
}
2.1.5. 문자 타입
- Rust에서
char
와 string
은 문자 데이터를 다루는 두 가지 다른 방식
2.1.5.1. char
char
타입은 Rust에서 하나의 유니코드 스칼라 값을 저장
char
는 작은 따옴표를 사용하여 표현
- 예를 들어,
'a'
, '中'
, '😊'
등이 모두 유효한 char
값입니다.
char
는 4바이트의 메모리 공간을 사용
2.1.5.2. String
String
타입은 유니코드 문자열을 저장
하고 처리하는 데 사용
- 이는 여러 개의 문자를 연속적으로 저장할 수 있는 컨테이너
String
은 큰 따옴표를 사용하여 생성
- 예를 들어,
"hello"
, "안녕"
, "👋"
등이 모두 유효한 String
값
String
은 내부적으로 Vec<u8>
을 사용하여 데이터를 저장
- 이는
String
이 메모리 상에서 자동으로 확장될 수 있음을 의미하며, 실행 중에 크기를 변경할 수 있음
2.1.5.3. 예제
let c: char = 'A';
let s: String = "Hello, world!";
2.2. 복합 타입
- 복합 타입 (compound type): 여러 값을 하나의 타입으로 묶을 수 있음
- 러스트에는
튜플 (tuple)
과 배열 (array)
, 두 가지 기본 복합 타입
2.2.1. tuple 타입
- 튜플은
다양한 타입의 여러 값을 묶어 하나의 복합 타입으로 만드는 일반적인 방법
- 튜플은 고정된 길이를 갖습니다.
- 즉, 한번 선언되면 그 크기를 늘리거나 줄일 수 없습니다.
- 괄호 안에 쉼표로 구분하여 값들의 목록을 작성하면 튜플을 만들 수 있습니다.
- 튜플 내의 각 위치는 타입을 갖고, 이 튜플 내의 타입들은 서로 달라도 됩니다.
- 다음은 (안 써도 괜찮지만) 타입을 명시해 본 예제입니다:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
- 튜플은 하나의 복합 요소로 취급되므로 변수 tup은 튜플 전체가 바인딩됩니다.
- 튜플로부터 개별 값을 얻어오려면, 아래와 같이 패턴 매칭을 하여 튜플 값을 구조 해체 (destructuring)해서 사용하면 됩니다:
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
- 마침표(.) 뒤에 접근하고자 하는 값의 인덱스를 쓰는 방식으로도 튜플 요소에 접근할 수 있습니다. 예를 들면:
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;
}
- 위의 프로그램은 튜플 x를 만들고, 인덱스를 사용하여 이 튜플의 각 요소에 접근합니다.
- 대부분의 언어가 그렇듯이 튜플의 첫 번째 인덱스는 0입니다.
- 아무 값도 없는 튜플은
유닛 (unit)
이라는 특별한 이름을 갖습니다.
- 이 값과 타입은 모두 ()로 작성되고, 빈 값이나 비어있는 반환 타입을 나타냄
- 표현식이 어떠한 값도 반환하지 않는다면 암묵적으로 유닛 값을 반환하게 됩니다.
2.2.2. 배열 타입
- 튜플과는 달리 배열의 모든 요소는 모두 같은 타입이여야 합니다.
- 몇몇 다른 언어들과는 달리 러스트의 배열은 고정된 길이를 갖습니다.
- 대괄호 안에 쉼표로 구분한 값들을 나열해서 배열을 만들 수 있습니다:
fn main() {
let a = [1, 2, 3, 4, 5];
}
- 배열을
튜플 or 벡터
보다 더 써야하는 경우
- 힙보다는 스택에 데이터를 할당하고 싶을 때(힙과 스택은 4장에서 더 다루겠습니다)
- 항상 고정된 개수의 요소로 이루어진 경우라면 배열이 유용
- 하지만 배열은 벡터 타입처럼 유연하지는 않습니다.
벡터는 표준 라이브러리가 제공하는 배열과 유사한 컬렉션 타입
인데 크기를 늘리거나 줄일 수 있습니다.
- 배열을 이용할지 혹은 벡터를 이용할지 잘 모르겠다면, 아마도 벡터를 사용해야 할 겁니다. 8장에서 벡터에 대해 더 자세히 다룰 예정입니다.
- 그러나 요소의 개수가 바뀔 필요가 없다는 것을 알고 있을 때라면 배열이 더 유용
- **한 가지 예로, 프로그램에서 달의 이름을 이용하려고 한다면, 이것이 언제나 12개의 요소만 가지고 있을 것이라는 사실을 알고 있으므로, 아마도 벡터보다는 배열을 사용할 것입니다:
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
- 배열은 아래와 같이 type annotation을 작성하기도 함 (대괄호 안에 요소의 타입을 쓰고 세미콜론을 쓴 뒤 요소의 개수를 적는 식)
let a: [i32; 5] = [1, 2, 3, 4, 5];
- 또한 다음과 같이 대괄호 안에 초깃값과 세미콜론을 쓴 다음 배열의 길이를 적는 방식을 사용하여 모든 요소가 동일한 값으로 채워진 배열을 초기화할 수도 있습니다:
let a = [3; 5]; # [3, 3, 3, 3, 3];
2.2.2.1. 배열 요소에 접근하기
- 배열은
스택에 할당될 수 있는 계산 가능한 고정된 크기의 단일 메모리 뭉치
- 아래와 같이 인덱스를 통해 배열 요소에 접근할 수 있습니다:
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
2.2.2.2. 유효하지 않은 배열 요소에 대한 접근
- 만약
배열의 끝을 넘어선 요소에 접근하려고 하면 어떤 일이 벌어지는지
알아봅시다.
- 프로그램은 인덱스 연산에서 잘못된 값을 사용한 시점에서 런타임 에러를 발생시켰습니다.
- 인덱스를 이용하여 요소에 접근을 시도하는 경우, 러스트는 여러분이 명시한 인덱스가 배열 길이보다 작은지 검사할 것입니다.
인덱스가 배열 길이보다 크거나 같을 경우 러스트는 패닉 (panic) 을 일으킵니다.
- 특히 위의 경우 이러한 검사는 런타임에서 일어나야 하는데, 이는 사용자가 코드를 실행한 뒤에 어떤 값을 입력할지 컴파일러로서는 알 수 없기 때문입니다.
- 이 예제는 러스트의 안전성 원칙이 동작하는 하나의 예입니다.
- 많은 저수준 언어들에서는 이러한 검사가 이루어지지 않고, 여러분이 잘못된 인덱스를 제공하면 유효하지 않은 메모리에 접근이 가능합니다.
- 러스트는 이런 메모리 접근을 허용하고 계속 실행하는 대신 즉시 실행을 종료함으로써 이런 종류의 에러로부터 여러분을 보호합니다.
- 러스트의 에러 처리 및 패닉을 일으키지 않으면서, 유효하지 않은 메모리 접근도 허용하지 않는 읽기 쉽고 안전한 코드를 작성하는 방법에 대해서는 9장에서 더 자세히 다루겠습니다.
3. 함수
- 러스트에서 가장 중요한 함수:
많은 프로그램의 시작점인 main 함수
- 또한
새로운 함수를 선언하도록 해주는 fn 키워드
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
- 러스트에서는 fn 뒤에 함수 이름과 괄호를 붙여서 함수를 정의
- 중괄호는 함수 본문의 시작과 끝을 컴파일러에게 알려줍니다.
- 함수의 이름 뒤에 괄호 묶음을 쓰면 우리가 정의해 둔 어떤 함수든 호출할 수 있습니다.
- another_function이 프로그램 내에 정의되어 있으므로, main 함수에서 해당 함수를 호출할 수 있습니다.
- 소스 코드 내에서 another_function이 main 함수 이후에 정의되어 있다는 점을 주목
- 러스트는 여러분의 함수 위치를 고려하지 않으며, 호출하는 쪽에서 볼 수 있는 스코프 어딘가에 정의만 되어있으면 됩니다.
3.1. 매개변수(parameter)
- 함수는 매개변수 (parameter) 를 갖도록 정의될 수 있으며, 이는 함수 시그니처 (function signiture) 의 일부인 특별한 변수(variable)
- 함수가 매개변수(parameter)를 갖고 있으면 이 매개변수에 대한 구체적인 값을 전달할 수 있습니다.
- 엄밀하게는 이러한 구체적인 값을 인수 (argument) 라고 부르지만,
- 일상적인 대화에서는 보통 함수 정의부 내의 변수나 함수를 호출할 때 전달되는 구체적인 값에 대해 매개변수와 인수라는 용어를 혼용하는 경향이 있습니다.
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}");
}
- 함수 시그니처에서는 각 매개변수의 타입을 반드시 선언해야 합니다.
- 이는 러스트를 설계하면서 신중하게 내린 결정 사항입니다:
- 함수의 정의에 타입 명시를 강제하면 이 함수를 다른 코드에서 사용할 때 여러분이 의도한 타입을 컴파일러가 추측하지 않아도 되게 됩니다.
- 컴파일러는 또한 함수가 기대한 타입이 무엇인지 알고 있으면 더욱 유용한 에러 메시지를 제공할 수 있습니다.
- 여러 매개변수를 정의하려면 아래처럼 쉼표 기호로 매개변수 정의를 구분하세요:
fn main() {
print_labeled_measurement(5, 'h');
}
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
3.2. 구문(statements)과 표현식(Expressions)
- 구문(statements):
어떤 동작을 수행하고 값을 반환하지 않는 명령
값을 반환하지 않기에
, 다른 변수에 할당하려고 하면 에러가 남.
- 구문(statements)는 종결을 나타내는
;
을 씁니다.
fn main() {
let y = 6;
}
let y = 6;
: 구문(statements)
- 또한
함수 정의도 구문
입니다; 위 예제는 그 자체로 구문
# 아래는 에러가 남.
fn main() {
let x = (let y = 6);
}
- 표현식(Expressions):
결괏값을 평가
(어떤 값을 평가)
- 값을 반환합니다!!
- 예
5+6
은 11
을 평가하는 표현식(expression)
ley y=6
구문(statement)에서, 6
은 6
이라는 값을 평가하는 표현식(expression)
- 표현식(expression)은 구문(statements)의 일부일 수 있음
- 여러분이 작성하는 러스트 코드의 대부분은 표현식(Expressions)
- 함수를 호출하는 것도, 매크로를 호출하는 것도 표현식(expression)
- 표현식(expression)은 종결을 나타내는
;
을 쓰지 않습니다.
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {y}");
}
- 아래 부분 전체는 표현식(expression)입니다. (x+1도 표현식(expression)입니다.)
{
let x = 3;
x + 1
}
- 함수 본문:
필요에 따라 표현식 (expression) 으로 끝나는 구문 (statement) 의 나열로 구성
- 러스트는 표현식(expression) 기반의 언어이므로, 구문(statement)과 표현식(expression)의 구분은 러스트를 이해하는데 중요합니다.
- 다른 언어들은 이런 구분이 없으므로, 구문과 표현식이 무엇이며 둘 간의 차이가 함수의 본문에 어떤 영향을 주는지 살펴보았습니다
3.3. Functions with Return Values
- 함수는 호출한 코드에 값을 반환할 수 있습니다.
- 반환되는 값을 명명해야 할 필요는 없지만, 그 값의 타입은 화살표 (->) 뒤에 선언되어야 합니다.
- 매우 중요: 러스트에서 함수의 반환 값은 함수 본문의 마지막 표현식(expression)의 값과 동일
- return 키워드와 값을 지정하여 함수로부터 일찍 값을 반환할 수 있지만, 대부분의 함수들은 암묵적으로 마지막 표현식 값을 반환합니다.
- 값을 반환하는 함수의 예를 보겠습니다:
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");
}
// 그래서 여기서는 여러 줄의 주석을 달 필요가 있을 정도로
// 복잡한 작업을 하고 있습니다! 휴우! 이 주석으로 무슨 일이
// 일어나고 있는지 설명할 수 있기를 바랍니다.
- 러스트에서 주석은 두개의 슬래시로 시작하며, 이 주석은 해당 줄의 끝까지 계속
- 한 줄을 넘기는 주석의 경우에는 위처럼 각 줄마다 //를 추가하면 됩니다:
- 하지만 아래와 같이 코드 앞줄에 따로 주석을 작성한 형태를 더 자주 보게 될 겁니다.
fn main() {
// 오늘 운이 좋은 느낌이에요
let lucky_number = 7;
}
- 러스트는 문서화 주석 (documentation comment) 라고 불리는 또다른 주석 형태를 가지고 있는데, 14장의 ‘Crates.io에 크레이트 배포하기’에서 다루도록 하겠습니다.
5. 제어 흐름문
5.1. if 표현식(expression)
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
- if 표현식(expression)의 조건과 관련된 코드 블록
{}
은 갈래(arms)로 불리기도 합니다.
- else 표현식(expression)도 사용
- 조건식이 bool이 아니면 에러가 발생합니다.
- 러스트는 부울린 타입이 아닌 값을 부울린 타입으로 자동 변환하지 않습니다.
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
5.1.1. else if로 여러 조건식 다루기
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
- else if 표현식을 너무 많이 사용하면 코드가 복잡해질 수 있으므로, 표현식이 두 개 이상이면 코드를 리팩터링하는 것이 좋습니다.
- 6장에서는 이런 경우에 적합한 match라는 러스트의 강력한 분기 구조에 대해 설명
5.1.2. let 구문에서 if 사용하기
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
- 하지만, 변수가 가질 수 있는 타입이 오직 하나이기 떄문에(컴파일 시점에 변수의 타입을 확실히 알아야 함), 아래와 같은 코드는 에러가 남
- 러스트에서는 number의 타입이 런타임에 정의되도록 할 수 없습니다.
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
5.2. 반복문을 이용한 반복
loop
, while
, for
의 3가지 반복문이 있음
5.2.1. loop로 코드 반복하기
- loop 키워드는 여러분이 그만두라고 명시적으로 알려주기 전까지, 혹은 영원히 코드 블록을 반복 수행하도록 해줍니다.
fn main() {
loop {
println!("again!");
}
}
- 루프 안에
break 키워드를 집어넣으면 루프를 멈춰야 하는 시점을 프로그램에게 알려줄 수 있습니다.
5.2.2. 반복문에서 값 반환하기
이를 위해서는 루프 정지를 위해 사용한 break 표현식(expression) 뒤에 반환하고자 하는 값을 넣으면 됩니다
- break는 표현식(expression)입니다!!!! 어떤 값을 평가합니다. (return 값을 내기 때문인듯?)
- break가 expression인 이유: 값을 반환하므로
- loop는 기본적으로 값을 반환하지 않습니다.
- 그러나 break에 값을 붙여 loop를 종료하면서 동시에 값을 반환할 수 있습니다.
- 아래 코드 매우 중요!!!!!!!!
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
# The result is 20
- 그런 경우 break 키워드와 counter * 2 값을 사용하였습니다. 루프 뒤에는 result에 값을 할당하는 구문을 끝내기 위해 세미콜론을 붙였습니다. 결과적으로 result의 값이 20으로 출력됩니다.
5.2.3. 루프 라벨로 여러 반복문 사이에 모호함 없애기
- 만일 루프 안에 루프가 있다면, break와 continue는 해당 지점의 바로 바깥쪽 루프에 적용됩니다.
- 루프에 루프 라벨 (loop label) 을 추가적으로 명시하면, 루프 라벨은 반드시 작은 따옴표로 시작해야 합니다. 아래에 루프가 두 개 중첩된 예제가 있습니다:
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
- 바깥쪽 루프는 'counting_up 이라는 라벨이 붙어있고, 0에서부터 2까지 카운트합니다.
- 라벨이 없는 안쪽 루프는 10에서 9까지 거꾸로 카운트합니다.
- 라벨이 명시되지 않은 첫 번째 break는 안쪽 루프만 벗어납니다.
- break 'counting_up; 구문은 바깥쪽 루프를 탈출할 것입니다.
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
5.2.4. while을 이용한 조건 반복문
- 반복문 내에서 조건을 검사하는 작업도 자주 사용
- 조건문이 true인 동안에는 계속 반복하는 형태
- 조건문이 true가 아니게 될 때 프로그램은 break를 호출하여 반복을 종료합니다.
- 이러한 반복문 형태는 loop, if, else와 break의 조합으로 구현할 수 있습니다;
- 여러분이 원하신다면 그렇게 시도해볼 수 있습니다. 하지만 이러한 패턴은 매우 흔하기 때문에 러스트에서는 while 반복문이라 일컫는 구조가 내장되어 있습니다.
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
5.2.5. for를 이용한 컬렉션에 대한 반복문
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
- 이러한 안전성과 간편성 덕분에 for 반복문은 러스트에서 가장 흔하게 사용되는 반복문 구성요소
- 심지어 while 반복문을 사용했던 예제 3-3의 카운트다운 예제처럼 어떤 코드를 몇 번 정도 반복하고 싶은 경우라도, 대부분의 러스타시안들은 for 반복문을 이용할 겁니다.
- 표준 라이브러리가 제공하는 Range 타입을 이용하면
- 특정 횟수만큼의 반복문을 구현할 수 있는데,
- Range는 어떤 숫자에서 시작하여 다른 숫자 종료 전까지의 모든 숫자를 차례로 생성
- for 반복문을 이용한 카운트다운 구현은 아래처럼 생겼습니다. 여기서 아직 살펴보지 않았던 rev 메서드는 범위값을 역순으로 만들어줍니다:
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
3!
2!
1!
LIFTOFF!!!