はじめに
Rustのチュートリアルのメモ②です。
リンク集
C++入門
プログラミングRust (日本語)
プログラミング言語Rust入門
実践Rust入門
モジュール
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
}
}
ジェネリック型、トレイト、ライフタイム
ジェネリック
関数にジェネリック
同じコードの重複を減らすために定義する
```rust: 変更前
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
}
}
```rust: ライフタイムが違うとエラー
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()
}
`