Rust

2023년 5월 15일 수정

러스트(Rust)는 컴퓨터 프로그래밍 언어 중 하나다. 주로 컴파일이 필요한 시스템 소프트웨어를 개발하기 위한 목적의 언어로 개발되었다.

초기에는 모질라 재단의 프로젝트에서 활용되었으며 독창적인 메모리 관리 체계의 우수성을 인정 받아서 다양한 프로젝트의 메인 언어로 활발하게 채용되고 있다.

주제별 글

Primitive Types

Signed Integer

i8, i16, i32, i64, isize

Unsigned Integer

u8, u16, u32, u64, usize

Floating Points

f32, f64

Booleans

bool

Characters

char

char can be cast to ant integer type, but only u8 can be cast to char.

String

str, &str

&str is a pointer type, but actually means a string type.

Variables and Constants

let decimal: i32 = 123;
let immutable_variable = 10;       // type inferences(i32)
let mut mutable_variable = 20i64;  // type inferences(i64), mutable
mutable_variable = 30;

// Constants
const some_constant: u32 = 100;

// Shadowing
let immutable_variable = immutable_variable * 2;

// Complex Initialization
let x = {
    let y = 10;
    y * 2   // expression (without return)
};

Option(Nullable, Optional)

let some_value = Some(5);
let another_some_value: Option<i32> = None;

if some_value.is_some() { ... }
if some_value.contains(&5) { ... }
if another_some_value.is_none() { ... }

println!("some_value = {}", some_value.unwrap());

match another_some_value {
    None => println!("another_some_value has no value!");
    Some(value) => {
        println!("another_some_value * 2 = {}", value * 2);
    }
}

if let Some(v) = some_value {
    println!("v = {}", v);
}

// function returns
fn get_position() -> Option<i32> {
    if condition {
        Some(2)
    } else {
        None
    }
}

Functions

fn function_name(x: i32, y: i64) -> u32 {
    ...
    loop {
        ...
        return 10;
    }
    20  // expression (same with return 20)
}

let r = function_name(1, 2);

Control Flow

if-else

if n == 100 {
    ...
} else if n > 100 {
    ...
} else if n < 100 && n != 0 {
    ...
} else {
    ...
}

let number = 10
let even = if number % 2 == 0 {
    true
} else {
    false
}

match

let r = match v {
    0 | 1 => "binary",
    2..=9 => "single integer",
    10 => "ten",
    _ => "unknown"
};

Loops

loop {
    // ...
    if condition {
        break;
    }
}

while condition {
    // ...
}

let values = [1, 2, 3, 4, 5];
for value in values.iter() {
    ...
}

for i in 0..10 {
    // i = 0 ~ 9
}

for i in (0..4).rev() {
    // i의 값은 3, 2, 1, 0 순서
}

Ownership

다른 언어들과 비슷하게 포인터나 레퍼런스 개념이 사용되면 메모리 관리에 여러 문제가 생길 수 있는데 Rust는 소유권(ownership)이라는 좀 특이한 방식으로 메모리를 관리한다.

let s1 = String::from("foo bar");
let s2 = s1;    // s2 owns s1
println!(s1);   // Error!

위 코드 두 번째 라인은 s1 이 s2 로 이동(move)했다고 표현한다. 따라서 이 코드 이후 s1 을 액세스 하는 행위는 컴파일 에러로 막힌다.

소유권의 이전은 함수 호출의 매개변수로 넘어갈 경우도 동일하다.

some_function(s1);

위 코드는 some_function 함수가 s1 을 소유하게 된다. 그리고 이 함수가 종료될 때 s1 도 같이 사라지게 된다.

대신 반환(return)은 소유권도 반환한다.

let s1 = another_function(s1);

위 코드에서 s1 은 another_function 함수로 소유권이 넘어갔다가 반환되면서 다시 현재 스코프로 소유권이 반환된다. 물론 이 함수 내부에서 입력 받은 매개변수를 그대로 반환했을 경우이긴 하지만 말이다.

복제를 하지 않고 소유권을 빌려줄 수 있는 방법이 있다.

let s3 = &s1;

함수 매개변수로도 가능하다.

fn foo(s: &String) {
    ...
}

다만 빌려주기만 할 뿐 불변(immutable)이기 때문에 읽기 참조만 가능하다.

참조에서 쓰기까지 가능하게 하려면 이렇게 할 수 있다.

let s3 = &mut s1;

가변 참조는 한 변수 당 하나 씩만 쓸 수 있다. 즉 하나의 포인터를 여러 곳에서 쓸(write) 수 없도록 제한하고 있다. 친절하다.

Structure

struct Human {
    name: String,
    family_name: String,
    age: u16
}

인스턴스 생성 시 초기화 하는 방법도 조금 생소하다.

let h = Human {
    name: String::from("Conrad"),
    family_name: String::from("Renn"),
    age: 20
};

생성 시 필드와 변수 이름이 동일한 경우에 한해서 아래와 같이 필드 이름을 생략하는 트릭이 있다.

fn create_human(name: String, family_name: String, age: u16) -> Human {
    Human {
        name,
        family_name,
        age
    }
}

이 외에도 초기화 방법으로 동일하게 미리 할당된 변수의 값을 그대로 가져오는 방법이 제공된다.

let h2 = Human {
    name: String::from("James"),
    ..h
};

이렇게 하면 name 을 제외한 나머지 데이터는 h 의 것을 이용해서 생성된다.

러스트의 구조체는 메서드 개념이 제공된다.

impl Human {
    fn birthday(&mut self) {
        self.age += 1;
    }

    // like as static method
    fn birth(name: String, family_name: String) -> Human {
        Human {
            name,
            family_name,
            age: 0
        }
    }
}

Enums

enum Gender {
    Male,
    Female,
    Other(String),
    Intersex(String, i32),
    Special { type: String, desc: String }
    Secret
}

사용할 때는 이런 식이다.

let gender = Gender::Female;

match 로 값에 해당하는 분기를 쉽게 만들 수 있다.

match gender {
    Male => println!("He is male");
    Female => println!("She is female");
    Other(name) => println!("This is {}", name);
    Intersex(name, identifier) => println!("{} type identifier {}", name, identifier);
    _ => println!("It is so difficult!");
}

만약 하나 정도의 케이스만 일치시킨다면 이것 보다는 if let 을 쓰는 편이 편할 것 같다.

if let Other(name) = gender { ... }

개인적으로 익숙한 Swift랑 용도가 달라서 왠지 오해할 수도 있을 것 같다.

Results

enum Result<T, E> {
    Ok(T),
    Err(E)
}

아는 사람은 알겠지만, Swift의 Result 타입과 거의 동일하다.

Complex and Collections

Tuple

let tup: (i32, i64, u8) = (500, 6.4, 1);
let (x, y, z) = tup;
let first = tup.0;

Python에서 가능한 튜플을 이용한 임시 변수 없는 교환(swap)이 가능할까 시험해 봤는데 문법 오류가 발생한다. 안타깝다.

Array

let list = [1, 2, 3, 4, 5];
let second = list[1];
let zeros = [0; 100];  // 0 x 100 elements
let len_zeros = zeros.len();

Vector

let v1 = vec![1, 2, 3];
let v1second: &i32 = &v1[1];
let v1third: Option<i32> = v.get(2);

가변 벡터는 물론 추가로 다양한 가변 명령을 사용할 수 있다.

let mut v2 = Vec::new();
v2.push(1);
v2.push(2);
v2.push(3);

반복문에서 참조 및 변경이 가능하다.

for i in &v1 {
    ...
}

for i in &mut v2 {
    ...
}

해시맵(Hash Map)

해시맵은 다른 언어들의 사전형(Dictionary)와 비슷하게 쓸 수 있는 Key - Value 콜렉션이다. 단지 원시 타입이 아니어서 모듈 임포트가 필요하다.

use std::collections:HashMap;

동적으로 해시맵을 생성하는 예는 이렇다.

let mut dict = HashMap::new();
dict.insert(String::from("apple"), String::from("사과"));
dict.insert(String::from("banana"), String::from("버내너"));
dict.insert(String::from("pear"), String::from("배"));

해시맵에 insert를 하는 과정에서 원시 타입 등은 복제가 진행되지 소유권 문제에서 자유로운 편이다.

값을 읽을 때는 Option 타입을 리턴함을 주의하자.

if let Some(banana) = dict.get(&String::from("banana")) {
    println!("banana -> {}", &banana);
}

나열도 간단한 편이다.

for (key, value) in &dict {
    println!("{} -> {}", key, value);
}

String

let s1 = "C String".to_string();
println!("length of s1 = {}", &s1.len());
let s1substr = &s1[1..4];  // -> " St"

let s2 = String::from("Rust String");
let s4: String = "Some Rust String".into();

당연하겠지만 가변형도 지원된다.

let mut s3 = String::new();
s3.push('f');       // s3 -> "f"
s3.push_str("oo");  // s3 -> "foo"
s3 += &s1;          // s3 -> "fooC String"
s3 += &s2;          // s3 -> "fooC StringRust String"

문자열 포맷은 println! 과 비슷하게 아래와 같은 식으로 쓸 수 있다.

let s4 = format!("s1 = {}, s2 = {}", s1, s2);

마치 Pythonformat() 을 연상시키는 문법이다.

외부 링크

소개 및 팁

사용기

Written by Rust

  • 🌏GitUI: Blazing 💥 fast terminal-ui for git written in rust 🦀