Rust の the book の 6〜10 章までを読む
はじめに
このスクラップでは Rust の the book を読んで学んでいく過程を記録していく。
前のスクラップ
Enums and Pattern Matching
列挙体の格値はバリアントと呼ばれるようだ。
Defining an Enum
- 列挙体は取り得る値(バリアント)のうちの 1 つであるものを表現するのに適している、例えば YES や NO など。
- YES や NO なら bool を使えば良いが、種類が 3 種類以上になると列挙体の出番だ。
enum IpAddrKind {
V4,
V6,
}
Rust では構造体や列挙体はキャメル記法のようだ、またバリアントもキャメル記法のようだ。
Enum Values
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
fn route(ip_kind: IpAddrKind) {}
route(IpAddrKind::V4);
route(IpAddrKind::V6);
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let home = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
C 言語などであれば上記で十分だが、Rust ではさらに下記のようにまとめられるようだ。
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IPAddr::V6(String::from("::1"));
バリアントごとに別のデータ型であっても問題はない。
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String)
}
let home = IpAddr::V4(127. 0, 0, 1);
let loopback = IPAddr::V6(String::from("::1"));
上記のように IP アドレスを格納するデータ型を自分で定義することもできるが、実用上は下記の標準ライブラリに含まれる std::net::IpAddr 列挙体を使えば良い。
標準ライブラリでは下記のように定義されている。
struct Ipv4Addr {
// private fields
}
struct Ipv6Addr {
// private fields
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
列挙体には下記のように様々な型のデータを組み込める。
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
バリアントに組み込むデータ型には下記のように構造体を使うこともできる。
struct QuitMessage;
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String);
struct ChangeColorMessage(i32, i32, i32);
ただし列挙体を使った方が関数の引数を与える時などに便利なのでなるべくこちらを使うのが良いのかも知れない。
なんと列挙体にメソッドを定義することもできる。
impl Message {
fn call(&self) {
// ...
}
}
let m = Message::Write(String::from("hello"));
m.call();
次は The Option Enum and Its Advantages Over Null Values
The Option Enum and Its Advantages Over Null Values
- Option は結果が何かの値か、あるいは値が何もないケースに使用される。
- 例えばリストの最初の要素を取得する関数の場合、空のリストであれば何も得られず、そうでなければ何かの与えが得られる。
- Rust には null がない、なんということだ。
- Null の発明者であるトニーさんは「null は 10 億ドル単位のミスだった」と言っているらしい。
- できれば参照が安全に使用されているかどうかをコンパイラが自動的にチェックできるようにしたかったが、null は実装するのがあまりにも簡単なのでその誘惑に抗うことができなかった。
- 結果的に数え切れないほどのエラーや脆弱性やクラッシュがもたらされ、過去 40 年間にわたって 10 億ドル単位の苦しみと損失を招いただろう。
トニーさんの経験談はとてもためにになる。
- Null が表現しようとしていること自体は便利なものだ。
- Null が表現しようとしていることは値が現在無効であるか何らかの理由で不在であることだ。
- それを表現しようとすることが問題なのではなく、それを null を使って実装しようとすることが問題である。
- Rust では null が無い代わりに
Option<T>
を使って 存在/不在の値を扱っている。 -
Option<T>
の定義は下記の通り。
enum Option<T> {
None,
Some(T),
}
-
Option<T>
列挙体はプレリュードに含まれているので明示的にインポートする必要がない。 - バリアントもプレリュードに含まれているので
Option::
も前置する必要がない。 -
<T>
はジェネリック型パラメーターと呼ばれるものであり後のチャプターで学ぶ。 - 今のところは
Option
列挙体はどんな型のデータにも使用できることを理解していれば良い。
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
次は下記から始めよう。
The type of some_number is Option<i32>.
- Some バリアントを使った場合には最初のパラメーターの型から
Option<T>
を推論できる。 - None バリアントを使った場合は型アノテーションを追加する必要がある。
-
Option<T>
が null よりも優れている点は、それがT
とは異なる型なのでコンパイラーがT
型の値と同じように扱うことを禁止する点だ。
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y; // この行でエラーになる。
上記のコードをコンパイルすると下記のエラーメッセージが表示される。
cannot add
Option<i8>
toi8
-
i8
型は常に値が存在することが保証されるが、Option<i8>
はそうではない。 - したがって使用前に値が存在することを確認する必要がある、言い換えると
Option<i8>
をi8
に変換する必要がある。 - コンパイル時にエラーになることによって本当は null なのに null ではないと仮定してしまう問題を検出できる。
-
Option<T>
を使うことで使用前に値が不在のケースを処理することが強制される。 - バリアントによって実行するコードを変えたい場合には match 式が便利である。
- match 式では列挙体に値が含まれる場合にその値にアクセスできる。
次は The match Control Flow Construct
The match Control Flow Construct
- Rust では match を使って特定のパターンにマッチする場合に式を評価したり文を実行したりする機能がある。
- パターンマッチにはリテラル、変数名、ワイルドカードなど様々なものが利用できるらしい。
- マッチ式の素晴らしい点はすべてのありうる可能性を処理している点をコンパイラが確認できる点である。
- マッチ式はコイン振り分け機に例えられる:コインがコロコロと転がっていて一番最初にフィットした穴に入っていくイメージである。
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
- マッチ式は if 式と似ているが条件が boolean ではなく値である点が異なる。
- マッチ式のそれぞれのパターンとコードの組み合わせは「アーム」と呼ばれる。
- アームでは
=>
オペレーターを使用する、TypeScript のアロウ関数とごっちゃになりそうだ。。。 - アームはカンマで区切られる。
- アームのコードは式であり、式の評価結果はマッチ式の評価結果になる。
- アームのコードには
{}
を使えるが、1行の場合は省略できる。 -
{}
を使った場合にはカンマはあってもなくても良い。
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
次は Patterns That Binds to Values
Patterns That Binds to Values
マッチ式の便利な機能は列挙体にバインドされた値を取り出せる点である。
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// ...
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
},
}
}
列挙体にバインドされた値を取り出すにはアームのパターン部に (state)
のように書く。
次は Matching with Option<T>
Matching with Option<T>
Coin 列挙体と同様に Option<T> 列挙体についても同様に値を取り出すことができる。
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(x) => Some(x + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
Matches Are Exhaustive
- match 式はすべての可能性が考慮されていなければならない。
- 例えば下記のコードをコンパイルすると
non-exhaustive patterns:
Nonenot covered
とエラーメッセージが表示される。
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(x) => Some(x + 1),
}
}
- Rust は全ての可能性を考慮していないことを検出するのに加え、どのようなパターンが考慮されていないかまでを教えてくれる。
- Exhaustive とは「網羅的」などに和訳され、漏れがないことを意味する。
- マッチ式が網羅的になっているおかげで null のように考慮していないケースがあることを防げる。
Catch-all Patterns and the _ Placeholder
other
を使うと該当しなかった全てのパターンにマッチさせることができる、switch 文の default:
のようなイメージだ。
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(dice_roll),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
列挙体の値を使用しない場合は other
の代わりに _
を使用する。
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
特定のパターンに該当しない時に何もしない場合は明示的に _ => ()
と書く。
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
other
や _
は最後のアームである必要がある。
次は Concise Control Flow with if let
Concise Control Flow with if let
マッチ式で 1 つのパターンにマッチした時のみコードを実行し、それ以外の場合は何もしない場合には if let
構文を使える。
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
上記の代わりに下記のように書ける。
let config_max = Some(3u8);
if let config_max = Some(max) {
println!("The maximum is configured to be {}", max);
}
-
=>
の代わりに=
を使う、==
と書いてしまいそうなので注意が必要。 -
if let
構文のデメリットとして網羅性のチェックが損なわれること。 -
if let
構文にはelse
をつなげることができ、catch-all にマッチした場合のコードを指定できる。
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}
if let
では左辺値と右辺値が入れ替わっても良いのだろうか?
fn main() {
let some = Some(1);
if let Some(value) = some {
println!("value is {value}");
}
}
上はコンパイルが通るが下はコンパイルが通らない。
fn main() {
let some = Some(1);
if let some = Some(value) {
println!("value is {value}");
}
}
error[E0425]: cannot find value `value` in this scope
--> src/main.rs:4:24
|
4 | if let some = Some(value) {
| ^^^^^ not found in this scope
error[E0425]: cannot find value `value` in this scope
--> src/main.rs:5:29
|
5 | println!("value is {value}");
| ^^^^^ not found in this scope
For more information about this error, try `rustc --explain E0425`.
error: could not compile `if_let` (bin "if_let") due to 2 previous errors
順序を間違えそうだ。
次は Managing Growing Projects with Packages, Crates, and Modules
Managing Growing Projects with Packages, Crates, and Modules
- パッケージ・クレート・モジュールは関連する機能をまとめたり異なる機能を別のファイルに分けたりすることで、ある機能が実装されている位置を探したり新しい機能を追加すべき場所を明確にすることを助ける。
- パッケージは複数のバイナリクレートと多くても 1 つのライブラリクレートを含む。
- パッケージが大きくなってきたら外部依存になる別のクレートに分けることができる。
- とても大きなプロジェクトの場合は Cargo はワークスペース機能が便利である。
- 実装の詳細を隠す方法についても学んでいく。
- 関連する概念としてスコープがある。
- コードが書かれているネストされた文脈にはスコープ内として定義された名前があり、プログラマーやコンパイラーはある名前が何を指して何を意味するのかを知る必要がある(?)
A related concept is scope: the nested context in which code is written has a set of names that are defined as “in scope.” When reading, writing, and compiling code, programmers and compilers need to know whether a particular name at a particular spot refers to a variable, function, struct, enum, module, constant, or other item and what that item means.
- 同じスコープ内で同じ名前は 1 回しか使うことができない。
- Rust ではコードを構成するための手段が提供されており、これらはモジュールシステムと呼ばれることがある。
- パッケージ:Cargo の機能であり、複数のクレートをビルド・テスト・配布できる。
- クレート:モジュールのツリーであり、ライブラリや実行可能ファイルを生成できる。
- モジュール:コードの構成・スコープ・パスの公開/非公開を制御できる。
- パス:構造体・関数・モジュールの名前を付ける方法。
次は Packages and Crates
Packages and Crates
- クレートはコンパイラが一度に考慮するコードの最小単位でである。
- 1 つのソースコードに対して rustc コマンドを実行した時であってもコンパイラはそのファイルをクレートとして扱う。
- クレートは複数のモジュールを含むことができる。
- クレートにはライブラリとバイナリの 2 種類がある。
- ライブラリクレートは main 関数を持たず、複数のプロジェクトで共有される機能を定義する。
- 単純にクレートと言った場合はライブラリクレートを指すことが多く、クレートは他のプログラミング言語ではライブラリやパッケージと同じような意味で用いられる。
- クレートルートとはコンパイラが最初に処理するソースコードであり、ルートモジュールが作成される。
- パッケージとは 1 つ以上のクレートの集合であり、これらのクレートをコンパイルするための情報が Cargo.toml に含まれている。
- Cargo 自体もクレートであり、cargo コマンドのバイナリクレートと API を提供するライブラリクレートが含まれている。
- パッケージは複数のバイナリクレートを含むことができるが、ライブラリクレートは最大で 1 つまで。
- Cargo.toml には src/main.rs がルートモジュールであることが記載されていないが、バイナリクレートのクレートルートのデフォルトが src/main.rs であるという規約があるので cargo はそれに従っている。
- この場合、バイナリクレートの名前はパッケージと同じになる。
- ライブラリクレートのデフォルトは src/lib.rs であるという規約もある。
- 複数のバイナリクレートを含めたい場合は src/bin ディレクトリにソースコードを配置する。
次は Defining Modules to Control Scope and Privacy
Defining Modules to Control Scope and Privacy
-
use
キーワードを使うとパスをスコープに含めることができる。 -
pug
キーワードを使うと関数や構造体などを公開できる。
Modules Cheat Sheet
- コンパイラーは最初にクレートルートファイルを探す、これはライブラリクレートの場合は src/lib.rs、バイナリクレートの場合は src/main.rs がデフォルトになっている。
- クレートルートファイルではモジュールを宣言できる。
- 例えば garden モジュールを定義する場合は
mod garden;
のように書く。 - モジュールの実装コードはインライン、src/garden.rs、src/garden/mod.rs のいずれかで定義される。
- クレートルートファイル以外でもサブモジュールを宣言できる。
- 例えば vegetables サブモジュールを定義する場合は src/garden.rs に
mod vegetables;
と書く。 - サブモジュールの実装コードはインライン、src/garden/vegetables.rs、src/garden/vegetables/mod.rs のいずれかで定義される。
- モジュールが公開する関数などには同じクレートのどこからでもアクセスできる。
- 例えば vegetables モジュールの Asparagus 構造体にアクセスするには
crete::graden::vegetables::Asparagus
と書く。 - デフォルトではモジュール内のコードは非公開であり、アクセスできるようにするにはモジュール自体と関数などの両方を
pub
キーワードを使って公開する必要がある。 -
use
キーワードを使うとパスを省略できる、例えばuse crete::graden::vegetables::Asparagus;
と書くと単にAsparagus
と書くだけで構造体を利用できるようになる。
cargo new backyard
cd backyard
mkdir src/garden
touch src/garden.rs
touch src/garden/vegetables.rs
疲れたので続きは後から。
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {:?}", plant);
}
pub mod vegetables;
#[derive(Debug)]
pub struct Asparagus {}
I'm growing Asparagus
- クレートルートファイルは src/main.rs である。
- 実際にコーディングする際にはトップダウンで書いていった方が良い、ボトムアップで書こうとすると VSCode の Rust エクステンションから何やらメッセージが表示される。
-
pub mod garden;
やpub mod vegetables;
を書くと garden.rs や vegetables.rs がクレートに含まれるようになる。
Grouping Related Code in Modules
- モジュールは可読性と再利用性を高めるためにクレート内でソースコードを整理するのに役立つ。
- モジュールではアイテムの公開/非公開を制御でき、デフォルトでは非公開になる。
- モジュールとアイテムの両方を公開することでモジュール外からアクセスできるようになる。
cd ~/workspace/rust
cargo new restaurant --lib
cd restaurant
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
- src/main.rs や src/lib.rs はクレートルートと呼ばれる。
- この名前の理由はこれらのファイルのいずれかの内容が
crete
と名付けられたモジュールを形成するからである。 -
crete
は「モジュールツリー」と呼ばれるクレートのモジュール構造の根本に位置する。
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
次は Paths for Referring to an Item in the Module Tree
Paths for Referring to an Item in the Module Tree
- パスはモジュールツリー内にあるアイテムを特定するために使用される。
- パスには絶対パスと相対パスの 2 種類がある、絶対パスは
crete
から始まり、相対パスはself
やsuper
などから始まる。 - パスの区切りにはダブルコロン
::
を使う。
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// 絶対パス
crete::front_of_house::hosting::add_to_waitlist();
// 相対パス
front_of_house::hosting::add_to_waitlist();
}
- 絶対パスと相対パスのどちらを使うかは選択する必要がある。
- ファイル移動する時に一緒に動くのであれば相対パスを使う方が良い。
- 絶対パスを使っていもて VSCode がよろしくやってくれそうな気がする。
- 上記のコードをコンパイルすると下記のエラーメッセージが表示される。
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
error[E0603]: module `hosting` is private
--> src/lib.rs:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors
洗濯が終わったので続きは午後にやろう。
- エラーメッセージの内容は hosting モジュールが非公開であること。
- Rust では原則としてアイテムはデフォルトで非公開となる。
- 親モジュールから子モジュールのアイテムにはアクセスできないが、その逆は可能である。
- アイテムを公開するには
pub
キーワードを使用する。
pub
keyword
Exposing Paths with the
- アイテムを公開するにはモジュールとアイテムの両方を公開する必要がある。
- 仮にモジュールだけを公開すると下記のエラーメッセージが表示される。
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:18:37
|
18 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:19:30
|
19 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
下記のように両方に pub
をつけることによってコンパイルが成功するようになる。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
crate::front_of_house::hosting::add_to_waitlist();
front_of_house::hosting::add_to_waitlist();
}
- 絶対パスは
crate
から始まり、crate
はクレートのモジュールツリーの根を表す。 -
front_of_house
モジュールはクレートルートに定義されており、公開はされていないが eat_at_restaurant 関数と同じモジュールに含まれているのでモジュールにはアクセスすることができる。 -
hosting
モジュールもpub
キーワードによって公開されているのでアクセスできる。 -
add_to_waitlist
関数もpub
キーワードによって公開されているのでアクセスできる。 - 相対パスも原理は同じだが最初が
crate
ではなくfront_of_house
から始まっている点が異なっている。 - 相対パスの場合は関数などが定義されている位置からの始点となる。
- クレートを開発して公開する場合、
pub
をつけたアイテムは依存するクレートに影響を与えるので慎重に考える必要がある。
次は Best Practices for Packages with a Binary and a Library
Best Practices for Packages with a Binary and a Library
- パッケージにはデフォルトのバイナリクレートとライブラリクレートの両方を含むことができる。
- この性質を利用してライブラリクレートにロジックを書いておき、バイナリクレートからライブラリクレートのコードを呼び出すという設計パターンがある。
- こうすることで他のプロジェクトはライブラリクレートの API にアクセスできる利点がある。
Starting Relative Paths with super
-
super
を使うことで親モジュールを起点とする相対パスでアイテムを指定できる。 - ファイルシステムの
..
によく似ている。
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
- 上記のコードでは fix_incorrect_order 関数内のコードは back_of_house モジュール内にある。
- back_of_house モジュールの親はクレートルートなので、クレートルートに定義された deliver_order を呼び出すことができる。
Making Structs and Enums Public
- 構造体や列挙体についても
pub
キーワードを使って公開できる。 - ただし構造体についてはフィールドやメソッドにも個々に
pub
を指定しないと公開されない。
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("What");
println!("I'd like {} toast please", meal.toast);
// 次の行は seasonal_fruit が非公開フィールドなのでコンパイルできない。
// meal.seasonal_fruit = String::from("blueberries");
}
上記のコードでは Breakfast 構造体は非公開フィールドを持っているので、インスタンスを作成する関連関数を設ける必要がある。
関連関数でなかったらどうなるのだろう?と気になって試したところ同じモジュール内の関数であれば問題はなかった。
ただ、関連関数として作成しておくほうがわかりやすくて良いだろう。
列挙体を公開するとバリアントも一緒に公開される。
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
構造体で一部のフィールドを非公開にすることはあっても、列挙体でバリアントの一部を非公開にすることはほぼないのでこのような仕様になっている。
use
Keyword
次は Bringing Paths Into Scope with the
use
Keyword
Bringing Paths Into Scope with the use
キーワードを使うことでスコープにパスを持ち込むことができ、毎回相対パスや絶対パスで指定する必要がなくなる。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
use は特定のスコープに対してのみパスを持ち込むので、スコープが異なる場合は注意する必要がある。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
mod customer {
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
}
error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
--> src/lib.rs:79:9
|
79 | hosting::add_to_waitlist();
| ^^^^^^^ use of undeclared crate or module `hosting`
|
help: consider importing this module through its public re-export
|
78 + use crate::hosting;
|
warning: unused import: `crate::front_of_house::hosting`
--> src/lib.rs:75:5
|
75 | use crate::front_of_house::hosting;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` (lib) due to 1 previous error; 1 warning emitted
use
Paths
Creating Idiomatic
モジュールと関数のどちらのパスに対して use
を使えば良いか悩ましいケースがあるが、慣習的にはモジュールに対してパスを使った方が良いようだ。
こうすることで別モジュールの関数呼び出しであることが明確化される。
一方、構造体や列挙体の場合はアイテムそのものに対して use
を使う方がより慣習的になっている。
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
もし名前が衝突してしまう場合にはモジュールをスコープに持ち込む方が良い。
use std::fmt;
use std::io;
fn function1() -> fmt::Result {}
fn function2() -> io::Result<()> {}
as
Keyword
Providing New Names with the
as
キーワードを使うことでスコープに持ち込むアイテムに別名をつけることができる。
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {}
fn function2() -> IoResult<()> {}
pub use
次は Re-exporting Names with
Re-exporting Names with pub use
use
を使ってスコープに持ち込まれる名前は公開されず、公開したい場合は pub use
を使う。
このようなテクニックは再エクスポートと呼ばれる。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
上記のコードでは restaurant::hosting::add_to_waitlist()
で関数を呼び出せるようになっている。
再エクスポートは内部の構造と外側から見えるインタフェースが異なる場合に便利である。
Using External Packages
外部パッケージを使用したい場合は Cargo.toml に依存関係を追加する。
そうするだけでビルド時に自動的に Cargo が依存関係をダウンロードして解決してくれる。
パッケージ内の名前を使うときには use
でスコープにインポートする。
std
も外部パッケージであるが Rust に同梱されているので依存関係として追加する必要はないが use
でインポートする必要はある。
use
Lists
Using Nested Paths to Clean Up Large
use std::cmp::Ordering;
use std::io;
上記のコードは下記のようにまとめられる。
use std::{cmp::Ordering, io};
use std::io;
use std::io::Write;
上記のコードは下記のようにまとめられる。
use std::io::{self, Write};
The Glob Operator
パス内に定義されているすべてのアイテムをスコープに持ち込みたい場合は下記のように書ける。
use std::collections::*;
このようなグロブオペレーターを使う場合は名前が何を指しているかやどこで定義されているかがわかりにくくなることがあるので注意する必要がある。
グロブオペレーターは主に単体テスト目的で使用される、また「プレリュード」と呼ばれるパターンでも使用される。
次は Separating Modules into Different Files
Separating Modules into Different Files
rm -rf restaurant
cargo new --lib restaurant
cd restaurant
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
上記のコードをファイルに分けていく。
touch src/front_of_house.rs
mkdir src/front_of_house
touch src/front_of_house/hosting.rs
まずは front_of_house モジュールのファイルを分ける。
pub mod hosting {
pub fn add_to_waitlist() {}
}
次に hosting サブモジュールのファイルを分ける。
pub mod hosting;
pub fn add_to_waitlist() {}
Alternative File Paths
上記の例で src/front_of_house.rs の代わりに src/front_of_house/mod.rs も利用できるがエディタで mod.rs だらけになるのであまり推奨されない。
mod.rs を使うのは古いスタイルとされている。
同じプロジェクトで両方を使うことはできるがわかりにくくなるので新しいプロジェクトでは mod.rs を使わない方が良い。
ただし同じモジュールに対して両方のファイルを同時に使おうとした場合はエラーになる。
Summary
- Rust ではパッケージを複数のクレートに分けることができ、クレートも複数のモジュールに分けることができる。
- モジュール内のアイテムには相対パスか絶対パスを使って参照できる。
-
use
文を使うことでパスをスコープに持ち込むことができる。 - モジュールのコードはデフォルトで非公開だが
pub
キーワードを使って公開することができる。
次は Common Collections
Common Collections
- コレクションは複数の値を含むことができるデータ構造である。
- 配列やタプルのデータはスタックに格納されるが、コレクションのデータはヒープに格納される点が異なる。
- このチャプターではベクタ、文字列、ハッシュマップの 3 つのコレクションを学んでいく。
Storing Lists of Values with Vectors
- ベクタは一連のメモリ上に複数の値を配置していくデータ構造である。
- 拡張できる配列と考えるとイメージがしやすい。
Creating a New Vector
let v: Vec<i32> = Vec::new();
- 空のベクトルを初期化する場合には型アノテーションが必要になる。
- ただし、後のコードで推論可能な場合は省略できるケースがある。
let v = vec![1, 2, 3];
初期値がある場合は vec!
マクロを使用できる、この場合は型アノテーションは不要である。
Updating a Vector
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
push
はベクタの末尾に要素を挿入するメソッドである。
他の変数と同様に更新するにはベクタを可変(mutable)にする必要がある。
次は Reading Elements of Vectors
Reading Elements of Vectors
cargo new vectors
cd vectors
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third = &v[2];
println!("The third element is {third}");
let third = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
}
The third element is 3
The third element is 3
- ベクタの要素を取得するには添え字アクセスか get メソッドを使用する。
- 添え字アクセスの場合は範囲を超えているとパニックになるが get メソッドの場合は戻り値が Option<T> 型なので match 式でコントロールできる。
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let third = &v[2];
v.push(6);
println!("The third element is {third}");
}
上記のコード例では借用チェッカーにより下記のエラーメッセーが表示される。
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:5:5
|
4 | let third = &v[2];
| - immutable borrow occurs here
5 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
6 | println!("The third element is {third}");
| ------- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
- 前に学んだ通り Rust では同時に存在できるのは 1 つの可変参照か 1 つ以上の不可変参照のいずれかだけなのでエラーになる。
-
v.push(6);
が最後の行になるとコンパイルできるようになる。 - ベクタでは要素の追加などによってメモリを再配置する可能性があり、再配置前に作成された参照は無効になる可能性がある。
次は Iterating over the Values in a Vector
Iterating over the Values in a Vector
for 文を使うことでベクタを走査できる、下記の例では各要素への不可変参照を取得している。
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
}
100
32
57
可変参照を取得するには &
の代わりに &mut
を使う。
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
for i in v {
println!("{i}");
}
}
150
82
107
ちなみに &
も &mut
もつけないことが可能なようだ。
要素の中身を書き換えるために *
演算子を使っているがこれについては後のチャプターで学ぶ。
for ループ内でベクタの要素を増減させようとするとコンパイルエラーになる。
for i in &mut v {
*i += 50;
v.pop();
}
error[E0499]: cannot borrow `v` as mutable more than once at a time
--> src/main.rs:21:9
|
19 | for i in &mut v {
| ------
| |
| first mutable borrow occurs here
| first borrow later used here
20 | *i += 50;
21 | v.pop();
| ^ second mutable borrow occurs here
For more information about this error, try `rustc --explain E0499`.
Using an Enum to Store Multiple Types
ベクタ単体では 1 つのデータ型しか扱えないが列挙型と組み合わせることで複数のデータ型を扱えるようになる。
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
fn main() {
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
for cell in &row {
match cell {
SpreadsheetCell::Int(v) => println!("Int: {v}"),
SpreadsheetCell::Text(v) => println!("String: {v}"),
SpreadsheetCell::Float(v) => println!("Float: {v}"),
}
}
}
Int: 3
String: blue
Float: 10.12
もしコンパイル時点でどのような型を格納するかを確定できない場合はトレイトオブジェクトを使えるらしい、これについてはチャプター 17 で学ぶ。
ベクタの便利なメソッドについては公式ドキュメントで詳しく説明されている。
次は Dropping a Vector Drops Its Elements
Dropping a Vector Drops Its Elements
ベクタもスコープから外れるとメモリが解放され有効ではなくなる。
{
let v = vec![1, 2, 3, 4];
} // <- v がスコープから外れてメモリが解放される。
ベクタが解放される時、ベクタ内の全ての要素も同時に解放される。
借用チェッカーはベクタ内の要素への参照がベクタ自体が有効である間だけ使用されていることを確認してくれる。
Storing UTF-8 Encoded Text With Strings
初心者にとって Rust の文字列は次の 3 つの理由でハマりやすい。
- 起こり得るエラーを表示する Rust の性質
- プログラマーが考えているより文字列が複雑なデータ構造であること
- UTF-8
文字列はバイト列なのでコレクションの 1 つではあるが、計算機が扱うバイトと人間が扱う文字に違いがあるので添え字アクセスなどで注意すべき点がある。
What Is a String?
- Rust では言語の機能として文字列スライスが提供されている、これは通常
&str
のように参照と組み合わせて使用する。 - 文字列スライスとは UTF-8 エンコードされた文字列データへの参照である。
- 例えば文字列リテラルはバイナリに含まれており、リテラルへのスライスはバイナリのアドレスへの参照となる。
- 一方、String 型は Rust の標準ライブラリから提供されている。
- String 型は追加・変更が可能でプログラムで所有された UTF-8 エンコードされた文字列データの型である。
- Rust で「文字列」と言う場合は String 型と文字列スライス参照のいずれかを指す。
次は Creating a New String
Creating a New String
文字列はバイトのベクタとして実装されており、保証・制約・能力が若干追加されている。
fn main() {
let data = "initial contents";
let s = data.to_string();
let s = "initial contents".to_string();
let s = String::from("initial contents")
}
Display トレイトが実装されている型であれば to_string メソッドを利用できる。
また、文字列リテラルの場合は String::from 関連関数が利用できる。
Updating a String
文字列は文字列や文字をプッシュしたり、+
演算子を使ったり、format! マクロを使うことで追加・変更できる。
Appending to a String with push_str and push
fn main() {
let mut s1 = String::from("foo");
s1.push_str("bar");
let s2 = String::from("baz");
s1.push_str(&s2);
s1.push('.');
println!("{} {}", s1, s2);
}
実行すると forbarbaz. baz
と表示される。
push_str メソッドに String インスタンスを渡す場合は参照を使う必要がある。
push メソッドには文字列ではなく文字を渡す。
+
Operator or the format! Macro
次は Concatenation with the
+
Operator or the format! Macro
Concatenation with the fn main() {
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;
println!("{}", s3);
}
実行すると Hello, world!
と表示される。
s3 を作成すると s1 の所有権が s3 へ移転するので s1 を使えなくなる。
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:6:20
|
2 | let s1 = String::from("Hello, ");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = String::from("world!");
4 | let s3 = s1 + &s2;
| -- value moved here
5 |
6 | println!("{}", 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
|
4 | let s3 = s1.clone() + &s2;
| ++++++++
For more information about this error, try `rustc --explain E0382`.
このようになる理由は +
演算子のシグニチャが下記のようになっているため。
fn add(self, s: &str) -> String {}
s2 が参照になる理由についても上記のシグニチャが起因している。
&s2
の型は &String であって &str ではないがコンパイルエラーにはならない、その理由はコンパイラが &s2[..]
へコワース(変換)するため。
こういうのは "deref coercion" と呼ばれるらしい。
let s3 = s1 + &s2
のやっていることは s1 に s2 の末尾追加を行い、s1 → s3 へ所有権を移転することだ。
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
println!("{}", s);
}
上記のコードは下記のようにも書ける。
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");
println!("{}", s);
}
format! マクロは println のようだがコンソール出力する代わりに文字列を返す。
format! マクロでは所有権の移転は発生しない。
次は Indexing into Strings
Indexing into Strings
fn main() {
let s1 = String::from("hello");
let h = s1[0];
}
error[E0277]: the type `String` cannot be indexed by `{integer}`
--> src/main.rs:3:16
|
3 | let h = s1[0];
| ^ `String` cannot be indexed by `{integer}`
|
= help: the trait `Index<{integer}>` is not implemented for `String`
= help: the following other types implement trait `Index<Idx>`:
<String as Index<RangeFull>>
<String as Index<std::ops::Range<usize>>>
<String as Index<RangeFrom<usize>>>
<String as Index<RangeTo<usize>>>
<String as Index<RangeInclusive<usize>>>
<String as Index<RangeToInclusive<usize>>>
For more information about this error, try `rustc --explain E0277`.
多くのプログラミング言語では文字列に添え字アクセスできるが Rust ではできないようになっている。
Internal Representation
UTF-8 では 文字列が Hola
の場合は文字数が 4 文字で 4 バイトであり、1 文字が 1 バイトである。
一方、文字列が Здравствуйте
の場合は文字数は 12 文字だが 24 バイトであり、1 文字が 2 バイトである。
キリル文字のような Unicode スカラー値は 2 バイトになる。
キリル文字の З
は 208 と 151 の組み合わせだが、1 文字目に添え字アクセスした時に求めているのは З
であって 208 ではない。
Rust では文字列の添え字アクセスをあえてエラーにすることで ASCII 文字以外が使用された時に発生するかも知れないバグを回避するような言語設計になっている。
次は Bytes and Scalar Values and Grapheme Clusters! Oh My!
Bytes and Scalar Values and Grapheme Clusters! Oh My!
UTF-8 では文字を区切る方法が 3 通りある:バイト毎・スカラー値毎・書記素クラスタ毎であり、書記素クラスタ毎が最も人間の感覚に近い。
インド語の “नमस्ते” はバイト毎(u8)では下記のようになる。
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]
一方、スカラー値毎(char)では下記のようになる、4 文字目と 6 文字目は文字ではなく直前のスカラー値を修飾する。
['न', 'म', 'स', '्', 'त', 'े']
さらに書記素クラスタ毎(文字列)では下記のようになる。
["न", "म", "स्", "ते"]
このように Rust では 3 通りの文字の区切りの方法があるため、コードではどの区切りの方法を使うのかを指定する必要がある。
Rust で文字列の添え字アクセスが許されていないもう一つの理由として定数時間で終わらないこともあるようだ、確かにスカラー値とか書記素クラスタで区切る場合は定数時間ではアクセスできなそうだ。
次は Slicing Strings
Slicing Strings
文字列への添え字アクセスの戻り値は明確ではない:ありうる値としてはバイト、スカラー値、書記素クラスタ、文字列スライスの 4 つがある。
文字列スライスが必要な場合は例えば &hello[0..4]
のように範囲を指定して添え字アクセスする。
この場合は 4 バイト分の文字列を含んだ文字列スライスとなり、データ型は &str となる。
範囲がスカラー値の区切りではない場合、実行時パニックとなる。
Methods for Iterating Over Strings
文字列を操作する場合は区切りを明示することが望ましく、スカラー値の場合は chars
メソッドが利用できる。
fn main() {
for c in "Зд".chars() {
println!("{c}");
}
}
З
д
一方、バイト値の場合は bytes
メソッドを利用できる。
fn main() {
for b in "Зд".bytes() {
println!("{b}");
}
}
208
151
208
180
書記素クラスタについては標準ライブラリに含まれていないので外部のクレートを使用する必要がある。
unicode-segmentation などが良さげ。
Strings Are Not So Simple
Rust の言語設計として文字列を適切に扱うことがプログラマに求められている。
他のプログラミング言語のように気軽に文字列を扱えるのも良いが、個人的には Rust のようにコンパイル時点やテスト時点で気づけるような仕組みになっている方が好きだ。
次は Storing Keys with Associated Values in Hash Maps
Storing Keys with Associated Values in Hash Maps
ハッシュマップ HashMap<K, V>
は K 型のキーから V 型の値へのマッピングを保存する。
ハッシュ関数というものが使用され、これらのキーと値をメモリにどのように格納するかを決める。
ハッシュマップは別のプログラミング言語では次のように呼ばれることがある。
- ハッシュ
- マップ
- オブジェクト
- ハッシュテーブル
- 辞書
- 連想配列
ハッシュマップはベクタのような添え字アクセスではなく任意の型のキーを使ってデータを探したい場面で便利である。
Creating a New Hash Map
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
println!("{score}");
}
空のハッシュマップを作成するには Hash::new 関数を使用し、要素を追加するには insert メソッドを使用する。
ベクタや文字列とは異なりハッシュマップを使用するには use 文を使って HashMap をスコープに追加する必要がある。
また、ベクタのように生成用のマクロもない。
一方、ハッシュマップがデータをヒープ上に格納する点とキーと値が同じ型である必要がある点は同一である。
次は Accessing Values in a Hash Map
Accessing Values in a Hash Map
ハッシュマップの get メソッドは Option<&V> 型の値を返し、キーに対応する値がなければ結果は None となる。
Option<&V> 型の copied メソッドを使うことで Option<V> 型の結果に変換できる。
最後に Option<V> 型の unwrap_or メソッドを使うことで値を取り出している、値がない場合のデフォルト値は 0 になる。
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores {
println!("{key}: {value}")
}
}
Yellow: 50
Blue: 10
ハッシュマップを走査するのに for 文を使える。
Hash Maps and Ownership
i32 のように Copy トレイトを実装する型の場合はハッシュマップに値がコピーされる。
String 型のように所有権がある型の場合、ハッシュマップに値を挿入すると所有権が移転する。
use std::collections::HashMap;
fn main() {
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
println!("{field_name} {field_value}");
}
error[E0382]: borrow of moved value: `field_name`
--> src/main.rs:10:15
|
4 | let field_name = String::from("Favorite color");
| ---------- move occurs because `field_name` has type `String`, which does not implement the `Copy` trait
...
8 | map.insert(field_name, field_value);
| ---------- value moved here
9 |
10 | println!("{field_name} {field_value}");
| ^^^^^^^^^^^^ 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
|
8 | map.insert(field_name.clone(), field_value);
| ++++++++
error[E0382]: borrow of moved value: `field_value`
--> src/main.rs:10:28
|
5 | let field_value = String::from("Blue");
| ----------- move occurs because `field_value` has type `String`, which does not implement the `Copy` trait
...
8 | map.insert(field_name, field_value);
| ----------- value moved here
9 |
10 | println!("{field_name} {field_value}");
| ^^^^^^^^^^^^^ 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
|
8 | map.insert(field_name, field_value.clone());
| ++++++++
For more information about this error, try `rustc --explain E0382`.
値ではなく参照を使って挿入する場合はハッシュマップに値が移転しないが、その場合はハッシュマップが有効である間は挿入に使用する値が有効である必要があり、そのためにはライフタイムという後から学ぶしくみが役に立つ。
次は Updating a Hash Map
Updating a Hash Map
ハッシュマップでは 1 つのキーに対して 1 つの値のみを対応づけることができる。
キーは一意だが、値は一意である必要はない。
ハッシュマップを更新する場合はいくつかの方法が考えられる。
- 上書きする
- 古い値がある場合は新しい値を無視する
- 新旧両方の値を使って新しい値を作成する
Overwriting a Value
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{:?}", scores);
}
{"Blue": 25}
上書きするには insert メソッドを使う。
Adding a Key and Value Only If a Key Isn't Present
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{:?}", scores);
}
{"Blue": 10, "Yellow": 50}
新しい値を無視する場合は entry メソッドと Entry 型の or_insert メソッドを使う。
Updating a Value Based on the Old Value
use std::collections::HashMap;
fn main() {
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", map);
}
{"world": 2, "wonderful": 1, "hello": 1}
新旧両方の値を使って新しい値を作成する場合は or_insert メソッドの戻り値に対して参照外し演算子を使用する。
次は Hashing Functions
久々
10 章まで読み切ったら気分転換で他の勉強をしていこう。
Hashing Functions
- デフォルトでは SipHash と呼ばれるハッシュ関数が使用される。
- このハッシュ関数は高速ではないがハッシュ DoS 攻撃に対する耐性がある。
- カスタムのハッシュ関数を使用したい場合は BuildHasher トレイトを実装した型を指定する。
Error Handling
- 多くの場合、Rust は起こり得るエラーに対して何らかのアクションを取ることをコンパイル時にプログラマーに求める。
- この点は運用前にエラーを発見して適切に処理するのを助けるのでプログラムの堅牢性を高める。
- Rust では復帰可能なエラーと復帰不能ななエラーに分けられる。
- 復帰可能なエラーな場合は予期されるものであり、エラーを報告して再試行をユーザーに求めるなどのアクションが取られる。
- 復帰不能なエラーの場合はプログラムが直ちに停止する。
- Rust には例外(exception)がない代わりに Result<T, E> 型と panic! マクロがあり、それぞれ復帰可能なエラーと復帰不能なエラーの発生時に使用される。
次は Unrecoverable Errors with panic!
Unrecoverable Errors with panic!
- 配列の範囲外にアクセスする等した時にパニックが発生する。
- panic! マクロを呼び出してパニックを明示的に発生させることもできる。
- パニックが発生するとエラーメッセージの表示・アンワインド・スタックのクリーンアップが行われた後にプログラムが停止する。
- 環境変数を設定することでコールスタックも表示することができる。
Unwinding the Stack or Aborting in Response to a Panic
- デフォルトではパニック発生時にアンワインディングが開始される。
- アンワインディングとはスタックを遡ってそれぞれの関数のデータをクリーンアップしていくことらしい。
- 設定によりアンワインディングを行わずに直ちに停止することできる。
- アンワインディングをしなければバイナリサイズは小さくなる。
そもそも OS がメモリをクリーンアップしてくれるのにアンワインディングは必要なのだろうか?
このページが参考になるかも知れない。
アンワインディングがないとデストラクタが呼ばれなくてリソースを占有する恐れなどがあるから必要なのかも知れない。
fn main() {
panic!("crash and burn");
}
thread 'main' panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
1 行目にパニックが発生したソースコード位置が出力され、2 行目にエラーメッセージガ表示されている。
3行目は RUST_BACKTRACE=1
と環境変数を設定することでバックトレース表示が可能になることを示唆している。
次は Using a panic! Backtrace
Using a panic! Backtrace
fn main() {
let v = vec![1, 2, 3];
v[99];
}
上記の例では存在しない 100 番目の要素にアクセスしようとしている。
[]
演算子は要素を返す仕様になっているので、返す要素がない場合はパニックになる。
C 言語などでは例え 100 番目の要素がなくても添え字からメモリ位置を計算して何らかの値が返される。
これはバッファーオーバーリードと呼ばれる。
cargo run
thread 'main' panicked at backtrace/src/main.rs:3:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
バックトレースを表示するには環境変数 RUST_BACKTRACE=1
を設定して実行する。
RUST_BACKTRACE=1 cargo run
thread 'main' panicked at backtrace/src/main.rs:3:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: rust_begin_unwind
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/std/src/panicking.rs:645:5
1: core::panicking::panic_fmt
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:72:14
2: core::panicking::panic_bounds_check
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:208:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/slice/index.rs:255:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/slice/index.rs:18:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/alloc/src/vec/mod.rs:2770:9
6: backtrace::main
at ./src/main.rs:3:6
7: core::ops::function::FnOnce::call_once
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
さらに RUST_BACKTRACE=full
とすることでさらに詳細なデータも取得できるようだ。
バックトレースとはパニックが発生する時点までに呼び出された全ての関数のリストのこと。
バックトレースを上から見ていくといずれは自分が書いたコードに辿り着く、そこにパニックの原因が隠れている。
バックトレースはリリース版ではデバッグシンボルの情報がなくて表示することができない。
試しに cargo run --release
と実行するとかなり情報が減っている。
thread 'main' panicked at backtrace/src/main.rs:3:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: rust_begin_unwind
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/std/src/panicking.rs:645:5
1: core::panicking::panic_fmt
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:72:14
2: core::panicking::panic_bounds_check
at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:208:5
3: backtrace::main
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
次は Recoverable Errors with Result
Recoverable Errors with Result
Result<T, E> 型を使うことで処理の成功/失敗に加えて成功時の結果またはエラー時の情報を一度に返すことができる。
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {error:?}"),
}
}
Result 型はプレリュードに含まれるので Result::Ok
のように書かなくても良い。
Matching on Different Errors
エラーによっては kind
メソッドのように種類の情報を取得することができ、match 式をネストすることでエラーの種類によって処理を変えることができる。
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
other_error => {
panic!("Problem opening the file: {other_error:?}");
}
},
}
}
次は Alternatives to Using match with Result<T, E>
Alternatives to Using match with Result<T, E>
久々なので一から書いてみよう。
use std::{fs::File, io::ErrorKind};
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {error:?}");
})
} else {
panic!("Problem opening the file: {error:?}");
}
});
}
クロージャを使うことで match 式だらけになることを防げる。
Shortcuts for Panic on Error: unwrap and expect
use std::fs::File;
fn main() {
let greeting_file = File::open("not-found.txt").unwrap();
}
thread 'main' panicked at src/main.rs:18:53:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
unwrap メソッドを使うと Ok の場合は値を取り出し、Err の場合はエラーを表示してからプログラムを以上終了できる。
use std::fs::File;
fn main() {
let greeting_file =
File::open("not-found.txt").expect("not-found.txt should be included in the project");
}
thread 'main' panicked at src/main.rs:19:37:
not-found.txt should be included in the project: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
called
Result::unwrap()on an
Err value:
の部分が not-found.txt should be included in the project:
に変わっていることがわかる。
できる限り expect を使った方がデバッグがしやすそうだ。
次は Propagating Errors
Propagating Errors
関数内でエラーを処理する代わりにエラーを呼び出し元に返すことができる。
これはエラーの伝搬と呼ばれる。
use std::{
fs::File,
io::{self, Read},
};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
fn main() {
let result = read_username_from_file();
match result {
Ok(username) => println!("Ok: {username}"),
Err(e) => println!("Err: {e:?}"),
}
}
上記のようなエラー伝搬は Rust でよく行われるので次のセクションで説明するような構文糖衣がある。
?
operator
A Shortcut for Propagating Errors: the
前セクションのコードは下記のようにも書ける。
use std::{
fs::File,
io::{self, Read},
};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
fn main() {
let result = read_username_from_file();
match result {
Ok(username) => println!("Ok: {username}"),
Err(e) => println!("Err: {e:?}"),
}
}
Result 型の値の後に ?
を置くと Ok の場合は値が取り出され、Err の場合は呼び出し元に返される。
match 式を使う場合と ?
を使う場合とでは違いがある。
?
を使う場合はエラー値は from 関数で変換される。
from 関数は From トレイトで定義される。
?
は次のように続けて書くこともできる。
use std::{
fs::File,
io::{self, Read},
};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
fn main() {
let result = read_username_from_file();
match result {
Ok(username) => println!("Ok: {username}"),
Err(e) => println!("Err: {e:?}"),
}
}
実は下記のように簡単に書くことができる。
use std::{fs, io};
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
fn main() {
let result = read_username_from_file();
match result {
Ok(username) => println!("Ok: {username}"),
Err(e) => println!("Err: {e:?}"),
}
}
?
Operator Can Be Used
次は Where The
?
Operator Can Be Used
Where The -
?
演算子を使うには関数の戻り値が Result<T, E> 型である必要がある。 - 例えば戻り値が
()
である main 関数では?
演算子を使うことができない。
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt");
}
- Result 型以外にも Option 型や FromResidual を実装する型で
?
演算子を利用できる。 - このような場合は関数の戻り値を変えるか、Result 型を match 式などを使って処理する。
- Option 型で
?
演算子を使用する例は下記の通り。
fn last_char_of_first_line() -> Option<char> {
text.lines().next()?.chars().last()
}
- ちなみに last メソッドの戻り値も Option<char> 型である。
- Option と Result とは互いに自動で変換できないので注意する必要がある。
- 変換するには Result.ok メソッドまたは Option.ok_or メソッドを使う。
main 関数も Result<T, E> 型を返すことができる、その場合は Ok(())
を返す必要がある。
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
- Box<dyn Error> はトレイトオブジェクトというものらしく、あらゆる種類のエラーを意味する。
- 今はよくわからないがいつかわかる日が来るだろう。
- main 関数が
Ok(())
返した場合は終了コードが 0 になり、エラーを返した場合は 0 以外になる。 - main 関数は Termination トレイトを実装するあらゆる型を返すことができる、その場合は report 関数を使ってエラーコードに変換される。
panic!
or Not to panic!
次は To
panic!
or Not to panic!
To - パニックが発生した場合、復帰する方法がない。
- 呼び出し元のコードで復帰できる可能性がある場合は Result を返す方が良さそう。
- コード例やプロトタイプやテストの場合はパニックを発生させた方が良い場合もある。
Examples, Prototype Code, and Tests
- コード例では unwrap や expect メソッドを使う方が伝えたい意図が明確になる。
- プロトタイプについてもこれらのメソッドを使う方がやりたいことを素早く実現できる。
- テストの場合もパニックが発生した場合はテストが失敗する。
Cases in Which You Have More Information Than the Compiler
- ハードコードした文字列のパースなど事前に成功することが確実な場合は unwrap などを使う方が良い。
- もっと良いのは expect を使って必ず成功する理由を記述すること。
Guidelines for Error Hnadling
- エラーの発生によってコードの前提条件が成立しない場合はパニックを発生させる方が良い。
- 基本的にはパニックよりも Result を返す方が望ましいが、Result を返すとより悪い状況が想定される場合にはパニックを起こした方が良い。
- 処理がユーザーに害を及ぼす恐れがある場合はパニックを発生させた方が良い。
続いは Functions often have contracts から。
- 入力が一定の条件を満たす場合に正しく動作する関数がある。
- 条件を満たさない場合はパニックが発生する方が望ましい、なぜなら多くの場合にそれは呼び出し側のコードのバグに起因するため。
Creating Custom Types for Validation
loop {
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip
}
}
次は下記の一文から。
The if expression checks whether our value is out of range, tells the user about the problem, and calls continue to start the next iteration of the loop and ask for another guess.
if 式の後は guess が 1〜100 であることが保証されている。
この実装は悪くないが折角であれば guess が 1〜100 であることを個々の関数でチェックするのではなくコンパイラにチェックさせたい。
そのために新しい型を作成して new 関数でチェックを行う。
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i31) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
この場合 Guess::new 関数は value 引数が 1〜100 であることを前提にしており、その前提が守られない場合は呼び出し元のコードの誤りであるのでパニックを発生させている。
value メソッドは公開されているが value フィールドが非公開であることもポイントである。
このようにすることで value フィールドが new 関数でのチェック後に書き換えられることを防いでいる。
Summary
- Rust のエラー処理機能は堅牢なコーディングを支援する。
- panic! マクロを使うことでパニックを発生させてプログラムを異常終了することができる。
- Result 列挙体は失敗するかも知れない処理の戻り値として使用する。
- panic! マクロと Result 列挙体の使い分は復帰可能かどうかがポイントになる。
- 基本的には Result を使った方が良いがコード例、プロトタイプ、テストコードではパニックを使う方が良い場合もある。
- 関数入力の前提条件を満たしていない場合もパニックを起こす方が望ましい。
いよいよ次回はチャプター 10 に突入
Generic Types, Traits, and Lifetimes
このセクションではジェネリクス、トレイト、ライフタイムの 3 つの概念について学んでいく。
Removing Duplication by Extracting a Function
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
}
fn largest(vec: &[i32]) -> &i32 {
let mut largest = &vec[0];
for number in vec {
if number > largest {
largest = number;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {result}");
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("The largest number is {result}");
}
上記のコードで行ったことは下記の通り。
- 重複している処理を探す
- 処理を抽出して関数を作成する
- 重複している処理を関数で置き換える
同じ方法でジェネリクスを使って重複する処理を減らしていく。
Generic Data Types
- ジェネリクスを使って関数を定義する際、引数や戻り値の型にジェネリクスを配置する。
- これにより、コードは柔軟かつ多機能になると共にコードの重複が削減される。
fn largest_i32(vec: &[i32]) -> &i32 {
let mut largest = &vec[0];
for number in vec {
if number > largest {
largest = number;
}
}
largest
}
fn largest_char(vec: &[char]) -> &char {
let mut largest = &vec[0];
for number in vec {
if number > largest {
largest = number;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest_i32(&number_list);
println!("The largest number is {result}");
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {result}");
}
- largest_i32 と largest_char は入出力のデータ型以外は同じであり、これらの関数をジェネリクスを使って重複を減らしていく。
- 型パラメーターの名前は何でも良いが慣習としては
T
のように大文字 1 字が使われる。 - 型パラメータは関数名の直後に配置される。
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for number in list {
if number > largest {
largest = number;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {result}");
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {result}");
}
こちらのコードはトレイトが指定されていないのでコンパイルに失敗する。
error[E0369]: binary operation `>` cannot be applied to type `&T`
--> src/main.rs:5:19
|
5 | if number > largest {
| ------ ^ ------- &T
| |
| &T
|
help: consider restricting type parameter `T`
|
1 | fn largest<T: std::cmp::PartialOrd>(vec: &[T]) -> &T {
| ++++++++++++++++++++++
For more information about this error, try `rustc --explain E0369`.
error: could not compile `largest_number` (bin "largest_number") due to 1 previous error
エラーメッセージに書かれている通り PartialOrd を指定することでコンパイルが通って実行できるようになる。
次は In Struct Definitions
In Struct Defenitions
構造体の場合も名前の次に型パラメーターを配置できる。
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
仮に Point { x: 5, y: 4.0 }
のように書くと x と y で型が一致しないのでエラーになり、コンパイルを通すには 2 つの型パラメーターが必要になる。
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
let wont_work = Point { x: 5, y: 4.0 };
}
型パラメーターは複数あっても大丈夫だが、多くなるとコードが読みにくくなる。
その場合はコードを細かいパーツに分解するなど構造を見直した方が良い。
次は In Enum Definitions から
In Enum Definitions
列挙体でも型パラメーターを使用できる、使い方は同じで名前の後に型パラメーターを配置する。
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
In Method Definitions
メソッドで型パラメーターを使う場合は impl
直後に配置する。
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
このようになっているので特定の型のみが持つメソッドを定義できる。
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
構造体定義の型パラメーターはメソッド定義の型パラメーターは必ずしも同じである必要はない。
struct Point<X1, Y1> {
x: X1,
y: Y1,
}
impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
次は Performance of Code Using Generics
Performance of Code Using Generics
- 型パラメーターを使っても使わない場合と比べてパフォーマンスが低下することはない。
- コンパイル時にジェネリクスを使うコードではモノモーファイゼーションが実行される。
- モノモーファイゼーションとはジェネリクスを使ったコードに具体的な型を埋め込んで変換していく処理のことのようだ。
fn main() {
let integer = Some(5);
let float = Some(5.0);
}
上記のコードはイメージ的には下記のように変換される。
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}
この変換処理は実行時ではなくコンパイル時に行われるので実行時のパフォーマンスには影響しないという寸法だ。
次回は Traints: Defining Shared Behavior から
Traints: Defining Shared Behavior
- トレイトは特定の型が持ち、他の型と共有する振る舞いを定義する。
- トレイト境界を使うことでジェネリックな型が特定の振る舞いを持つことを指定できる。
- トレイトは他の言語で「インタフェース」と呼ばれる機能に似ている。
Defining a Trait
- 型の振る舞いは呼び出せるメソッドから構成される。
- 型が異なっても同じメソッドを呼び出せる場合、これらの型は振る舞いを共有している。
- トレイト定義は一定の目的を達成するために必要な振る舞いの集合を定義するためにメソッドのシグニチャ(名前+引数+戻り値)をグループ化する方法である。
pub trait Summary {
fn summarize(&self) -> String;
}
- 上記では summarize メソッドを持つ Summary トレイトを定義している。
- トレイトが規定するのはメソッド群のシグニチャだけであり、トレイトを実装する型がメソッドの内容を提供する。
- コンパイラはトレイトを実装する型がそのトレイトによって規定された全てのメソッドを持ち、また、シグニチャが一致することを強制する。
次回は Implementing a Traint on a Type から
Implementing a Traint on a Type
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
- トレイトを実装することはメソッドを実装するのによく似ている。
- 違いは
Summary for
のようにトレイト名 + for があること。
次は実装したトレイトを使うところから始める。
トレイトを使う
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
use traits::{Summary, Tweet};
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
}
1 new tweet: horse_ebooks: of course, as you probably already know, people
Summary トレイトをインポートしていることが普通のメソッドとは異なる点。
Summary トレイトは公開されているので、別のクレートで Summary トレイトを実装する型を作ることができる。
ただし型かトレイトのいずれかまたは両方がそのクレートのものである必要がある。
例えば他のクレートから NewsArticle や Tweet の Summary トレイト実装を定義することはできない。
この制約は coherence と呼ばれる、日本語訳は一貫性になるようだ。
よく読むと coherence の一部であり、より正確には orphan rule と呼ばれるようだ。
このルールのおかげで他の人が自分のコードを壊したり、その逆を防げる。
このルールが無いと 2 つのクレートが同じ型の同じトレイトを実装できることになり、コンパイラがどちらを使えば良いかわからなくなる。
Default Implementations
トレイトを定義する時にメソッドの内容を書くことでデフォルトの振る舞いとすることができる。
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more ...)")
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
// fn summarize(&self) -> String {
// format!("{}, by {} ({})", self.headline, self.author, self.location)
// }
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
use traits::{NewsArticle, Summary, Tweet};
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
};
println!("New article available! {}", article.summarize());
}
デフォルト実装を上書きする構文はトレイトのメソッドを実装する構文と同じなので、デフォルト実装は後から追加することもできる。
デフォルト実装からトレイトの他のメソッドを呼び出すこともできる。
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more {}...)", self.summarize_author())
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize_author(&self) -> String {
self.author.clone()
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
なお、同じメソッドのオーバーライド実装からデフォルト実装を呼び出すことはできないようだ。
Traits as Parameters
トレイトの使い方として特定のトレイトを実装する型をパラメーターとして受け取りたい場合は impl Trait
構文を使うことができる。
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
};
notify(&tweet);
notify(&article);
}
Breaking news! (Read more @horse_ebooks...)
Breaking news! (Read more Iceburgh...)
Trait Bound Syntax
Bound syntax はトレイト境界と訳して良いものかどうか調べたら結構議論されているもののようだ。
予想はしていたけど impl Trait
構文はシンタックスシュガーであり、下記と同じ意味になるようだ。
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
個人的にはこの方がわかりやすいが、T
が必要ないという点では impl Trait
構文の方がシンプルかも知れない。
トレイト境界の方がより複雑なケースに対応できる、例えば下記では item1 と item2 が同じ方であることを指定できる。
pub fn notify<T: Summary>(item1: &T, item2: &T) {}
notify(&tweet, &article); // NG
pub fn notify(item1: &impl Summary, item2: &impl Summary) {}
notify(&tweet, &article); // OK
Specifying Multiple Trait Bounds with the + Syntax
- 演算子を使って複数のトレイトを実装することを指定できる。
pub fn notify(item: &(impl Summary + Display)) {}
// or
pub fn notify<T: Summary + Display>(item: &T) {}
where
Clauses
Clearer Trait Bounds with
トレイト境界が多くなると読みにくくなる。
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
そのような場合は where
キーワードを使うことができる。
fn some_function(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{}
Returning Types That Implement Traits
なんと impl Trait
構文を戻り値の型に指定することができる。
use traits::{NewsArticle, Summary, Tweet};
fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
fn main() {
let tweet = returns_summarizable();
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
};
notify(&tweet);
notify(&article);
}
この機能はクロージャとイテレータを使う時に便利なようだ、これらについては後のチャプターで学ぶ。
クロージャなどはコンパイラだけが知っている型や指定するにはとても長い型を作成するらしい。
impl Trait
構文を使うことで長い型を手入力することなく関数が Iterator トレイトを実装することを指定できるらしい、なるほど。
impl Trait
構文を使っても関数は同じ型のインスタンスを返す必要がある。
例えば次のようなコードはエラーになる。
fn returns_summarizable(switch: bool) -> impl Summary {
if (switch) {
return Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
}
}
mismatched types
expectedTweet
, foundNewsArticle
ちなみにトレイトオブジェクトというものを使えばこういうことが可能になるようだ。
次は Using Trait Bounds to Conditionally Implement Methods
Using Trait Bounds to Conditionally Implement Methods
トレイト境界と impl
ブロックを使うことで特定のトレイトを実装する型に限定したメソッドを定義することができる。
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: PartialOrd + Display> Pair<T> {
fn cmd_display(&self) {
if self.x >= self.y {
println!("The largest number is {}", self.x);
} else {
println!("The largest number is {}", self.y);
}
}
}
fn main() {
let pair = Pair::new(1, 2);
pair.cmd_display();
// let pair = P?air::new((), ());
// pair.cmd_display(); // Error
}
The largest number is 2
あるトレイトを実装する型に対して別のトレイトを実装することができる。
このような実装はブランケット実装と呼ばれるようだ。
一例として標準ライブラリでは Display トレイトを実装する型に対して ToString トレイトのブランケット実装を提供している。
そのコードは下記のようになっている。
impl<T: Display> ToString for T {}
これにより、println! マクロで表示できる変数に対して to_string() メソッドを呼び出すことができる。
トレイトとトレイト境界を使うことで重複を減らし、また、コンパイラに対して型パラメーターが特定の振る舞いを持つことを指定できる。
コンパイラは型パラメーターに与えられる具体的な型をチェックして、適合しない場合はエラーとなる。
動的型付け言語の場合、このエラーは実行時に発生するが、Rust ではコンパイル時に見つけることができる。
また、型をチェックするコードを書く必要もないことが Rust のメリットである。
これにより、ジェネリクスの柔軟性を諦めることなくパフォーマンスを向上させることができる。
次は Validating References with Lifetimes
次はいよいよ最後のページのライフタイム。
これもなかなか読み応えがありそうだ。
Validating References with Lifetimes
ライフタイムもジェネリクスの 1 つであり、参照が有効であることを確かにするための道具のようだ。
全ての参照はライフタイムを持っており、ライフタイムとは参照が有効なスコープを意味する。
多くのケースではライフタイムは暗黙的に推論されるが、1 つの答えに推論できない場合は注釈する必要がある。
ライフタイムの注釈は他のプログラミング言語ではあまり出てこない概念なので、新しいことを学ぶ気持ちで取り組む必要がありそうだ。
Preventing Dangling References with Lifetimes
ライフタイムの主な目的はダングリング参照を防止することである。
ダングリング参照とは意図しないデータへの参照である。
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
上記のコードはコンパイルが失敗する。
x
does not live long enough
borrowed value does not live long enough
失敗の原因は r が参照する値が内側のスコープが終了した時点で無効になり、無効になった値を使おうとしているためである。
内部的には内側のスコープが終了する時点で Rust は x のデータ用のメモリを解放するが、r はメモリ解放後も有効であり、参照しているのは解放後のメモリ領域である。
解放後のメモリ領域への参照を使うのはどんな状況であっても望ましいことではない。
ちなみに上記のコードでは初期値のない変数を宣言しており、null のない Rust のルールに反するように見えるかもしれないが、初期化前に値を使おうとするとコンパイル時にエラーが発生するのでルールはしっかり守られる。
used binding
r
is possibly-uninitialized
r
used here but it is possibly-uninitialized
次は The Borrow Checker
このセクションではどのようにコードの有効/無効を判断しているかを学ぶようだ。
The Borrow Checker
Rust の借用チェッカーは借用が有効であることを調べるためにスコープを比較する。
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
'b のスコープは 'a のスコープよりも小さい。
上記コードでは 'a のライフタイムを持つ参照 r
が、'b のライフタイムを持つ変数 x
を参照しようとしている。
参照対象が参照よりも短命なのでコンパイルは失敗する。
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {r}"); // | |
// --+ |
} // ----------+
上記コードでは参照対象が 'b のライフタイムを持ち、参照が 'a のライフタイムを持つ。
'b は 'a よりも大きく、r は常に有効なのでコンパイルは成功する。
Generic Lifetimes in Functions
次のセクションでは関数におけるパラメーターと戻り値のジェネリックライフタイムについて学んでいく。
Generic Lifetimes in Functions
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
return x;
} else {
return y;
}
}
missing lifetime specifier
this function's return type contains a borrowed value, but the signature does not say whether it is borrowed fromx
ory
Compiling lifetimes v0.1.0 (/Users/susukida/workspace/rust/lifetimes)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `lifetimes` (bin "lifetimes") due to 1 previous error
Rust が longest 関数の戻り値が x
を参照するのか y
を参照するのかがわからないためコンパイルが失敗する。
関数定義時は当り前だが if ブロックが実行されるのか、else ブロックが実行されるのかがわからない。
そのため借用チェッカーが参照が有効であるかを判断できなくなる。
コンパイルを成功させるにはパラメーターと戻り値のライフタイムの関係を定義するジェネリックライフタイムパラメーターが必要になる。
Lifetime Annotation Syntax
ライフタイム注釈によって参照の有効期間が変化することはない。
ライフタイム注釈を使うことで複数の参照のライフタイムの関係を明確にできる。
関数はジェネリックライフタイムパラメーターを指定することでライフタイム注釈された参照を受け取る事ができる。
ライフタイム注釈を行うには &'a i32
のように &
の直後に '
とパラメーター名を書く、パラメーター名には a
や 'b
などが使用される事が多い。
次は Lifetime Annotations in Function Signatures