러스트 프로그래밍 - 1장

러스트란?

Rust는 Firefox 를 개발한 Mozilla 재단이 지원하는 오픈소스. 안정성, 속도, 동시성을 3대 요소로 개발 목표를 잡았다. 시스템 프로그래밍에 사용하던 C/C++ 외에, Firefox 속도 향상에 큰 기여를 한 Rust가 주목받게 된다. C/C++ 언어에서 발생하는 메모리 관리의 복잡성을 해소한다고 하는데, 언어를 배우면서 어떻게 해결하는지 공부하고자 한다.

러스트는 패키지 관리 시스템 'Cargo' 가 존재한다. 빌드 시스템 역할도 수행한다.

  • 러스트는 소유권 시스템으로 메모리를 관리한다.

  • 러스트는 타입 추론 기능이 있다.

  • 러스트 설치 명령어 : curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

  • 러스트 업데이트 명령어 : rustup update stable

  • 러스트 개발 편의성을 위한 VSCode Extension : rust-analyzer

  • 러스트 소스 코드 파일 확장자 : .rs

  • 러스트 컴파일 및 실행 명령어 : rustc 파일명.rs, ./파일명

fn main() {
    println!("Hello World!");
}
  • println! 이라는 매크로를 사용하여 Hello World! 를 출력한다.

매크로란? (간단하게)

  • 매크로란, 컴파일하기 전에 코드를 받아서, 다른 Rust 코드 를 만들어내는 기능이다.
// 1. say_hello 라는 매크로를 생성한다. 
// 호출 시 println!("Hello"); 를 만들어 낸다.
macro_rules! say_hello {
    () => {
        println!("Hello!");
    };
}

fn main() {
    say_hello!();
}
// 2. create_function 이라는 매크로를 생성한다.
// $name : 매크로 안에서 사용할 매크로 변수
// ident : $name이 "식별자" 종류의 Rust 문법을 받는다고 정의
macro_rules! create_function {
    ($name:ident) => {
        fn $name() {
            println!("Hello!");
        }
    };
}

create_function!(hello);
/*
fn hello() {
    println!("Hello!");
} 가 만들어짐.
*/

create_function!(world);
/*
fn world() {
    println!("Hello!");
} 가 만들어짐.
*/


fn main() {
    hello();
    world();
}
  • 반복되는 코드 생성, 특정 코드에 기능 추가, 여러 개 인자 처리, 컴파일 타입 검사 등의 역할에 사용된다고 한다.
  • 위는 macro_rules! 기반의 선언적 매크로고, 아래와 같이 절차적 매크로도 있다고 한다.
  • 절차적 매크로란, "러스트 소스 코드" 를 입력으로 받아 새 러스트 코드를 반환하는 기능이다. 개념이 어려우니 일단 기초 단계에서는 넘어가고, 나중에 제대로 학습.

#[derive(Debug)]  // 절차적 매크로 예시
#[tokio::main]
#[route("/")]

러스트 - 사칙연산

// 러스트에서의 정수 나눗셈
fn main() {
    let a = 10;
    let b = 3;
    println!("a / b : {}", a / b); // a / b : 3
}
// 러스트에서의 실수 / 정수 나눗셈
fn main() {
    let a = 10.0; 
    let b = 3;
    println!("a / b : {}", a / b); // 아래 오류가 발생한다.
    // 숫자라도 타입이 다르면 맞춰줘야 한다.
}
error[E0277]: cannot divide `{float}` by `{integer}`
 --> hello.rs:4:30
  |
4 |     println!("a / b : {}", a / b);
  |                              ^ no implementation for `{float} / {integer}`
  |
// 러스트에서의 실수 나눗셈
fn main() {
    let a = 10.0;
    let b = 3.0;
    println!("a / b : {}", a / b); // a / b : 3.3333333333333335
}

궁금증 : 다른 연산에서도 타입 다른 것이 문제가 되는가?

fn main() {
    let a = 10.0;
    let b = 3;
    println!("a + b : {}", a + b);
}
error[E0277]: cannot add an integer to a float
 --> hello.rs:4:30
  |
4 |     println!("a + b : {}", a + b);
  |                              ^ no implementation for `{float} + {integer}`
  |

이 외 빼기, 곱하기도 비슷한 오류를 낸다. 정확하게는, Rust는 해당 타입 조합에 대해 "연산이 구현되어 있어야 한다." ChatGPT Rust 이항연산 구현

러스트 - 조건문, 반복문

// 러스트의 조건문
if 조건 {

} else if 조건 { 

} else {
    
}

let a = if n % 2 == 0 { "even" } else { "odd" };

// 러스트의 반복문
for 변수 in 시작..종료+1 {

}

조건문을 괄호로 감싸면

fn main() {
    if (3 > 1) {
        println!("Hi");
    }
}
warning: unnecessary parentheses around `if` condition
 --> hello.rs:2:8
  |
2 |     if (3 > 1) {
  |        ^     ^
  |
  = note: `#[warn(unused_parens)]` (part of `#[warn(unused)]`) on by default
help: remove these parentheses
  |
2 -     if (3 > 1) {
2 +     if 3 > 1 {
  |

warning: 1 warning emitted

러스트 - 포맷 지정자

println!("{}", value);
println!("{value}");

let arr = [1, 2, 3];
println!("{:?}", arr); // 디버그 출력. 배열, 벡터, 구조체를 확인할 때 사용

println!("{:<10}", "Hi"); // 10칸 왼쪽 정렬
println!("{:>10}", "Hi"); // 10칸 오른쪽 정렬
println!("{:^10}", "Hi"); // 10칸 가운데 정렬

println!("{:-<10}", "Hi"); // 10칸 왼쪽 정렬, 빈칸은 - 로 채우기
println!("{:->10}", "Hi"); // 10칸 오른쪽 정렬, 빈칸은 - 로 채우기
println!("{:-^10}", "Hi"); // 10칸 가운데 정렬, 빈칸은 - 로 채우기

println!("{:.2}", pi); // 3.14
println!("{:.4}", pi); // 3.1416
println!("{:10.2}", pi); // ______3.14 전체 10칸 중 소수점 두 자리까지

println!("{:b}", n); // 2진수 출력
println!("{:o}", n); // 8진수 출력
println!("{:x}", n); // 16진수 출력
println!("{:X}", n); // 16진수 대문자 출력

println!("{:#b}", n); // 0b 진법 접두사 포함 2진수 출력
                      // 그 외 0o, 0x..

println!("{:+}", 42); // 부호 출력 : +42
println!("{:+}", -42); // 부호 출력 : -42

println!("{:e}", 123456.0); // e 표기법. 1.23456e5
println!("{:E}", 123456.0); // E 표기법. 1.23456E5

println!("{value:.1}"); // 변수명과 같이 넣고 싶을때
println!("{value:.precision$}"); // 변수명을 지정자로 넣고 싶을때. 끝에 $ 필수

러스트 - 변수 선언

let a = 30; // 러스트에서 변수는 기본적으로 immutable 이다.
let mut b = 30; // mut 키워드로 mutable 변수를 선언할 수 있다. 
# mut 없을 시 오류
error[E0384]: cannot assign twice to immutable variable `a`
 --> hello.rs:3:5
  |
2 |     let a = 1;
  |         - first assignment to `a`
3 |     a = a + 1;
  |     ^^^^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut a = 1;
  |         +++

error: aborting due to 1 previous error

러스트 - 데이터 타입

// i[ ] : [ ]비트 정수
// i8, i16, i32, i64, i128
// isize : 현재 OS 비트와 동일 크기의 정수

// u[ ] : [ ]비트 부호 없는 정수
// u8, u16, u32, u64, u128
// usize : 현재 OS 비트와 동일 크기의 부호 없는 정수

// f32 : 32비트 실수
// f64 : 64비트 실수

fn main() {
    // 해당 데이터 타입의 최소/최대 값은 타입::MIN, 타입::MAX 로 알 수 있다.
    println!("i32 : {} ~ {}", i32::MIN, i32::MAX);
    println!("isize = i{}", isize::BITS);

    // 타입 추론을 이용하지 않고, 직접 타입을 지정할 수 있다.
    let a : i32 = 30;
    let b : isize = 60;

    // 타입 변환은 as 키워드로 가능하다.
    let a = 'A' as i16; // char 타입을 i16 타입으로 강제 변환
}

러스트 - 함수 정의

fn main() {
    println!("{}", add(3, 2));
}

fn add(a: i64, b: i64) -> i64 {
    println!("덧셈 함수이다");
    a + b
}
  • 파라미터와 리턴값의 타입을 무조건 선언해야 한다. (변수명: 타입) -> 반환 타입
  • 리턴값은 값만 적어놓거나, return 값; 으로 사용한다.
fn main() {
    println!("{}", add(7, 2));
}

fn add(a: i64, b: i64) -> i64 {
    println!("덧셈 함수이다");
    if a > 3 {
        return a - b;
    } else {
        a + b
    }
}

러스트 - 클로저

러스트의 람다

fn main() {
    let print = |str| { print!("{}", str); }; // 타입 추론을 하기 때문에, 인수에 타입 정의를 안해도 된다.
    print("Hi");
}

클로저의 borrow?

fn main() {
    let mut a = 1;
    let increase = || { a + 1 };

    println!("{}", increase());

    a = 3;

    println!("{}", increase());
}

increase가 a를 참조해야 함 -> a를 불변 borrow -> increase가 나중에도 다시 사용됨 -> a에 대한 borrow를 계속 유지해야 함 중간에서 a의 값을 바꾸면 가변 접근이 필요하다. "어떤 값을 불변 참조하고 있는 동안에는 그 값을 변경할 수 없다." 가변 borrow 를 위해서는 increase도 mut 키워드가 필요하다.

fn main() {
    let mut a = 1;
    let mut increase = || { 
        a = a + 1;
        a
    };

    println!("{}", increase());

    a = 10;

    println!("{}", increase());
}

이것도 불가능하다. increase() 가 이번엔 가변으로 캡처하고 있기 때문. increase를 이후에도 사용하겠다고 판단되면 그 상태로 &mut a 로 가지고 있기 때문에 a에 직접 접근이 불가능 클로저가 a 를 "캡처해서 가지고 있는다"는 느낌? 으로 생각하면 된다. 중간에 직접 접근할거면, 차라리 클로저의 인수로 받는 편이 더 좋다.

fn main() {
    let mut a = 1;
    let mut increase = || { 
        a = a + 1;
        a
    };

    println!("{}", increase());

    println!("{}", a);
    
    a = 10;

    println!("{}", a);
}

더 안 쓸거면 가능. 오류 안남.

러스트 - 배열 선언

fn main() {
    // 고정 길이 배열 선언
    let arr1 = [1, 2, 3, 4, 5];

    println!("{}", arr1[0]);
    println!("{}", arr1.len());

    let arr2: [i32; 3] = [1, 2, 3]; // i32 타입으로 길이 3 배열
    let arr3 = [0; 5]; // 길이 5만큼 0으로 초기화

        // 중요한 점 : "길이"까지 타입의 일부다!
        // [i32; 3] 과 [i32; 4] 는 "다른 타입"이다.
        // 따라서 길이는 바꿀 수 없다.
    
    // 가변 길이 배열 선언 (Vec 사용)
    let mut arr4 = vec![1, 2, 3]; // 타입은 Vec<i32> 가 된다.

    arr4.push(4); arr4.push(5);
    println!("{arr4:?}");

    let mut arr5: Vec<i32> = Vec::new();
}

러스트 - 참조 값

fn main() {
    let mut a = 10;
    add5(&mut a); // 변경될 변수는 &mut 키워드와 함께 전달
    println!("{a}");
}

fn add5(a: &mut i64) { // 인수에서도 &mut
    *a += 10; // C의 포인터처럼 역참조
}