Open7

Rust入門

ぱんだぱんだ

入門していく。

何からやったらいいのかわからなかったが公式のラーニングプログラム的なやつの非公式日本語翻訳があったのでやってみる。the bookと呼ばれてるやつ

https://doc.rust-jp.rs/book-ja/

ぱんだぱんだ

1. ことはじめ

インストール

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

rustupインストール。rustupはRustのルールチェーン管理ツール。Rustのバージョン管理だけでなくrustfmtやcargo, clippyなどもついてくる。

rustfmtはフォーマット、clippyは静的解析、cargoはパッケージマネージャー的なやつ。

vscodeで開発する場合は以下の拡張を入れておけば大丈夫。

https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer

rust-analyzerはCargo.tomlとかの配置で動かないときとかがあるかもしれない。

ちなみに、Rustプロジェクトの初期化は以下のコマンドでいける。

cargo init
.
├── Cargo.lock
├── Cargo.toml
├── README.md
└── src
    └── main.rs

以下はHell Worldのプログラム。

fn main() {
    println!("Hello, world!");
}

Rustにおける関数定義はfnで宣言する。println!はマクロの呼び出しだと思うけど詳しくは飛ばす。Rustにおけるmain 関数はエントリーポインントとなっているので実行可能なRustプログラムにおいては必須。

Rustの実行に関しては以下。

rustc src/main.rs
./main

これはコンパイル言語の一般的な流れでファイルを指定してコードを実行可能バイナリを出力して実行する。しかし、通常Cargoを使ってビルド、実行する。

// ビルド
cargo build

// 実行
cargo run

cargo buildtargetディレクトリ配下にコンパイルされたバイナリなどが配置される。cargo buildのビルドは2種類あって、通常ビルドはtarget/debug配下に実行バイナリが配置されるがcargo build --releaseを使うとtarget/release配下に実行バイナリが配置される。

本番環境には--releaseオプションを指定する。ビルド時間は長くなるが本番環境用に最適化されている。

実行に関してはcargo runを使うことでコンパイルと実行がいっぺんにできる。

ぱんだぱんだ

workspace

Rustでworkspaceを使用して複数のRustプロジェクトを管理したい場合は以下のようにルートのCargo.tomlを作成すればいい。

Cargo.toml
[workspace]
members = ["hello"]
resolver = "3"

実行は以下のようにpacakgeを指定して実行もできるし、すべてのpackageを対象にビルドするみたいなこともできる。

cargo run -p hello
cargo build
ぱんだぱんだ

2. 数あてゲーム

入力値を受付処理する

とりあえず、以下のようなコードで標準入力を受付、その結果を処理することができる。

use std::io;

fn main() {
    print!("Guess the number!");
    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", guess);
}
  • Rustの変数はすべてimmutableで不変
  • 可変な変数として扱いたい場合はmutキーワードをつける
  • String::new()はRustの標準モジュール(prelude)のString型の関連関数の呼び出し
  • 関連関数とはいわゆるstatic(静的)な関数のこと
  • 具体的には&selfを使わずに定義された関数
  • let mut guess = String::new()で可変の文字列型を生成して変数に束縛している
  • ioモジュールはString同様標準ライブラリだが、preludeに含まれていないのでインポート文が必要
  • Rustではuse std::ioと書く
  • インポート文なしにstd:io::stdin()としても実行できる
  • read_lineは参照値を受け取るようになっているので&mut guessのようにして参照渡しをする必要がある
  • read_lineはResult型を返す
  • expect()はResult型に定義された関数で、ResultがErrの場合、panicする
  • println! マクロは{}で任意の値を埋め込むことができる
  • いまさらだがRustは小文字のスネークケースで書くのが一般的なようなので変数もディレクトリ名も関数名も小文字のスネークケースにする
  • ちなみに、Rustでは処理していないResult型があると、コンパイルエラーになる

乱数を生成する

use rand::Rng;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..101);

    println!("The secret number is: {}", secret_number); //秘密の数字は次の通り: {}

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", guess);
}

  • Rustで乱数を生成する機能は標準ライブラリにはないが、randというクレートとして公開されている
  • Rustにおけるクレートはプログラムの集まりでモジュールやライブラリのようなものと理解しておけばよさそう
  • で、公開されているクレートを依存関係に追加するのに手動でCargo.tomlに記載してもいいがcargo add randを実行してもよい
  • 追加したクレートのバージョンを上げる場合、cargo updateを使うと良い
  • ただ、cargo updateはマイナーバージョンを最新にしてくれますがメジャーバージョンは上げてくれない
  • メジャーバージョンを上げたい場合は手動でCargo.tomlを修正する必要がある
  • ちなみにクレートはバイナリクレートライブラリクレートがありる
  • バイナリクレートは実行可能なクレートで、ライブラリクレートはそれ単体では実行できずにプログラムに組み込んで使う
  • cargo updateではCargo.lockファイルは更新されるが、Cargo.tomlの方は更新されない
  • クレートに関してはCrates.ioを参照すればだいたいわかる
  • randクレートのthread_rng()で乱数生成器を取得し、gen_range()で指定の範囲の乱数を取得する
  • また、gen_range関数はRngtrait内で定義されているため、use rand:Rngのようにtraitを読み込む必要がある

入力値と乱数の比較

use rand::Rng;
use std::{cmp::Ordering, io};

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..101);

    println!("The secret number is: {}", secret_number); //秘密の数字は次の通り: {}

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!"); //数値を入力してください!

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}
  • まず入力値はStringで乱数はu32のため型が違うため値の比較ができない
  • let guess: u32 = guess.trim().parse().expect("Please type a number!");の部分でtrimしたあとにパースしている
  • パースの結果はResultのためexpectもつける必要がある
  • ちなみに、guessという変数名が重複しているがRustではシャドーイングと言って前の変数名を再利用ができる
  • これは型変換のときによく使われる
  • ageStrとかageNumとかほんとやりたくないからこの機能いいな
  • 比較にはcmp()を使う
  • cmp()Ordトレイトが実装してあれば使うことができ、u32は実装済みのため使うことができる
  • cmpstd::cmp::Orderingenumを返し、これをパターンマッチで扱うためuse std::cmp::Orderingで読み込む必要がある

入力をループさせる

これで数あてゲームは完成

use rand::Rng;
use std::{cmp::Ordering, io};

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..101);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

  • Rustは普通にfor文もあるけど無限ループをするにはloopが使える
  • また、入力値のパースでパースできなかったときにpanicさせていた箇所をパターンマッチでハンドリングしてErrのときはcontinueしてpanicさせないようにした

終わり!

ぱんだぱんだ

3. 一般的なプログラミングの概念

  • 定数はconstキーワードで宣言できる
  • Rustには以下の4種類のスカラー型が存在する
    • 整数型
    • 浮動小数点形
    • 論理値型
    • 文字型
  • Rustにはスカラー型とは別に複数の型を一度に表現できる複合型がある
    • タプル型
      • タプル型は異なる型を一度に扱える
      • タプルのそれぞれの要素にアクセスしたい場合は
      • let (x, y, z) = tuppleみたいにパターンマッチで取り出せる
    • 配列型
      • Rustの配列は固定長で長さは変えられない
      • 長さを変えたい場合は標準ライブラリのベクタ型を使う
      • 配列はヒープよりもスタックにメモリを確保したいときに有効

関数について

関数本体はを含むため、文と式について理解しておくことは重要。文は値を返さないでセミコロンがあるやつ。式は値を返し、セミコロンがないやつ。

何が式で何が文かはプログラミング言語で異なり、 Rustにおいては例えば、代入は文なので値を返さず、以下はコンパイルエラー。

fn main() {
    let x = (let y = 6);
}

CやRubyは代入で値を返すのでこのようなコードは有効となる。

Rustは式指向言語のため明示的なreturnなく最後の値が返り値になる。ただし、セミコロンをつけてしまうと文となり返り値がないためセミコロンをつけずに式とする必要がある。

fn add(x: i32, y: i32) -> i32 {
    // x + y; これだとコンパイルエラーになってしまう
    x + y
}

ブロックも式なので以下のような感じで書いたりもできる。

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {}", y);
}

制御構文

Rustのif式は以下

fn main() {
    let number = 6;

    if number % 4 == 0 {
        // 数値は4で割り切れます
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        // 数値は3で割り切れます
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        // 数値は2で割り切れます
        println!("number is divisible by 2");
    } else {
        // 数値は4、3、2で割り切れません
        println!("number is not divisible by 4, 3, or 2");
    }
}

Rustのifは文ではなく式なのでletの右辺に持っていったりもできる。

Rustのループはloopwhileforの3種類ある。

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {}", count);
        let mut remaining = 10;

        loop {
            println!("remaining = {}", remaining);
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {}", count);
}

これはloopの例でloopはbreakするまで無限ループする。loopがネストしているときはラベルをつけることで指定のラベルのbreakやcontinueを実行することができる。

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number -= 1;
    }

    // 発射!
    println!("LIFTOFF!!!");
}

whileはloopとifとbreakを使えば書けるけどwhileを使った方がより簡潔に書けることが多い。

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {}", element);
    }
}

配列のようなコレクションをループで回すときはfor-inを使うと良い。

ぱんだぱんだ

4. 所有権

Rustやるなら避けられない所有権。Rustではメモリ管理にGCを採用していないため所有権システムを使って開発者が管理する。開発者が管理するがコンパイラが所有権システムに通じて教えてくれるためコンパイルが通るように実装すればよい。よいのだが、おそらくそれが難しいんだろう

他の言語はともかくRustのようなシステムプログラミングではメモリに対しての理解を深めておいたほうが良いそうだ。メモリにはスタックヒープの2種類があり、スタックのほうが高速。

じゃあ、スタックを使うのがいいじゃないかと思うがちゃんとヒープを使わないといけない場面がある。それはスタック上のデータは既知のサイズである必要があるためコンパイル時にデータのサイズがわからなかったりサイズが可変のものはヒープに保存する必要がある。

そのため、Rustにおける配列はスタックに保存され可変長のベクタはヒープに保存される。

スタックのメモリの仕組みをAIに教えてもらったけどそんなに簡単でもなかったがスタックがヒープよりも高速なのはヒープのメモリ位置がバラバラになる一方でスタックは事前にスタック領域が決まっていて連続して配置されているためメモリキャッシュが効きやすいとかそういう理由らしい。

スタックのデータが有効かどうかはスタックポインタの位置で決まるそうで、例えば特定のブロック内にスタック領域のメモリ位置を使う変数があったとして、そのブロックを抜けたときに変数へのアクセスは不可能になるためスタッックポインタの位置が元に戻り、その変数のスタック領域は無効になり別の変数のデータで上書きされ同じメモリ位置が再利用されることなどがある。

Rustの文字列について。let str = "hello";みたいにするとこの型は&'static strという型になるがこれはいわゆる文字列リテラルで、メモリでいうとstatic領域という領域にデータ自体は保存され、その参照がスタック領域に積まれる。ということが型で表されている。

Rustにはもう一つString型というのがあるがこれは可変の文字列を扱うことができるためヒープ領域に保存される。

Rustの所有権システムはGCを使うことなくメモリを解放する必要があるが、スコープを抜けるときにdrop関数を呼ぶことでGCを使うことなく使用しなくなったメモリをOSに返却することができる。ただ、このメモリの解放はヒープについてのみ起こる。スタックは積まれていくので。

let x = 5;
let y = x;

このような場合、xもyもスタックに積まれるのでそれぞれ5という値を持つ。

let s1 = String::from("hello");
let s2 = s1;

このような場合、Stringはlen, capacityと実際の値のポインタを持つデータ型でありこれはスタックに積まれ、実際の値はヒープ領域に保存される。

s1をs2に代入するということは以下のようなこと。

実際の値まではコピーされない。s2に入るデータはスタック領域のデータ。これはプログラミングでよく言われるshallow copyとdeep copyの話でヒープ領域のデータまでもコピーするのがdeep copy。deep copyが非効率であることがメモリ位置まで考えるとよくわかる。

ヒープ領域のデータはスコープを抜けるときにdrop関数が呼ばれメモリが解放されると学んだが以下の場合どうなるだろうか。

{
  let s1 = String::from("hello");
  let s2 = s1;
}

s1もs2も同じヒープ領域のデータを参照しているため両方ともメモリの解放が行われてしまうと同じヒープ領域に対してメモリの解放が行われてしまう。つまり、メモリの2重解放であり、非常に問題がある。

Rustではこれが所有権システムにより起こらなくなっている。以下のようにs2にs1を代入したあとに、s1を使おうとするとエラーが起こる。

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

なぜなら、Rustではs2にs1が代入された時点でs1は有効ではないとコンパイラが判断するからであり、これはムーブと呼ばれる。こうすることでs1に関してはもはやメモリの解放が起こらないのでメモリの2重解放が起こらなくなる。

Rustにおいてヒープ領域のデータのコピーまで必要名場合、clone()を呼びだすことで実現可能。

そして、ムーブはスタック領域のデータでは起きない。なぜなら、スタック上の実際のデータをコピーしても高速だから。

これはCopyトレイトが関係していて、Copyトレイトを実装している型はムーブが起こらず古い変数を参照してもエラーにならない。Copyトレイトはスカラー型のようなメモリ確保が必要ないデータ型は実装済みなため、ムーブが起こらない。

また、関数呼び出しは変数代入と意味的には同じで、変数を関数に渡した時点でムーブは発生する。