Rust の the book の 1〜5 章までを読む
はじめに
お世話になっている先輩から「これからは Rust やで!」みたいな話を聞いたので興味が復活した。
このスクラップでは the book を読みながら Rust について学んでいく過程を記録していこう。
インストール
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
info: downloading installer
Welcome to Rust!
This will download and install the official compiler for the Rust
programming language, and its package manager, Cargo.
Rustup metadata and toolchains will be installed into the Rustup
home directory, located at:
/Users/susukida/.rustup
This can be modified with the RUSTUP_HOME environment variable.
The Cargo home directory is located at:
/Users/susukida/.cargo
This can be modified with the CARGO_HOME environment variable.
The cargo, rustc, rustup and other commands will be added to
Cargo's bin directory, located at:
/Users/susukida/.cargo/bin
This path will then be added to your PATH environment variable by
modifying the profile files located at:
/Users/susukida/.profile
/Users/susukida/.zshenv
You can uninstall at any time with rustup self uninstall and
these changes will be reverted.
Current installation options:
default host triple: aarch64-apple-darwin
default toolchain: stable (default)
profile: default
modify PATH variable: yes
1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
>1
info: profile set to 'default'
info: default host triple is aarch64-apple-darwin
info: syncing channel updates for 'stable-aarch64-apple-darwin'
info: latest update on 2024-02-08, rust version 1.76.0 (07dca489a 2024-02-04)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-std'
info: downloading component 'rustc'
54.7 MiB / 54.7 MiB (100 %) 38.2 MiB/s in 2s ETA: 0s
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
14.7 MiB / 14.7 MiB (100 %) 11.9 MiB/s in 1s ETA: 0s
info: installing component 'rust-std'
info: installing component 'rustc'
54.7 MiB / 54.7 MiB (100 %) 26.5 MiB/s in 1s ETA: 0s
info: installing component 'rustfmt'
info: default toolchain set to 'stable-aarch64-apple-darwin'
stable-aarch64-apple-darwin installed - rustc 1.76.0 (07dca489a 2024-02-04)
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, run:
source "$HOME/.cargo/env"
C コンパイラのインストール
xcode-select --install
xcode-select: error: command line tools are already installed, use "Software Update" in System Settings to install updates
どうやらすでにインストールされているようだ。
Hello, World!
mkdir ~/workspace/hello_rust
cd ~/workspace/hello_rust
touch main.rs
fn main() {
println!("Hello, world!");
}
rustc main.rs
./main
Hello, world!
無事に Hello World が完了した。
次にやること
Anatomy of Rust Programming を読んでいく。
Anatomy of Rust Programming
- C 言語などと同様に main() 関数はプログラムのエントリーポイントとなる。
- main() 関数は引数を取らず、戻り値もない。
- 関数の内容は
{}
で囲まれる、これも C 言語などと同様。 - Rust では関数名と同じ行に 1 文字半角スペースを開けて
{
を書くのが良いとされる。 - rustfmt コマンドを使えば良い感じにフォーマットしてくれる。
- Rust では半角スペース 4 文字でインデントを揃える、タブではない。
-
println!()
は関数に見えるが実際にはマクロ、println()
なら関数。 - マクロについては相当後の章(第 19 章)で説明される。
- 現状では
!
で終わる場合はマクロを呼び出しており、関数とは若干異なるということだけを知っていれば OK。 - Rust では式の末尾が
;
で終わる、これも C 言語などと同じ。
fn main() {
}
Hello, Cargo!
cargo --version
cargo 1.59.0 (49d8809dc 2022-02-10)
ノートパソコンを使っているので古いかも知れない。
Cargo を使ったプロジェクト作成
cargo new hello_cargo
cd hello_cargo
Created binary (application) `hello_cargo` package
Cargo.toml を見てみる
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-
package
セクションではパッケージに関する設定を行う。 - name, version, edition はコンパイルを行うために必須。
-
dependencies
セクションはプロジェクトの依存関係。 - Rust ではパッケージはクレートと呼ばれる、クレートは木箱を意味する英単語。
ディレクトリ構成
- ソースコードは src ディレクトリに入れる。
- README やライセンスや構成に関するファイルはトップレベルに入れる。
Cargo ビルド
cargo build
Compiling hello_cargo v0.1.0 (/Users/susukida/workspace/rust/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 19.77s
- 実行可能ファイルは target/debug/hello_cargo に出力されるので
target/debug/hello_cargo
を実行すれば "Hello, world!" と表示される。 - デフォルトのビルドはデバッグビルドなので target/debug ディレクトリ内に出力される。
-
cargo build
を実行すると Cargo.lock ファイルが作成され、このファイルには依存関係のバージョンが保存される。
Cargo Run
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/hello_cargo`
Hello, world!
- 1 つのコマンドでビルド&実行できる。
- ソースコードが編集されていなくてビルドが不要な場合はスキップされる。
- 編集は更新日時で判断しているらしく、内容が変わっていなくてもビルドはされる。
Compiling hello_cargo v0.1.0 (/Users/susukida/workspace/rust/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.19s
Running `target/debug/hello_cargo`
Hello, world!
Cargo Check
cargo check
Checking hello_cargo v0.1.0 (/Users/susukida/workspace/rust/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
コンパイルが通るかを確認するだけであればこちらの方が早そう。
Cargo コマンドまとめ
-
cargo new
: プロジェクト作成 -
cargo build
: ビルド -
cargo run
: ビルド&実行 -
cargo check
: コンパイルが通るかの確認
Cargo コマンドの利点として Windows だろうが Mac / Linux だろうが同じコマンドで良いことが挙げられる。
リリース版のビルド
cargo build --release
Compiling hello_cargo v0.1.0 (/Users/susukida/workspace/rust/hello_cargo)
Finished release [optimized] target(s) in 0.20s
- 実行可能ファイルは target/release ディレクトリに出力される。
- リリース版のビルドは最適化がされるので時間がかかるが実行は高速になる。
Rust バージョン更新
rustup update
今はテザリング中なので実行しない。
ドキュメントを読む方法
rustup doc
ブラウザでドキュメントページが開かれる。
次は Programming a Guessing Game
プロジェクト作成
cargo new guessing_game
cd guessing_game
コーディング
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}");
}
- ユーザー入力を取得するには std::io ライブラリを使用する必要がある。
- std::io ライブラリを使えるようにするには
use std::io;
と冒頭に書く。 - プレリュードというものがよくわからないが全てのプログラムに必要とされるものを一気にインポートしてくれるようだ。
- 使いたいものがプレリュードに含まれていない場合は明示的にインポートする必要がある。
Storing Values with Variables
下記のコードはユーザー入力を保存する変数を作成している。
let mut guess = String::new();
一方、下記のコードは 5 にバインド(?)された変数を作成している。
let apples = 5;
Rust ではデフォルトで変数は immutable(不変)であり、変更することはできない。
後から変更できるようにするには mut
キーワードを使う必要がある。
let apples = 5; // immutable
let mut bananas = 5; // mutable
ちなみに //
はコメント。
String::new()
は文字列のインスタンスを作成して返す関数。
String は growable(多分長さを増やせるという意味)な UTF-8 文字列の型であり、標準ライブラリから提供される。
String::
は String 型に関連付けられた関数であることを示している。
new()
という関数名は Rust ではかなり一般的に使用され、何かの新しい値を作成する関数に使用される。
Receiving User Input
io::stdin()
.read_line(&mut guess)
io
モジュールの stdin()
関数を呼び出すと標準入力にアクセスできるようだ。
use std:io;
を使わない場合は std::io:stdin
とフルネームを書く必要がある。
stdin()
関数の戻り値は std::io:Stdin
型のインスタンスであり、このタイプはターミナルからの標準入力を扱うため型である。
続いて read_line()
は標準入力型のメソッドであり、ユーザーからの入力を取得するために使用される。
引数としては &mut guess
が与えられており、guess 変数にユーザー入力が与えられる。
read_line() メソッドは上書きするのではなく追記するようだ。
&
は参照であることを示し、コードの複数の部分がコピーなしでデータにアクセスできるようにする。
参照は複雑な機能であり、安全かつ簡単に参照を使えることは Rust の主要な利点である。
変数のように参照もデフォルトでは不変である、C で言うところの const char *
みたいな感じがする。
&guess
と書くと変更できない参照になるが、&mut guess
と変更できる参照になる。
次は Handling Potential Failure with Result
Handling Potential Failure with Result
.expect("Failed to read line");
read_line() 関数は Rusult と言う列挙型の値を返す。
列挙型のそれぞれの値はバリアントと呼ばれる。
Result 型はエラー処理情報を符号化(?)するために利用され、バリアントは Ok
と Err
の 2 つ。
Ok は処理が成功したことを示し、Ok に含まれているものは成功して生成された値が含まれる。
Err は処理が失敗したことを示し、Err には処理が失敗した理由に関する情報が含まれている。
Result 型は expect() メソッドを持っており、Err の場合は expect() はプログラムをクラッシュさせ、引数として与えられたメッセージをコンソールに出力する。
もし read_line() 関数が Err を返した場合、OS のエラーの可能性が高い。
一方、Ok を返した場合、expect() 関数は Ok インスタンスが保持している戻り値を返す。
read_line() の場合はユーザー入力のバイト数になる。
もし expect() 関数を呼び出さない場合は下記の警告が表示される。
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.52s
Rust では Result 型の値を使用しないと警告が表示され、それはプログラムがエラー処理を省略している恐れがあることを示す。
警告が表示されないようにするにはエラー処理のコードを書けば良いが、今回のケースではただプログラムをクラッシュさせれば良いだけなので、その場合は expect() 関数を呼び出すのが便利。
次は Printing Values with println! Placeholders
Printing Values with println! Placeholders
文字列内の {}
はプレースホルダーと呼ばれ、カッコ内に変数名を書くと内容が表示される。
変数ではなく式の評価結果を表示する場合、空のプレースホルダーを置いて第 2 引数に式を書く。
let x = 5;
let y = 10;
println!("x = {x} and y + 2 = {}", y + 2);
このコードは実行結果は下記のようになる。
x = 5 and y + 2 = 12
次は Generating a Secret Number
rand クレート
Random number generators and other randomness functionality.
これは実行可能なバイナリークレートではなく、ライブラリークレートと呼ばれて他のプログラムから使われることが意図されており、実行することができない。
依存関係の追加
[dependencies]
rand = "0.8.5"
0.8.5
は ^0.8.5
の省略記法であり、0.8.5 以上 0.9 未満であることを指定する。
この状態でビルドしてみる。
cargo build
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.17
Compiling libc v0.2.153
Compiling getrandom v0.2.12
Compiling rand_core v0.6.4
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (/Users/susukida/workspace/rust/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
先ほどのコメントアウトを消し忘れていたのでエラーになってしまったが rand の依存関係がコンパイルされている様子がわかる。
ダウンロードされている様子が見当たらないのは VSCode によって事前にダウンロードされたからかも知れない。
次は Ensuring Reproducible Builds with the Cargo.lock File
Ensuring Reproducible Builds with the Cargo.lock File
- Cargo は同じ成果物を再ビルドできるしくみを持っている。
- Cargo.lock にビルド時に使用したバージョンを書き込むことで実現している。
- したがって Cargo.lock はバージョン管理に含めるべき。
Updating a Crate to Get a New Version
クレートのバージョンを更新したい場合は下記のコマンドを実行する。
cargo update
Cargo は Cargo.lock ファイルを無視して Cargo.toml を見て最新バージョンを取得する。
もし rand クレートのバージョン v0.8.6 がリリースされていたら下記のように表示される。
Updating crates.io index
Updating rand v0.8.5 -> v0.8.6
もし v0.9.x がリリースされていても更新されない。
v0.9.x に更新したい場合は Cargo.toml を編集して明示的に指定する必要がある。
[dependencies]
rand = "0.9.0"
次は Generating a Random Number
Generating a Random Number
use rand::Rng;
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");
println!("You guessed: {guess}");
}
Rng トレート(?)は乱数生成器が実装するメソッドを定義している。
これらのメソッドを使うためにトレイトをスコープに含める必要がある。
rand::thread_rng() 関数は現在のスレッドに対してローカル(?)で OS によってシードされる乱数生成器を作成する。
gen_range() メソッドは Rng トレイトで定義されており、引数としてはレンジ式を受け取ってレンジ内の乱数を生成する。
start..=end
は start と end が inclusive なので 1..=100
は 1〜100 までの範囲となる。
ドキュメント表示
下記のコマンドを実行するとクレートのドキュメントを読める。
cargo doc --open
Comparing the Guess to the Secret Number
use rand::Rng;
use std::{cmp::Ordering, 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");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
このコードをビルドしようとすると下記のエラーが表示される。
Compiling guessing_game v0.1.0 (/Users/susukida/workspace/rust/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:21:21
|
21 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
| |
| arguments to this method are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
note: method defined here
--> /Users/susukida/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/cmp.rs:814:8
|
814 | fn cmp(&self, other: &Self) -> Ordering;
| ^^^
For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1 previous error
データ型が異なるようだ。
std::cmp::Ordering
比較関数の結果を表す列挙型で Less, Greater, Equal の 3 つになる。
match 式
スイッチ文に似ている。
match 式は Rust の重要な機能のようで後の章で詳しく説明されるようだ。
コードの修正
use rand::Rng;
use std::{cmp::Ordering, 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!"),
}
}
シャドウィング
Rust では変数名の再定義が可能。
データ型の変換時によく使用される。
再定義する場合、右辺は前の値になる。
trim() メソッド
文字列の先頭と末尾の空白文字を削除する。
read_line() メソッドを使うと改行文字が末尾に付加されるので数値に変換する前に削除が必要になる。
parse() メソッド
let guess: u32
と左辺で指定しているので prase() 関数は符号なし 32 ビット整数と Rust は理解する。
parse() ではパースが失敗する可能性があるので結果は Result 型になっている。
したがって値を取り出すには expect() メソッドを使う必要がある。
コードの現状
use rand::Rng;
use std::{cmp::Ordering, 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!"),
}
}
loop と break
use rand::Rng;
use std::{cmp::Ordering, io};
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
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 = 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!");
break;
}
}
}
}
- loop を使うとブロックを繰り返せる。
- match 式でもブロックを使える。
- break を使うとループを抜け出せる。
次は Handling Invalid Input
Handling Invalid Input
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
match 式はこんな風にも使える。
JavaScript の switch 文とは異なり、値も取り出せるのはすごい便利だ。
気になったので Result の定義を調べたら下記のようになっている。
#[derive(Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
#[rustc_diagnostic_item = "Result"]
#[stable(feature = "rust1", since = "1.0.0")]
pub enum Result<T, E> {
/// Contains the success value
#[lang = "Ok"]
#[stable(feature = "rust1", since = "1.0.0")]
Ok(#[stable(feature = "rust1", since = "1.0.0")] T),
/// Contains the error value
#[lang = "Err"]
#[stable(feature = "rust1", since = "1.0.0")]
Err(#[stable(feature = "rust1", since = "1.0.0")] E),
}
Err(_) のアンダーバーは catchall value と呼ばれ、Err だったらとにかくマッチする。
ということはそうではないケースもあるということだ。
次は Chapter 3
Variables and Mutability
繰り返しになるが変数はデフォルトで変更不可となる。
プロジェクトのために新たなワークスペースを作成する。
cd ~/workspace/rust
cargo new variables
cd variables
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
上記のコードは動かない、なぜなら変更不可の x に再代入しようとしているため。
cargo run
実行時のエラーメッセージは下記の通り。
Compiling variables v0.1.0 (/Users/susukida/workspace/rust/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: consider making this binding mutable: `mut x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error
変更可能にするには let
の後に mut
と書く。
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
Compiling variables v0.1.0 (/Users/susukida/workspace/rust/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.35s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
次は Constants
Constants
- 変更不可な変数と定数は異なる。
- 定数に
mut
キーワードは使用できない。 - 定数は型を明示する必要がある。
- 定数はグローバルを含むどんなスコープでも宣言できる。
- 定数の右辺値は定数式のみ。
- 定数名には大文字キャメルケースを使う。
- 定数式では足し算や掛け算など基本的な演算を行える。
Shadowing
- 同じ名前の変数を宣言できる。
- 右辺にその変数が含まれる場合は直前の値が使用される。
- これは「1 番目の変数が 2 番目の変数によってシャドウされる」と呼ばれる。
mkdir ~/workspace/rust
cargo new shadowing
cd shadowing
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
println!("The value of x is: {x}");
}
cargo run
Compiling shadowing v0.1.0 (/Users/susukida/workspace/rust/shadowing)
Finished dev [unoptimized + debuginfo] target(s) in 2.58s
Running `target/debug/shadowing`
The value of x in the inner scope is: 12
The value of x is: 6
2 回目のシャドウィングはスコープが終わると同時に終わり、そのあとは 1 回目のシャドウィングの値である 5 + 1 = 6 が表示される。
もし let
を忘れたらコンパイルエラーが表示される、なぜなら mut
キーワードが無くて再代入ができないため。
シャドウィングでは前後で同じ型である必要はない、これは型変換を行うときには便利そうだ。
let spaces = " ";
let spaces = spaces.len();
もし下記のように書いたら型が異なるのでエラーになる。
let mut spaces = " ";
spaces = spaces.len();
Compiling shadowing v0.1.0 (/Users/susukida/workspace/rust/shadowing)
error[E0308]: mismatched types
--> src/main.rs:14:14
|
13 | let mut spaces = " ";
| ----- expected due to this value
14 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
|
help: try removing the method call
|
14 - spaces = spaces.len();
14 + spaces = spaces;
|
For more information about this error, try `rustc --explain E0308`.
error: could not compile `shadowing` (bin "shadowing") due to 1 previous error
次は Data Types
Data Types
- Rust は静的に型付けされた言語であり、コンパイル時に型が決まっている必要がある。
- コンパイラーが 1 つの型を推論できるケースがあるので明示的な指定は必須ではない。
- ただし、複数の型が推論されるケースでは曖昧さを無くすために明示的な指定が必要となる。
let guess: u32 = "42".parse().expect("Not a number!");
上記のコードで : u32
を省いたら下記のようなエラーメッセージが表示される。
Compiling data_types v0.1.0 (/Users/susukida/workspace/rust/data_types)
error[E0284]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ ----- type must be known at this point
|
= note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
|
2 | let guess: /* Type */ = "42".parse().expect("Not a number!");
| ++++++++++++
エラーメッセージの内容は「型アノテーションが必要」とのこと。
Scalar Types
- スカラー型は単一の値を表す。
- Rust には整数、浮動小数点数、真偽値、文字の 4 つのスカラー型がある。
Integer Types
- 符号のありなしとビット数(8, 16, 32, 64, 128, アーキテクチャ)によって i8 / u8 〜 i128 / u128 + isize / usize がある。
- アーキテクチャが 64 ビット系だと 64 ビットになる。
- 10 進数のリテラルは
98_222
のように書ける。 - 2 / 8 / 16 進数のリテラルは
0b / 0o / 0x
を前置して書ける、_
も使える。 - u8 だけだが b'A' のように書くと ASCII コードを表示できる。
-
57u8
のように整数のデータ型を後置することでリテラルのデータ型を明示的に指定できる。 - Rust ではデフォルトでは i32 が使用される。
- 何にするか決めかねた場合はまずは i32 を使ってみる。
次は Integer Overflow から
Integer Overflow
- Rust では整数値のオーバーフローが発生した場合、デバッグ版だとパニックする。
- パニックとはプログラムがエラーで終了すること。
- リリース版だとパニックせずに C 言語のような動作になる、2 の補数ラッピングと呼ばれるらしい。
- 意図的に 2 の補数ラッピングを使いたい場合は wrapping_add などのメソッドを使用する。
- オーバーフローで
None
を返したい場合は checked_add などを使う。 - オーバーフローの有無と結果を返したい場合は overflowing_add などを使う。
- 最大値・最小値で飽和させたい場合は saturating_add などを使う。
次は Floating Point Types
Floating Point Types
- Rust では
f32
とf64
の 2 つの浮動少数点数型がある。 - デフォルトでは f64 になる。
Numeric Operations
fn main() {
// addition
let sum = 5 + 10;
// subtraction
let difference = 95.5 - 4.3;
// multiplication
let product = 4 * 30;
// division
let quotient = 56.7 / 32.2;
let truncated = -5 / 3; // Results in -1
// remainder
let remainder = 43 % 5;
}
- Rust では C 言語や Java と同じように算術演算を書ける。
-
-5 / 3
が-1
になるのは整数と整数の演算だからだろう。
The Boolean Type
- 真偽値のデータ型には
bool
を使う。 - 情報量的には 1 ビットなのに 1 バイトを使うらしいのでちょっともったい無い感じがする。
The Character Type
- 文字型のリテラルには
'
シングルクオートを使う、C 言語と同じだ。 - Rust の文字型は 4 バイトであり、Unicode のスラカー値を表現する。
- 日本語や絵文字も格納できる、多分サロゲートペア
👨👩👧👧
は無理そう。
Compound Types
- 複合型を使うと複数の値を 1 つの型にまとめることができる。
- Rust ではタプルと配列の 2 つの複合型がある。
次は The Tuple Type
せっかくなので手を動かそう。
The Tuple Type
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
- タプル型は複数の値を扱う型であり、それぞれの値の型は異なっても良い。
- タプル型は固定長であり、後から増減させることはできない。
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
The value of y is: 6.4
- タプルをパーツごとに異なる変数にバインドすることを destructuring と呼ぶ、日本語では分解か?
-
tup.0
のようにアクセスすることもできる。
ユニット
- 値のないタプル
()
は「ユニット」と呼ばれる、Lisp の nil みたいだ。 - 値も型も
()
と表現される。 - 何も返さない式は実はユニットを返している。
The Array Type
fn main() {
let a = [1, 2, 3, 4, 5];
}
- 配列もタプルのように複数の値を扱う型だがタプルとは異なりデータ型は同じである必要がある。
- 要素数についてはタプルと同様に固定長である。
- 配列はヒープではなくスタックにデータを格納したい時やデータが固定長であることを確認したい場合に便利である。
- 配列とは別にベクタがあり、こちらは可変長である。
- 配列かベクタを使うかはコーディング時に要素数が確定しているかどうかで決まる。
- もし確定しているならほとんどのケースでは配列が適している。
- 配列のデータ型は
[i32; 5]
のように書ける。 - この書き方は配列の初期化時にも使える、例えば
[3; 5]
は[3, 3, 3, 3, 3]
と同じである。
Accessing Array Elements
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
- 配列にアクセスするには他の言語と同じように
a[0]
のように書く。
Invalid Array Element Access
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Please enter an array index.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");
let element = a[index];
println!("The value of the element at index {index} is: {element}");
}
上記のコードに 10 などを入力すると下記のパニックが発生する。
thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
そのプロセスについては明日改めて読んでみよう。
- Rust は C 言語とは異なり、配列の添え字チェックを行ってくれる。
- これはリリース版でも行われるのだろうか?
- 試したところ行われるようだ。
Functions
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
- Rust では変数名や関数名にスネークケースを用いる。
- Rust では同じスコープに含まれていれば関数の定義位置はどこでも良い。
Parameters
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}.");
}
fn main() {
print_labeled_measurement(5, 'h');
}
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The value of x is: {value}{unit_label}.");
}
次は Statements and Expressions から
Statements and Expressions
- 文は値を返さない。
- 式は値として評価される。
- 例えば
let y = 6;
は文である。 - 代入は文なので
let x = (let y = 6);
のように書くことはできない。 - C 言語などでは代入は式として扱われ、
x = y = 6
のように書ける。 - 例えば
5 + 6
は式であり、11
に評価される。 - 関数やマクロの呼び出しは式として扱われる。
- 新しいスコープブロックも式として扱われる。
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {y}");
}
- 上記のコードでは y の値は 4 になる。
-
x + 1
の最後にセミコロンをつけないように気を付ける、つけたら式ではなく文になる。
Functions with Return Values
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");
}
- 関数の戻り値に名前をつける必要はないが型を明示的に指定する必要がある。
- 関数の最後に書かれた式が戻り値になる。
-
return
キーワードを使って関数の途中で値を返すこともできる。
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1
}
次は Comments
Comments
Rust にはドキュメント用のコメントがあり、後のチャプターで説明される。
ここまで勉強が続けば良いな 😂
Control Flow
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
- Rust の if 文では
()
はいらない、あっても大丈夫だが警告が表示される。 - よく見たら if 文ではなく if 式だ、したがって変数の右辺値としてバインドができそうだ。
- if 式に関連付けられたコードブロックは「アーム」と呼ばれる。
- else は省略できるが式として使う場合は if アームの型をユニット型にする必要がある、つまりブロックの最後は式ではなく文で終わらせる必要がある。
- if の条件は必ず bool 型である必要がある。
Handling Multiple Conditions with else if
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
これについては他の言語とほぼ同様だが Rust では mutch 式があるのでこちらを使った方が良さそうだ。
Using if in a let Statement
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
if を式として使う場合は同じ型である必要がある。
次は Repetition with Loops
Repetition with Loops
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
- Rust では
loop
,while
,for
の 3 つの種類のループがある。 -
break
がcontinue
が使えるのは他の言語と同じ、ただしbreak
で値が返せる。
Loop Labels to Disambibuate Between Multiple Loops
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}");
}
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
- break と continue は何も指定しないと最も内側のループブロックに対して行われる。
- ループラベルを指定することで対象ブロックを明示的に指定できる。
- ループラベルはシングルクオートから始める必要がある。
Conditional Loops with while
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
- while については他の言語と同じなので特筆すべき点はないが文なのか式なのかどっちなのか?
- 少し試したところ文なのかも知れない。
Looping Through a Collection with for
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index += 1;
}
}
- 上記の方法でも配列を走査できるが配列の要素数が減ったりするとパニックになってしまう。
- また、毎回添え字が範囲内にあるかをチェックするので実行速度も遅いようだ。
- このようなケースでは for を使う方が良い。
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
for はレンジ式と組み合わせることでさらに便利になる。
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
次は Understanding Ownership
オーナーシップは Rust の特徴的な機能のようなのでより理解していきたい。
Understanding Ownership
オーナーシップは Rust の特徴というよりも独自性という方が良いのかも知れない。
オーナーシップがあることでガベージコレクションを使わずにメモリ安全を実現している。
ボロウィング、スライス、メモリ内のデータ配置について理解することが重要なようだ。
What Is Ownership?
- オーナーシップとは Rust のプログラムがどのようにメモリを管理するかを統治するルールである。
- 言語によってはガベージコレクションや明示的なメモリ確保/解放によってメモリ管理を行なっている。
- Rust ではこれらの方法を用いずにオーナーシップの仕組みを用いてメモリ管理を行なっている。
- オーナーシップの規則に反するようなプログラムを書くとコンパイルが通らない。
- オーナーシップのしくみによって実行時の速度が犠牲になることはない。
オーナーシップは新しい概念なので慣れるまでには時間がかかる、すぐに理解できなくても焦る必要はない。
The Stack and the Heap
- 普段プログラミングをする時にはデータがスタックにあるかヒープにあるかを意識することは少ないかも知れないが、Rust のようなシステムプログラミング言語ではかなり重要になる。
- オーナーシップはスタックやヒープにも関わるので軽くおさらいしてみる。
- スタックもヒープもプログラムで利用可能なメモリである点は共通している。
- スタックは固定長である必要があるが、コンパイル時に長さが不定な場合はヒープを使う必要がある。
- スタックはお皿、ヒープはレストランと考えると理解しやすい。
- スタックの方が空いているメモリ領域を探す必要がないので比較的高速である。
- 関数を呼び出す時には引数はスタックに格納される。
- オーナーシップの主な目的はヒープ上のデータを管理することである、ということを知っておくとオーナーシップがなぜこのようになっているのかを理解しやすい。
Ownership Rules
- Rust では個々の値にはオーナーがいる。
- ある時点ではオーナーは 1 つしかいない。
- オーナーがスコープから外れると、値も一緒に削除される。
上は暗記するくらいに後から繰り返した方が良いと思うので繰り返そう。
- 個々の値にオーナーがアイル。
- ある時点では 1 つのオーナーしかいない。
- オーナーがスコープから外れると値も一緒に削除される。
Variable Scope
スコープとは変数などが有効である範囲である。
{ // この時点では s はまだ宣言されていないので有効ではない。
let s = "hello"; // s はこの行より後で有効になる。
} // ここがスコープの終端なので s は有効ではなくなる。
-
s
がスコープに入った時点で有効になる。 -
s
はスコープを出るまで有効であり続ける。
次は The String Type
The String Type
- 基本的なデータ型は関数などの引数に渡されると単純にコピーされる。
- String のようなデータ型はヒープを利用するのでオーナーシップを学ぶのに適している。
- 文字列リテラルは変更不可であ理、可変の文字列を扱うには String 型が必要になる。
- 文字列リテラルから String インスタンスを作成するには
String::from()
関数を使う。
fn main() {
let mut s = String::from("hello");
s.push_str(", world!");
println!("{}", s);
}
hello, world!
-
::
演算子は String 名前空間にある from() 関数にアクセスするために使用される。
文字列リテラルは不変であるのに対して String 型は可変であり、この違いはこれらの 2 種類の型がメモリをどう扱っているかによってもたらされている。
Memory and Allocation
- 文字列リテラルはコンパイル時に内容が確定しており、実行ファイルにハードコードされる。
- これにより高速だが不変となっている。
- 一方、String 型ではヒープ上にメモリ領域を確保することで可変になっている。
- String 型では実行時にアロケーターに対してメモリが要求され、メモリを使い終わったらアロケーターにメモリを返却する手段が必要となる。
- メモリ要求は
String::from()
関数を呼び出すことによって行われる。 - Rust では変数がスコープから外れた時点で自動的にメモリ領域が返却される。
{
let s = String::from("hello"); // s は次の行から有効になる。
} // スコープが終わったので s が有効ではなくなる。
- 変数がスコープから外れた時、Rust は特殊な関数を呼び出し、その関数は drop() と呼ばれる。
- String 型の実装者はメモリ領域を返却するコードを drop() 関数に書く。
- ちなみに C++ ではこのようなパターンは RAII (resource Aquisition Is Initialization) と呼ばれる。
- これは単純に見えるが Rust コードの書き方に大きなインパクトを与える。
次は Variables and Data Interacting with Move
このセクションは長くて内容も濃そうなので集中できるタイミングで読んでいこう。
Variables and Data Interacting with Move
let x = 5;
let y = x;
上記のコードでは x と y はそれぞれ 5 にバインドされ、スタックには 2 つの 5 が積まれる。
let s1 = String::from("hello");
let s2 = s1;
上記のコードでは s1 は s2 にコピーされない。
スタックには 1 つの String インスタンスだけが積まれる。
String 型はポインタ、長さ、容量の 3 つのデータから構成されており、インスタンスを作るとこれらのデータがスタックに積まれる。
s2 に s1 を代入するとポインタ、長さ、容量の 3 つのデータはコピーされるが、ポインタの先にある実際の文字列データはコピーされない。
変数がスコープから外れると drop() 関数が呼ばれるが、この場合は s1 と s2 の 2 つの変数があるので 2 回呼ばれてしまいそうな気がする。
しかし実際には 1 回しか呼ばれない、なぜなら、s2 に s1 を代入した時点で s1 が有効ではなくなるため。
仮に下記のようなコードを書いた場合はコンパイルが失敗する。
fn main() {
let s1 = String::from("Hello");
let s2 = s1;
println!("{}, world!", s1);
}
Compiling strings v0.1.0 (/Users/susukida/workspace/rust/strings)
warning: unused variable: `s2`
--> src/main.rs:9:9
|
9 | let s2 = s1;
| ^^ help: if this is intentional, prefix it with an underscore: `_s2`
|
= note: `#[warn(unused_variables)]` on by default
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:11:28
|
8 | let s1 = String::from("Hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
9 | let s2 = s1;
| -- value moved here
10 |
11 | println!("{}, world!", s1);
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
9 | let s2 = s1.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
warning: `strings` (bin "strings") generated 1 warning
error: could not compile `strings` (bin "strings") due to 1 previous error; 1 warning emitted
String インスタンスの代入はシャロウコピーのように見えるが、実際には Rust は右辺値の変数を無効にする点が異なっている。
これはシャロウコピーではなく「移転」と呼ばれる。
したがってs1 = s2;
は「s1 は s2 に移転された」と説明される。
Rust は決して自動的にディープコピーを作成することはない。
したがって通常の代入はパフォーマンスの観点からはそれほど高価な処理ではない。
次は Variables and Data Interacting with Clone
Variables and Data Interacting with Clone
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
移転ではなくディープコピーをしたい場合は String の clone() メソッドを使う。
Stack-Only Data: Copy
fn main() {
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
}
上記のコードは clone() メソッドを使っていないが問題なくコンパイルできる。
この理由は整数のような型はコンパイル時にサイズが決まっているのでスタックに積むことができ、素早くコピーすることができる。
したがって y に x を代入した時点で x を無効にする必要がない。
換言するとディープコピーとシャローコピーに違いがない。
Rust には Copy と呼ばれる特別なアノテーションがあり、このアノテーションを使うとスタックに積める型を作れる。
Copy トレイトが実装されている型の変数では移転が起こらず、コピーがスタック内で行われる。
ある型が Drop トレイトを実装している場合、Copy でアノテートすることができない。
これはある型が Drop トレイトを実装している型を含む場合も同様である。
スカラー値は Copy トレイトが実装されている。
Copy トレイトを実装している型だけからなるタプルも Copy を実装していると見なされる。
Ownership and Functions
関数に値を渡すことも代入と同じように見なされる、つまり移転からコピーのいずれかになる。
次は Return Values from Scope
Return Values and Scope
関数が値を返すことも所有権の移転になる。
fn main() {
let s1 = gives_ownership();
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
}
fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string
}
fn takes_and_gives_back(a_string: String) -> String {
a_string
}
ヒープ上のデータを含む変数がスコープから外れると drop() 関数が実行されるが、所有権が移転される場合はこの限りではない。
上記のコードは意図した通りに動作するが所有権が移転したり戻ったりを取り扱うのが煩雑になっている。
関数にパラメーターを渡したいが所有権を渡したくない場合は参照を利用する。
次は References and Borrowing
References and Borrowing
- 参照はポインタのようなもので他の変数が所有権を持つデータにアクセスできる。
- ポインタと異なるのは有効な値を指していることが保証されている点である。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
The length of 'hello' is 5.
- 変数の参照を渡す時には
&
を前置する。 - 変数の参照を受け取る時も
&
を前置する。 - calculate_length() 関数に渡されるのは参照なので
s
がスコープから外れても参照先のデータは無効にならない。 - したがって以前のコードのように s を返す必要がない。
- 参照を作成することはボロウィング(借用?)と呼ばれる。
下記のコードはコンパイルが失敗する。
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
Compiling ownership v0.1.0 (/Users/susukida/workspace/rust/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:48:5
|
48 | some_string.push_str(", world");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
47 | fn change(some_string: &mut String) {
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
参照もデフォルトでは不可変であり、可変にするには可変参照を使う必要がある。
Mutable References
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
変更箇所は次の 3 つである。
- 変数を
mut
にする。 - 参照に
&mut
を前置する。 - 引数に
&mut
を前置する。
&mut
というのがなんとなく覚えにくい "mutable reference" なら mut&
なので覚えやすいのだが。
可変参照を使う場合(多分、同じスコープに)他の参照を設けることができない、不可変参照でもダメ。
ただし可変参照を関数のパラメーターとして渡すのは大丈夫。
最初の可変借用は println!() を呼び出すまで続き、その間に別の借用を作成しようとするとエラーになる。
どうやら最初の可変借用を使い終わるまで、別の可変借用は作成できないようだ。
これは図書館で借りた本を返すまで、他の人がその本を借りられないのに似ている気がする。
時間が来てしまったのでこの途中から再開しよう。
The restriction preventing multiple mutable references to the same data at the same time allows for mutation but in a very controlled fashion.
ここは大事そうなので全訳してしまおう
The restriction preventing multiple mutable references to the same data at the same time allows for mutation but in a very controlled fashion.
複数の可変参照が同時に同じデータにアクセスすることを防ぐ制約はとても管理された状態でデータを変更することを可能とします。
It’s something that new Rustaceans struggle with because most languages let you mutate whenever you’d like.
この制約は Rust 初学者が(理解するのに)苦戦する点です、なぜなら多くの言語では好きな時にいつでもデータを変更できるからです。
The benefit of having this restriction is that Rust can prevent data races at compile time.
この制約を設けることの利点は Rust がコンパイル時にデータレースを防げる点です。
A data race is similar to a race condition and happens when these three behaviors occur:
データレースはレースコンディションに似ており、下記 3 つの挙動が起こる時に発生します。
Two or more pointers access the same data at the same time.
2 つ以上のポインタが同時に同じデータにアクセスする時
At least one of the pointers is being used to write to the data.
少なくとも 1 つのポインタがデータを書き込むのに使われる時
There’s no mechanism being used to synchronize access to the data.
データへのアクセスを同期するのに使われるメカニズムがない時
Data races cause undefined behavior and can be difficult to diagnose and fix when you’re trying to track them down at runtime;
データレースは未定義の挙動を発生させ、実行時にこれらを追跡しようと努めている時に原因調査やバグ修正を困難にします。
Rust prevents this problem by refusing to compile code with data races!
Rust はデータレースのあるコンパイルを拒否することでこの問題を防いでいます。
Mutable References 残り
あるデータへの不可変参照がある時に同じデータへの可変参照を作成することもできない。
不可変参照の使用者は値が変更されていることを想定している。
複数の不可変参照は OK、なぜなら他のデータ読み込みに対して影響を及ぼさないため。
参照のスコープは作成されてから最後に使われるところまでとなっている。
不可変参照を使い終わった後、同じデータへの可変参照を作成するのは大丈夫。
この制約は煩わしく感じられるが、アクセスしたデータが考えていた値と異なるという状況が発生せず、追跡する必要がなくなるので便利である。
次は Dangling References
Dangling References
- ダングリングポインタとはよく理解できていないが、ポインタの指している先が既に解放されたメモリ領域であり、もしかすると無関係のデータを格納するために使われているメモリ領域である恐れのあるものと考えて良いのだろうか?
- Rust ではコンパイラがダングリングポインタ/参照が無いことを保証してくれるようだ。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
上記のコードを実行されると下記のエラーメッセージが表示されるようだ。
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | 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
|
5 | fn dangle() -> &'static String {
| +++++++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` due to previous error
エラーメッセージのなかで下記が重要らしい。
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
この関数の戻り値の型は借用された値を含んでいるがそのような値が無い、みたいな意味だろうか?
String インスタンスの s
のメモリ領域は dangle() 関数が終わると共に解放される(多分スタックとヒープの両方が解放される)。
したがってこのコードのコンパイルが通ってしまうと解放されたメモリ領域を指してしまうことになる。
上記のコードを直すには下記のようにして所有権ごと返せば良い。
fn no_dangle() -> String {
let s = String::from("from");
s
}
The Rules of References
- ある時点で持てるのは 1 つの可変参照か複数の不可変参照のみである。
- 参照先のデータは常に有効である必要がある、無効な場合はコンパイルが通らない。
次は The Slice Type
The Slice Type
- スライスを使うとコレクション内の要素の連続した並びを参照できる。
- スライスは参照なので所有権はない。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
len()
}
- String 型の as_bytes() メソッドを呼び出すことで u8 の配列への参照を取得できる。
- 配列の iter() メソッドを呼び出すことで要素を取得するためのイテレーターを取得できる。
- イテレーターの enumerate() メソッドを呼び出すことで
for
でタプルとしてアクセスできる。 - タプルの 2 つ目は参照なので参照先のデータ自体にアクセスするために
&
を使っている。 - Space の ASCII コードを取得するために
b' '
を使っている。
次は String Slices から
String Slices
文字列スライスは文字列の一部への参照である。
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
スライス自体は始点(inclusive)と終点(exclusive)で指定するが内部的には始点と長さを保持しているようだ。
..
はレンジ記法と呼ばれる。
始点が 0 の場合や終点が文字列の長さと同じ場合は省略できる。
例えば [..5]
や [6..]
のように書ける、また、両方省略して [..]
のようにも書ける。
文字列スライスは UTF-8 文字の境界に対して使用する必要がある。
文字列スライスのインデックスはバイト単位のようなので、絵文字とかではなく普通の日本語を扱う時も気をつける必要がありそうだ。
文字列スライスを使って first_word() 関数を書き換えると次のようになる。
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];
}
}
return &s[..];
}
文字列スライスの利点は参照先の文字列が有効であるかをコンパイラがチェックしてくれる点にある。
例えば次のようなプログラムを書くとコンパイルが失敗する。
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error!
println!("the first word is: {}", word);
}
Compiling string_slices v0.1.0 (/Users/susukida/workspace/rust/string_slices)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let word = first_word(&s);
| -- immutable borrow occurs here
5 |
6 | s.clear(); // error!
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("the first word is: {}", word);
| ---- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `string_slices` (bin "string_slices") due to 1 previous error
時間が来たので次は下記の部分から始めよう。
Recall from the borrowing rules that if we have an immutable reference to something, we cannot also take a mutable reference.
不可変参照がある場合、可変参照を持つことはできないルールがある。
clear() メソッドは文字列のデータを操作するので可変参照を取得する必要がある。
メソッド呼び出しはイメージ的には可変参照を引数とする関数を呼び出す感じなのかも知れない。
不可変参照を使い終わっていないのに、可変参照を取得しようとしてコンパイルエラーになっている。
文字列スライスを使うことは不可変参照を使っているのと同じとみなされるようだ。
String Literals as Slices
前に学んだように文字列リテラルは実行ファイル(バイナリ)内に格納されている。
let s = "Hello, world!";
のコードで s
の型は &str
になり、この参照はバイナリのあるメモリ領域を指している。
String Slices as Parameters
first_word() 関数のシグニチャは fn first_word(s: &String) -> &str
だが、fn first_word(s: &str) -> &str
と書くことで引数に文字列参照または文字列スライスを渡せるようになる。
こうすることで引数が文字列スライスの場合はそのまま渡せるのに加え、文字列の場合は参照かスライスを渡すことができるようになる。
let my_string = String::from("hello world");
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
let word = first_word(&my_string); // この行は上と同じになる。
let my_string_literal = "hello world";
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
let word = first_word(my_string_literal);
Other Slices
文字列スライスの他にも配列スライスがある。
配列の一部を参照したい場合は下記のように書く。
let array = [1, 2, 3, 4, 5];
let slice = &[1..3];
assert_eq(slice, &[2, 3]);
slice
の型は &[i32]
になる。
配列リテラルは文字列リテラルと同様に開始位置と件数を格納している。
まだ学んでいないがコレクションと呼ばれるものについてはスライスが利用できる。
次は Using Structs to Structure Related Data
Using Structs to Structure Related Data
- 構造体とは自分で作るデータ型であり、複数の値を 1 つにまとめて名前を付けることができる。
- 複数の値を扱う点ではタプルと似ているが、それぞれ適するユースケースが異なる。
Defining and Instantiating Structs
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
println!(
"{}, {}, {}, {}",
user1.active, user1.username, user1.email, user1.sign_in_count
);
}
- 構造体内の個々の値は「フィールド」と呼ばれる。
- 構造体のインスタンスを作成するには
User { ... }
のように書く。 - インスタンス化のフィールドの並びは自由だがなるべく合わせた方がわかりやすいかも。
- フィールドにアクセスするには
.
記法を使う。
Using the Field Init Shorthand
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
構造体のフィールド名が変数やパラメーターの名前と同じ場合は省略できる。
Creating Instances from Other Instances with Struct Update Syntax
let user1 = build_user(
String::from("someone@example.com"),
String::from("someusername123"),
);
let user2 = User {
email: String::from("another@example.com"),
..user1
};
構造体更新構文を使うことで一部を除いて同じ内容の別の構造体を簡潔に作成できる。
TypeScript と似ているが ..
とドットが 2 個の点と元の構造体を最後に書く点が異なる。
構造体更新構文は代入を使っているので、所有権の移転が行われることに注意する。
上の例の場合は user1.username の所有権が user2.username に移転しているので、user1.username を使うことができなくなる。
In this example, we can no longer use user1 as a whole after creating user2 because the String in the username field of user1 was moved into user2.
ドキュメントには上記のように書かれているが user1.username 以外は問題なく使える気がする。
もし username についても新しい文字列インスタンスを指定していれば所有権の移転は発生しないので、user1 の全てのフィールドは問題なく使える。
Copy トレイトを実装していればわざわざ明示的に指定しなくても所有権の移転を行わずに済みそうだ。
次は Using Tuple Structs Without Named Fields to Create Different Types
Using Tuple Structs Without Named Fields to Create Different Types
- Rust ではタプルとよく似たタプル構造体をサポートしている。
- タプル構造体はフィールドの型だけを指定する。
- タプル構造体はタプルに名前を付け、他のタプルとは異なることを示すのに便利である。
- また色や座標などフィールド名をつけなくてもわかりそうなものを扱うのに便利である。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
- 上記の例ではタプルとしての型は同じだが別の型として扱われる。
-
.
を使ってフィールドにアクセスできる点や分解できる点は通常のタプルと同じである。
Unit Like Structs Without Any Fields
- フィールドを持たない構造体も定義でき、unit-like 構造体と呼ばれる。
- 日本語訳はユニット類似型だろうか?
- 何かの型のトレイトを実装するのに便利とのことで、トレイトについては後から学習する。
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
Imagine that later we’ll implement behavior for this type such that every instance of AlwaysEqual is always equal to every instance of any other type, perhaps to have a known result for testing purposes.
上の文章の理解が及ばない、下記は機械翻訳の結果。
後で、AlwaysEqual のすべてのインスタンスが他の型のすべてのインスタンスと常に等しいように、この型の動作を実装して、おそらくテスト目的で既知の結果を取得することを想像してください。
おそらくテスト目的で既知の結果を得るために、後ほど私たちがこの型の振る舞いを AlwaysEqual のすべてのインスタンスが他の型のすべてのインスタンスと等しいように実装することを想像してください。
これは現段階では理解が及ばないが、トレイトがわかったら理解できるようになるのかも知れない。
次は Ownership of Struct Data
Ownership of Struct Data
- User 構造体では
&str
ではなくString
を使っているがこれは意図的な選択である。 - 理由は構造体のそれぞれのインスタンスがデータの所有権を持っており、構造体が有効な限りデータも有効であるようにしたかったから。
-
&str
を使うことも可能だが、そうするためには「ライフタイム」と呼ばれる Rust の機能を使う必要があり、ライフタイムについては後のチャプターで学習する。 - ライフタイムは構造体によって参照されるデータが有効であることを構造体が有効である限り確実にする。
struct User {
active: bool,
username: &str,
email: &str,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: "someusername123",
email: "someone@example.com",
sign_in_count: 1,
};
}
もし上記のようなコードを書いた場合には下記のエラーメッセージが表示される。
Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
--> src/main.rs:3:15
|
3 | username: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 ~ username: &'a str,
|
error[E0106]: missing lifetime specifier
--> src/main.rs:4:12
|
4 | email: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 | username: &str,
4 ~ email: &'a str,
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` due to 2 previous errors
名前付きライフタイムパラメーターが必要とのこと。
An Example Program Using Structs
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
The area of the rectangle is 1500 square pixels.
上記のコードを構造体を使ってリファクタリングしていく。
Refactoring with Tuples
fn main() {
let rect1 = (30, 50);
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
少しよくなったが 0 番目が横幅なのか高さなのかわからない点でまだ改善の余地がある。
次は Refactoring with Structs: Adding More Meaning
Refactoring with Structs: Adding More Meaning
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
- 構造体を使うとグループと個々のフィールドに名前を与えることができる。
- area() 関数の引数は参照なっており、借用を使うことでデータの所有権は main() 関数から移転しない。
もうこれでリファクタリング完了というレベルな気がする。
Adding Useful Functionality with Derived Traits
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {}", rect1);
}
上記のコードをコンパイルすると下記のエラーメッセージが表示される。
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
--> src/main.rs:33:29
|
33 | println!("rect1 is {}", rect1);
| ^^^^^ `Rectangle` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error
- println! マクロは
{}
を読み取ると Display と呼ばれるものを使ってエンドユーザー向けの出力を生成する。 - 整数などの基本的な型は Display を実装しているが、構造体の場合は自分で実装する必要がある。
-
{}
の代わりに{:?}
を使うと下記のエラーメッセージが表示される。
error[E0277]: `Rectangle` doesn't implement `Debug`
--> src/main.rs:33:31
|
33 | println!("rect1 is {:?}", rect1);
| ^^^^^ `Rectangle` cannot be formatted using `{:?}`
|
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `Rectangle` with `#[derive(Debug)]`
|
22 + #[derive(Debug)]
23 | struct Rectangle {
|
For more information about this error, try `rustc --explain E0277`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error
-
{:?}
は Debug と呼ばれるものを使って出力を生成することを伝えている。 - Debug トレイトを使うと構造体の内容を開発者向けに出力できる。
- ただしデバッグ情報の出力は明示的にオプトインする必要があり、そのためには
#[derive(Debug)]
を前の行に書く。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
rect1 is Rectangle { width: 30, height: 50 }
次は下記から始めよう。
Nice! It’s not the prettiest output, but it shows the values of all the fields for this instance, which would definitely help during debugging.
{:#?}
を使うと改行して表示されるようになるらしい、やってみよう。
rect1 is Rectangle {
width: 30,
height: 50,
}
さらに !dbg
というマクロもあり、ファイル名や行番号も表示してくれるようだ。
ただし所有権が行ったり戻ったりするので参照を渡すように注意した方が良さそうだ。
また、出力先が標準エラー出力となる点についても println!
マクロと異なる。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
[src/main.rs:31:16] 30 * scale = 60
[src/main.rs:35:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
Rust では Debug の他にも derive
可能なトレイトが用意されており、これらを使うことで自作の型に便利な振る舞いを追加できる。
またトレイト自体を自作することもできる。
また derive
の他にも属性がある。
area 関数は Rectangle 専用なので、さらに Rectangle に強く関連付けられたら便利である。
このために次はメソッドについて学ぶ。
Method Syntax
- メソッドは関数と似ているが構造体などの文脈内で定義されることや、最初のパラメーターが
self
である点が異なる。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
The area of the rectangle is 1500 square pixels.
- メソッドは
impl
ブロック内で定義していく。 - 第 1 引数は
&self
とする、型アノテーションは不要。 - メソッドを呼び出す時にはインスタンスの後に
.area()
と書く、これはメソッド構文と呼ばれるらしい。 -
&self
はself: &Self
の省略形らしい、impl ブロック内ではSelf
は構造体などを指すようだ、この場合は Rectangle になる。 - メソッドは所有権を取得することもできるようだ、また可変参照もできる。
- メソッドは構造体などに関連付けられているだけで基本的に関数と同じことは全てできそうだ。
- 可変参照が必要な場合は第 1 引数を
&mut self
とすれば良い、所有権が必要な場合はself
とすれば良い。 - とはいえ多くのケースでは
&self
を使い、次いで&mut self
を使うことになりそうだ。 -
self
を使うケースとしては何かに変換し、その後は変換前の値を使わせないようにする、みたいなケースのようだ。 - メソッドを使う主な目的はソースコードの組織化にあり、構造体などに関連するメソッドを集約することにある。
- メソッド名はフィールドと同じ名前であっても構わない。
- 多くのケースではメソッド名とフィールド名が同じ場合はそのまま値を返すだけの関数を実装し、これはゲッターと呼ばれる。
- Rust はゲッターを自動的には実装しない。
- ゲッターはフィールドを読み込み専用にしたい場合などに便利である。
- フィールドやメソッドの公開/非公開については後のチャプターで学習する。
次は Whare's the -> Operator?
Whare's the -> Operator?
- C や C++ ではオブジェクトかオブジェクトへのポインターかによって
.
と->
を使い分ける必要があった。 - Rust ではメソッドの第 1 引数によって
&
や&mut
や*
が自動的に付加される。
Methods with More Parameters
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Self) -> bool {
return self.width > other.width && self.height > other.height;
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
Can rect1 hold rect2? true
Can rect1 hold rect3? false
Associated Functions
- 第 1 引数が
&self
などではないメソッドは関連関数と呼ばれる。 - 例えば
String::from
などがそうである。 - 関連関数はコンストラクタとしてよく使われ、
new
と名付けられることが多い。 - 関連関数を呼び出すにはインスタンス名 +
.
ではなく構造体名 +::
を使う。 -
::
演算子は関連関数だけではなくモジュール名前空間にも使われるようだ。
impl
Blocks
Multiple
- 同じ構造体に対して複数の
impl
ブロックがあっても問題ない。 - 今はこの利便性がわからないが後のチャプターで学んでいく。
次のスクラップ
だいぶ長くなってきたので 6〜10 章用のスクラップを別に作ろう。
あとスクラップの名前も変えよう。