Rustを基本の基本から学ぶ

概要
The Rust Programming Language 日本語版
を一から読み進めて学習する学んだ内容を整理して書き連ねる

はじめに
Rust は低レベル制御と、人間へのやさしさのトレードオフの競合の解消に挑戦している
Rust 利点
- コンパイラが門番の役割
- 並行性のバグなどを見つける門番 - 現代的な開発ツールをシステムプログラミング世界に導入
- Cargo
- 依存関係管理ツール兼ビルドツール
- Rustfmt
- 一貫したコーディングスタイルを保証
- Rust 言語サーバー IDE と統合し、コード保管やインラインメッセージにも対応
- Cargo

事始め
Rustのインストール
rustup
を使用してダウンロード
rustup
はRustのバージョンと関連ツールの管理するコマンドラインツール
Rustは安定性を保証していて、
本の例でコンパイルできるものは新しいバージョンでもコンパイルできる。
出力は異なるかもしれない
頻繁にエラーメッセージと警告を改善しているため
助力を得られる場所は色々ある
- RustのDiscordのbeginnersチャンネル
- Zulip rust-lang-jpなど
rustup doc
でブラウザでドキュメンテーションを閲覧可能
rustup update
でrustup自体の更新
Hello, world!
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
を新たに予約語にするなど
エディションについては↓に記載
cargo fix
でソースを新しいエディションに更新できる
今の最新エディションは2021ということは、今年中に新しいエディションがリリースされる?

エディションガイド
横道それたついでにエディションガイドについての学習を進める
エディションの移行について
cargo fix
を使うことでエディションの移行ができる
ただし完璧とは限らない
新しいプロジェクトを作成する
Cargoは新規プロジェクト作成時、自動で最新エディションをコンフィグに追加する
cargo +nighty new foo
エディションを移行する場合
cargo fix --edition
-
Cargo.toml
のeditionを新しいものに変更する -
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
チュートリアルを見ずに実装
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;
}
}
}
}
[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
などがある
Ok
かErr
の列挙子をもつ列挙型
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);
}