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