bin

3. 보편적인 프로그래밍 개념

satorare 2022. 10. 23. 14:26

https://rinthel.github.io/rust-lang-book-ko/ch03-00-common-programming-concepts.html

 

보편적인 프로그래밍 개념 - The Rust Programming Language

이번 챕터에서는 모든 프로그래밍 언어가 대부분 가진 개념이 Rust에서는 어떻게 다루어지는지 알아보고자 합니다. 많은 프로그래밍 언어가 보편적인 핵심요소를 갖습니다. 이번 챕터에서 Rust

rinthel.github.io

1. 변수

Rust의 변수는 기본적으로 불변(immutable)입니다.

 

1.1 가변(mutable) 변수의 선언

 

let x = 6; // 이후 x = 3과 같이 값을 변경할 경우 컴파일 에러를 일으킨다.

 

1.2 상수와 불변 변수 (Constants Vs. Immutable Variable)

(1) 상수는 mut 키워드를 사용할 수 없습니다.

(2) 상수는 let 키워드 대신 const 키워드를 사용해야합니다.

(3) 상수는 할당할 값의 타입을 반드시 지정해야합니다.

(4) 상수는 반드시 상수 표현식을 사용해서 값을 할당해야합니다. (런타임 연산에서 얻은 값을 사용할 수 없다.)

(5) 암묵적인 상수 명명 규칙에 따라 대문자만 사용하며, 단어 사이에 언더바(_)로 구분합니다.

 

const MAX_POINTS: u32 = 100_000; // 숫자의 자릿수를 구분하기 위해서 언더바(_)를 사용했다.

 

(6) 상수는 전역 범위를 비롯해 어디에서든 선언 가능하며, 선언된 범위 내에서 항상 유효합니다.

 

1.3 쉐도잉 (Shadowing)

선언된 불변 변수의 값을 가리는(Shadowing) 기능입니다.

 

let x = 5;
let x = x + 1; // 두 번째 x는 첫 번째 변수 x에 대해 Shadowing해서 6이 된다.
let x = x + 2; // 마찬가지로 7이 된다.

 

mut 키워드와 shadowing의 차이는 변수의 타입을 다르게 선언할 수 있다는 데 있습니다.

 

let spaces = " ";
let spaces = spaces.len();
// spaces 변수는 문자열 변수로 선언되었지만, 두 번째 변수 spaces가 첫 번째 변수와 이름만 같을 뿐
// 숫자 타입을 사용하는 새로운 변수이기 때문에 문제없다.

 

반면, mut 키워드를 사용하는 경우에는 이러한 작동이 불가능합니다.

 

let mut spaces = " ";
spaces = spaces.len(); // 타입을 변경할 수 없으므로 컴파일 에러를 발생시킨다.

2. 데이터 타입

러스트는 컴파일 시점에 모든 변수의 타입이 특정되어야 합니다. 따라서 컴파일러는 타입 추론을 통해 변수에 할당된 값이나 변수의 사용을 보고 실제 타입을 예측합니다.

 

2.1 스칼라(Scalar) 타입

스칼라는 하나의 값을 표현한다. 가장 대표적인 스칼라 타입은 다음 4종입니다.

  • 정수(Integer) : 부호가 있다면 접두어 i(integer)가 붙고, 부호가 없다면 접두어 u(unsigned integer)가 붙습니다. 또한 정수의 크기에 따라 8, 16, 32, 64, arch로 나뉩니다. 정수의 기본 타입은 i32입니다.
  • 부동 소수점 숫자(Floating Point Numbers) : f32와 f64를 제공하며, 기본 타입은 f64입니다.
  • 불리언(Booleans) : true or false 값, 각각의 크기는 1byte이며 type annotation을 이용하는 경우 bool 키워드를 이용합니다.
  • 문자(Characters) : 가장 기본적인 알파벳 타입은 char이며, char 리터럴은 작은 따옴표로 묶습니다. 러스트의 char 타입은 4byte 크기의 유니코드 스칼라 타입이므로, ASCII보다 훨씬 많은 문자를 표현할 수 있습니다.

 

2.2 컴파운드(Compound) 타입

하나의 타입으로 여러 개의 값을 그룹화한 타입입니다.

  • 튜플 : 서로 다른 타입의 값들을 하나의 컴파운드 타입으로 그룹화합니다. 한 번 정의하면 그 크기를 줄이거나 키울 수 없습니다.

 

let tup = (500, 6.4, 1); // 굳이 타입이 같을 필요가 없다.
let tup: (i32, f64, u8) = (500, 6.4, 1); // type annotation을 이용함.

 

2.2.1 튜플의 해체(destruct) - 개별 변수에 대한 패턴매칭을 이용하여 이를 해결합니다.

 

let tup = (500, 6.4, 1)
let (x, y, z) = tup;
println!("y의 값 : {}", y);

 

2.2.2 마침표(.)를 이용한 튜플 개별참조(Index는 0부터 시작합니다.)

 

let x : (i32, f64, u8) = (500, 6.4, 1);
let one = x.2; // one == 1,

 

  • 배열 : 튜플과 달리 각 요소는 모두 같은 타입이어야하며, 고정된 길이를 갖습니다. 항상 고정된 갯수의 요소들을 다룰 때 유용합니다.

 

// 배열의 요소에 접근하기 위해선 인덱싱을 활용한다. 인덱스는 0부터 시작한다.
let a = [1, 2, 3, 4, 5];
let first = a[0]; // first = 1;

let b: [i32; 5] = [1, 2, 3, 4, 5]; // type annotation을 이용한 배열

 

2.2.3 같은 값을 가지는 배열

 

let a = [3; 5]; // 모두 3이라는 초기값을 가진 5개의 요소의 배열을 선언

3. 함수

함수의 시그니처

  • fn 함수이름 ( 매개변수 ) { 내용 }

3.1 rust에서는 함수의 선언 순서는 별로 중요하지 않습니다.

 

fn main() {
// 함수 선언은 fn 키워드로 시작한다.
}

fn another_function() {
// 함수의 명명은 소문자와 함께 단어 구분을 언더바로 구별한다.(Snake Case)
// 여기서 another_function()과 main() 함수는 같은 프로그램안에 선언되었으므로 main 내에서 해당 함수를 호출 가능하다.
}

 

3.2 매개변수는 그 타입을 항상 명시해주어야 합니다.

함수에는 매개변수(parameter)가 존재하며, 매개변수에 전달된 구체적인 값을 인수(argument)라고 부릅니다.

 

fn another_function(x: i32) {
    println!("x의 값 : {}", x);
}

 

다수의 매개변수는 쉼표로 구분하여 씁니다.

 

fn another_function(x: i32, y: i32) {
    println!("{}, {}", x, y);
}

 

3.3 러스트는 표현식 기반 언어이기 때문에, 함수 본문은 구문(statements) 혹은 표현식(expression)으로 끝이납니다.

  • 구문 : 어떤 동작을 실행하지만 값을 리턴하지는 않는 명령
  • 표현식 : 최종 결과값으로 평가되는 값

let 키워드 또한 '구문'의 형식입니다. 이는 let 구문을 다른 let 구문에 대입할 수 없다는 걸 의미합니다.

 

3.3.1 하나의 구문으로 된 main 함수

 

fn main() {
    let x = 5;
}

 

3.3.2 구문으로 구문에 대입

 

let y = (let x = 5); // 사실상 y = x = 5와 의미가 같다.

 

이 때 let x = 5는 구문으로써 반환값이 없기 때문에, y에 대입할 값이 존재하지 않게됩니다. 따라서 실행 시 컴파일 에러가 뜹니다.

 

반면 표현식은 어떤 값으로 '평가'되는 모든 것이 표현식입니다.

 

let y = {
    let x = 3;
    x + 1 // x + 1은 4라는 값으로 평가되며, 이 값은 다시 y에 할당된다. 표현식의 마지막은 항상 세미콜론이 없다는 것을 유의하라
};

 

값을 반환하는 함수의 경우, 리턴값의 타입을 "->" 기호로 지정해주어야 합니다.

 

fn five() -> i32 {
    5
}

 

이 때 대부분의 함수는 그 함수의 마지막 표현식의 결과를 리턴합니다. 또한 함수 실행 중간에 return 키워드를 이용하여 특정한 값을 리턴할 수도 있습니다.

 

함수의 반환값을 이용하여 변수를 초기화 할 수도 있습니다.

 

let x = five(); // let x = 5와 같다.

 

3.3.3 표현식에 세미콜론을 붙이는 건 어떻게 평가되는가?

 

 

컴파일러는 i32 리턴 타입을 기대했지만, 마지막 "x + 1;"은 구문으로써 빈 튜플()로 표현되어 타입 불일치 에러가 발생합니다.

 

4. 주석

주석은 두 개의 슬래시(//)로 시작합니다. 보통은 관련된 코드의 윗줄에 작성하는 것이 일반적입니다.

 

5. 흐름 제어

5.1 if 표현식

 

if number < 5 {
    // number 가 5이하 일 때 해당 블럭으로 분기한다.
}
else {
    // 그 외 일 때 해당 블럭으로 분기한다.
}

 

if문의 조건부는 항상 불리언 타입(true, false)을 반환해야합니다.

 

예를 들면 number가 0일 때만 실행하고픈 프로그램을 구현하고 싶다하여도, 러스트에서는 0이 false를 대변해주지 않기 때문에 불가능합니다. (다른언어에선 될지도 모르지만 러스트는 조건부의 정수 타입을 불리언으로 자동 변환시키지 않습니다.)

 

또한 else if구문을 통해 조건 분기점을 늘릴 수 있습니다.

 

5.1.1 만약 여러 분기들의 조건들이 서로 부분집합 관계에 놓여있다면, 러스트는 항상 처음으로 조건이 일치하는 블럭만 실행시키고 나머지는 건너뛴다는 걸 유의해야합니다.

 

5.1.2 if는 표현식이므로 let 구문 오른쪽에 위치할 수 있습니다.

 

let number = if condition {
    5
}
else {
    6
};

 

이 때 if 표현식의 각 분기가 리턴하는 타입은 모두 같아야합니다.

6. 반복문

6.1 loop를 이용한 반복

loop 키워드는 루프를 중지하라고 명시적으로 설정하지 않으면, 무한으로 반복합니다.

 

loop {
    println!("다시 실행!");
}

 

루프안에서 break 키워드를 이용해 값을 리턴할 수도 있습니다.

 

let result = loop {
    counter += 1;
    if counter == 10{
        break counter * 2; // break를 이용하여 리턴할 떄에느 세미콜론으 ㄹ붙인다.
    }
}; // let result = 20;

 

6.2 while을 이용한 조건 반복

 

while number != 0 {
    // statements
}

 

while 반복문은 조건식이 일치하는 동안에는 코드가 실행되며, 불일치하면 루프를 탈출합니다.

 

6.3 for을 이용한 조건 반복

주로 컬렉션 형태의 데이터를 반복 처리할 때 사용합니다.

 

let a = [10, 20, 30, 40, 50];
for element in a.iter() {
    println!("요소의 값 : {}", element);
}

 

굳이 컬렉션이 아니더라도 while문의 역할을 for문이 대신할 수도 있습니다. 이 때 한 숫자에서 다른 숫자 사이에 존재하는 모든 숫자를 생성해 주는 Range 타입 ()을 이용합니다.

 

for number in (1..4).rev() {
    // 반복할 구문
}

 

6.3.1 rev() 메소드는 범위를 뒤집어서 생성한다.