[rust] 3. 일반적인 프로그래밍 개념

About_work·2024년 5월 30일
0

rust

목록 보기
3/16

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은 러스트에서 사용되는 정수형들을 보여줍니다. 이 변형 중 어떤 것이라도 정숫값의 타입 선언에 사용할 수 있습니다.
  • 각각의 타입은 부호 있는 (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 타입으로 추정됩니다.
      1. 타입 접미사를 사용한 리터럴
let x = 42u8;  // 8비트 부호 없는 정수
let y = 42i32; // 32비트 부호 있는 정수
  • 여러 숫자 타입이 될 수 있는 숫자 리터럴에는 57u8과 같은 타입 접미사를 사용하여 타입을 지정할 수 있습니다.
    1. 구분자를 사용한 리터럴: 1000처럼 시각적인 구분으로 읽기 쉽게 하기 위해서 을 사용할 수 있는데, 이는 1000이라고 쓴 것과 똑같은 값이 됩니다.
let long_number = 1_000_000; // 100만을 더 쉽게 읽기 위한 표현
    1. 진법을 명시한 리터럴
let bin = 0b101010;        // 2진수 리터럴
let oct = 0o52;            // 8진수 리터럴
let hex = 0x2A;            // 16진수 리터럴
  • 그러면 어떤 타입의 정수를 사용해야 하는지는 어떻게 알아낼까요?
    • 확실히 정해진 경우가 아니라면 러스트의 기본값인, 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에서 charstring은 문자 데이터를 다루는 두 가지 다른 방식
2.1.5.1. char
  • char 타입은 Rust에서 하나의 유니코드 스칼라 값을 저장
    • 즉, 하나의 문자만을 저장할 수 있습니다.
  • char는 작은 따옴표를 사용하여 표현
    • 예를 들어, 'a', '中', '😊' 등이 모두 유효한 char 값입니다.
  • char4바이트의 메모리 공간을 사용
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+611을 평가하는 표현식(expression)
        • ley y=6 구문(statement)에서, 66이라는 값을 평가하는 표현식(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}");
}

4. 주석 (comment)

// 그래서 여기서는 여러 줄의 주석을 달 필요가 있을 정도로
// 복잡한 작업을 하고 있습니다! 휴우! 이 주석으로 무슨 일이
// 일어나고 있는지 설명할 수 있기를 바랍니다.
  • 러스트에서 주석은 두개의 슬래시로 시작하며, 이 주석은 해당 줄의 끝까지 계속
  • 한 줄을 넘기는 주석의 경우에는 위처럼 각 줄마다 //를 추가하면 됩니다:
  • 하지만 아래와 같이 코드 앞줄에 따로 주석을 작성한 형태를 더 자주 보게 될 겁니다.
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!!!

profile
새로운 것이 들어오면 이미 있는 것과 충돌을 시도하라.

0개의 댓글