yikegaya’s blog

仕事関連(Webエンジニア)と資産運用について書いてます

写経しながら実践Rustプログラミング入門を読んだ(環境構築〜基本文法)

Rustが気になるので写経しながら「実践Rustプログラミング入門」を読んだ。

www.shuwasystem.co.jp

写経したコードと解説の切り抜きをメモがてら書いてみる。

環境構築

以下のサイトを開いてインストールコマンド実行

www.rust-lang.org

1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
>1

カスタムインストールの確認があるけどとりあえずはデフォルトの”1”でOK。

インストール後以下のコマンド実行

source $HOME/.cargo/env

これでcargoというツールを使ってRustのプロジェクトを作成できるようになる

# プロジェクト作成
cargo new hello_world
# 実行
cd hello_world
cargo run

開発環境の構築

rlsのインストール

Rust Language Server(rls)をインストールする。

goto definitionや型情報の表示、コード補完を提供してくれる

rustup component add rls rust-analysis rust-src

その後エディタがVSCodeの場合Rustの拡張ツールをインストールする。

cargo-editのインストール

であるcargo-editはクレート(Rustのライブラリをクレートと呼ぶ)を追加する時に便利なツール。

cargo install cargo-edit

cargoのサブコマンドとしてadd run rmが使えるようになる。yarnとかnpmでライブラリ管理するのと同じ感覚っぽい。

基本的な型

Rustにはコアライブラリと標準ライブラリがある。最も標準的な機能が含まれているのがコアライブラリで標準ライブラリはコアライブラリに次いで大切なライブラリ

文字列型

コアライブラリで定義されている文字列型はstrのみ。標準ライブラリではStringという型も定義されている。 strはメモリ上の文字列のスタート地点と長さを示すものなので文字列そのものの変更はできない。

Stringは文字列データの変更や長さの変更が可能。なのでStringを使うケースの方が多い。

Stringをstrに変換するときはポインタと文字列長をコピーしてスライスを作るのでメモリを圧迫しない。 しかしstrをStringに変換するときはメモリの確保が行われる。

# String <=> strのコピー
let s1: String = String::from("Hello World");
let s2: &str = &s1;
let s3: String = s2.to_string();

タプル型

異なる型を収めることができる集合

let mut t = (1, "2");
t.0 = 2;
t.1 = "3";

配列

特定の型の値を連続に収めた集合。配列のサイズはコンパイル時に決まっている必要あり。

let mut a: [i32; 3] = [0, 1, 2];
let b: [i32; 3] = [0; 3];
a[1] = b[1];
a[2] - b[2];
println!("{:?}", &a[1..3]);

ユーザ定義型

構造体と列挙型がユーザ定義できる型となる。

構造体はstructを用いて定義することができる。

struct Person {
    name: String,
    age: u32,
}
let p = Person {
    name: String::from("John"),
    age: 8,
};

列挙型はenumを用いて定義することができる。

enum Event {
    Quit,
    KeyDown(u8),
    MouseDown { x: i32, y: i32 }
}
let e1 = Event::Quit;
let e2 = Event::MouseDown { x: 10, y: 10 };

Option

Optionはデータが存在する場合と存在しない場合を表現できる列挙型。

pub enum Option<T> {
    None,
    Some(T),
}

Result

処理の結果が成功がエラーかを表現できる列挙型

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

matchやif letを使ってパターンマッチングして使う

let Result: Result<i32, String> = Ok(200);

match Result {
    Ok(code) => println!("code: {}", code),
    Err(err) => println("Err: {}", err)
}

if let Ok(code) = result {
    println!("code: {}", code)
}

matchやif letはコードのネストが深くなったり重厚な印象を与えるため回避するための書き方が用意されている。

let result: Result<i32, String> = Ok(200);
println!("code: {}", result.unwrap_or(-1));
let result: Result<i32, String> = Err("error".to_string());
println!("code: {}", result.unwrap_or(-1));

and_thenはOKだった場合だけ指定した関数を実行できる

fn func(code: i32) -> Result<i32, String> {
    println!("code: {}", code);
    Ok(100)
}

let next_result = result.and_then(func);
let result: Result<i32, String> = Err("error".to_string());
let next_result = result.and_then(func);

?演算子はOkだった場合に値を展開、Errだった場合はそのErrをそのままreturnする

fn error_handling(result: Result<i32>, String) -> Result<i32, String> {
    let code = result?;
    println!("code: {}", code);
    Ok(100)
}

Vec

配列とは違い、内部に収められる要素の数を増減させることができる。

let v1 = vec![1, 2, 3, 4, 5];
let v2 = vec![0; 5];

for文で繰り返し処理ができる。

let v = vec![1, 2, 3, 4, 5];
for element in &v {
    println!("{}", element)
}

Box

Rustの値は多くの場合メモリのスタック領域に確保される。スタック上のデータはコンパイル時にサイズがわかっており固定サイズでなくてはいけない。

Boxを使うと値はヒープ領域に確保される。ヒープ領域に確保する値はコンパイル時にサイズがわかってなくても問題ない。 Boxを用いて次のようなことができる

  • コンパイル時にサイズがわからない型を格納すること
  • 大きなサイズの型を渡す際にデータの中身をコピーせずポインタで渡すこと
  • 共通のトレイトを実装した様々な型を画一的にポインタで扱うこと
let byte_array = [b'h', b'e', b'1', b'1', b'o'];
print(Box::new(byte_array));
let byte_array = [b'w', b'o', b'r', b'1', b'd', b'!'];
print(Box::new(byte_array));

fn print(s: Box<u8>) {
    println!("{:?}", s);
}

printに渡すときもポインタで渡しているのでどのようなサイズでも渡すことができる。 バイト列が長いものだったとしてもポインタで渡すため値のコピーを行わない。

変数宣言

let、mut

letは変更不可能、変更可能にしたい場合は変数宣言位mutをつける。

let immut_val = 10;
let mut mut_val = 20;

mut_val += immut_val;

定数

定数はconstとstaticで宣言できる。 constは常に変更不可。staticは変更可能にできる。

制御構文

if文

他の言語同様if文が使える。ただしRustにおけるif文は式なので評価した値を変数に束縛することや関数の引数にすることが可能。

let number = 1;
let result = if 0 <= number {
    number
} else {
    -number
};

ループ

loop、for、whileという2種類のループが存在する。

# loop
let mut count = 0;
let result = loop {
    println!("{}", count);
    count += 1;
    if count == 10 {
        break count;
    }
};

# while
let mut count = 0;
while count < 10 {
    println!("count: {}", count);
    count += 1;
}

# for
let count: i32;
for count in 0..10 {
    println!("count: {}", count);
}

let array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for element in &array {
    println!("element: {}", element)
}

loop、for、whileにはラベルをつけることができ、breakするときにそのラベルを指定して繰り返しを抜けることができる。

'main: loop {
    println!("main loop start");
    'sub: loop {
        println!("sub loop start");

        break 'main;

    println!("sub loop end");
    }
    println!("main loop end");
}

match

C言語のswitch文に似たmatch文が存在する。ただしmatchはswitchよりも強力な「パターン」による分岐ができる。 Rustにおける「パターン」という言葉は型の構造に一致しているか確認するための記法を指す。パターンは、数字や文字列のような単純な型だけでなく列挙型、タプル、構造体などの比較をしたり値の範囲やワイルドカードを使って広範囲に比較させることもできる。

let i: i32 = 1;
match i {
    1 => println!("1"),
    2 => println!("2"),
    3 => println!("3"),
    _ => println!("misc"), // アンダースコアはあらゆる値にマッチする 
}

enum Color {
    Red,
    Blue,
    Green,
}

let c = Color::Red;
match c {
    Color::Red => println!("Red"),
    Color::Blue => println!("Blue"),
    // Color::Green => println!("Greeen")
    // Greenをコメントアウトするとmatchが列挙の要素を全て網羅していないことになりエラーになる
}

Range

forループで特定の範囲の数値を指定するときにはRange型を使う。

for member in 1..5 {
    println("{}", number)
}

Iterator

自作した型にIteratorトレイトを追加すればforループで繰り返し処理ができて便利

fn main() {
    let it = Iter {
        current: 0,
        max: 10,
    };
    for num in it {
        println!("{}", num)
    }
}

struct Iter {
    current: usize,
    max: usize
}

impl Iterator for Iter {
    type Item = usize;

    fn next(&mut self) -> Option<usize> {
        self.current += 1;
        if self.current -1 < self.max {
            Some(self.current - 1)
        } else {
            None
        }
    }
}

関数

fnキーワードを使って関数を定義してimplキーワードを使って構造体にメソッドを紐付けることができる。 Goと同じ仕組みっぽい。

C言語Pythonのようにreturnによる記述をしなくても関数の最後にセミコロンなしで記述された値を戻り値として扱うルールがある。 処理の途中で戻り値を返したい場合はreturnを使って記述する。

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

impl Person {
    fn say_name(&self) {
        println!("I am {}", self.name);
    }

    fn say_age(&self) {
        println!("I am {} year(s) old.", self.age);
    }
}

fn main() {
    let p = Person {
        name: String::from("Taro"),
        age: 20,
    };

    p.say_name();

    p.say_age();
}

メソッドの戻り値に自分自身の型を指定することでメソッドチェーンを使うことができる。

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

impl Person {
    fn say_name(&self) -> &Self {
        println!("I am {}", self.name);
        self
    }

    fn say_age(&self) -> &Self {
        println!("I am {} year(s) old.", self.age);
        self
    }
}

fn main() {
    let p = Person {
        name: String::from("Taro"),
        age: 20,
    };

    p.say_name().say_age();
}

第一引数にselfを使わなかった場合、それは関連関数になる。関連関数はインスタンスからメソッドを呼ぶのではなく型から関数を呼ぶ形式で定義される関数のこと。

以下のnewが関連関数

impl Person {
    fn say_name(&self) -> &Self {
        println!("I am {}", self.name);
        self
    }

    fn say_age(&self) -> &Self {
        println!("I am {} year(s) old.", self.age);
        self
    }

    fn new(name: &str, age: u32) -> Person {
        Person {
            name: String::from(name),
            age: age
        }
    }
}

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

fn main() {
    let p = Person::new("Taro", 20);

    p.say_name();
}

マクロ

マクロ呼び出しには!がつき関数呼び出しと見た目で区別できるようになっている。 Rustの構文と見た目が違うような括弧が慣例的に使われる。例えば関数呼び出し的なマクロなら()、コードブロックを引数に取るマクロなら{}など。

format!、concat!は文字列操作のマクロ。println!やwrite!、dbg!はデータ出力用のマクロ。

use std::io::Write;

fn main() {
    // データ出力マクロ
    print!("hello");
    print!("hello {}", "world");
    eprint!("hello {}", "error");
    eprint!("hello");

    let mut w = Vec::new();
    write!(&mut w, "{}", "ABC");
    writeln!(&mut w, " is 123");
    dbg!(w);

    // 異常終了用のマクロ
    panic!("it will panic");

    // ベクタ初期化マクロ
    let v = vec![1, 2, 3];

    // プログラム外のリソースにアクセスするマクロ
    // file!マクロが呼び出されたファイル名を取得
    println!("defined in file: {}", file!());
    // line!マクロが呼び出された行を出力するマクロ
    println!("defined on line: {}", line!());
    // コンパイラから該当フラグが渡されていればtrue、そうでなければfalse
    println!("is test: {}", cfg!(unix));
    // コンパイル時の環境変数を取得
    println!("CARGO HERE: {}", env!("CARGO_HERE")); 

    // アサーション用のマクロ
    assert!(true);
    assert_eq!(1, 1);
    assert_ne!(1, 0);
    debug_assert!(false);
    debug_assert_eq!(1, 1);
    debug_assert_ne!(1, 0);
}

実装補助用のマクロ

unimplemented!、todo!、unreachable!。いずれも実装が行われていない部分があるソースコードの型検査を通すためのマクロ。

enum Emotion {
    Anger,
    Happy,
}

trait Emotional {
    fn get_happy(&mut self) -> String;
    fn get_anger(%mut self) -> String;
    fn tell_state(&self) -> String;
}

struct HappyPerson {
    name: String,
    state: Emotion,
}

impl Emotional for HappyPerson {
    fn get_anger(&mut self) -> String {
        unimplemented!()
    }

    fn get_happy(&mut self) -> String {
        format!("{} is always happy.", self.name)
    }

    fn tell_state(&self) -> String {
        todo!()
    }
}

fn main () {
    let mut p = HappyPerson {
        name: "Takahashi".to_string(),
        state: Emotion::Happy
    };
    println!("{}", p.get_happy())
}

トレイトの導出

標準ライブラリのいくつかのトレイトは自作の型に対する標準的な実装を自動的に導出することができる。

#[derive(Eq, PartialEq)]
struct A(i32);

#[derive(PartialEq, PartialOrd)]
struct B(f32);

#[derive(Copy, Clone)]
struct C;

#[derive(Clone)]
struct D;

#[derive(Debug)]
struct E;

#[derive(Default)]
struct F;

fn main() {
    println!("{:?}", A(0) == A(1));

    println!("{:?}", B(1.0) > B(0.0));

    let c0 = C;
    let c1 = c0;
    let c2 = c0;

    let d0 = D;
    let _d1 = d0.clone();

    println!("{:?}", E);

    let _f = F::default();
}

長くなってきたので一旦区切る。

基本文法は他の言語に比べてそこまで異質なものはなさそうな印象。