Open19

Rustを基本の基本から学ぶ

ねおさんねおさん

はじめに

Rust は低レベル制御と、人間へのやさしさのトレードオフの競合の解消に挑戦している

Rust 利点

  • コンパイラが門番の役割
     - 並行性のバグなどを見つける門番
  • 現代的な開発ツールをシステムプログラミング世界に導入
    • Cargo
      • 依存関係管理ツール兼ビルドツール
    • Rustfmt
      • 一貫したコーディングスタイルを保証
    • Rust 言語サーバー IDE と統合し、コード保管やインラインメッセージにも対応
ねおさんねおさん

事始め

Rustのインストール

rustupを使用してダウンロード
rustupはRustのバージョンと関連ツールの管理するコマンドラインツール

Rustは安定性を保証していて、
本の例でコンパイルできるものは新しいバージョンでもコンパイルできる。

出力は異なるかもしれない
頻繁にエラーメッセージと警告を改善しているため

助力を得られる場所は色々ある

  • RustのDiscordのbeginnersチャンネル
  • Zulip rust-lang-jpなど

rustup docでブラウザでドキュメンテーションを閲覧可能

rustup updateでrustup自体の更新

Hello, world!

main.rs
fn main(){
    println!("Hello, world!");
}
> rustc main.rs
> ./main
Hello, world!

main関数は常に全ての実行可能プログラムで走る最初のコード

rustfmtでコードを整形できる

rustfmt main.rs
ねおさんねおさん

今後、サンプルコードは少し改変して試すなどしてみる。

ねおさんねおさん

Hello, Cargo!

RustではCargoをしようしてプロジェクト管理

Cargoがインストールされていることを確認

cargo --version      
cargo 1.78.0-nightly (a4c63fe53 2024-03-06)

Cargoでプロジェクト作成

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

作成されたファイル構造確認

tree
.
├── Cargo.toml
└── src
    └── main.rs

基本的に同時に.gitignoreが作成されるが、
既存リポジトリ内でcargo newしたところで.gitignoreは作成されない

この挙動はオプションで制御可能

cargo new --vcs=git

svnはできないらしい

 cargo new test --vcs=svn
error: invalid value 'svn' for '--vcs <VCS>'
  [possible values: git, hg, pijul, fossil, none]

Cargo.tomlの内容を確認

[package]
name = "hello-cargo-test"
version = "0.1.0"
edition = "2021"

[dependencies]

[package]
パッケージの設定を記述

[dependencies]
プロジェクトの依存を列挙するためのセクションの始まり
Rustではパッケージのことをクレートという

editionについて

3年ごとに完全に更新されたドキュメントとツール群とともにパッケージにまとめている
後方互換性を失わせるリリースは別エディションにまとめる。
最新バージョンでもエディションが古ければ後方互換性がある
例)asyncを新たに予約語にするなど
エディションについては↓に記載
https://doc.rust-jp.rs/edition-guide/

cargo fixでソースを新しいエディションに更新できる

今の最新エディションは2021ということは、今年中に新しいエディションがリリースされる?

ねおさんねおさん

エディションガイド

横道それたついでにエディションガイドについての学習を進める

エディションの移行について

cargo fixを使うことでエディションの移行ができる

ただし完璧とは限らない

新しいプロジェクトを作成する

Cargoは新規プロジェクト作成時、自動で最新エディションをコンフィグに追加する
cargo +nighty new foo

エディションを移行する場合

  1. cargo fix --edition
  2. Cargo.tomlのeditionを新しいものに変更する
  3. cargo build cargo testなどで修正を検証
ねおさんねおさん

マクロの移行について

「エディションの衛星性」と呼ばれる仕組みがある。
マクロ内のトークンがどのエディションからかが記録されている。
ex)let dynを含むマクロがある2015エディションのクレートを2018エディションから呼び出すことはできる
ただし、上記の2015エディションのクレートを2018エディションに変更したときパース失敗する

イディオムリント

cargo fix --edition-idiomsで古い書き方を新しくできたりする

ねおさんねおさん

Rust2015

「安定性」がテーマ
Rust1.0から

Rust2018

エディションシステムが作られてた
「生産性」がテーマ

  • プロジェクトにクレートをインポートする際にextern crateが要らなくなるなどimport関連
  • トレイト関数の匿名パラメータの非推奨化
  • 新キーワード dyn async await try
    などなど

Rust2021

全てのエディションで配列がIntoIteratorを実装するようになる

などなど

ねおさんねおさん

Cargoプロジェクトをビルドし、実行

cargo build

これにより実行ファイルがtarget/debug/{プロジェクト名}に作成される

./target/debug/{プロジェクト名} # これで実行可能

build時、Cargo.lockが生成される。依存関係の正確なバージョンを記録
手動変更は不要

cargo runでコンパイルから実行の一連の流れが実施できる。
このとき、変更がなければコンパイルをスキップしてバイナリを実行する

cargo checkは、コードがコンパイルできるかをチェックするだけ

リリースビルド

cargo build --releaseを実行すると、最適化状態でビルドできる
target/releaseに実行ファイルを生成する。
コンパイルが長くなるが、実行が早くなる。
実行時間をベンチマークするならリリースビルド

ねおさんねおさん

数当てゲームプログラミング

新規プロジェクト作成
1から100までのランダムな整数を生成し、
ユーザの入力に対して大きいか小さいかを出力
一致している場合、ループ終了

cargo new guessing_game && cd guessing_game 
チュートリアルを見ずに実装
main.rs
use proconio::input;
use rand::{thread_rng, Rng};
use std::cmp::Ordering;

const RAND_MIN: usize = 1;
const RAND_MAX: usize = 100;
fn main() {
    let mut rng = thread_rng();
    let ans: usize = rng.gen_range(RAND_MIN..=RAND_MAX);

    loop {
        input! { i: usize };
        match ans.cmp(&i) {
            Ordering::Less => println!("{i}より小さいです"),
            Ordering::Greater => println!("{i}より大きいです"),
            Ordering::Equal => {
                println!("正解!おめでとう!");
                return;
            }
        }
    }
}
Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
proconio = "0.4.5"
rand = "0.8.5"

ねおさんねおさん

Result型について

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

Result型は汎用のResultとその他サブモジュール用のio::Resultなどがある
OkErrの列挙子をもつ列挙型
Okには正常な値 Errには失敗までの過程や理由などが含まれている
expectメソッドで、中身がErrだった時に引数のメッセージを表示させる。
対してunwrapすると中身がErrだった時パニックする。
またexpectしなかった時、コンパイルは通るがwarnが出る。

ねおさんねおさん

println!マクロについて

波括弧プレースホルダーで変数を表示

秘密も数字を生成する

rand クレートを使用する

Cargo.toml にrand = "0.8.3"を追加

[dependecies]はプロジェクトが依存する外部クレートと必要なバー音を Cargo に伝える

↑ はセマンティックバージョニング
^0.8.3の省略記法で、0.8.3 以上 0.9.0 未満の任意のバージョンを指定
0.9.0 移行は同じ API を持つことを保障しない

外部依存を含むビルド時、
依存関係が必要なすべてをレジストリから取得
レジストリは Cargo.io のコピー

レジストリ更新後、まだ取得してない依存関係を DL

Cargo.lock ファイルで再現可能なビルドを確保

ビルド時、これが存在するときはそのバージョンを使用する

クレートを更新したくなったとき

cargo update

Cargo.lock を無視して Cargo.toml のすべての指定に適合する最新バージョンを算出、それを新しい Cargo.lock に記録

rand の 0.9.x 系を使いたい場合は、Cargo.lock を更新する

乱数生成

use rand::Rng

Rng トレイトは乱数生成器が実装すべきメソッドを定義しており、それらを使用するにはこのトレイトがスコープ内にないといけない

乱数生成器生成は現在のスレッドに固有で、OS からシード値を取得している。

gen_range はハン意識を引数にとる

cargo doc --openは依存クレートが提供するドキュメントをローカルでビルドしてブラウザで開いてくれる

予想と秘密の数字を比較

use std::cmp::Ordering
Ordering は enum Less Grater Equal という列挙子を持っている

match 式で値がそのパターンにマッチした時に実行されるコードで構成


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

整数型について

整数型のデフォルトは i32

同じ名前の変数で宣言することでシャドーイングできる。
型変換の際によく使われるらしい

let guess:u32 という方注釈をつけることで、parse で変換する型を指定している

parse はよく失敗するので result 型を返すようになっている

ループ

loopは無限ループを作成する

breakでループを脱する

不正な入力を処理する

parse で err になったら何も値を返さずに continue してしまう

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue, // errの中身がどんな値の時でもマッチさせる
        };
ねおさんねおさん

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

変数と可変性

rust の変数はデフォルトでは不変

    let x = 5;
    println!("The value of x is: {}", x);     // xの値は{}です
    x = 6;
    println!("The value of x is: {}", x);

これはコンパイルできない

値が変わらない前提で処理を行った結果、実は変わっていたなんてのはバグの原因になる

rust コンパイラは、値が不変であるとしたらコンパイラが担保してくれる。

mut キーワードをつけることで変数を可変にできる


    let mut x = 5;
    println!("The value of x is: {}", x); // xの値は{}です
    x = 6;
    println!("The value of x is: {}", x);
}

これはコンパイルできる

mut とのトレードオフ

バグ予防以外にもある

大きなデータ構造を扱う場合など
インスタンスを可変にすれば、いちいちコピーする必要がない
小規模なデータ構造であれば関数型っぽいコードで書く方が考えやすくなるときもある

変数と定数の違い

定数に mut は使えない
const で宣言し、型注釈は必須
どんなスコープでも宣言可能
定数は定数式にしかセットできない
関数結果や実行時に評価されるやつは値にセットできない

定数の命名規則は、アッパースネークケース

定義されたスコープで有効

シャドーイング

同じ名前の値をつくるのは容認される。
例えば、別の型にしたいときなど
mut とは別で、別の変数を宣言している

データ型

rust におけるデータはすべて何らかの型になっている

型推論で複数の型が予想される場合は注釈をつけて明示してやる必要がある

スカラー型

単独の値

整数

明示的なサイズと、符号有無を選択できる

整数リテラル
_を見た目上の区切りにできる
0xff は 16 進数
0o77 は 8 進数
0b1111_0000 は 2 進数
b'A' はバイト ただし u8 だけ

基本的に i32 が最速になる

isize と usize を使う主な状況は何らかのコレクションにアクセスすること

浮動小数点

f32 と f64 がある
どちらもほぼ同じ速さのため、f64 が標準

論理

bool 型で、true と false しかない

文字

char 型
シングルクォートで囲む

ユニコードのスカラー値を示せる

fn main() {
    let c = 'z';
    let z = 'ℤ';
    let heart_eyed_cat = '😻';    //ハート目の猫
}

複合型

タプル型

複数の型の何らかの値を一つにまとめるときに一般的な手段

パターンマッチングでバラすことができる

もしくは.で直接アクセスすることもできる

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

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

    let five_hundred = tup.0;

    let six_point_four = tup.1;

    let one = tup.2;
}

配列型

全要素が同じ型で、固定長

fn main() {
    let a = [1, 2, 3, 4, 5];
}

ヒープよりもスタック領域にデータを確保したいとき or 固定長の要素があることを確認したいとき

ベクタは可変長

方注釈は下の漢字
[i32;5]
長さ 5 で i32 の配列
[3;5]の場合、3 が 5 つ並んだリテラル

配列の長さを超えた index にアクセスすると死ぬ

panic する
panic はプログラムがエラー終了したことを示す rust 用語

rust はメモリアクセスを許可するが不正なアクセスは即座にプログラムを終了させることで保護

関数

関数と変数の命名規則は基本的にスネークケース

fnで関数の宣言を開始

関数は引数を持つようにもできる
型宣言は必須

複数の仮引数を持たせたいときは仮引数定義をカンマ区切り

関数本体は文俊樹を含む

  • let 宣言
  • fn 宣言

文は値を返さない

let y = 6;

↑ これは文

let x = (let y = 6);

これはコンパイルエラー

何かに評価されるもの

式は文の一部になる
ブロックも式


    let y = {
        let x = 3;
        x + 1
    };
{
    let x = 3;
    x + 1
}
// これは式
// セミコロンつけたら文になっちゃう

戻り値あり関数

fn five() -> i32 {
    5
}

最後に記載した式が返却値

コメント

2 連スラッシュで行末までコメント

制御フロー

最も一般的なのは if 式とフロー

if 式

if 式の条件式は bool にしなければならない

else ブロックを作ることもできる

else if もできるが、枝分かれが多数なら match 式の方がよさげ

let 内で使うこともできる。つまり三項演算子みたいな扱いもできる
この場合は同じ型を返却するようにしないといけない

ループ

break と continue は最も内側のループに適用

ループラベルで break と continue の対象を選択できる

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

break まで無限ループ

while

条件式が true である限りループする

for

コレクションをのぞき見る

各値に対して中身を実行できる

所有権について

これのおかげでガベージコレクタなしで安全性担保できる Rust 特有の概念

所有権とは

メモリはコンパイラがチェックする規則とともに所有権システムを通じて管理

所有権機能がプログラムの動作を遅くすることはない

スタックとヒープ

スタック
FILO
スタックは高速

サイズが可変亜データはヒープに格納できる
ヒープにデータ置くとき、あるサイズのスペースが求められる

使用するとき、適切なヒープを確保し、使用中にしてそこへのポインタを返却

スタックよりヒープの方が遅い

どのコードがヒープ上のデータを使用しているか把握すること、ヒープ上の重複するデータを最小化すること
ヒープ上を掃除することは所有権により解決

所有権規則

  • 各値は所有者と呼ばれる変数と対応
  • 常に所有者は一つ
  • 所有者がスコープから外れたら値は破棄

String 型

文字列リテラルと、String 型
前者がスタック領域の物で不変
後者がヒープ領域の物で可変

前者を後者に変換することはできる

ねおさんねおさん

メモリと確保

文字列リテラルはコンパイル時に内容が判明しているので、バイナリに直接ハードコードされる
実行時にメモリをOSに要求する
使い終わったらメモリをOSにお返しする

rustは、変数がスコープを抜けたら自動的に解放される
この時に呼ばれる関数はdropと呼ばれる

変数とデータの相互作用:move

let a = 5;
let b = a;
// どちらもスタックに積まれる値なので、どちらも5
let s1 = "hello".to_string();
let s2 = s1;
// どちらもヒープに積まれる。一般的言語ならどちらも同じメモリを指すようになる。いわゆるシャローコピー
// Rustではs1が持っていた実体はs2に移動する。s1を参照しようとするとコンパイルエラー
// もともとs1が持っていた実体がs2しかもっていない形になる。s2がスコープを抜けたら解放すればいい

clone

ディープコピーしたければcloneを実行する

copy

スタックのみのデータであればコピーで事足りる
コピーが高速なので。
Copyトレイトの実装により実現
Dropで特別な操作をするような実装(手動でDropトレイトの実装)をしているものはCopyトレイトを使えない

ねおさんねおさん

参照

可変参照は同一スコープ内で同一データに対するものは一つしか持てない
可変参照があるとき、不変参照を作ることはできない
不変参照があるとき、可変参照を作ることはできない

ダングリングポインタ

fn main() {
    let reference_to_nothing = dangle();
    let reference_to_nothing_int = dangle_int();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

fn dangle_int() -> &i32 {
    let x = 42;

    &x
}
error[E0106]: missing lifetime specifier
 --> src/main.rs:6:16
  |
6 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
6 | fn dangle() -> &'static String {
  |                 +++++++
help: instead, you are more likely to want to return an owned value
  |
6 - fn dangle() -> &String {
6 + fn dangle() -> String {
  |

error[E0106]: missing lifetime specifier
  --> src/main.rs:12:20
   |
12 | fn dangle_int() -> &i32 {
   |                    ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
   |
12 | fn dangle_int() -> &'static i32 {
   |                     +++++++
help: instead, you are more likely to want to return an owned value
   |
12 - fn dangle_int() -> &i32 {
12 + fn dangle_int() -> i32 {
   |

本体が解放されるのに、それの参照を渡そうとしている
コンパイラはそれを阻止する

ねおさんねおさん

スライス型

コレクションの一部を参照する所有権がないデータ型

fn main() {
    let s = String::from("hello world");

    // let hello = &s[0..5];
    let hello = &s[..5]; //と書くこともできる
                         // let world = &s[6..11];
    let world = &s[6..]; //と書くこともできる
    let all = &s[..];

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

    println!("{}", all);

    let first = first_word(&s);
    println!("first is :{}", first);
}

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

world
hello
hello world
first is :hello

文字列リテラルの型は文字列スライス &str つまり不変参照

ねおさんねおさん

構造体

一部のフィールドのみ可変にすることはできない

フィールド初期化記法

変数名と同じフィールドがあればそれを使うことができる

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

構造体更新記法

スプレッド構文みたいなやつ

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    ..user1
};

タプル構造体

名前のないフィールド型のみのもの
値へのアクセスはタプルと同じような感じで使える

struct Color(i32, i32, i32)

ユニット様構造体

フィールドがない構造体
トレイト実装するがデータは保持しない時など

構造体の所有権について

構造体のフィールドとして、参照を保持することができるが、ライフタイムの機構を使用する必要がある。

ねおさんねおさん

パッケージ

main.rs の場合バイナリクレート
lib.rsの場合ライブラリクレート

別のモジュールで同じ名前のトレイトや構造体を定義することも可能

モジュールを定義してスコープとプライバシーを制御

use

パスをスコープに持ちこむ

pub

要素を公開

ねおさんねおさん

不変参照がスコープ内にある状態で、変更しようとしているのでコンパイルエラー

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("The first element is: {}", first);
}