Rust 배우기

junyojeo·2024년 8월 6일
fn main() {
    println!("Hello World!");
}

Rust의 모토인 무비용 추상화(zero-cost abstraction)
소유권(Ownership) 시스템

변수

let x: f64 = 3.14159;(타입명시) == let x = 3.14159f64;(리터럴 표현)

  • 변수는 let 키워드를 사용해 선언
  • 변수 이름은 언제나 snake_case
  • 변경 가능(mutable)한 값은 mut 키워드로 표시 (**읽기만 할 값은 mut 키워를 빼면 됨. 변경할 값은 mut 키워드를 써야함. const처럼)
    let mut x = 42;

자료형

  • 부울 값 - 참/거짓 값을 나타내는 bool
    let bv = true;
  • 부호가 없는 정수형 - 양의 정수를 나타내는 u8 u16 u32 u64 u128
    let a = 42u8;
  • 부호가 있는 정수형 - 양/음의 정수를 나타내는 i8 i16 i32 i64 i128
    let a = 42i8;
  • 포인터 사이즈 정수 - 메모리에 있는 값들의 인덱스와 크기를 나타내는 usize isize
    let a = 42usize;
    시스템의 아키텍처에 따라 자동으로 32bit(8byte) 64bit(8byte) 조정됨
  • 부동 소수점 - f32 f64
    let a = 3.14f32;
  • 튜플(tuple) - stack에 있는 값들의 고정된 순서를 전달하기 위한 (값, 값, ...)
    let a: (&str, i32, bool) = ("Alice", 30, true);
  • 배열(array) - 컴파일 타임에 정해진 길이를 갖는 유사한 원소들의 모음(collection)인 [값, 값, ...]
    let a: [&str; 5] = ["월", "화", "수", "목", "금"];
  • 슬라이스(slice) - 런타임에 길이가 정해지는 유사한 원소들의 collection
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..4]; // [2, 3, 4]
  • str(문자열 slice) - 런타임에 길이가 정해지는 텍스트
    let a: &str = "문자열";

자료형 변환

let a = 42u8;
let a = a as u32

상수

const PI = 3.14159f32;

  • 사용될 때 값이 복사되는 변수와 달리, 상수는 컴파일 타임에 텍스트 지정자를 직접 값으로 대체.
  • 상수의 이름은 언제나 SCREAMING_SNAKE_CASE

배열

let a: [i32; 3] = [1, 2, 3];

  • 고정된 길이로 된 모두 같은 자료형의 자료를 가진 collection
  • array 의 자료형은 [T;N]로 표현하며, 여기서 T는 원소의 자료형, N은 컴파일 타임에 주어지는 고정된 길이
  • 각각의 원소는 [x] 연산자로 가져올 수 있는데, 여기서 x는 당신이 원하는 원소의 (0에서부터 시작하는) usize 형의 인덱스

함수

fn add(x: i32, y: i32) -> i32 {
    return x + y;
}
  • 함수의 이름은 언제나 snake_case

튜플

let (a, b) = swap(321, 123);

fn swap(x: i32, y: i32) -> (i32, i32) {
    return (y, x);
}

// 리턴 값의 튜플을 리턴
let result = swap(123, 321);
println!("{} {}", result.0, result.1);

// 튜플을 두 변수명으로 분해
let (a, b) = swap(result.0, result.1);
println!("{} {}", a, b)
  • 함수는 튜플(tuple) 을 리턴함으로써 여러개의 값을 리턴할 수 있다.
    return (y, x);
  • 함수에 리턴형을 지정하지 않은 경우 빈 tuple을 리턴하는데, 이는 unit이라고도 한다
  • 'unit_' 아무 것도 없는 것은 출력하기 힘들기 때문에 디버그 문자열을 출력한다.
    return ();

조건

if/else if/else

if x < 0 {
	} else if x == 0 {
	} else {
}
  • 모든 일반적인 관계 연산자와 논리 연산자가 그대로 동작: \==, !=<><=>=!||&&.

loop

  • 무한 반복문이다.
loop {
	x += 1;
	if x == 42 {
		break;
	}
}
  • break과 동시에, 값을 리턴할 수 있다.
let v = loop {
	x += 1;
	if x == 13 {
		break "13 찾았다";
	}
};

while

  • while은 반복문에 조건을 간단히 넣을 수 있게 해준다.
while x != 42 {
	x += 1;
}

for

  • 어떠한 표현이든지, 그로부터 평가된 반복자의 값을 반복한다.
for x in 0..5 {
	println!("{}", x);
} // 0~5 '전'까지의 숫자들을 생성 하며 {}를 반복한다.

결과:
0
1
2
3
4

for x in 0..=5 {
	println!("{}", x);
} // 0~5 까지의 숫자들을 생성 하며 {}를 반복한다.

결과:
0
1
2
3
4
5
  • 반복자는 더 가진게 없을 때까지 "다음에 가진게 뭐야?" 라고 요청할 수 있는 객체
  • .. 연산자는 시작 숫자에서 끝 숫자 전까지의 숫자들을 생성하는 반복자를 만든다.
  • ..= 연산자는 시작 숫자에서 끝 숫자까지의 숫자들을 생성하는 반복자를 만든다.

match

  • switch case문의 대체제다.
  • match의 조건은 빠짐 없이 모든 케이스를 처리해야 한다.
  • match와 destructuring(분해 let (a, b) = (구조체, 열거형, 튜플))의 조합은 현재까지 Rust에서 가장 흔하게 사용하는 패턴이다.
match x {
	0 => {
		println!("0 발견");
	}
	// 여러 개 값과 대조할 수 있다
	1 | 2 => {
		println!("1 또는 2 발견!");
	}
	// 범위로 대조할 수 있다
	3..=9 => {
		println!("3에서 9까지의 숫자 발견");
	}
	// 찾은 숫자를 변수에 바인딩할 수 있다
	matched_num @ 10..=100 => {
		println!("10에서 100까지의 숫자 {} 발견!", matched_num);
	}
	// 모든 케이스가 처리되지 않았을 경우 반드시 존재해야 하는 기본 match
	_ => {
		println!("뭔가 다른거 발견!");
	}
}

3항 연산

let a = if x < 0 { -1 } else { 1 };

특이점

  • ifmatch, 함수, 또는 범위 블록의 마지막 구문에 ';'가 없다면 Rust는 그 값을 블록의 리턴 값으로 간주한다.
let food = "햄버거";
let result = match food {
	"핫도그" => "핫도그다",
	// 리턴문 하나 뿐이라면 중괄호는 필수가 아님
	_ => "핫도그가 아니다",
};

결과: result == "핫도그가 아니다";

let v = {
	// 이 범위 블록은 함수 범위를 더럽히지 않고 값을 가져오게 해준다
	let a = 1;
	let b = 2;
	a + b
};

v == a + b

구조체

  • struct는 필드(field)들의 collection
  • field는 간단하게 데이터 구조에 연관된 데이터 값
  • 메모리 상에 field들을 어떻게 배치할지에 대한 컴파일러의 청사진과 같다.
struct SeaCreature {
    // String은 struct다
    animal_type: String,
    name: String,
    arms: i32,
    legs: i32,
    weapon: String,
}

메소드 호출

  • 스태틱 메소드(static methods) - 자료형 그 자체에 속하는 메소드로서, :: 연산자를 이용하여 호출
    String::from("HI!");
  • 인스턴스 메소드(instance methods) - 자료형의 인스턴스에 속하는 메소드로서, . 연산자를 이용하여 호출
    a.len();

메모리

  • 데이터 메모리(data memory) - 크기가 고정 되었으며 static (i.e. 프로그램이 실행되는 동안 항상 사용 가능)한 데이터용.

  • 이런 종류의 데이터는 컴파일러가 많은 최적화를 하며, 위치가 알려져 있고 고정되어 있기 때문에 일반적으로 사용하기에 매우 빠르다고 여긴다
    "Hello World!" 이 텍스트의 바이트들은 오직 한 곳에서만 읽히므로 이 영역에 저장될 수 있습니다.

  • 스택 메모리(stack memory) - 함수 내에서 변수로 선언되는 데이터용.

  • 이 메모리의 위치는 함수 호출 동안에는 절대 변하지 않기 때문에 컴파일러가 코드를 최적화할 수 있으며, 이로 인해 접근하기에 매우 빠릅니다.
    let a = 0;

  • 힙 메모리(heap memory) - 애플리케이션이 실행되는 동안 생성되는 데이터용.

  • 이 영역의 데이터는 추가하거나, 이동하거나, 제거하거나, 크기를 바꾸거나, 등을 할 수 있습니다.

  • 이런 동적 속성 때문에 일반적으로 사용하기에 느리다고 여기지만, 훨씬 더 창의적인 메모리 사용이 가능합니다.

  • 데이터가 이 영역에 추가되면 할당(allocation)이라고 부릅니다.

  • 데이터가 이 영역에서 제거되면 해제(deallocation)라고 부릅니다.
    let mut a = Vec::new();

메모리에 데이터 생성

  • 코드에서 struct를 인스턴스화(instantiate) 하면 프로그램은 연관된 field 데이터들을 메모리 상에 나란히 생성됨.
    StructName { ... }
  • 와 같이 안에 모든 field 값을 지정함으로써 instantiate 한다.
  • struct의 field 값들은 . 연산자를 통해 접근한다.
  • 게이
struct SeaCreature {
    animal_type: String,
    name: String,
    arms: i32,
    legs: i32,
    weapon: String,
}

fn main() {
    // SeaCreature의 데이터는 stack에 있음
    let ferris = SeaCreature {
        // String struct도 stack에 있지만,
        // heap에 있는 데이터에 대한 참조를 갖고 있음
        animal_type: String::from("게"),
        name: String::from("Ferris"),
        arms: 2,
        legs: 4,
        weapon: String::from("집게"),
    };

    println!(
        "{}는 {}이다. {}개의 팔과, {}개의 다리와, {}를 무기로 갖고 있다.",
        ferris.name, ferris.animal_type, ferris.arms, ferris.legs, ferris.weapon
    );
}

Ferris는 게이다. 2개의 팔과, 4개의 다리와, 집게를 무기로 갖고 있다.
*Tour of Rust에서 발췌

Tuple 같은 구조체

struct Locatuon(i32, i32);

// 이것도 여전히 stack에 있는 struct임
let a = Location(42, 32);

Unit 같은 구조체

  •  unit 은 빈 tuple인 ()의 또 다른 이름이다. => Unit(빈 tuple)
  • 거의 쓰이지 않는다.
    struct Macker;
    let a = Marker

열거형

  • enum 키워드를 통해 몇 가지 태그된 원소의 값을 가질 수 있는 새로운 자료형을 생성
  • match는 모든 가능한 enum 값을 빠짐없이 처리할 수 있도록 함
#![allow(dead_code)] // 이 줄은 컴파일러 경고를 방지해줌

enum Species {
    Crab,
    Octopus,
    Fish,
    Clam,
}

struct SeaCreature {
    species: Species,
    name: String,
    arms: i32,
    legs: i32,
    weapon: String,
}

fn main() {
    let ferris = SeaCreature {
        species: Species::Crab,
        name: String::from("Ferris"),
        arms: 2,
        legs: 4,
        weapon: String::from("claw"),
    };

    match ferris.species {
        Species::Crab => println!("{}는 게이다", ferris.name),
        Species::Octopus => println!("{}는 문어이다", ferris.name),
        Species::Fish => println!("{}는 물고기이다", ferris.name),
        Species::Clam => println!("{}는 조개이다", ferris.name),
    }
}

열거형과 데이터

  • enum의 원소들은 C의 union 처럼 동작할 수 있도록 한 개 이상의 자료형을 가질 수 있다.
  • enum이 match를 통해 패턴 일치될 때, 각각의 데이터 값에 변수명을 붙일 수 있다.
  • enum 데이터 값은 가장 큰 원소의 메모리 크기와 같은 메모리 크기를 가진다.
  • 이는 가능한 모든 값이 동일한 메모리 공간에 들어갈 수 있게 해준다.
  • 원소의 자료형에 더하여, 각 원소는 무슨 태그에 해당하는지 나타내는 숫자값도 갖는다. (예) 0, 1, 2, 3)
  • Rust의 enum은 tagged union 으로도 알려져 있다.
  • Rust가 대수적 자료형(algebraic types) 을 갖고 있다고 할 때 이는 자료형을 조합하여 새 자료형을 만드는 것을 의미한다.
#![allow(dead_code)] // 이 줄은 컴파일러 경고를 방지해줌

enum Species {
    Crab,
    Octopus,
    Fish,
    Clam,
}
enum PoisonType {
    Acidic,
    Painful,
    Lethal,
}
enum Size {
    Big,
    Small,
}
enum Weapon {
    Claw(i32, Size),
    Poison(PoisonType),
    None,
}

struct SeaCreature {
    species: Species,
    name: String,
    arms: i32,
    legs: i32,
    weapon: Weapon,
}

fn main() {
    // SeaCreature의 데이터는 stack에 있음
    let ferris = SeaCreature {
        // String struct도 stack에 있지만,
        // heap에 있는 데이터에 대한 참조를 갖고 있음(String::from())
        species: Species::Crab,
        name: String::from("Ferris"),
        arms: 2,
        legs: 4,
        weapon: Weapon::Claw(2, Size::Small),
    };

    match ferris.species {
        Species::Crab => match ferris.weapon {
            Weapon::Claw(num_claws, size) => {
                let size_description = match size {
                    Size::Big => "큰",
                    Size::Small => "작은",
                };
                println!(
                    "ferris는 {}개의 {} 집게를 가진 게이다",
                    num_claws, size_description
                )
            }
            _ => println!("ferris는 다른 무기를 가진 게이다"),
        },
        _ => println!("ferris는 다른 동물이다"),
    }
}

Generic 자료형

  • struct나 enum을 부분적으로 정의하여, 컴파일러가 컴파일 타임에 코드 사용을 기반으로 완전히 정의된 버전을 만들 수 있게 해준다.
  • 널 허용(nullable) 값을 표현할 때에도 쓰이며 (i.e. 아직 값이 없을 수도 있는 변수), 오류 처리, collection에 사용된다.
struct BagOfHolding<T> {
    item: T,
}
  • Rust의 제네릭은 C++의 템플릿과 비슷하지만, 타입 안전성을 위해 제약 조건(`trait bounds`)을 명시적으로 요구하며, 컴파일러가 타입의 유효성을 더 엄격하게 검사한다.
fn print_area<T: HasArea>(shape: T) { // <T: HasArea>가 trait bounds이다.
    println!("The area is {}", shape.area());
}
  • Turbofish로 알려진 ::<T> 연산자를 사용해 자료형을 명시할 수 있다.
// 일부만 정의된 struct 자료형
struct BagOfHolding<T> {
    item: T,
}

fn main() {
    // 중요: 여기서 generic 자료형을 쓰면, 컴파일 타임에 생성된 자료형을 생성하게 됨.
    // Turbofish를 쓰면 명시적일 수 있다.
    let i32_bag = BagOfHolding::<i32> { item: 42 };
    let bool_bag = BagOfHolding::<bool> { item: true };

    // Rust는 generic에도 자료형을 유추할 수 있다!
    let float_bag = BagOfHolding { item: 3.14 };

    // 중요: 실생활에서는 가방 속에 가방을 넣지 마시오
    let bag_in_bag = BagOfHolding {
        item: BagOfHolding { item: "쾅!" },
    };

    println!(
        "{} {} {} {}",
        i32_bag.item, bool_bag.item, float_bag.item, bag_in_bag.item.item
    );
}

NULL 표현하기

  • 다른 언어에서 null 키워드는 값이 없음을 나타낸다.
  • Rust에는 null이 없다.
  • None을 사용한다.
enum Item {
    Inventory(String),
    // None은 항목의 부재를 나타냄
    None,
}

struct BagOfHolding {
    item: Item,
}

Option

  • Rust에는 null을 쓰지 않고도 nullable한 값을 표현할 수 있는 Option이라 불리는 내장된 generic enum이 있다.
enum Option<T> {
	None,
	Some(T),
}

언제나 Some과 None로 Option을 인스턴스화(생성)할 수 있다.

// 일부만 정의된 struct 자료형
struct BagOfHolding<T> {
    // 자료형 T의 인자는 다른 곳으로 넘겨질 수 있음
    item: Option<T>,
}

fn main() {
    // 중요: i32를 위한 가방에 아무 것도 안들었다!
    // Rust가 무슨 자료형의 가방인지 알 수가 없으므로 자료형을 지정해야 함.
    let i32_bag = BagOfHolding::<i32> { item: None };

    if i32_bag.item.is_none() {
        println!("가방에 아무 것도 없다!")
    } else {
        println!("가방에 뭔가 있다!")
    }

    let i32_bag = BagOfHolding::<i32> { item: Some(42) };

    if i32_bag.item.is_some() {
        println!("가방에 뭔가 있다!")
    } else {
        println!("가방에 아무 것도 없다!")
    }

    // match는 Option을 우아하게 분해하고, 모든 케이스를 처리하도록 해준다!
    match i32_bag.item {
        Some(v) => println!("가방에서 {}를 찾았다!", v),
        None => println!("아무 것도 찾지 못했다"),
    }
}

오류 처리

Result

  • Rust에는 실패할 가능성이 있는 값을 리턴할 수 있도록 해주는 Result라 불리는 내장된 generic enum가 있다.
enum Result<T, E> {
	Ok(T),
	Err(E),
}

언제나 Ok와 Err로 Result을 인스턴스화(생성)할 수 있다.

fn do_something_that_might_fail(i: i32) -> Result<f32, String> {
    if i == 42 {
        Ok(13.0)
    } else {
        Err(String::from("맞는 숫자가 아닙니다"))
    }
}

fn main() {
    let result = do_something_that_might_fail(12);

    // match는 Result를 우아하게 분해하고, 모든 케이스를 처리하도록 해준다!
    match result {
        Ok(v) => println!("{} 발견", v),
        Err(e) => println!("오류: {}", e),
    }
}

main문 Return

  • main은 Result<T, E>{Ok(T), Err(E)}을 리턴할 수 있다.
fn main() -> Result <(), String> {
    // 모든 일이 잘 끝났음을 표현하기 위해.
    // Result Ok 안에 unit 값을 쓰고 있는걸 잘 봐두십시오.
	Ok(())
}
profile
치킨강정

0개의 댓글