モジュール
Rustには、コードの再利用を体系化された形で行うこができる
mod : モジュールを宣言
pub : 名前空間の外からも見えるようになる
use : モジュールやモジュール内の定義をスコープに入れる
fooという名前のモジュールにサブモジュールがなければ、fooの定義は、 foo.rsというファイルに書くべきです。
communicator
├── network
└── client
mod network {
fn connect() {
}
}
mod client {
fn connect() {
}
}
fooというモジュールに本当にサブモジュールがあったら、fooの定義は、 foo/mod.rsというファイルに書くべきです
└── src
├── client.rs
├── lib.rs
└── network
├── mod.rs
└── server.rs
// [src/client.rs]
fn connect() {
}
// [src/lib.rs]
mod client;
mod network;
// [src/network/mod.rs]
fn connect() {
}
mod server;
// [src/network/server.rs]
fn connect() {}
pubで公開するか制御する
-
要素が公開なら、どの親モジュールを通してもアクセス可能
-
要素が非公開なら、直接の親モジュールとその親モジュールのみアクセス可能
extern crate コマンドを使用して、communicatorライブラリクレートをスコープに導入する
Cargoは、src/main.rsをバイナリクレートのルートファイルとして扱い、これはルートファイルがsrc/lib.rsになる既存のライブラリクレートとは区別される
このパターンは、実行形式プロジェクトで一般的
ほとんどの機能はライブラリクレートにあり、バイナリクレートはそれを使用する
結果として、他のプログラムもライブラリクレートを使用でき、良い責任の分離になる
src/lib.rsに記述するモジュールはルートモジュール
関数を公開にする
バイナリクレートで関数をしようするには関数名の前にpubをつける
pub mod client; // 使用できる
mod network; // 使用できない
ルートモジュールをpubにしても、関数が非公開の場合
// src/client.rs
pub fn connect(){}
プライバシー
mod outermost {
pub fn middle_function() {} // ライブラリ内
fn middle_secret_function() {} // 親モジュール内
mod inside {
pub fn inner_function() {} // 親モジュール内
fn secret_function() {} // // サブモジュール内
}
}
fn try_me() {
outermost::middle_function();
outermost::middle_secret_function(); // エラー
outermost::inside::inner_function(); // エラー
outermost::inside::secret_function(); // エラー
}
異なるモジュールの名前を参照する
useキーワードで名前をスコープに導入する
pub mod a {
pub mod series {
pub mod of {
pub fn nested_modules() {}
}
}
}
use a::series::of;
use a::series::of::nested_modules;
fn main() {
a::series::of::nested_modules(); // フルパスを指定する
of::nested_modules(); // useキーワードで名前をスコープに導入
nested_modules(); // 関数を直接参照
}
enumにもuseキーワードを使用する
enum TrafficLight {
Red,
Yellow,
Green,
}
use TrafficLight::{Red, Yellow};
use TrafficLight::*; // Globで全ての名前をスコープに導入
fn main() {
let red = Red;
let yellow = Yellow;
let green = Green;
}
superを使用して親モジュールにアクセスする
パスは常に現在のモジュールに対して相対的になり、ここではtestsモジュールになっている
ライブラリクレート内にいる場合にいて、クレート内のモジュールをテストする場合は、superを使って階層を1つ上げてパスを合わせる
pub mod client;
pub mod network;
# [cfg(test)]
mod tests {
#[test]
fn it_works() {
// testsモジュールは、clientモジュールがスコープに存在する必要がある
client::connect(); // エラー
// superで階層を1つあげる
super::client::connect(); // OK
}
}
一般的なコレクション
コレクションは複数の値を含むことができる
組み込みの配列とタプル型とは異なり、これらのコレクションが指すデータはヒープに確保され、データ量はコンパイル時にわかる必要はなく、プログラムの実行にあわせて、伸縮可能になる
- ベクタ型: 可変長の値を並べて保持できる
- 文字列: 文字のコレクション
- ハッシュマップ: 値を特定のキーと紐付ける
ベクタ Vec
ベクタはメモリ上に値を隣り合わせに並べる単独のデータ構造に2つ以上の値を保持させる
ベクタには、同じ型の値しか保持できない
要素のリストがある場合に有用
// 新しいベクタを生成
let v: Vec<i32> = Vec::new();
// 初期値のあるVec<T>を生成
let v = vec![1,2,3];
// ベクタを更新
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
println!("{:?}", v);
// output
[5,6,7];
もちろん、スコープを抜ければVecも解放される
{
let v2 = vec![5,6,7,8];
}
ベクタの要素を読む
let v = vec![1, 2, 3, 4];
// --------------------------------
// レンジ外を要素にアクセスした時
// --------------------------------
// プログラムがパニックを起こす
// パニックになると、コンパイルはRunningとなりコンパイルは完了しない
let third: &i32 = &v[200]; // 200番目の要素にアクセス
// Noneを返す
let third: Option<&i32> = v.get(200); // 200番目の要素にアクセス
要素への参照を保持しつつ、ベクタに要素を追加しようとするとエラーとなる
新規要素をベクタの終端に追加すると、ベクタが現在存在する位置に隣り合って要素入れるだけの領域がなかった場合に、メモリの新規確保をして古い要素を新しいスペースにコピーする必要があるかもしれないから
その場合、最初の要素を指す参照は、開放されたメモリを指すことになる
借用規則により、そのような場面に陥らないよう回避される
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6)
ベクタの値を走査する
let mut v = vec![1, 2, 3, 4, 5];
// ベクタ要素への可変な参照を走査する
for i in &mut v {
*i += 1;
}
enumを使って複数の方を保持する
enum SpredsheetCell {
Int(i32),
Float(f64),
Text(String),
}
fn main() {
let row = vec![
SpredsheetCell::Int(3),
SpredsheetCell::Text(String::from("blue")),
SpredsheetCell::Float(10.12),
];
}
文字列でUTF-8エンコードされたテキストを保持する
3つの概念の組み合わせにより、文字列でよく行き詰まる
Rustのありうるエラーを晒す性質、多くのプログラマが思っている以上に文字列が複雑なデータ構造である
文字列
文字列リテラル : let data = "hello";
String : let data = String::form("hello"); // ""内は文字列リテラル
Rustには、言語の各として1種類しか文字列型が存在しない
文字列スライスのstrで、通常借用された携帯&strで見かける
String型は、言語の各として組み込まれるのではなく
Rustの標準ライブラリで提供されており、伸縮可能、可変、所有権のあるUTF-8エンコードされた文字列型
新規文字列を生成する
let data = "initial contents";
let s = data.to_string();
let s = String::from("initial contents");
Stirng::from関数を使っても、文字列リテラルをto_stringメソッドは、全く同じことをしている
文字列はUTF-8エンコードされていればどんな言語でも使用できる
文字列を更新する
let mut s = Stirng::from("foo");
s.push_str("bar"); // fooのあとにbarを追加
let s2 = "bar";
s.push_str(s2); // 上と同じ
+演算子、またはformat!マクロで連結
+演算子はaddメソッドを使用している
// addメソッドのシグネチャ
fn add(self, s: &str) -> String{}
// add関数がs1の所有権をムーブし、s2の中身のコピーを追記し、結果の所有権を返す
// 実装は単純にコピーするよりも効率的
let s1 = String::from("Hello, ");
let s2 = String::from("World!");
let s3 = s1 + &s2;
println!("s3: {}", s3);
println!("s2: {}", s2);
// format!マクロで連結する
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
println!("{}", s);
文字列を操作するメソッド
// char型に分ける
for c in "hello".chars() { }
// バイトに分ける
for b in "hello".bytes() { }
// output
h
e
l
l
o
104
101
108
108
111
ハッシュマップに紐付いたキーを格納する
HashMapをuseする
vec!のような組み込みマクロがない
ベクタとまったく同様に、ハッシュマップはデータをヒープに保持する
ハッシュマップは均質。キーは全て同じ型で、値も全て同じ型。
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// タプルのベクタに対してcollectメソッドを使用する
let teams = vec![String::from("Blue"), String::from("Red")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
println!("{:?}", scores);
// output
{"Blue": 10, "Red": 50}
ハッシュマップと所有権
i32のようなCopyトレイトを実装する方について、値はハッシュマップにコピーされる
Stringのような所有権のある値なら、値はムーブされ、ハッシュマップはそれらの値の所有者になる
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
// mapに所有権がムーブするため、ここでfield_nameとfield_valueは使えなくなる
map.insert(field_name, field_value);
ハッシュマップの値にアクセスする
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
// scoresの"Blue"チームの値にアクセスする
let score = scores.get(&team_name);
// forループでハッシュマップのキーと値のペアを走査する
for (key, value) in &scores {
println!("{}: {}", key, value);
}
ハッシュマップを更新する
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{:?}", scores);
// output
{"Blue": 25}
キーに値がなかった時のみ値を挿入する
存在しないかどうかを確認するのにentryと呼ばれるAPIを使う
返り値はenumのEntry
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// entryメソッドでキーに値があるか確かめてから挿入する
scores.entry(String::from("Red")).or_insert(30); // 新しく挿入される
scores.entry(String::from("Blue")).or_insert(30); // すでに存在するので挿入されない
println!("{:?}", scores);
// output
{
Blue: 10
Yellow: 50
Red: 30
}
古い値に基づいて値を更新する
ハッシュマップの別の一般的なユースケースは、キーの値を探し、古い値に基づいてそれを更新する
// 各単語があるテキストに何回出現するかを数え上げる
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
// キーに値がなければ0
let count = map.entry(word).or_insert(0);
// キーの値をプラス1
*count += 1;
println!("{}", word);
}
println!("{:?}", map);
エラー処理
Rustの信頼性への傾倒は、エラー処理のも及ぶ
ソフトウェアにおいて、エラーは生きている証
Rustには何かがおかしくなる場面を扱う機能がたくさんある
多くの場合で、コンパイラは、プログラマにエラーの可能性を知り、コードのコンパイルが通るまでに何かしらの対応を行うことを要求してくる
この要求により、エラーを発見し、コードを実用に供する前に適切に対処していることを確認することでプログラムを頑健なものにしてくれる
Rustでは、エラーは大きく2つに分類される
回復可能と回復不能なエラー
ファイルが見つからないなどの回復可能なエラーには、問題をユーザに報告し、処理を再試行することが合理的になる
回復不能なエラーは、常にバグの兆候
例えば、配列の境界を超えた箇所にアクセスしようとするなど
バックトレース
自分のフィアルではないファイルが原因でエラーが発生した場合にエラーが起こるまでの全関数の呼び出しの一覧を表示する
cargoコマンドの前にRUST_BACKTRACE=1
RUST_BACKTRACE=1 cargo run
## Resultで回復可能なエラー
enum Result<T, E> {
OK(T),
Err(E),
}
use std::fs::File;
fn main() {
// File::openが成功した場合、変数fの値はファイルハンドルを含むOKインスタンスになる
// 失敗した場合には、発生したエラーの樹類に関する情報をより多く含むErrインスタンスがfの値になる
let f = File::open("hello.txt");
}
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
// ファイルを開く際に問題がありました
panic!("There was a problem opening the file: {:?}", error)
},
};
}
結果がOkの時に、Ok列挙子から中身のfile値を返すように指示し、それからそのファイルハンドル値を変数fに代入している
matchの後には、ファイルハンドルを使用して読み込んだり書き込むことができる
matchのもう1つのアームは、File::openからErr値が得られたケースを処理
色々なエラーにマッチする
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
// [ マッチガード ]
// アームのパターンを更に洗練する
// パターンのrefは、errorがガード条件式にムーブされないように必要で、単にガード式に参照される
// &は参照にマッチし、その値を返す
// refは値にマッチし、それへの参照を返す
// マッチガードで精査したい条件は、error.kind()による返り値が、ErrorKind enumのNotFoud列挙子であるかどうか
// もしそうなら、File::createでファイル作成を試みる
// ところが、File::createも失敗する可能性があるので、内部にもmatch式を追加する必要がある
Err(ref error) if error.kind() == ErrorKind::NotFound => {
match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => {
panic!(
//ファイルを作成しようとしましたが、問題がありました
"Tried to create file but there was a problem: {:?}",
e
)
},
}
},
Err(error) => {
panic!(
"There was a problem opening the file: {:?}",
error
)
},
};
}
エラー時にパニックするショートカット
matchの使用は、いささか冗長になり得る上、必ずしも意図を良く伝えるとは限らない
Result型には、いろいろな作業をするヘルパーメソッドが多く定義されている
// [ unwrap ]
// File::openのmatch式の短縮メソッド
// 失敗するとpanic!メソッドが呼び出される
let f = File::open("hello.rc").unwrap();
// [ expect ]
// パニックの原因をたどりやすくするメソッド
let f = File::open("hello.rc").expect("Failed to open hello.txt");
エラーを委譲する
戻り値型がResult型
ジェネリック引数のTは具体型String、Eは具体型io::Error
成功すればOk値を受け取る、
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
エラー委譲のショートカット ?演算子
エラーであれば?でエラー値が、成功であればOk内の値が返される
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?; // エラーであればここで終了
let mut s = String::new();
f.read_to_string(&mut s)?; // エラーならここで終了
Ok(s) // 成功ならsを出力
}
?演算子は、Resultを返す関数でしか使用できない
use std::fs::File;
fn main() {
let f = File::open("hello.txt")?; // エラー
}
panic!かResultか
panicしたら、回復する手段はない
Resultは、場面に合わせて回復を試みることを決定したり、Err値を回復不能と断定して、panic!を呼び出し、回復可能だったエラーを回復不能に変換することもできる
故に、Resultを返却することは、失敗する可能性のある関数を定義する際には、いい第一選択肢になる
プロトタイプコード、テスト
例を記述してから何らかの概念を具現化している時、頑健な処理コードも例に含むことは、例の明瞭さを欠くことになりかねない
unwrapなどのパニックする可能性のあるメソッド呼び出しは、アプリケーションにエラーを処理してほしい方法へのプレースホルダーを意味していると理解され、これは残りのコードがしていることによって異なる可能性がある
つまり、エラーの処理方法を決定する準備ができる前、プロトタイプの段階では、非常に便利
それらにより、コードにプログラムをより頑健にするときの明らかなマーカーが残される
メソッド呼び出しがテスト内で失敗したら、そのメソッドがテスト下に置かれた機能ではなかったとしても、テスト全体が失敗してほしい
panic!が、テストが失敗と印づけらえる手段なので、unwrapやexpectの呼び出しはズバリ起こるべきこと
エラー処理のガイドライン
- 悪い状態がときに起こるときは予想されないとき
- この時点以降、この悪い状態にないことを頼りにコードが書かれている時
- 使用している方にこの情報をコード化するいい手段がないとき
検証のために独自の方を作る
pub struct Guess {
value: u32,
}
impl Guess {
pub fn new(value: u32) -> Guess {
if value < 1 || value > 100 {
// 予想の値は1から100の範囲でなければなりませんが、{}でした
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess {
value
}
}
pub fn value(&self) -> u32 {
self.value
}
}
ジェネリック型、トレイト、ライフタイム
ジェネリック
関数にジェネリック
同じコードの重複を減らすために定義する
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn largest_char(list: &[char]) -> char {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
構造体にジェネリック
// 複数の型にたいしてジェネリックな型引数を使用できる
// xがT型で、yがU型
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}
enum定義にジェネリック
enum Result<T,E>{
Ok(T),
Err(E),
}
メソッド定義にジェネリック
struct Point {
x: T,
y: U,
}
// mixupメソッドでは、新しいPoint構造体を作成
// xは元のxの値、yは新しく渡されたPoint構造体のyの値
// したがって、返り値は元のPoint構造体と引数として渡されたPoint構造体のシグネチャと違う可能性がある
// p1 : i32, f64
// p2 : String, char
// p3 : i32, char
impl Point {
fn mixup(self, other: Point) -> Point {
Point {
x: self.x, // 元のx
y: other.y, // 渡された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);
}
ジェネリクスを使用したコードのパフォーマンス
Rustでは、ジェネリクスを、具体的な型があるコードよりの実行速度と同じになるように実装してある
// このコードをコンパイルすると、単相化を行う
その過程で、コンパイラはOption<T>のインスタンスに使用された値を読み取り、2種類のOption<T>を識別する
// ジェネリックな定義を特定の定義に置き換える
let integer = Some(5); // Option_i32に展開
let float = Some(5.0); // Option_f64に展開
トレイト
トレイトにより、Rustコンパイラに特定の型に存在し、他の型と共有できる機能について知らせる
トレイトを使用して共通の振る舞いを抽象的に定義できる
トレイト境界を使用して、あるジェネリックが特定の振る舞いのあるあらゆる型になり得ることを指定できる
型にトレイトを実装する
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)
}
}
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());
}
デフォルト実装
メソッドシグニチャだけを定義するのではなく、デフォルトの文字列を指定する方法もある
// デフォルト実装
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,
}
// NewsArticleのSummaryの空にする
impl Summary for NewsArticle {}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
// こっちのTweetのSummaryの実装に影響が及ばない
// 理由は、デフォルト実装をオーバーライドする記法がデフォルト実装のないトレイトメソッドを実装する記法と同じだから
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
impl Summary for NewsArticle {}
fn main() {
let article = NewsArticle {
headline: String::from(""),
location: String::from(""),
author: String::from(""),
content: String::from(""),
};
println!("New article available! {}", article.summarize());
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());
}
トレイト境界
トレイト境界を使用してジェネリックな型を制限し、型が特定のトレイトや振る舞いを実装するものに制限されることを保証する
/// 上記、省略
// 引数itemに対してsummarizeメソッドを呼び出す関数notifyを定義でき、この引数はジェネリックな型T
// itemのsummarizeを呼ぶ時にジェネリックな型Tがメソッドsummarizeを実装してないというエラーが出ないように、Tのトレイト境界を使ってitemがSummaryトレイトを実装する型でなければならないと指定できる
pub fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
}
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!("{:?}", notify(tweet));
}
トレイト境界が多すぎる時
// 多すぎてよくわからない
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
// where節をつかって、読みやすくする
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
トレイト境界でlargest関数を修正する
ジェネリックがないlargest関数では、最大i32かcharを探そうとするだけ
i32やcharのようなプリミティブ(既知のサイズ型)は、スタックに格納できるので、Copyトレイトを実装している
しかし、largest関数をジェネリクスにすると、list引数がCopyトレイトを実装しない型を含む可能性もでてきた
結果として、list[0]から値をlargestにムーブできず、エラーになった
// Tのトレイト境界にPartialOrdとCopyを追加することで対処する
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
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);
}
トレイト境界を使用して、メソッド実装を条件分けする
ジェネリックな型引数を持つ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: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest nember is x = {}", self.x);
} else {
println!("The largest number is y ={}", self.y);
}
}
}
fn main() {
let v = Pair::new(vec![1, 2, 3], vec![4, 5, 6]);
let n = Pair::new(1, 2);
v.cmp_display(); // エラー
n.cmp_display(); // OK
}
ライフタイム
ライフタイムで参照を検証する
Rustにおいては参照は全てライフタイムを保持する
ライフタイムとは、参照が有効になるスコープを名前を付けて明示する。
多くの場合、型が推論されるように、大体の場合、ライフタイムも暗黙的に推論される
借用精査機
Rustコンパイラには、スコープを比較して全ての借用が有効であるかを決定する借用チェッカーがある
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b | w
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
これから分かることは、'bのライフタイムは'aのライフタイムより短いのでプログラムは拒否をする
'b > 'a なのでプログラムは受け入れる
{
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
}
関数のジェネリックなライフタイム
// 戻り値の型はジェネリックなライフタイム引数である必要がある
// なぜなら、返している参照がxかyのどちらを参照しているか、コンパイラにはわからないため
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
- Aはあるリソースへのハンドルを取得した
- AはBにリソースへの参照を付与する
- Aはリソースを使い終わり、それを解放することを
- Bはリソースを使おうとする! (解放後の使用 or 段グリングポインタ)
① リソース 参照 解放
A ◯
B
② リソース 参照 解放
A ◯
B ◯
③ リソース 参照 解放
A ◯
B ◯ A
BはAの参照を持っているが、Aはすでにリソースを解放しているためにリソースを呼び出すことができない!
このようなことが絶対に起こらないようにする必要がある
Rustはライフタイムと呼ばれる概念を通じてこれを防ぐ
それは、参照の有効なスコープを明示的に記述するもの
// 黙示的に
fn foo(x: &i32) {
}
// 明示的に
fn bar<'a>(x: &'a i32){
}
'aを「ライフタイムa」と読む
技術的には参照はすべてそれに関するライフタイムを持つが、一般的な場合にはコンパイラがそれらを省略してもよいように計らってくれる
関数は関数名の後の<>に「ジェネリックパラメータ」を持つことができ、ライフタイムはその一種
&mut i32 と &'a mut i32 は同じ
&mut i32 はライフタイム、「i32のミュータブルな参照」
&'a mut i32 は、「ライフタイム'aを持つi32へのミュータブルな参照」
関数シグニチャにおけるライフタイム注釈
// この関数シグニチャは、何らかのライフタイム'aに対して、どちらの引数も同じライフタイム'aと同じだけ生き残る文字列スライスであるとコンパイラに教えている
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}
// output
error[E0597]: `string2` does not live long enough
ライフタイムの観点で思考する
究極的にライフタイム記法は、関数のいろんな引数と戻り値のライフタイムを接続することに関する。一旦、つながりが出来たら、メモリ安全な処理を許可するのに十分な情報がコンパイラにはあり、ダングリングポインタを生成するであろう処理を不許可し、さもなくばメモリ安全を侵害する
// yのライフタイムは指定していないがエラーにならない
// なぜならyのライフタイムはxや戻り値のライフタイムとは何の関係もないから
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
// 戻り値型にライフタイム'aを指定していても、戻り地のライフタイムは、引数のライフタイムと全く関係がないので、この実装はコンパイルできない
fn longest<'a>(x: &str, y: &str) -> &'a str {
// 本当に長い文字列
let result = String::from("really long string");
result.as_str()
}
入出力プロジェクト:コマンドラインプログラムを構築する
grepプロジェクトは、ここまでに学んできた多くの概念を集結させる
- コードを体系化する(モジュール)
- ベクタと文字列を使用する(コレクション)
- エラーを処理する
- 適切な箇所でトレイトとライフタイムを使用する
- テストを記述する
引数の値を読み取る
Rustの標準ライブラリで提供されているstd::env::args関数を用いる
この関数は、minigrepに与えられたコマンドライン引数のイテレータを変えす
イテレータは一連の値を生成する、イテレータに対してcollect関数を呼び出し、イテレータが生成する要素全部を含むベクタなどのコレクションに変えられる
use std::env;
fn main() {
// 引数を受け取る
let args: Vec<String> = env::args().collect();
// 引数の値を変数に保存する
let query = &args[1];
let filename = &args[2];
// {}を探しています
println!("Searching for {}", query);
// {}というファイルの中
println!("In file {}", filename);
}
リファクタリングしてモジュール性とエラー処理を向上させる
1つ目の問題:
機能を小分けにすることで、各関数が1つの仕事のみに責任を持つようになり、関数の動作の正しさを確認することがしやすくなり、テストも行いやすくなり、機能を壊さずに変更できるようになるなり。
2つ目の問題:
mainが長くなるほど、スコープに入れるべき変数も増える
そして、スコープにある変数が増えれば、各々の目的を追うのも大変になる
設定用変数を1つの構造に押し込め、目的を明確化するのが最善
3つ目の問題:
ファイルを開く行為は、ファイルが存在しない以外にもいろんな方法で失敗することがある
例えば、ファイルは存在しても開く権限がないなど
「ファイルが見つかりませんでした」のみのエラー文だと、ユーザに間違った情報を与える
4つ目の問題:
異なるエラーを処理するのにexpectを繰り返し使用しているので、ユーザが十分な数の引数を渡さずにプログラムを起動した時に、問題を明確に説明しない「範囲外のアクセス(out of bounds)」というエラーがRustから得られる
エラー処理のコードが全て1箇所に存在し、将来エラー処理ロジックが変更になった時に、メンテナンス者が1箇所のコードのみを考慮すればいいようにするのが最善
バイナリプロジェクトの責任の分離
main関数に複数の仕事の責任を割り当てるという構造上の問題は、多くのバイナリプロジェクトであふれている
結果として、mainが肥大化し始めた際にバイナリプログラムの個別の責任を分割するためにガイドラインとして活用できる工程をRustコミュニティは開発した
- プログラムをmain.rsとlib.rsに分け、ロジックをlib.rsに移動する
- コマンドライン引数の解析ロジックが小規模な限り、main.rsにおいても良い
- コマンドライン引数の解析ロジックが複雑化の様相を停止初めたら、main.rsから抽出してlib.rsに移動する
この工程の後に、main関数に残る責任は以下に限定される
- 引数の値でコマンドライン引数の解析ロジックを呼び出す
- 他のあらゆる設定を行う
- lib.rsのrun関数を呼び出す
- runがエラーを返した時に処理する
step1 引数解析機を抽出する
引数の値を変数に保存して返す関数を作成する
fn main() {
let args: Vec<String> = env::args().collect();
let (query, filename) = parse_config(&args);
// 引数解析機
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let filename = &args[2];
(query, filename)
}
}
step2 設定値をまとめる
関数がタプルを返し、即座にタプルを分解して再度個別の値にしている...これは、正しい抽象化をまだできていないかもしれない兆候
まだ改善の余地があると示してくれる他の兆候は、parse_configのconfigの部分、返却している2つの値は関係があり、1つの設定値の1部にどちらもなることを暗示している
この2つの値を1構造体に置き換え、構造体のフィールドそれぞれに意味のある名前をつけることもできる
そうすることで将来このコードのメンテナンス者が、異なる値が相互に関係する仕方や、目的を理解しやすくなる
fn main() {
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
let mut f = File::open(config.filename).expect("file not found");
// --snip--
}
struct Config {
query: String,
filename: String,
}
// parse_configのシグニチャは、これでConfig値を返すようになった
fn parse_config(args: &[String]) -> Config {
// cloneによって参照のライフタイムを管理する必要がなくなる
// が、Configインスタンスが所有するデータの総コピーが生成されるので、文字列データへの参照を保持するよりも時間とメモリを消費する
let query = args[1].clone(); //
let filename = args[2].clone();
Config { query, filename }
}
Configのコンストラクタを作成する
今やparse_config関数の目的はConfigインスタンスを生成することになったので、parse_configをただの関数からConfig構造体に紐づくnewという関数に変えることができる
この変更によって、コードがより慣習的になる
String::newと同様に、Config::newで呼び出せるようになる
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);)
// -- snip ---
}
// -- snip --
### エラー処理を修正する
ベクタが引数が2個以下の要素しか含んでいない時にargsベクタの添字1か2にアクセスしようとすると、プログラムがパニックする(out of bounds)
### エラーメッセージを改善する
new関数に、添字1と2にアクセスする前にスライスが十分ながいことを実証するチェックをする、スライスの長さが足りなければ、エラーメッセージを表示する
```rust
fn new(args: &[String]) -> Config {
if argsl.len() < 3 {
panic!("not enough arguments");
}
}
### panic!を呼び出す代わりにnewからResultを返す
成功時に、Configインスタンスを含み、エラー時には問題に言及するResult値を返すようにする
Config::newからErr値を返すことにより、main関数は、new関数から返ってくるResult値を処理し、エラー時により綺麗にプロセスから抜け出すことができる
```rust
impl Config{
// &'static strは文字列リテラル型(今回のエラーメッセージの型)
fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
// -- snip --
Ok(Config { query, filename })
}
}
### Config::newを呼び出し、エラー処理をす
Configの戻り値が変更したので、mainの処理を変更する必要がある
unwrap_or_elseを用い、独自のエラー処理を定義できる
unwrapは、Ok値の時にOkが包んでいる中身を返す
unwrap_or_elseは、Err値なら、このメソッドの場合、クロージャ内でコードを呼び出し、クロージャによってプログラムを綺麗に終了させる
### mainからロジックを抽出する
現在main関数に存在する設定のセットアップやエラー処理にかかわらない全てのロジックを保持することになるrunという関数を抽出する
```rust
fn main() {
// --snip--
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
run(config);
}
fn run(config: Config) {
let mut f = File::open(config.filename).expect("file not found");
let mut contents = String::new();
f.read_to_string(&mut contents)
.expect("something went wrong reading the file");
println!("With text:\n{}", contents);
}
// --snip--
run関数からエラーを返す
Boxは、関数がErrorトレイトを実装する型を返すことを意味する
戻り値の型を具体的に指定しなくても良い
これにより、エラーケースによって異なる型のエラー値を返す柔軟性を得る
fn run(config: Config) -> Result<(), Box<Error>> {
// expectを呼び出してパニックさせるかわりに
// ?演算子で呼び出し元が処理できるように、現在の関数からエラー値を返す
let mut f = File::open(config.filename)?;
let mut contents = String::new();
// expect → ?
f.read_to_string(&mut contents)?;
println!("With text:\n{}", contents);
// (())という記法は、runを副作用のためだけに呼び出していると示唆する慣習的な方法
Ok(())
}
mainでrunから返ってきたエラーを処理する
if let Err(e) = run(config) {
println!(Application error: {}", e);
process:exit(1);
}
テスト開発駆動でライブラリの機能を開発する
- 失敗するテストを書き、走らせて想定通りの理由で失敗することを確かめる
- 十分な量のコードを書くか変更して新しいテストを通過するようにする
- 追加または変更したばかりのコードをリファクタリングし、テストが通り続けることを確認する
- 手順1から繰り返す
失敗するテストを記述する
search関数が未実装なのでテストどころかコンパイルが通らない
# [cfg(test)]
mod test {
use super::*;
#[test]
fn one_result() {
let query = "duct";
// Rustは
// 安全で速く生産性も高い。
// 3つ選んで。
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(
vec!["safe, fast, productive."],
search(query, contents)
);
}
}
search関数を実装する
// 'aがserachのシグネチャで定義し、contents引数と戻り値で使用する
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
環境変数を取り扱う
- 大小小文字を区別しないsearch関数用に失敗するテストを書く
- Config構造体にinsensitiveを追加
- impl Configでenv:varを用いて環境変数を確認する
- シェルでrcファイルに環境変数を追記する
- 成功するテストを完成させる
pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
// スライス長が十分でなければエラー
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
// 環境変数CASE_INSENSITIVEが何かにセットされていれば
// falseを返す
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config {
query,
filename,
case_sensitive, // 追加
})
}
}
pub fn run(config: Config) -> Result<(), Box<Error>> {
// expectを呼び出してパニックさせるかわりに
// ?演算子で呼び出し元が処理できるように、現在の関数からError値を返す
let mut f = File::open(config.filename)?;
let mut contents = String::new();
// expect → ?
// contentsに読み込んだfを文字列に変換して入れる
f.read_to_string(&mut contents)?;
// 環境変数に応じて処理を分岐
let results = if config.case_sensitive {
search_case_sensitive(&config.query, &contents)
} else {
search_case_insensitive(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
// (())という記法は、runを副作用のためだけに呼び出していると示唆する慣習的な方法
Ok(())
}
// メソッド名を変更
pub fn search_case_sensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
// -- snip --
}
// 大小文字を区別しないsearch関数
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
# [cfg(test)]
mod test {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.:";
assert_eq!(
vec!["safe, fast, productive."],
search_case_sensitive(query, contents)
);
}
#[test]
fn case_insensitive() {
let query = "rUsT";
// (最後の行のみ)
// 私を信じて
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
$ CASE_INSENSITIVE=1 cargo run to poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
標準出力ではなく標準エラーにエラーメッセージを書き込む
現時点では、すべての出力をprintln!関数を使用して端末に書き込んでいる
多くの端末は2種類の出力を提供する
普通の情報用の標準出力(stdout)とエラーメッセージ用の標準エラー出力(stderr)
この差異のおかげで、ユーザはエラーメッセージを画面に表示しつつ、プログラムの成功した出力をファイルにリダイレクトすることを選択できる
prinln!関数は、標準出力に出力する能力しか無いので、標準エラーに出力するには他のものを使用しなければならない
エラーが書き込まれる場所を確認する
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
プロジェクトルートにファイルが作成されている
エーラを標準エラーに出力する
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
// エラーは画面にみえつつ、output.txtは何も含まない
// 成功のときはは、画面に見えず、output.txtに結果が含まれる
// これはコマンドラインプログラムに期待する動作
eprintln!("Problem parseing argumetns: {}", err);
process::exit(1);
);
}
入出力プロジェクト:コマンドラインプログラムを構築する
grepプロジェクトは、ここまでに学んできた多くの概念を集結させる
- コードを体系化する(モジュール)
- ベクタと文字列を使用する(コレクション)
- エラーを処理する
- 適切な箇所でトレイトとライフタイムを使用する
- テストを記述する
引数の値を読み取る
Rustの標準ライブラリで提供されているstd::env::args関数を用いる
この関数は、minigrepに与えられたコマンドライン引数のイテレータを変えす
イテレータは一連の値を生成する、イテレータに対してcollect関数を呼び出し、イテレータが生成する要素全部を含むベクタなどのコレクションに変えられる
use std::env;
fn main() {
// 引数を受け取る
let args: Vec<String> = env::args().collect();
// 引数の値を変数に保存する
let query = &args[1];
let filename = &args[2];
// {}を探しています
println!("Searching for {}", query);
// {}というファイルの中
println!("In file {}", filename);
}
リファクタリングしてモジュール性とエラー処理を向上させる
1つ目の問題:
機能を小分けにすることで、各関数が1つの仕事のみに責任を持つようになり、関数の動作の正しさを確認することがしやすくなり、テストも行いやすくなり、機能を壊さずに変更できるようになるなり。
2つ目の問題:
mainが長くなるほど、スコープに入れるべき変数も増える
そして、スコープにある変数が増えれば、各々の目的を追うのも大変になる
設定用変数を1つの構造に押し込め、目的を明確化するのが最善
3つ目の問題:
ファイルを開く行為は、ファイルが存在しない以外にもいろんな方法で失敗することがある
例えば、ファイルは存在しても開く権限がないなど
「ファイルが見つかりませんでした」のみのエラー文だと、ユーザに間違った情報を与える
4つ目の問題:
異なるエラーを処理するのにexpectを繰り返し使用しているので、ユーザが十分な数の引数を渡さずにプログラムを起動した時に、問題を明確に説明しない「範囲外のアクセス(out of bounds)」というエラーがRustから得られる
エラー処理のコードが全て1箇所に存在し、将来エラー処理ロジックが変更になった時に、メンテナンス者が1箇所のコードのみを考慮すればいいようにするのが最善
バイナリプロジェクトの責任の分離
main関数に複数の仕事の責任を割り当てるという構造上の問題は、多くのバイナリプロジェクトであふれている
結果として、mainが肥大化し始めた際にバイナリプログラムの個別の責任を分割するためにガイドラインとして活用できる工程をRustコミュニティは開発した
- プログラムをmain.rsとlib.rsに分け、ロジックをlib.rsに移動する
- コマンドライン引数の解析ロジックが小規模な限り、main.rsにおいても良い
- コマンドライン引数の解析ロジックが複雑化の様相を停止初めたら、main.rsから抽出してlib.rsに移動する
この工程の後に、main関数に残る責任は以下に限定される
- 引数の値でコマンドライン引数の解析ロジックを呼び出す
- 他のあらゆる設定を行う
- lib.rsのrun関数を呼び出す
- runがエラーを返した時に処理する
step1 引数解析機を抽出する
引数の値を変数に保存して返す関数を作成する
fn main() {
let args: Vec<String> = env::args().collect();
let (query, filename) = parse_config(&args);
// 引数解析機
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let filename = &args[2];
(query, filename)
}
}
step2 設定値をまとめる
関数がタプルを返し、即座にタプルを分解して再度個別の値にしている...これは、正しい抽象化をまだできていないかもしれない兆候
まだ改善の余地があると示してくれる他の兆候は、parse_configのconfigの部分、返却している2つの値は関係があり、1つの設定値の1部にどちらもなることを暗示している
この2つの値を1構造体に置き換え、構造体のフィールドそれぞれに意味のある名前をつけることもできる
そうすることで将来このコードのメンテナンス者が、異なる値が相互に関係する仕方や、目的を理解しやすくなる
fn main() {
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
let mut f = File::open(config.filename).expect("file not found");
// --snip--
}
struct Config {
query: String,
filename: String,
}
// parse_configのシグニチャは、これでConfig値を返すようになった
fn parse_config(args: &[String]) -> Config {
// cloneによって参照のライフタイムを管理する必要がなくなる
// が、Configインスタンスが所有するデータの総コピーが生成されるので、文字列データへの参照を保持するよりも時間とメモリを消費する
let query = args[1].clone(); //
let filename = args[2].clone();
Config { query, filename }
}
Configのコンストラクタを作成する
今やparse_config関数の目的はConfigインスタンスを生成することになったので、parse_configをただの関数からConfig構造体に紐づくnewという関数に変えることができる
この変更によって、コードがより慣習的になる
String::newと同様に、Config::newで呼び出せるようになる
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);)
// -- snip ---
}
// -- snip --
### エラー処理を修正する
ベクタが引数が2個以下の要素しか含んでいない時にargsベクタの添字1か2にアクセスしようとすると、プログラムがパニックする(out of bounds)
### エラーメッセージを改善する
new関数に、添字1と2にアクセスする前にスライスが十分ながいことを実証するチェックをする、スライスの長さが足りなければ、エラーメッセージを表示する
```rust
fn new(args: &[String]) -> Config {
if argsl.len() < 3 {
panic!("not enough arguments");
}
}
### panic!を呼び出す代わりにnewからResultを返す
成功時に、Configインスタンスを含み、エラー時には問題に言及するResult値を返すようにする
Config::newからErr値を返すことにより、main関数は、new関数から返ってくるResult値を処理し、エラー時により綺麗にプロセスから抜け出すことができる
```rust
impl Config{
// &'static strは文字列リテラル型(今回のエラーメッセージの型)
fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
// -- snip --
Ok(Config { query, filename })
}
}
### Config::newを呼び出し、エラー処理をす
Configの戻り値が変更したので、mainの処理を変更する必要がある
unwrap_or_elseを用い、独自のエラー処理を定義できる
unwrapは、Ok値の時にOkが包んでいる中身を返す
unwrap_or_elseは、Err値なら、このメソッドの場合、クロージャ内でコードを呼び出し、クロージャによってプログラムを綺麗に終了させる
### mainからロジックを抽出する
現在main関数に存在する設定のセットアップやエラー処理にかかわらない全てのロジックを保持することになるrunという関数を抽出する
```rust
fn main() {
// --snip--
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
run(config);
}
fn run(config: Config) {
let mut f = File::open(config.filename).expect("file not found");
let mut contents = String::new();
f.read_to_string(&mut contents)
.expect("something went wrong reading the file");
println!("With text:\n{}", contents);
}
// --snip--
run関数からエラーを返す
Boxは、関数がErrorトレイトを実装する型を返すことを意味する
戻り値の型を具体的に指定しなくても良い
これにより、エラーケースによって異なる型のエラー値を返す柔軟性を得る
fn run(config: Config) -> Result<(), Box<Error>> {
// expectを呼び出してパニックさせるかわりに
// ?演算子で呼び出し元が処理できるように、現在の関数からエラー値を返す
let mut f = File::open(config.filename)?;
let mut contents = String::new();
// expect → ?
f.read_to_string(&mut contents)?;
println!("With text:\n{}", contents);
// (())という記法は、runを副作用のためだけに呼び出していると示唆する慣習的な方法
Ok(())
}
mainでrunから返ってきたエラーを処理する
if let Err(e) = run(config) {
println!(Application error: {}", e);
process:exit(1);
}
テスト開発駆動でライブラリの機能を開発する
- 失敗するテストを書き、走らせて想定通りの理由で失敗することを確かめる
- 十分な量のコードを書くか変更して新しいテストを通過するようにする
- 追加または変更したばかりのコードをリファクタリングし、テストが通り続けることを確認する
- 手順1から繰り返す
失敗するテストを記述する
search関数が未実装なのでテストどころかコンパイルが通らない
# [cfg(test)]
mod test {
use super::*;
#[test]
fn one_result() {
let query = "duct";
// Rustは
// 安全で速く生産性も高い。
// 3つ選んで。
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(
vec!["safe, fast, productive."],
search(query, contents)
);
}
}
search関数を実装する
// 'aがserachのシグネチャで定義し、contents引数と戻り値で使用する
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
環境変数を取り扱う
- 大小小文字を区別しないsearch関数用に失敗するテストを書く
- Config構造体にinsensitiveを追加
- impl Configでenv:varを用いて環境変数を確認する
- シェルでrcファイルに環境変数を追記する
- 成功するテストを完成させる
pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
// スライス長が十分でなければエラー
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
// 環境変数CASE_INSENSITIVEが何かにセットされていれば
// falseを返す
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config {
query,
filename,
case_sensitive, // 追加
})
}
}
pub fn run(config: Config) -> Result<(), Box<Error>> {
// expectを呼び出してパニックさせるかわりに
// ?演算子で呼び出し元が処理できるように、現在の関数からError値を返す
let mut f = File::open(config.filename)?;
let mut contents = String::new();
// expect → ?
// contentsに読み込んだfを文字列に変換して入れる
f.read_to_string(&mut contents)?;
// 環境変数に応じて処理を分岐
let results = if config.case_sensitive {
search_case_sensitive(&config.query, &contents)
} else {
search_case_insensitive(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
// (())という記法は、runを副作用のためだけに呼び出していると示唆する慣習的な方法
Ok(())
}
// メソッド名を変更
pub fn search_case_sensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
// -- snip --
}
// 大小文字を区別しないsearch関数
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
# [cfg(test)]
mod test {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.:";
assert_eq!(
vec!["safe, fast, productive."],
search_case_sensitive(query, contents)
);
}
#[test]
fn case_insensitive() {
let query = "rUsT";
// (最後の行のみ)
// 私を信じて
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
$ CASE_INSENSITIVE=1 cargo run to poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
標準出力ではなく標準エラーにエラーメッセージを書き込む
現時点では、すべての出力をprintln!関数を使用して端末に書き込んでいる
多くの端末は2種類の出力を提供する
普通の情報用の標準出力(stdout)とエラーメッセージ用の標準エラー出力(stderr)
この差異のおかげで、ユーザはエラーメッセージを画面に表示しつつ、プログラムの成功した出力をファイルにリダイレクトすることを選択できる
prinln!関数は、標準出力に出力する能力しか無いので、標準エラーに出力するには他のものを使用しなければならない
エラーが書き込まれる場所を確認する
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
プロジェクトルートにファイルが作成されている
エーラを標準エラーに出力する
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
// エラーは画面にみえつつ、output.txtは何も含まない
// 成功のときはは、画面に見えず、output.txtに結果が含まれる
// これはコマンドラインプログラムに期待する動作
eprintln!("Problem parseing argumetns: {}", err);
process::exit(1);
);
}
入出力プロジェクト:コマンドラインプログラムを構築する
grepプロジェクトは、ここまでに学んできた多くの概念を集結させる
- コードを体系化する(モジュール)
- ベクタと文字列を使用する(コレクション)
- エラーを処理する
- 適切な箇所でトレイトとライフタイムを使用する
- テストを記述する
引数の値を読み取る
Rustの標準ライブラリで提供されているstd::env::args関数を用いる
この関数は、minigrepに与えられたコマンドライン引数のイテレータを変えす
イテレータは一連の値を生成する、イテレータに対してcollect関数を呼び出し、イテレータが生成する要素全部を含むベクタなどのコレクションに変えられる
use std::env;
fn main() {
// 引数を受け取る
let args: Vec<String> = env::args().collect();
// 引数の値を変数に保存する
let query = &args[1];
let filename = &args[2];
// {}を探しています
println!("Searching for {}", query);
// {}というファイルの中
println!("In file {}", filename);
}
リファクタリングしてモジュール性とエラー処理を向上させる
1つ目の問題:
機能を小分けにすることで、各関数が1つの仕事のみに責任を持つようになり、関数の動作の正しさを確認することがしやすくなり、テストも行いやすくなり、機能を壊さずに変更できるようになるなり。
2つ目の問題:
mainが長くなるほど、スコープに入れるべき変数も増える
そして、スコープにある変数が増えれば、各々の目的を追うのも大変になる
設定用変数を1つの構造に押し込め、目的を明確化するのが最善
3つ目の問題:
ファイルを開く行為は、ファイルが存在しない以外にもいろんな方法で失敗することがある
例えば、ファイルは存在しても開く権限がないなど
「ファイルが見つかりませんでした」のみのエラー文だと、ユーザに間違った情報を与える
4つ目の問題:
異なるエラーを処理するのにexpectを繰り返し使用しているので、ユーザが十分な数の引数を渡さずにプログラムを起動した時に、問題を明確に説明しない「範囲外のアクセス(out of bounds)」というエラーがRustから得られる
エラー処理のコードが全て1箇所に存在し、将来エラー処理ロジックが変更になった時に、メンテナンス者が1箇所のコードのみを考慮すればいいようにするのが最善
バイナリプロジェクトの責任の分離
main関数に複数の仕事の責任を割り当てるという構造上の問題は、多くのバイナリプロジェクトであふれている
結果として、mainが肥大化し始めた際にバイナリプログラムの個別の責任を分割するためにガイドラインとして活用できる工程をRustコミュニティは開発した
- プログラムをmain.rsとlib.rsに分け、ロジックをlib.rsに移動する
- コマンドライン引数の解析ロジックが小規模な限り、main.rsにおいても良い
- コマンドライン引数の解析ロジックが複雑化の様相を停止初めたら、main.rsから抽出してlib.rsに移動する
この工程の後に、main関数に残る責任は以下に限定される
- 引数の値でコマンドライン引数の解析ロジックを呼び出す
- 他のあらゆる設定を行う
- lib.rsのrun関数を呼び出す
- runがエラーを返した時に処理する
step1 引数解析機を抽出する
引数の値を変数に保存して返す関数を作成する
fn main() {
let args: Vec<String> = env::args().collect();
let (query, filename) = parse_config(&args);
// 引数解析機
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let filename = &args[2];
(query, filename)
}
}
step2 設定値をまとめる
関数がタプルを返し、即座にタプルを分解して再度個別の値にしている...これは、正しい抽象化をまだできていないかもしれない兆候
まだ改善の余地があると示してくれる他の兆候は、parse_configのconfigの部分、返却している2つの値は関係があり、1つの設定値の1部にどちらもなることを暗示している
この2つの値を1構造体に置き換え、構造体のフィールドそれぞれに意味のある名前をつけることもできる
そうすることで将来このコードのメンテナンス者が、異なる値が相互に関係する仕方や、目的を理解しやすくなる
fn main() {
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
let mut f = File::open(config.filename).expect("file not found");
// --snip--
}
struct Config {
query: String,
filename: String,
}
// parse_configのシグニチャは、これでConfig値を返すようになった
fn parse_config(args: &[String]) -> Config {
// cloneによって参照のライフタイムを管理する必要がなくなる
// が、Configインスタンスが所有するデータの総コピーが生成されるので、文字列データへの参照を保持するよりも時間とメモリを消費する
let query = args[1].clone(); //
let filename = args[2].clone();
Config { query, filename }
}
Configのコンストラクタを作成する
今やparse_config関数の目的はConfigインスタンスを生成することになったので、parse_configをただの関数からConfig構造体に紐づくnewという関数に変えることができる
この変更によって、コードがより慣習的になる
String::newと同様に、Config::newで呼び出せるようになる
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);)
// -- snip ---
}
// -- snip --
### エラー処理を修正する
ベクタが引数が2個以下の要素しか含んでいない時にargsベクタの添字1か2にアクセスしようとすると、プログラムがパニックする(out of bounds)
### エラーメッセージを改善する
new関数に、添字1と2にアクセスする前にスライスが十分ながいことを実証するチェックをする、スライスの長さが足りなければ、エラーメッセージを表示する
```rust
fn new(args: &[String]) -> Config {
if argsl.len() < 3 {
panic!("not enough arguments");
}
}
### panic!を呼び出す代わりにnewからResultを返す
成功時に、Configインスタンスを含み、エラー時には問題に言及するResult値を返すようにする
Config::newからErr値を返すことにより、main関数は、new関数から返ってくるResult値を処理し、エラー時により綺麗にプロセスから抜け出すことができる
```rust
impl Config{
// &'static strは文字列リテラル型(今回のエラーメッセージの型)
fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
// -- snip --
Ok(Config { query, filename })
}
}
### Config::newを呼び出し、エラー処理をす
Configの戻り値が変更したので、mainの処理を変更する必要がある
unwrap_or_elseを用い、独自のエラー処理を定義できる
unwrapは、Ok値の時にOkが包んでいる中身を返す
unwrap_or_elseは、Err値なら、このメソッドの場合、クロージャ内でコードを呼び出し、クロージャによってプログラムを綺麗に終了させる
### mainからロジックを抽出する
現在main関数に存在する設定のセットアップやエラー処理にかかわらない全てのロジックを保持することになるrunという関数を抽出する
```rust
fn main() {
// --snip--
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
run(config);
}
fn run(config: Config) {
let mut f = File::open(config.filename).expect("file not found");
let mut contents = String::new();
f.read_to_string(&mut contents)
.expect("something went wrong reading the file");
println!("With text:\n{}", contents);
}
// --snip--
run関数からエラーを返す
Boxは、関数がErrorトレイトを実装する型を返すことを意味する
戻り値の型を具体的に指定しなくても良い
これにより、エラーケースによって異なる型のエラー値を返す柔軟性を得る
fn run(config: Config) -> Result<(), Box<Error>> {
// expectを呼び出してパニックさせるかわりに
// ?演算子で呼び出し元が処理できるように、現在の関数からエラー値を返す
let mut f = File::open(config.filename)?;
let mut contents = String::new();
// expect → ?
f.read_to_string(&mut contents)?;
println!("With text:\n{}", contents);
// (())という記法は、runを副作用のためだけに呼び出していると示唆する慣習的な方法
Ok(())
}
mainでrunから返ってきたエラーを処理する
if let Err(e) = run(config) {
println!(Application error: {}", e);
process:exit(1);
}
テスト開発駆動でライブラリの機能を開発する
- 失敗するテストを書き、走らせて想定通りの理由で失敗することを確かめる
- 十分な量のコードを書くか変更して新しいテストを通過するようにする
- 追加または変更したばかりのコードをリファクタリングし、テストが通り続けることを確認する
- 手順1から繰り返す
失敗するテストを記述する
search関数が未実装なのでテストどころかコンパイルが通らない
# [cfg(test)]
mod test {
use super::*;
#[test]
fn one_result() {
let query = "duct";
// Rustは
// 安全で速く生産性も高い。
// 3つ選んで。
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(
vec!["safe, fast, productive."],
search(query, contents)
);
}
}
search関数を実装する
// 'aがserachのシグネチャで定義し、contents引数と戻り値で使用する
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
環境変数を取り扱う
- 大小小文字を区別しないsearch関数用に失敗するテストを書く
- Config構造体にinsensitiveを追加
- impl Configでenv:varを用いて環境変数を確認する
- シェルでrcファイルに環境変数を追記する
- 成功するテストを完成させる
pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
// スライス長が十分でなければエラー
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
// 環境変数CASE_INSENSITIVEが何かにセットされていれば
// falseを返す
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config {
query,
filename,
case_sensitive, // 追加
})
}
}
pub fn run(config: Config) -> Result<(), Box<Error>> {
// expectを呼び出してパニックさせるかわりに
// ?演算子で呼び出し元が処理できるように、現在の関数からError値を返す
let mut f = File::open(config.filename)?;
let mut contents = String::new();
// expect → ?
// contentsに読み込んだfを文字列に変換して入れる
f.read_to_string(&mut contents)?;
// 環境変数に応じて処理を分岐
let results = if config.case_sensitive {
search_case_sensitive(&config.query, &contents)
} else {
search_case_insensitive(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
// (())という記法は、runを副作用のためだけに呼び出していると示唆する慣習的な方法
Ok(())
}
// メソッド名を変更
pub fn search_case_sensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
// -- snip --
}
// 大小文字を区別しないsearch関数
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
# [cfg(test)]
mod test {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.:";
assert_eq!(
vec!["safe, fast, productive."],
search_case_sensitive(query, contents)
);
}
#[test]
fn case_insensitive() {
let query = "rUsT";
// (最後の行のみ)
// 私を信じて
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
$ CASE_INSENSITIVE=1 cargo run to poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
標準出力ではなく標準エラーにエラーメッセージを書き込む
現時点では、すべての出力をprintln!関数を使用して端末に書き込んでいる
多くの端末は2種類の出力を提供する
普通の情報用の標準出力(stdout)とエラーメッセージ用の標準エラー出力(stderr)
この差異のおかげで、ユーザはエラーメッセージを画面に表示しつつ、プログラムの成功した出力をファイルにリダイレクトすることを選択できる
prinln!関数は、標準出力に出力する能力しか無いので、標準エラーに出力するには他のものを使用しなければならない
エラーが書き込まれる場所を確認する
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
プロジェクトルートにファイルが作成されている
エーラを標準エラーに出力する
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
// エラーは画面にみえつつ、output.txtは何も含まない
// 成功のときはは、画面に見えず、output.txtに結果が含まれる
// これはコマンドラインプログラムに期待する動作
eprintln!("Problem parseing argumetns: {}", err);
process::exit(1);
);
}
関数言語の機能: イテレータとクロージャ
クロージャとイテレータをマスターすることは、慣用的で早いRustコードを書くことで重要
クロージャ: 変数に保存できる関数に似た文法要素
イテレータ: 一連の要素を処理する方法
その他: パターンパッチング、enum
低レベルのパフォーマンスで、高レベルの考えを明確に表現するというのがRustの能力に貢献している
クロージャとイテレータの実装は、実行時のパフォーマンスが影響されないようになっている
これは、ゼロ代償抽象化を提供するのに努力を惜しまないRustの目標の一部
クロージャで動作の抽象化を行う
use std::thread;
use std::time::Duration;
// 標準出力して、2秒待ってから、渡した数値をなんでも返す関数
fn simulated_expensive_calculation(intensity: u32) -> u32 {
// ゆっくり計算します
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
intensity
}
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(
simulated_user_specified_value,
simulated_random_number
);
}
クロージャでリファクタリングして、コードを保存する
fn generate_workout(intensity: u32, random_number: u32) {
// ||は引数
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
println!(
"Today, do {} pushups!",
expensive_closure(intensity)
);
println!(
"Next, do {} situps!",
expensive_closure(intensity)
);
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_closure(intensity)
);
}
}
}
クロージャの型推論と注釈
クロージャでは、fn関数のように引数の型や戻り値の型を注釈する必要はない
クロージャではコンパイラが型推論をおこなう
本当に必要な以上に冗長になることと引き換えに、明示生徒明瞭性を向上させたいなら、変数に型注釈を加えることもできる
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |xsl: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
let example_closure = |x| x;
let s = example_closure( String::from("hello"));
// sの型推論でクロージャがStringと推測したため
// 数値を入れると型エラーとなる
let n = example_closure(5);
ジェネリック引数とFnトレイトを使用してクロージャを保存する
重いクロージャの結果を再利用できるように変数に保存し、クロージャを再度呼ぶ代わりに、結果が必要になるか箇所でそれぞれでその変数を使用する
しかしながら、この方法は同じコードを大量に繰り返す化膿性がある
クロージャやクロージャの呼び出し結果の値を保持する構造体をつくれる
結果の値が必要な場合にのみその構造体はクロージャを実行し、その結果の値をキャッシュするので、残りのコードは、結果を保存し、再利用する責任を追わなくて済む
クロージャを保持する構造体を作成するために、クロージャの方を指定する必要がある構造体定義は、各フィールドの型を泊する必要があるから
各クロージャインスタンスには、独自の匿名の型がある
つまり、2つのクロージャが全く同じシグニチャでも、その型はそれでも違うものと考えられる
Cacher構造体は、ジェネリックな型Tのcalculationフィールドを持つ
Tのトレイト境界は、Fnトレイトを使うことでクロージャであると指定している
calculationフォールドに保存したいクロージャは全て、1つのu32の引数(Fnの後のカッコ内で指定している)を取り、u32(->のあとに指定されている)を返さなければならない
struct Cacher<T> where T: Fn(u32) -> u32 {
calculation: T,
vlaue: Option<u32>,
}
## イテレータ
```rust
fn main() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(Some(&1), v1_iter.next());
assert_eq!(Some(&2), v1_iter.next());
assert_eq!(Some(&3), v1_iter.next());
}
next()の呼び出しで得られる値は、ベクタへの不変な参照
### イテレータを消費するメソッド
Iteratorトレイトには、標準ライブラリが提供してくれているデフォルト実装のある多くの異なるメソッドがある
これらのメソッドの中には、定義内でnextメソッドを呼ぶものもあり、故にiteratorトレイトを実装する際には、nextメソッドを実装する必要がある
nextを呼び出すメソッドは、消費アダプタ(consuming adaputors)と呼ばれる
## パフォーマンス比較: ループ VS イテレータ
イテレーションのほうが些か高速になる
```terminal:文字列を読み込むテスト
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
CargoとCreates.io
- リリースプロファイルでビルドをカスタマイズ
- creates.ioでライブラリを公開する
- ワークスペースで巨大なプロジェクトを体系化する
- creates.ioからバイナリをインストールする
- 独自のコマンドを使用してCargoを拡張する
リリースプロファイルでビルドをカスタマイズする
// devプロファイル (開発用)
cargo build
// releaseプロファイル (リリース用)
cargo build --releaseプロファイル
Cargo.tomlファイルに[profile.*]セクションが存在しない際に適用される各プロファイル用のデフォルト設定が、Cargoには存在する
カスタマイズしたいプロファイル用の[profile.*]セクションを追加することで、デフォルト設定の一部を上書きすることができる
[profile.dev]
opt-level=0 // コンパイル時間が短い
[profile.release]
opt-level=3 // 長い
Crates.ioにクレートを公開
///でドキュメンテーションコメントできる
Markdown記法もサポートしている
/// Adds one to the number given.
/// 与えられた数値に1を足す。
///
/// # Examples
///
/// ```
/// let five = 5;
///
/// assert_eq!(6, my_crate::add_one(5));
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
cargo doc --open
その他、省略
Cargoのワークスペース
プロジェクトの開発が進むにつれて、ライブラリクレートの肥大化が続き、その上で複数のライブラリクレートにパッケージを分割したくなる
この場面において、Cargoはワークスp−エスという強調して開発された関連のある複数のパッケージを管理するのに役立つ機能を提供している
ワークスペースを生成する
ワークスペースは、同じCargo.lockと出力ディレクトリを共有する一連のパッケージ
ワークスペースを使用したプロジェクトを作成し、ワークスペースの構造に集中できるよう、瑣末なコードを使用する
一般的にはバイナリ1つとライブラリ2を含むワークスペースをつくる
mkdir add
cd add
[workspace]
members = [
"adder",
]
addディレクトリ内でcargo new を実行することでadderバイナリクレートを作成する
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
$ cargo new add-one --lib
Created library `add-one` project
├── Cargo.lock
├── Cargo.toml
├── add-one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
pub fn add_one(x: i32) -> i32 {
x + 1
}
failed to read cargo.tomlと警告文がでるとき
[package]にpathを追加する
[package]
path = "src/main.rs"
ワークスペースにライブラリクレートが存在するようになったら、バイナリクレートadderをライブラリクレートのadd-oneに依存させられる
まず、add-oneへのパス依存をadder/Cargo.tomlに追加する
[dependencies]
add-one = { path = "../add-one" }
// add-oneクレートを使う
extern crate add_one;
fn main() {
let num = 10;
println!("{}", add_one::add_one(num));
}
cargo run -p adder
Cargo.lockはワークスペースに1つだけ存在する (cargo buildはワークスペースの最上位階層で実行するので)
ワークスペースの外部クレートに依存する
[dependencies]
rand = "0.3.14"
randはワースくペースのどこかで使用されているにもかかわらず、それぞれのCargo.tomlファイルにも、randを追加しない限り、ワークスペースの他のクレートでそれを使用することは出来ない
error[E0463]: can't find crate for `rand`
--> adder/src/main.rs:2:1
|
2 | extern crate rand;
| ^^^^^^^^^^^^^^^^^^ can't find crate
他のクレートで使用するためにCargo.tomlにrandを追記してbuildすれば使用できるようになる
これによりrandのファイルが追加でダウンロードされることはない
Cargoが、ワークスペースのrandを使用するどのクレートも、同じバージ ョンを使っていることを確かめてくれる
ワークスペース全体でrandの同じバージョンを使用することにより、複数のコピーが存在しないのでスペースを節約し、ワークスペースのクレートが相互に互換性を維持することがdケイル
独自のコマンドでCargoを拡張する
$PATHにあるバイナリがcargo-somethingという名前ならcargo somethingを実行することで、Cargoのサブコマンドであるかのうように実行することができる
cargo --list
スマートポインタ
Rustで最もありふれた種類のポインタは、参照
参照は&で示唆され、指している値を借用する
データを参照すること以外に特別な能力は何もない
オーバーヘッドもなく、最も頻繁に使われる種類のポインタ
スマートポインタは、ポインタのように振る舞うだけでなく、追加のメタデータと能力があるデータ構造
スマートポインタの概念はRust特有のものではない
C++に端を発し、他の言語にも存在している
Rustでは、標準ライブラリに定義された様々なスマートポインタが、参照以上の機能を提供する
その1つの例が、参照カウント方式のスマートポインタ
このポインタにより、所有者の数を追いかけることでデータに複数の所有者をもたせることができ、所有者がいなくなったら、データの片付けをする
所有権と借用の概念を使うRustで、参照とスマートポインタの別の差異は、参照はデータを借用するだけのポインタであること
対象的に多くの場合、スマートポインタは指しているデータを所有する
StringやVecのように、メモリを所有し、それを弄ることができるので、スマートポインタに数えられる
また、メタデータ(キャパシティなど) や追加の能力、あるいは保証(Stringならデータが常に有効なUTF-8であることを保証するなど)もある
スマートポインタは普通、構造体を使用して実装されている
スマートポインタを通常の構造体と区別する特徴は、スマートポインタは、DerefとDropトレイトを実装していること
Derefトレイトにより、スマートポインタ構造体のインスタンスは、参照のように振る舞うことができるので、参照あるいはスマートポインタのどちらとも動作するコードを書くことができる
Dropトレイトにより、スマートポインタのインスタンスがスコープを外れた時に走るコードをカスタマイズすることができる
多くのライブラリに独自のスマートポインタがあり、自分だけのスマートポインタを書くことさえできる
標準ライブラリの最もありふれたスマートポインタを学ぶ
- ヒープに値を確保するBox
- 複数の所有権を可能にする参照カウント型のRc
- RefCallを通してアクセスされ、コンパイル時ではなく実行時に借用規則を強制する型のRefとRefMut
さらに、不変な型が、内部の値を可変化するAPIを晒す内部可変性パターンについて
また循環参照について、これによってメモリがリークする方法と回避方法
ヒープのデータを指すBoxを使用する
最も素直なスマートポインタはボックスであり、その型はBoxと記述される
ボックスにより、スタックではなくヒープにデータを格納することができる
スタックに残るのは、ヒープデータへのポインタ
- コンパイル時にはサイズを知ることが出来ない型があり、正確なサイズを要求する文脈でその方の値を使用する時
- 多くのデータがあり、所有権を転送したいが、その際にデータがコピーされないようにしたい時
- 多くのデータの所有権を転送するには、データがスタック上でコピーされるので、長い時間がかかり得る、この場合でパフォーマンスを向上させるには、多くのデータをヒープ上にボックスとして格納する
- 値を所有する必要があり、特定の型ではなく特定のトレイトを実装する型であることのみ気にかけている時
Boxを使ってヒープにデータを格納する
// bがmainの終わりでするようにボックスがスコープを抜けたら、メモリから解放される
fn main(){
let b = Box::new(5);
println!("b={}", b);
}
メモリの解放はボックスと指しているデータに対して起きる
(スタックに格納されているポインタと、ヒープに格納されているデータ)
ボックスで再帰的な方を可能にする
コンパイル時に、コンパイラは、ある方が取る領域を知る必要がある
コンパイル時にサイズがわからない方の1つ、再帰的な型であり、これは、型の1部として同じ型の他の値を持つもの
この値のネストは、理論的には無限に続く化膿性があるので、コンパイラは再帰的な型の値が必要とする領域を知ることができない
しかしながら、ボックスは既知のサイズなので、再帰的な型の定義にボックスを挟むことで再帰的な型を存在させることができる
コンスリスト
コンスリストは関数型プログラミングでは一般的なデータ型
これを再帰的な型の例として探求する
Lisp(list processing)プログラミングとその方言に由来するデータ構造
Lispでは、cons関数(construct function)が2つの引数から新しいペアを構成し、この引数は通常、単独の値と別のペアからなる
これらのペアを含むペアがリストをなす
コンスリストの各要素は、2つの要素を含む
現在の要素の値と次の要素
リストの最後の要素は、次の要素なしにNilと呼ばれる値のみを含む(nullやnilの概念とは異なる)
// 再帰的なenumを定義しようとするとエラーになる
// 自身の別の値を直接保持しようとしたため
// 結果として、コンパイラは、List値を格納するのに必要な領域が計算できない
enum List {
Cons(i32, List),
Nil,
}
use List::{Cons, Nil};
fn main() {
// 無限のCons列挙子からなる無限のList
let list = Cons(1, Cons(2, Cons(3, Cons(4, Nil))));
}
// output
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ recursive type has infinite size
2 | Cons(i32, List),
| ---- recursive without indirection
|
= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` representable
= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` representable
// ConsがBoxを保持しているので、無限にサイズがあるわけではないのでコンパイルが通る
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Derefトレイトでスマートポインタを普通の参照のように扱う
Derefトレイトを実装するkとで参照外し演算子の*の振る舞いをカスタマイズすることができる
スマートポインタを普通の参照のように扱えるようにDerefを実装することで、参照に対して処理を行うコードを書き、そのコードをスマートポインタとともに使用できる
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, y); // yは参照なので、値と比較出来ずエラーとなる
}
// output
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:5:2
|
5 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
fn main() {
let x = 5;
// xの値の参照ではない
// xの値を指すボックスのインスタンスにyをセットしている
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, y);
}
// output
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:5:2
|
5 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
fn main() {
let x = 5;
// xの値の参照ではない
// xの値を指すボックスのインスタンスにyをセットしている
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // 参照外しでxの値を指す
}
// output
エラーなし
独自のスマートポインタを定義する
標準ライブラリが提供しているBox型に似たスマートポインタを構築して、スマートポインタは規定で参照に比べてどう異なるのかを経験する
Box型は究極的に1要素のタプル構造体として定義されている
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
// output
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:16
|
14 | assert_eq!(5, *y);
| ^^
error: aborting due to previous error
Boxと同じようにMyBoxを使おうとすると、参照外しできないとエラーになる
Derefトレイトを実装して型を参照のように扱う
Derefトレイトを実装して型を参照のように扱う
トレイトを実装するには、トレイトの必須メソッドに実装を提供する必要がある
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
// Derefトレイトが使用する関連型を定義している
type Target = T;
// selfを借用し、内部のデータへの参照を返す
fn deref(&self) -> &T {
&self.0
}
}
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // 水面下でコンパイラは*(y.deref())を実行している
}
derefメソッドが値への参照を返し、*(y.deref())のカッコの外の何の変哲もない参照外しがそれでも必要な理由は、所有権システム
derefメソッドが値への参照ではなく、値を直接返したら、値はselfから外にムーブされてしまう
今回の場合や、参照外し演算子を使用する多くの場合にはMyBoxの中の値の所有権を奪いたくない
Dropトレイトで片付け時にコードを走らせる
スコープを抜けそうになった時に起こることをカスタマイズできる
どんな型に対してもDropトレイトの実装を提供することができ、指定したコードは、ファイルやネットワーク接続などのリソースを解放するのに活用できる
Dropトレイトの機能は、ほぼ常にスマートポインタを実装する時に使われている
BoxはDropをカスタマイズしてボックスが示しているヒープの領域を開放している
ある言語では、プログラマがスマートポインタのインスタンスを使い終わるたびにメモリやリソースを解放するコードを呼ばなkればならない
忘れてしまったら、システムは詰め込み過ぎになりクラッシュする可能性がある
Rustでは、値がスコープを抜けるたびに特定のコードが走るように指定でき、コンパイラはこのコードを自動的に挿入する
結果として、特定の型のインスタンスを使い終わったプログラムの箇所全部にクリーンアップコードを配置するのに配慮する必要はない
それでもリソースをリークすることはない
Rcは、参照カウント方式のスマートポインタ
複数の所有権を可能のにするため、RustにはRcを使う
reference counting(参照カウント)
Rc型は、値がまだ使用中かどうか決定する値への参照の数を追跡する
値への参照が0なら、どの参照も無効にすることなく、値は片付けられる
Rcでデータを共有する
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
// aはbにムーズ済みなため、再びaをムーブすることはできない
let c = Cons(4, Box::new(a));
}
// compile
error[E0382]: use of moved value: `a`
--> src/main.rs:12:27
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | // aはbにムーズ済みなため、再びaをムーブすることはできない
12 | let c = Cons(4, Box::new(a));
| ^ value used here after move
use std::rc::Rc;
enum List {
Cons(i32, Rc<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
// List列挙子を使う前に初期化処理に含まれていないので、use文を追加して、Rc<T>をスコープに導入する必要がある
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
// Rc::cloneの呼び出しは、参照カウントをインクリメントするだけであり、時間はかからない
// 参照カウントにRc::cloneを使うことで、視覚的にディープコピーをするたぐいのクローンと参照カウントを増やす種類のクローンを区別することができる
let b = Cons(3, Rc::clone(&a)); // aの参照カウント2
let c = Cons(4, Rc::clone(&a)); // aの参照カウント3
}
Rcを参照数を確認する
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
// a生成後のカウント = {}
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
// b生成後のカウント = {}
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
// c生成後のカウント = {}
println!("count after creating c = {}", Rc::strong_count(&a));
}
// cがスコープを抜けた後のカウント = {}
}
//output
count after creating a = 1
count after creating b = 2
count after creating c = 3
RefCell と内部可変パターン
内部可変性は、そのデータへの不変参照がある時でさえもデータを可変化できるRustでのデザインパターン
普通、この行動は借用規則により許可されていない
ReCellで実行時に借用規則を強制する
Rcと異なり、RefCell型は、保持するデータに対して単独の所有権を表す
なぜ、RefCellがBoxのような型と違うのか
- いかなる時も(以下の療法でなく,)1つの可変参照かいくつもの不変参照のどちらかが可能になる
- 参照は常に有効でなければならない
参照とBoxでは、借用規則の不変条件は、コンパイル時に強制されている
RefCellでは、これらの不変条件は、実行時に強制される
参照でこれらの規則を破ったら、コンパイルエラーになる
RefCellでこれらの規則を破ったら、プログラムはパニックし、終了する
多くの場合でコンパイル時に借用規則を精査することが最善の選択肢であり、これがRustの規定になっている
借用規則を実行時に変わりに精査する利点は、コンパイル時の精査では許容されない特定のメモリ安全な筋書きが許容されること
Rustコンパイラのような静的解析は、本質的に保守的
コードの特性には、コードを解析するだけでは検知できないものもある
最も有名な例は停止性問題
不可解な分析もあるので、Rustのコンパイラが、コードが所有権規則に応じていると確証を得られない場合、正しいプログラムを拒否する可能性がある
コードが借用規則に従っているとプログラマは確証を得ているがコンパイラがそれを理解し保証することが出来ない時にRefCell型は有用
- Rcは、同じデータに複数の所有者を持たせている
- BoxとRefCellは単独の所有者
- Boxでは、不変借用も可変借用もコンパイル時に精査できる
- RefCellは実行時に精査される可変借用を許可するので、RefCellが不変でもRefCell内の値を可変化できる
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: 'a + Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 0.75 && percentage_of_max < 0.9 {
// 警告: 割当の75%以上を使用していました
self.messenger
.send("Warning: You've used up over 75% of your quota1");
} else if percentage_of_max >= 0.9 && percentage_of_max < 1.0 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 1.0 {
self.messenger.send("Error: Your are over your quota!");
}
}
}
# [cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
// 借用チェカーが許可しない
sent_messages: Vec<String>, }
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger { sent_messages: vec![] }
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
// output
error[E0596]: cannot borrow immutable field `self.sent_messages` as mutable
(エラー: 不変なフィールド`self.sent_messages`を可変で借用できません)
--> src/lib.rs:52:13
|
51 | fn send(&self, message: &str) {
| ----- use `&mut self` here to make mutable
52 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ cannot mutably borrow immutable field
# [cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>, // 内部可変
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger { sent_messages: RefCell::new(vec![]) }
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
コンパイル時ではなく実行時に借用エラーをキャッチするということは、開発過程の遅い段階でコードのミスを発見し、コードをプロダクションにデプロイするときまで発見しない可能性もあることを意味する
また、コンパイル時ではなく、実行時に借用を追いかける結果として、少し実行時にパフォーマンスを犠牲にする
しかしながら、RefCellを使うことで、不変値のみが許可される文脈で使用しつつ、自身を変更して見かけたメッセージを追跡するモックオブジェクトを書くことが可能になる
代償はあるが、RefCellを使用すれば、普通の参照よりも多くの機能を得ることができる
RcとRefCellを組み合わせることで可変なデータに複数の所有者を持たせる
use std::cell::RefCell;
use std::rc::Rc;
# [derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
// output
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
借用規則を実行時に精査することでデータ競合を防ぎ、時としてデータ構造でちょっとのスピードを犠牲にこの柔軟性を得るのは価値がある
Cellは、内部や外部へコピーされる点を除き似ている
Mutexは、スレッド感で使用するのが安全な内部可変性を提供する
循環参照は、メモリをリークすることもある
RcとRefCellを使用してメモリリークを許可してしまうこともできる
(要素がお互いに循環して参照してしまう)
use std::cell::RefCell;
use std::rc::Rc;
use List::{Cons, Nil};
# [derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match *self {
Cons(_, ref item) => Some(item),
Nil => None,
}
}
}
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
// aの最初の参照カウント = {}
println!("a initial rc count = {}", Rc::strong_count(&a));
// aの次の要素は = {:?}
println!("a next item = {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
// b作成後のaの参照カウント = {}
println!("a rc count after b creation = {}", Rc::strong_count(&a));
// bの最初の参照カウント = {}
println!("b initial rc count = {}", Rc::strong_count(&b));
// bの次の要素 = {:?}
println!("b next item = {:?}", b.tail());
if let Some(link) = a.tail() {
// a.tailでaのnext要素をbに置き換える
// Some(RefCell{value:Nil}) を Some(RefCell {value: Cons(5, RefCell {value: Nill })})
*link.borrow_mut() = Rc::clone(&b);
}
// aを変更後のbの参照カウント = {}
println!("b rc count after changing a = {}", Rc::strong_count(&b));
// aを変更後のaの参照カウント = {}
println!("a rc count after changing a = {}", Rc::strong_count(&a));
// Uncomment the next line to see that we have a cycle;
// it will overflow the stack
// 次の行のコメントを外して循環していると確認してください; スタックオーバーフローします
// println!("a next item = {:?}", a.tail()); // aの次の要素 = {:?}
}
循環参照を回避する RcをWeekに変換する
Rc::cloneを呼び出すとRcインスタンスのstrong_countが増え、strong_countが0になった時にRcインスタンスは片付けられる
Rc::downgradeを呼び出し、Rcへの参照を渡すことで、Rcインスタンス内部の値への弱い参照(weak reference)を作ることもできる
Weak型のスマートポインタは、strong_countを1増やす代わりにweak_countが1増える
違いは、Rcが片付けられるのに、weak_countが0である必要はない
強い参照と弱い参照
強い参照は、Rcインスタンスの所有権を共有する方法
弱い参照は、所有権関係を表現しない
ひとたび、関係する値の強い参照カウントが0になれば、弱い参照が関わる循環はなんでも破壊されるので、循環参照にならない
Weakのupgradeメソッドでドロップされているかどうかを判別する Option> を返す
ドロップ前ならSome
ドロップ後ならNone
use std::rc::{Rc, Weak};
use std::cell::RefCell;
# [derive(Debug)]
struct Node {
// 親のブランチへの弱い参照があるNode
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
// 親を参照(弱い参照)している
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
// leafの親 = {:?}
// output: None
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
// 子を参照(強い参照)している
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
// downgradeメソッドにより循環参照は断たれる
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
// output
leaf parent = None
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })
無限の出力が欠けているということは、このコードは循環参照していないことを示唆している