Post

[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-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize
  • 부호 있는 정수(2의 보수 형태를 사용)
    • 범위 = -2^(n-1) ~ 2^(n-1) - 1
  • 부호 없는 정수
    • 범위 = 0 ~ 2^n - 1
  • isizeusize 타입은 프로그램이 작동하는 컴퓨터 환경에 따라 결정
    • 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 표준을 따름

 f32f64
크기32-bit64-bit
정밀도1배수 정밀도(단정밀도)2배수 정밀도(배정밀도)
부호 지원있음있음

수치 연산

연산자의미
+덧셈
-뺄셈
*곱셈
/나눗셈
%나머지
  • 모든 숫자 타입에는 기본 수학 연산 기능을 제공
  • 정수 나눗셈은 가장 가까운 정숫값으로 버림 처리

불리언 (:boolean)

truefalse 값을 가지며 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 에러 발생
          • 불리언이 아닌 타입을 자동으로 불리언으로 변환하지 않기 때문에 항상 명시적으로 불리언 타입의 조건식을 제공해 주어야 함

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 표현식 뒤에 반환하고자 하는 값을 넣어 줌

루프 라벨로 여러 반복문 사이에 모호함 없애기

만일 루프 안에 루프가 있다면, breakcontinue는 해당 지점의 바로 바깥쪽 루프에 적용

  • 루프에 루프 라벨(loop label) 을 명시하면 breakcontibue와 함께 이 키워드 들이 바로 바깥쪽 루프 대신 라벨이 적힌 특정한 루프에 적용
    • 루프 라벨은 반드시 따옴표로 시작해야 함
    • 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!!!");
      }
      
This post is licensed under CC BY 4.0 by the author.