Rust – 문법 정리

By | 2024년 10월 25일
Table of Contents

Rust – 문법 정리

아래의 설명들은 여기 에 설명된 방식으로 개발환경이 구축된 상황을 전제로 설명합니다.

변수

fn main() {
    let x: i32 = 10;
    let mut y: i32 = 20;
    y = y + x;
    println!("x: {x}");
    println!("y: {y}");
}

변수는 기본적으로 불변(immutable) 입니다.
변경 가능하게 하려면 mut 키워드를 붙여주어야 합니다.

변수 타입은 생략 가능합니다.
생략된 타입은 컴파일러가 자동으로 타입을 추론합니다.

타입 추론

fn takes_u32(x: u32) {
    println!("u32: {x}");
}

fn takes_i8(y: i8) {
    println!("i8: {y}");
}

fn main() {
    let x = 10;
    let y = 20;

    takes_u32(x);
    takes_i8(y);
}

변수의 타입은 생략할 수 있습니다.
컴파일러는 변수가 사용된 상태를 기반으로 변수의 타입을 자동추론해줍니다.

상수

const DIGEST_SIZE: usize = 3;
static BANNER: &str = "Welcome to Rust";

fn main() {
    let _size = DIGEST_SIZE;
    let _banner = BANNER;
}

const, static 모두 상수를 표현하지만 작동이 다릅니다.
const 는 함수에서 이용될 때 함수에 할당된 영역으로 값이 복사됩니다.
static 의 경우는 프로그램 전역에 단 하나의 값만이 존재하고, 모든 함수는 이 값을 이용합니다.

메모리 사용량 면에서는 static 이 유리하지만, 하나의 함수에서 문자열의 내용을 수정할 경우 모든 함수에서 영향을 받으므로 위험성도 늘어납니다.

일반적으로 const 를 사용하는 것이 권장됩니다.

변수 숨기기(Shadowing)

fn main() {
    let a = 10;
    println!("before: {a}");
    {
        let a = "hello";
        println!("inner scope: {a}");

        let a = true;
        println!("shadowed in inner scope: {a}");
    }

    println!("after: {a}");
}

범위 바깥 뿐만 아니라, 범위 내부의 변수도 숨길 수 있습니다.
동일 범위 내부에서의 Shadowing 은 새로운 변수를 생성하는 것과 같아서,
타입을 변경할 수도 있습니다.

열거형(enum)

use std::any::type_name;
use std::mem::{align_of, size_of};

#[derive(Debug)]
enum CoinFace {
    Front, Back
}

fn main() {
    println!("Coin face is {:?}", CoinFace::Front);
    println!("Coin face is {:?}", CoinFace::Back);

    println!("{}: size {} bytes, align: {} bytes",
        type_name::<CoinFace>(), size_of::<CoinFace>(), align_of::<CoinFace>());
}

enum 에 값을 할당하지 않는 경우 0, 1, 2, … 의 값이 할당됩니다.

enum 의 사이즈는 필요한 최소한의 사이즈를 가집니다.
(하지만 이 부분은 설명이 복잡해지므로 직접 확인하기 바랍니다.)

흐름 제어

블록

fn main() {
    let _x = {
        3 + 4
    };

    let _y = {
        let _ = 3 + 4;
    };
}

모든 블록은 리턴 값과 타입을 가집니다.
리턴하는 값이 없으면 () 이 리턴값이 됩니다.
리턴값은 세미콜론 ; 이 없습니다.

if

fn main() {
    let mut x = 10;
    if x % 2 == 0 {
        x = x / 2;
    } else {
        x = 3 * x + 1;
    }
}

else if 가 지원되지만 match 를 쓰는 것이 권장됩니다.

fn main() {
    let mut x = 10;
    x = if x % 2 == 0 {
        x / 2
    } else {
        3 * x + 1
    };
}

블록이 리턴값이 있듯이, if 역시 리턴값을 가집니다.
리턴 타입이 동일하지 않으면 컴파일러 오류가 발생합니다.

for

fn main() {
    let v = vec![10, 20, 30];

    for x in v {
        println!("x: {x}");
    }

    for i in (0..10).step_by(2) {
        println!("i: {i}");
    }
}

다른 언어와 마찬가지로 break 와 continue를 사용할 수 있습니다.

while

fn main() {
    let mut x = 10;
    while x != 1 {
        x = if x % 2 == 0 {
            x / 2
        } else {
            3 * x + 1
        };
    }
    println!("Final x: {x}");
}

break, continue

fn main() {
    let v = vec![10, 20, 30];
    let mut iter = v.into_iter();
    'outer: while let Some(x) = iter.next() {
        println!("x: {x}");
        let mut i = 0;
        while i < x {
            println!("x: {x}, i: {i}");
            i += 1;
            if i == 3 {
                break 'outer;
            }
        }
    }
}

중첩 루프에서 라벨을 이용해 빠져 나올 수 있습니다.
rust 는 goto 를 지원하지 않습니다.

loop

fn main() {
    let mut x = 10;
    loop {
        x = if x % 2 == 0 {
            x / 2
        } else {
            3 * x + 1
        };
        if x == 1 {
            break;
        }
    }
    println!("Final x: {x}");
}

특수한 흐름 제어

if let

fn main() {
    let arg = std::env::args().next();
    if let Some(value) = arg {
        println!("Program name: {value}");
    } else {
        println!("Missing name?");
    }
}

while let

fn main() {
    let v = vec![10, 20, 30];
    let mut iter = v.into_iter();

    while let Some(x) = iter.next() {
        println!("x: {x}");
    }
}

match

fn main() {
    match std::env::args().next().as_deref() {
        Some("cat") => println!("Will do cat things"),
        Some("ls")  => println!("Will ls some files"),
        Some("mv")  => println!("Let's move some files"),
        Some("rm")  => println!("Uh, dangerous!"),
        None        => println!("Hmm, no program name?"),
        _           => println!("Unknown program name!"),
    }
}

역시 블록이므로 리턴값을 가질 수 있지만 모든 리턴값의 타입은 동일해야 합니다.

패턴 매칭

fn main() {
    let input = 'x';

    match input {
        'q'                   => println!("Quitting"),
        'a' | 's' | 'w' | 'd' => println!("Moving around"),
        '0'..='9'             => println!("Number input"),
        _                     => println!("Something else"),
    }
}

_ 는 위의 매칭이 모두 실패했을 때 매칭됩니다.

열거형(enum) 매칭

enum Result {
    Ok(i32),
    Err(String),
}

fn divide_in_two(n: i32) -> Result {
    if n % 2 == 0 {
        Result::Ok(n / 2)
    } else {
        Result::Err(format!("cannot divide {n} into two equal parts"))
    }
}

fn main() {
    let n = 100;
    match divide_in_two(n) {
        Result::Ok(half) => println!("{n} divided in two is {half}"),
        Result::Err(msg) => println!("sorry, an error happened: {msg}"),
    }
}

구조체(struct) 매칭

struct Foo {
    x: (u32, u32),
    y: u32,
}

#[rustfmt::skip]
fn main() {
    let foo = Foo { x: (1, 2), y: 3 };
    match foo {
        Foo { x: (1, b), y } => println!("x = 1, b = {b}, y = {y}"),
        Foo { y: 2, x: i }   => println!("y = 2, x = {i:?}"),
        Foo { y, .. }        => println!("y = {y}, other fields were ignored"),
    }
}

배열(array) 매칭

#[rustfmt::skip]
fn main() {
    let triple = [0, -2, 3];
    println!("Tell me about {triple:?}");
    match triple {
        [0, y, z] => println!("First is 0, y = {y}, and z = {z}"),
        [1, ..]   => println!("First is 1 and the rest were ignored"),
        _         => println!("All elements were ignored"),
    }
}

[.., z] 같은 형식으로도 매칭할 수 있습니다.

조건문 매칭

#[rustfmt::skip]
fn main() {
    let pair = (2, -2);
    println!("Tell me about {pair:?}");
    match pair {
        (x, y) if x == y     => println!("These are twins"),
        (x, y) if x + y == 0 => println!("Antimatter, kaboom!"),
        (x, _) if x % 2 == 1 => println!("The first one is odd"),
        _                    => println!("No correlation..."),
    }
}

if 조건문을 이용해 매칭할 수 있습니다.

메모리 관리

소유권

struct Point(i32, i32);

fn main() {
    {
        let p = Point(3, 4);
        println!("x: {}", p.0);
    }
    println!("y: {}", p.1);
}

모든 변수는 범위를 가지고, 범위를 벗어나면 변수는 사라집니다.
범위 안에서 변수는 값을 소유(Ownership) 한다고 표현니다.

소유권 이동 : 변수 할당

fn main() {
    let s1: String = String::from("Hello!");
    println!("s1: {s1}");

    let s2: String = s1;
    // println!("s1: {s1}");   // compiler error
    println!("s2: {s2}");
}

값을 다른 변수에 할당하면 소유권을 상실합니다.
소유권은 단 하나의 변수만 가집니다.
소유권을 상실하면 값에 대한 모든 접근이 차단됩니다.

fn main() {
    let s1: String = String::from("Hello!");
    println!("s1: {s1}");

    let s2: String = s1.clone();
    println!("s1: {s1}");   // ok
    println!("s2: {s2}");
}

복사할때에는 명시적으로 clone을 사용합니다.

fn main() {
    let i1 = 10;
    println!("i: {i1}");

    let i2 = i1;
    println!("i1: {i1}");   // ok
    println!("i2: {i2}");
}

정수같은 간단한 값은 복사가 됩니다.

소유권 이동 : 함수 파라미터 할당

fn say_hello(name: String) {
    println!("Hello {name}")
}

fn main() {
    let name = String::from("Alice");
    say_hello(name);
    // say_hello(name);     // compiler error
}

함수 파라미터로 전달하는 것만으로도 역시 소유권이 이동합니다.

fn say_hello(name: &String) {
    println!("Hello {name}")
}

fn main() {
    let name = String::from("Alice");
    say_hello(&name);
    say_hello(&name);     // ok
}

참조로 전달하면 소유권 이동은 발생하지 않습니다.

복사(copy)와 복제(clone)

fn main() {
    let x = "string";
    let y = x;
    println!("x: {x}");
    println!("y: {y}");
}

객체가 아니라 문자열 참조가 할당되므로 소유권 이동이 없습니다.
정수와 같이 간단한 값으로 취급되기 때문입니다.

#[derive(Copy, Clone, Debug)]
struct Point(i32, i32);

fn main() {
    let p1 = Point(3, 4);
    let p2 = p1;
    println!("p1: {p1:?}");
    println!("p2: {p2:?}");
}

Copy 트레잇을 구현하여 복사를 할 수 있습니다:
명시적으로 p1.clone() 을 사용하여 데이터를 복사할 수 있습니다.

복사와 복제의 차이

복사(copy) 는 아무런 커스터마이징 없이 원본 값을 그대로 복사합니다.
복제(clone) 는 커스터마이징이 가능합니다.
String 은 값 자체가 아니라 문자열에 대한 주소값만 가지고 있기에 copy 를 구현하지 않습니다.
따라서 구조체의 맴버 중 하나가 String 을 포함하면 그 구조체도 copy 를 구현할 수 없습니다.

참조 제한

공유가능한 불변 과 유일한 가변 으로 표현합니다.

한번에 하나 이상의 &T (수정 불가한 참조) 값을 가지거나, 또는
정확히 하나의 &mut T (수정 가능한 참조) 값만을 가질 수 있습니다.

공유가능한 불변

하나의 변수에 대해 다수의 불변 변수는 가능합니다.

fn main() {
    let a: i32 = 10;
    let b: &i32 = &a;
    let c: &i32 = &a;

    println!("a: {a}");
    println!("b: {b}");
    println!("c: {c}");
}

유일한 가변

가변 참조는 단 하나만 가능합니다.

fn main() {
    let a: &mut i32 = &mut 10;
    let b: &mut i32 = a;
    let c: &mut i32 = a; // error[E0499]: cannot borrow `*a` as mutable more than once at a time

    *b = 20;
    *c = 30;

    // println!("a: {a}");
    println!("b: {b}");
    println!("c: {c}");
}

구조체(struct)

구조체 일반

struct Person {
    name: String,
    age: u8,
}

fn main() {
    let mut peter = Person {
        name: String::from("Peter"),
        age: 27,
    };
    println!("{} is {} years old", peter.name, peter.age);

    peter.age = 28;
    println!("{} is {} years old", peter.name, peter.age);

    let jackie = Person {
        name: String::from("Jackie"),
        ..peter
    };
    println!("{} is {} years old", jackie.name, jackie.age);
}

..peter 문법은 한 구조체에서 다른 구조체로 대부분의 값을 복사하려고 하는 경우에 하나하나 타이핑하는 수고를 덜어줍니다.
반드시 맨 마지막에 와야 합니다.

튜플 구조체

struct Point(i32, i32);

fn main() {
    let p = Point(17, 23);
    println!("({}, {})", p.0, p.1);
}

필드명 없이 구조체를 생성할 수도 있습니다.

구조체 메소드

#[derive(Debug)]
struct Person {
    name: String,
    age: u8,
}

impl Person {
    fn new(name: String, age: u8) -> Person {
        Person { name, age }
    }
    fn say_hello(&self) {
        println!("Hello, my name is {}", self.name);
    }
}

fn main() {
    let peter = Person::new(String::from("Peter"), 27);
    println!("{peter:?}");
    peter.say_hello();
}

impl 키워드를 이용해 메소드를 생성할 수 있습니다.

impl Person {
    fn new1(name: String, age: u8) -> Self {
        Self { name, age }
    }
}

PersonSelf 로 변경할 수 있습니다.

메소드의 첫번째 파라미터가 &self 인것을 확인하세요.

메서드 리시버(Receiver)

파라미터에 self 를 할당하는 것을 리시버라고 합니다.

fn say_hello(&self) { ... }
fn say_hello(&mut self) { ... }
fn say_hello(self) { ... }
fn say_hello(mut self) { ... }

&self : 불변 참조로 받습니다.
&mut self : 값을 수정할 수 있습니다.
self : 객체의 소유권을 가져옵니다. 다시 객체에 소유권을 반환하지 않으면 객체는 객체는 drop(해제)됩니다.
mut self : 객체의 소유권과 수정할 수 있는 권한까지 받습니다.

모듈

mod foo {
    pub fn do_something() {
        println!("In the foo module");
    }
}

mod bar {
    pub fn do_something() {
        println!("In the bar module");
    }
}

fn main() {
    foo::do_something();
    bar::do_something();
}

가시성

mod outer {
    fn private() {
        println!("outer::private");
    }

    pub fn public() {
        println!("outer::public");
        inner::public();
    }

    mod inner {
        fn private() {
            println!("outer::inner::private");
        }

        pub fn public() {
            println!("outer::inner::public");
            super::private();
            private();
        }
    }
}

fn main() {
    outer::public();
}

pub 를 제거하면 외부에서 접근이 불가능합니다.
pub 키워드는 모듈에도 사용할 수 있습니다.

파일 분리

파일을 분리한 경우 파일명이 모듈명이 됩니다.
파일명은 기본 라이브러리의 네임스페이스와 중복이 되지 않도록 해야 합니다.

mylib.rs

fn private() {
    println!("outer::private");
}

pub fn public() {
    println!("outer::public");
    inner::public();
}

mod inner {
    fn private() {
        println!("outer::inner::private");
    }

    pub fn public() {
        println!("outer::inner::public");
        super::private();
        private();
    }
}

main.rs

mod mylib;

fn main() {
    mylib::public();
}

제너릭(generic)

#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
    println!("{integer:?} and {float:?}");
}

제너릭 메서드

#[derive(Debug)]
struct Point<T>(T, T);

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.0  // + 10
    }

    // fn set_x(&mut self, x: T)
}

fn main() {
    let p = Point(5, 10);
    println!("p.x = {}", p.x());
}

단형화

제너릭은 실제 호출이 있는 시점에 실제로 생성됩니다.
미리 만들어 놓은 타입들을 사용하는 것이 아닙니다.

트레잇(Trait)

```

```rust

https://google.github.io/comprehensive-rust/ko/modules.html

구조체

조건문

반복구문

함수

제너릭(generic)

상속

답글 남기기