ちゃなべの備忘録

ほぼ備忘録です。

Rustを触ってみた①【備忘録】

はじめに

いままでRustを触ったことがなかったので、触ってみる。

環境構築から含めてやってみよう。

公式の紹介を読んでみる

これを読んでいきながら、Rustの良さを最初に知ろうかな。

www.rust-lang.org

Rustのいいところ①:パフォーマンス

ガベージコレクタがないらしくて、これがパフォーマンス重視に繋がるらしい?なんでだろ、だって自動で不要なメモリを削除してくれるのがガベージコレクタでしょ?それがどうやってパフォーマンスに繋がるんだろうか。

qiita.com

  • GC(ガベージコレクタ)は実行時のオーバーヘッドが存在する
  • Rustはスコープを出た瞬間に変数に入った値もオブジェクトも破棄される
  • 値の所有者になれる変数は一時期に一つだけ
    • 変数から変数に代入したり、関数に代入したりして引き渡すとその所有者も引き渡され、元の変数は参照できなくなる

所有権という考え方がおもろいっすね。

Rustのいいところ②:信頼性

所有権モデルによってメモリとスレッドの安全性が保証されるって。

あと型いいね、型があるだけで嬉しい。

Rustのいいところ③:生産性

  • ドキュメントが豊富
  • 有用なエラーメッセージ付きのコンパイラ
  • パッケージマネージャ
  • ビルドツール
  • 公式リンター
  • 公式フォーマッター

すげぇーーー

The book

intro

  • 低レイヤーの操作(メモリ操作など)もできるし、コンパイラで事前にバグを回避できる。
  • パッケージマネージャーとビルドツールを併せ持つ Cargo ちゅうもんがあるらしい。
  • Rustfmt というフォーマッターもあるって。
  • rust-analyzer ちゅうもんが、IDEを強力にサポートするって
  • 低レイヤーから操作などをするアプリから、Webアプリまでいろんなものに使えるらしい
  • Rustはスピードと安定性を求める人のための言語
    • スピードは実行速度とコーディングの速度の両方
    • コンパイラやコードチェッカーによって安定性も実現される
  • (各章でやる内容が全部面白そう)

1. getting started

1.1 install

rustup というツールを使ってinstallするらしい。

dockerでlinuxの環境を立ち上げて、以下のコマンドでRustのinstallを完了させる。

# install rust
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
.
.
.
1) Proceed with standard installation (default - just press enter)
2) Customize installation
3) Cancel installation
>1
.
.
.
  stable-aarch64-unknown-linux-gnu installed - rustc 1.79.0 (129f3b996 2024-06-10)


Rust is installed now. Great!

To get started you may need to restart your current shell.
This would reload your PATH environment variable to include
Cargo's bin directory ($HOME/.cargo/bin).

To configure your current shell, you need to source
the corresponding env file under $HOME/.cargo.

This is usually done by running one of the following (note the leading DOT):
. "$HOME/.cargo/env"            # For sh/bash/zsh/ash/dash/pdksh
source "$HOME/.cargo/env.fish"  # For fish

# confirm to surccess installing rust
$ rustc --version
rustc 1.79.0 (129f3b996 2024-06-10)

rustupを使ってrustのバージョンを変えたい時は

$ rustup update

と打てば大丈夫。

1.2. Hello world!

フォルダ作って、それぞれでプロジェクトをやるみたい、今回の章だと以下

$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world

拡張子は .rs で、ファイル名はsnake_caseらしい。

以下のファイルを作る。

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

そして実行

$ rustc main.rs
error: linker `cc` not found
  |
  = note: No such file or directory (os error 2)

error: aborting due to 1 previous error

あれ、エラーがでたっちゃ。どうやら gcc を入れてないことが問題らしい。

$ apt-get install gcc

もっかい。

$ rustc main.rs

$ ./main
Hello, world!

おおおーーバイナリファイルが作成されて、出た。

main.rsの解説を見る。

  • main関数は特別らしく、rustでは常に最初に実行されるらしい。
  • Rustのスタイルは4つのスペースでインデントします。
  • println!という書き方はrustのマクロを実行するらしい
    • !をつけるとrustのマクロを呼び出すと知っておいてほしい。
  • あと式はセミコロンで終わります。
  • 生成されるバイナリファイルはrustがinstallされてなくても動作します。
    • これはコンパイルするコマンドがいるというデメリットに対する恩恵です。

あとこの段階でVSCoderust-analyzerという拡張機能は入れたよ。

1.3. Hello, Cargo!

Cargoとはビルドシステムであり、パッケージマネージャです。

実はinstallはrustupでinstallされていたらしい。

$ cargo --version
cargo 1.79.0 (ffa9cf99a 2024-06-03)

cargoを使えば新しいプロジェクトを簡単に始められる。

$ cargo new hello_cargo
    Creating binary (application) `hello_cargo` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

$ ls hello_cargo/
Cargo.toml  src

今回はgitレポジトリを作っていないが、それはgitプロジェクトの中にあるかららしい。もしなかったら作成される。

Cargo.tomlというファイルができているが、これはcargoの設定ファイルです。

[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"

[dependencies]
  • [package] の次に書かれている最初の3行はプロジェクトをコンパイルするために必要な情報です。
  • [dependencies]はプロジェクトの依存関係を列挙するらしい。
  • Rustではパッケージのことを crates (クレート) と呼ぶらしい

rustcではなくcargoでbuildしてみましょう。

$ cargo build
   Compiling hello_cargo v0.1.0 (/usr/src/projects/hello_cargo)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.36s

するとrustcとは打って変わって、22個もの新規ファイルができます。

  • Cargo.logk: プロジェクトの依存関係のバージョンを固定する
  • defaultではdebugモードでのbuildを行う
  • buildして実行をする時は、cargo runというコマンドをする
  • 前回のbuildと変更がない場合はbuildしない
    • コンパイルする必要があるかどうかをcheckするコマンドがcargo checkです
    • cargo checkはbuildに比べてはるかに高速なので、それだけを確認したい場合はこちらを使いましょう。
  • releaseするためのbuildは cargo build --releaseです。これは実行速度は速いですが、buildに時間がかかります。

2. Programming a Guessing game

指定された数字を当てるゲームを実装します。実装を通じて基本的な概念を学びましょう。

$ cargo new guessing_game
$ cd guessing_game

んで、ここら辺になってきたくらいで target内のファイルがウザくなってきたので、.gitignoreファイルを用意した。

# Generated by Cargo
# will have compiled files and executables
debug/
target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

あとrustfmtとかがいつまで経っても効かないことに気づいたので、devcontainerで起動した。あれ、devcontainer ってこんなに直感的に簡単に起動するっけ...?

{
  "dockerComposeFile": ["../compose.yaml"],
  "service": "rust",
  "workspaceFolder": "/usr/src",
  "customizations": {
    "vscode": {
      "extensions": [
        "rust-lang.rust-analyzer"
      ]
    }
  }
}

それでも適用されないと思ってたら、どうやらtomlの位置をvscodeに指定してあげないといけないらしい。↓をしたらできた。

RustのCargo.tomlの場所がrootに無い時にVSCodeでrust-analyzerのエラーを回避する方法

そしてコードを書いた。

use std::io;

fn main() {
    println!("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は標準ライブラリに定義されたアイテムのセットを持つ
    • このセットのことを prelude (プレリュード)と呼ぶ。
  • 使いたい型がpreludeにない場合はuse文で明示的に宣言する。
    • しかし今回の場合は、useで呼ばなくても、std::io::stdin()と呼び出すこともできる。
  • Rustは基本的に変数はimmutable(不変的)です。もしmutableにしたい場合はmutと宣言しましょう。
  • Stringは文字列型を表し、newで新しいインスタンスを返します。
  • 間の::構文はnewがString型の関連関数であることを示します。
    • 関連関数とはある型に対して実装される関数のこと
  • 関数の引数にポインタを代入しているが、基本的にポインタもimmutableである。
    • 不変な変数を参照させるためには、&guessではなく&mut guessと書く必要がある。
  • read_line()はRResultというenumを返す。
    • enumのvariantはOkErr
    • Okの場合expectはOkが保持している戻り値を取り出し、それを返す。
    • Errの場合expectはクラッシュさせ、引数で受け取った値を返す。

これで実行させるとこんな感じ

$ cargo run
   Compiling guessing_game v0.1.0 (/usr/src/projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.11s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
3
You guessed: 3

ランダムな値を取得する関数が必要だが、Rustの標準ライブラリには存在しない。ここでcrateを引っ張ってくる必要が出てきた。

[dependencies]
rand = "0.8.5"

これでbuildをすると、randはもちろん、randが依存しているcrateまでもコンパイルする。crateのバージョンをプロジェクト内で一致させたい場合は、Cargo.lockを共有するようにしましょう。新しいバージョンのcrateを使いたい場合はcargo updateで更新されます。

それではrandを使って、追記しましょう。

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

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

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

    println!("The secret number is: {secret_number}");
    
    println!("Please input your guess.");
    .
    .

rand::Rngと呼び出しているのになんでRng::とメソッドを実行しないんだ??と思いましたが、どうやら「クレート」という概念らしい、後ほど学ぶって。

あと1..=100という範囲の書き方がキモい、みたことなさすぎる、=ってなんやねん。

この後に比較のためのコードを書いたけど、armっていうものとPatternっていうものがあってそれが強力らしい。どうやらarmとは条件分岐した先の文のことらしいな

同じ変数名を再宣言できるらしい、型変換の時によく使うって。

Stringにあるtrim()メソッドは前後の空白や改行を消してくれるやつで、intへの型変換の際によくやる。

rustの静的型付けは、シナリオ的な型推察をすることもできるよ。

parseはエラー起こることがとてもよくしばしば起こるので、Resultを返すよ、expectで条件分岐しとこうね。しかもどうやらparseは左で定義した型を見てどういう型でparseすればいいか推察する。キモいのとすごいのと。

use rand::Rng;
use std::cmp::Ordering;
use std::io;

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

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

    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!"),
    }
}

これで、1回だけ推察できるアプリはできた。じゃあ次は推察を何回でもできるアプリをやってみよう。その時はloop()を入れて、breakを入れてあげればいいよ。

そして、別の記述で、こんな書き方ができる。これはstringを受け取っちゃった時に、loopをやり直すような処理。lambda関数みたいなインスタンスでこんなの定義できるんだ、いいね。

        // let guess: u32 = guess.trim().parse().expect("Please type a number!");
        // ↓

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

最終的にこんな感じのコードに!!お疲れ様でした。

use rand::Rng;
use std::cmp::Ordering;
use std::io;

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

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

    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;
            }
        }
    }
}