// book chapter
動かして学ぶRustプログラミング問題集第22章 ライフタイム.
第21章で、トレイトオブジェクトと関連型を扱い、トレイトを中心とした抽象化の道具がひととおり揃いました。トレイト編の最後となるこの章では、ここまでとは少し毛色の違うライフタイムを7問で扱います。
第8章で、参照には「参照先の値より長く生きてはいけない」という規則があることを学びました。あのときはコンパイラが自動で検査してくれていて、私たちは書く位置を直すだけで済みました。1つの関数の中なら、コンパイラは参照がどこで作られ、どこまで使われるかをすべて見渡せるからです。ところが関数の境界をまたぐと事情が変わります。参照を受け取って参照を返す関数や、参照をフィールドに持つ構造体では、「この参照はどの値から借りたものか」をコンパイラが決められない場面が出てきます。そこで登場するのが、'aと書くライフタイム注釈です。
なぜライフタイムがトレイト編に入っているのかというと、ライフタイム注釈がジェネリクスの一種だからです。<T>が「どんな型でもよい」を表すのと同じように、<'a>は「どんな期間でもよい」を表します。宣言する場所も、関数名・構造体名・implの直後と、型引数とまったく同じです。第17章からの知識がそのまま活きます。
進め方は第21章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - 参照を返す関数
ライフタイム注釈・ライフタイム・参照に関する問題です。
次のコードはコンパイルエラー(E0106)になります。エラーメッセージとリンク先の辞書を手がかりに、関数longestのシグネチャだけを直して修正してください。
長い方: プログルストア渋谷本店// 文字数の多い方の文字列を返す(同じなら a を返す)
fn longest(a: &str, b: &str) -> &str {
if a.chars().count() >= b.chars().count() {
a
} else {
b
}
}
fn main() {
let shop = String::from("プログルストア新宿店");
let branch = String::from("プログルストア渋谷本店");
let result = longest(&shop, &branch);
println!("長い方: {result}");
}Playgroundで開く解答例と解説
// 文字数の多い方の文字列を返す(同じなら a を返す)
fn longest(a: &str, b: &str) -> &str {
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.chars().count() >= b.chars().count() {
a
} else {
b
}
}
fn main() {
let shop = String::from("プログルストア新宿店");
let branch = String::from("プログルストア渋谷本店");
let result = longest(&shop, &branch);
println!("長い方: {result}");
}Playgroundで開くエラーメッセージはmissing lifetime specifier(E0106)——「ライフタイムの指定がない」です。続けてthis function's return type contains a borrowed value, but the signature does not say whether it is borrowed from 'a' or 'b'(戻り値は借用した値だが、aとbのどちらから借りたのかシグネチャに書かれていない)と理由も教えてくれます。
コンパイラが困っていること
第8章の問題08で、引数で借りた値の一部を参照で返すのは問題ないと学びました。longestが返すのも引数aかbのどちらかなので、ダングリング参照にはなりません。それでもエラーになるのは、コンパイラが関数を1つずつ別々に検査するからです。
main側を検査するとき、コンパイラはlongestの中身を見ません。見るのはシグネチャだけです。fn longest(a: &str, b: &str) -> &strというシグネチャからは、戻り値がshopを指しているのかbranchを指しているのか、つまり戻り値をいつまで使ってよいのかが分かりません。ifの結果で変わるのですから、中身を見たとしても決められません。
ライフタイム注釈で関係を伝える
そこで、ライフタイム注釈を使って「戻り値の参照は、aとbの両方が生きている間だけ有効」という関係をシグネチャに書きます。
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str
// ^^^^ ① 'a というライフタイムパラメータを宣言する
// ^^^ ^^^ ^^^ ② 3つの参照を同じ 'a で結ぶ書き方は第17章のジェネリクスとそっくりです。<T>で型引数を宣言してからTを使ったように、<'a>でライフタイムパラメータを宣言してから、&'a strのように&の直後に書いて使います。名前はアポストロフィで始まる短い小文字で、最初の1つは'aとするのが慣例です。
3つの参照を同じ'aで結んだことで、コンパイラは呼び出し側で「戻り値はshopとbranchのどちらか短い方の寿命の範囲でしか使えない」と判断できるようになります。今回は2つともmainの終わりまで生きているので、そのまま通ります。
02 - ライフタイムは短い方に合わせる
ライフタイム・スコープ・借用に関する問題です。
問題01で完成したlongestを使ったコードですが、今度はmain側でコンパイルエラー(E0597)になります。longestと変数の宣言はそのままに、println!の位置を動かして修正してください。
長い方: プログルストア渋谷本店fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.chars().count() >= b.chars().count() {
a
} else {
b
}
}
fn main() {
let shop = String::from("プログルストア新宿店");
let result;
{
let branch = String::from("プログルストア渋谷本店");
result = longest(&shop, &branch);
}
println!("長い方: {result}");
}Playgroundで開く解答例と解説
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.chars().count() >= b.chars().count() {
a
} else {
b
}
}
fn main() {
let shop = String::from("プログルストア新宿店");
let result;
{
let branch = String::from("プログルストア渋谷本店");
result = longest(&shop, &branch);
println!("長い方: {result}");
}
println!("長い方: {result}");
}Playgroundで開くエラーメッセージは'branch' does not live long enough(E0597)——「branchは十分長く生きていない」です。borrowed value does not live long enough(借用された値が、参照より先に破棄される)という説明と、branchが破棄される位置(内側のブロックの閉じ波括弧)も示してくれます。
’aは「短い方」になる
問題01で、longestの戻り値は「aとbの両方が生きている間だけ有効」と宣言しました。両方が生きている期間とは、つまり短い方に合わせた期間です。
| 参照 | 参照先が生きている期間 |
|---|---|
&shop |
mainの終わりまで |
&branch |
内側のブロックの終わりまで |
戻り値result |
短い方=内側のブロックの終わりまで |
ところが元のコードでは、resultをブロックの外のprintln!で使っています。resultが実際に指しているのはbranchの方なので、これは第8章の問題08で学んだダングリング参照そのものです。コンパイラはlongestの中身を見なくても、シグネチャの'aだけからこの危険を見抜いています。
修正は「使う位置」を動かすだけ
resultを使ってよいのはbranchが生きている間、つまり内側のブロックの中です。println!をブロックの中へ移せば、参照が使われている間は参照先も生きているので検査が通ります。第8章の問題07で学んだとおり、参照のライフタイムは「最後に使われる地点まで」で判定されるので、使う位置を変えるだけで結果が変わるのです。
もし人間には安全だと分かっていても
このコードは、文字数の関係から実際にはbranchの方が返ります。しかし仮にshopの方が返るような値だったとしても、コンパイラはやはりエラーにします。'aの宣言は「どちらが返るか分からないので、短い方に合わせる」という意味だからです。コンパイラが見ているのは実際の値ではなくシグネチャに書かれた関係であり、その関係の範囲で安全が保証されるコードだけが通ります。
03 - 注釈が省略できる場合
ライフタイム注釈と関数に関する問題です。
次の関数item_numberはライフタイム注釈を付けて書かれていますが、この形なら注釈をすべて取り除いてもコンパイルが通ります。<'a>と'aをすべて削除し、出力が変わらないことを確認してください。
商品番号: 1234// 商品コード "PRG-1234" から先頭の "PRG-" を除いた番号部分を返す
fn item_number<'a>(code: &'a str) -> &'a str {
&code[4..]
}
fn main() {
let code = String::from("PRG-1234");
let number = item_number(&code);
println!("商品番号: {number}");
}Playgroundで開く解答例と解説
// 商品コード "PRG-1234" から先頭の "PRG-" を除いた番号部分を返す
fn item_number<'a>(code: &'a str) -> &'a str {
fn item_number(code: &str) -> &str {
&code[4..]
}
fn main() {
let code = String::from("PRG-1234");
let number = item_number(&code);
println!("商品番号: {number}");
}Playgroundで開く問題01のlongestと同じく参照を受け取って参照を返す関数なのに、こちらは注釈なしで通ります。実は、第8章の問題09や第10章以降で書いてきた参照を返す関数の多くがこの形でした。
ライフタイム省略規則
引数の参照が1つしかなければ、戻り値の参照が「どこから借りたものか」は考えるまでもなくその引数です。このような決まりきったパターンについて、コンパイラは次の3つのライフタイム省略規則で注釈を補ってくれます。
- 引数側の省略されたライフタイムは、それぞれ別のライフタイムパラメータになる
- 引数側のライフタイムがちょうど1つなら、それが戻り値のライフタイムになる
- メソッドで受け手が
&selfまたは&mut selfなら、そのライフタイムが戻り値のライフタイムになる
item_numberに当てはめると、規則1でcodeに'aが付き、引数のライフタイムは1つだけなので規則2で戻り値も'aになります。つまり、コンパイラの中では元のコードとまったく同じfn item_number<'a>(code: &'a str) -> &'a strとして扱われています。省略は「注釈が不要」なのではなく、「書かなくてもコンパイラが同じものを補える」ということです。
一方、問題01のlongestは、規則1でaとbに別々の'a・'bが付いた後、引数のライフタイムが2つあるので規則2が使えず、メソッドでもないので規則3も使えません。戻り値のライフタイムが決まらないので、E0106になっていたわけです。
| シグネチャ | 規則 | 結果 |
|---|---|---|
fn item_number(code: &str) -> &str |
1 → 2 | 省略できる |
fn longest(a: &str, b: &str) -> &str |
1のみ | 決められない(E0106) |
省略できるなら省略する
省略規則で書ける関数にわざわざ注釈を付けるのは、読み手に「何か特別な事情があるのか」と思わせるだけなので、Rustでは省略するのが一般的です。注釈を書くのは、問題01のように規則では決められない場合に限られます。
04 - 参照を持つ構造体
ライフタイム注釈・構造体・参照に関する問題です。
次のコードはコンパイルエラー(E0106)になります。構造体Receiptの定義だけを直して修正してください。
プログルストア新宿店 合計: 1500円struct Receipt {
shop_name: &str,
total: u32,
}
fn main() {
let shop = String::from("プログルストア新宿店");
let receipt = Receipt {
shop_name: &shop,
total: 1500,
};
println!("{} 合計: {}円", receipt.shop_name, receipt.total);
}Playgroundで開く解答例と解説
struct Receipt {
shop_name: &str,
struct Receipt<'a> {
shop_name: &'a str,
total: u32,
}
fn main() {
let shop = String::from("プログルストア新宿店");
let receipt = Receipt {
shop_name: &shop,
total: 1500,
};
println!("{} 合計: {}円", receipt.shop_name, receipt.total);
}Playgroundで開くエラーメッセージは問題01と同じmissing lifetime specifier(E0106)で、今度はexpected named lifetime parameter(名前付きのライフタイムパラメータが必要)と指摘されます。
構造体では省略できない
参照をフィールドに持つ構造体では、ライフタイム注釈は常に必須です。関数の省略規則のような仕組みはありません。構造体のインスタンスは関数の引数と違って、作られてからどこまで持ち運ばれるかが定義の時点では分からないためです。
書き方は第17章のジェネリック構造体Point<T>とそっくりです。構造体名の直後に<'a>で宣言し、フィールドの型で&'a strとして使います。
Receipt<'a>が表していること
Receipt<'a>という型は「'aの期間だけ有効な参照を中に持っている構造体」です。言い換えると、Receiptのインスタンスはshop_nameの参照先より長くは生きられないという制約を、型そのものが背負っています。
struct Receipt<'a> {
shop_name: &'a str,
total: u32,
}
fn main() {
let receipt;
{
let shop = String::from("プログルストア新宿店");
receipt = Receipt {
shop_name: &shop,
total: 1500,
};
println!("{} 合計: {}円", receipt.shop_name, receipt.total);
} // shop はここで破棄される。receipt もここまでしか使えない
}Playgroundで開くこのprintln!をブロックの外に出すと、問題02と同じE0597になります。Receiptの中に参照があることを、コンパイラは<'a>から知っているからです。
05 - implブロックとライフタイム
ライフタイム注釈・implブロック・メソッドに関する問題です。
構造体Receiptに、コメントで指示した2つのメソッドを定義してください。
プログルストア新宿店
プログルストア新宿店 合計: 1500円struct Receipt<'a> {
shop_name: &'a str,
total: u32,
}
// Receiptに次の2つのメソッドを定義せよ
// header : shop_name をそのまま参照(&str)で返す
// summary : 「〇〇 合計: 〇〇円」の形の String を返す
fn main() {
let shop = String::from("プログルストア新宿店");
let receipt = Receipt {
shop_name: &shop,
total: 1500,
};
println!("{}", receipt.header());
println!("{}", receipt.summary());
}Playgroundで開く解答例と解説
struct Receipt<'a> {
shop_name: &'a str,
total: u32,
}
// Receiptに次の2つのメソッドを定義せよ
// header : shop_name をそのまま参照(&str)で返す
// summary : 「〇〇 合計: 〇〇円」の形の String を返す
impl<'a> Receipt<'a> {
fn header(&self) -> &str {
self.shop_name
}
fn summary(&self) -> String {
format!("{} 合計: {}円", self.shop_name, self.total)
}
}
fn main() {
let shop = String::from("プログルストア新宿店");
let receipt = Receipt {
shop_name: &shop,
total: 1500,
};
println!("{}", receipt.header());
println!("{}", receipt.summary());
}Playgroundで開くimpl<'a> Receipt<'a>と書く理由
ポイントはimplブロックの書き出しです。Receipt<'a>のライフタイムは構造体の型の一部なので、第17章でimpl<T> Point<T>と書いたのと同じように、impl<'a> Receipt<'a>と宣言し直します。impl Receipt {と書くと、「ライフタイムパラメータが足りない」という別のエラー(E0726)になります。
impl<'a>の'aは、構造体定義の'aと同じ名前である必要はありません。impl<'r> Receipt<'r>でも同じ意味です。型引数のTを好きな名前にできるのと同じです。
メソッドの戻り値に注釈がいらない理由
headerは参照を返していますが、fn header(&self) -> &strと注釈なしで書けています。これは問題03の省略規則3のおかげです。受け手が&selfなので、戻り値の参照はselfから借りたものとして扱われます。実際に返しているself.shop_nameは'aの参照ですが、&selfの期間は必ず'aの範囲に収まるので、selfの期間として返しても安全です。
summaryの方はStringを返すので、ライフタイムは関係ありません。format!で新しい文字列を組み立てて所有権ごと返しています。
06 - ’staticライフタイム
ライフタイム注釈と文字列リテラルに関する問題です。
次のコードはコンパイルエラー(E0106)になります。関数shop_nameは引数を受け取らないのに参照を返そうとしています。エラーメッセージの提案を参考に戻り値の型を直し、さらにmainの後半が問題02と似た形なのにエラーにならない理由を考えてください。
プログルストア: いらっしゃいませ
渋谷本店fn shop_name() -> &str {
"プログルストア"
}
fn main() {
let greeting = "いらっしゃいませ";
println!("{}: {}", shop_name(), greeting);
let branch;
{
let name = "渋谷本店";
branch = name;
}
println!("{branch}");
}Playgroundで開く解答例と解説
fn shop_name() -> &str {
fn shop_name() -> &'static str {
"プログルストア"
}
fn main() {
let greeting = "いらっしゃいませ";
println!("{}: {}", shop_name(), greeting);
let branch;
{
let name = "渋谷本店";
branch = name;
}
println!("{branch}");
}Playgroundで開くエラーメッセージはmissing lifetime specifier(E0106)です。今度はthis function's return type contains a borrowed value, but there is no value for it to be borrowed from(借用した値を返しているが、借りる元になる値がない)と言われ、consider using the 'static lifetime('staticライフタイムの使用を検討せよ)と提案されます。
'staticはプログラム全体で有効なライフタイム
'staticは、「プログラムが動いている間ずっと有効」という意味の特別なライフタイムです。宣言せずに使える唯一の名前で、自分で<'static>と宣言することはできません。
第6章で、文字列リテラルの型は&strだと学びました。正確に書くと&'static strです。リテラルの文字データは実行ファイルの中に直接埋め込まれていて、プログラムの開始から終了まで消えることがありません。だから「借りる元の値」がなくても、リテラルへの参照は常に有効であり、'staticとして返せます。
mainの後半がエラーにならない理由
branchとnameの形は問題02とそっくりですが、決定的な違いがあります。問題02のbranchはString::fromで作った値で、内側のブロックを抜けるときに破棄されました。今回のnameは文字列リテラルへの参照、つまり&'static strです。破棄されるのは参照を入れていた変数nameだけで、参照先の文字データはプログラムが終わるまで生きています。参照先が生きている限り参照は有効なので、ブロックの外で使っても問題ありません。
| 値 | 参照先が生きている期間 |
|---|---|
String::from("...")(問題02) |
所有する変数のスコープの終わりまで |
"..."(文字列リテラル) |
プログラムの終了まで('static) |
07 - 応用: 最長の告知文
ジェネリクス・トレイト境界・ライフタイム注釈・Displayトレイトに関する問題です。
トレイト編のまとめとして、ジェネリクス・トレイト境界・ライフタイム注釈を1つのシグネチャにまとめた関数longest_with_announcementを実装し、テストに合格させてください。
use std::fmt::Display;
// 関数 longest_with_announcement を定義せよ
// 引数 : x: &str, y: &str, ann: T(T は Display を実装する任意の型)
// 動作 : 「お知らせ: 〇〇」の形で ann を出力した後、
// x と y のうち文字数(chars().count())の多い方を返す(同じなら x)
// 戻り値: &str
struct Sale {
rate: u32,
}
impl Display for Sale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "全品{}%オフ", self.rate)
}
}
#[test]
fn test_returns_longer() {
assert_eq!(
longest_with_announcement("りんご", "グレープフルーツ", "本日のセール"),
"グレープフルーツ"
);
assert_eq!(
longest_with_announcement("コーヒー豆", "紅茶", 20260821),
"コーヒー豆"
);
}
#[test]
fn test_same_length_returns_first() {
assert_eq!(
longest_with_announcement("紅茶", "緑茶", Sale { rate: 20 }),
"紅茶"
);
}
#[test]
fn test_accepts_string_reference() {
let shop = String::from("プログルストア新宿店");
let branch = String::from("渋谷本店");
let result = longest_with_announcement(&shop, &branch, Sale { rate: 30 });
assert_eq!(result, "プログルストア新宿店");
}Playgroundで開く解答例と解説
use std::fmt::Display;
// 関数 longest_with_announcement を定義せよ
// 引数 : x: &str, y: &str, ann: T(T は Display を実装する任意の型)
// 動作 : 「お知らせ: 〇〇」の形で ann を出力した後、
// x と y のうち文字数(chars().count())の多い方を返す(同じなら x)
// 戻り値: &str
fn longest_with_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where
T: Display,
{
println!("お知らせ: {ann}");
if x.chars().count() >= y.chars().count() {
x
} else {
y
}
}
struct Sale {
rate: u32,
}
impl Display for Sale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "全品{}%オフ", self.rate)
}
}
#[test]
fn test_returns_longer() {
assert_eq!(
longest_with_announcement("りんご", "グレープフルーツ", "本日のセール"),
"グレープフルーツ"
);
assert_eq!(
longest_with_announcement("コーヒー豆", "紅茶", 20260821),
"コーヒー豆"
);
}
#[test]
fn test_same_length_returns_first() {
assert_eq!(
longest_with_announcement("紅茶", "緑茶", Sale { rate: 20 }),
"紅茶"
);
}
#[test]
fn test_accepts_string_reference() {
let shop = String::from("プログルストア新宿店");
let branch = String::from("渋谷本店");
let result = longest_with_announcement(&shop, &branch, Sale { rate: 30 });
assert_eq!(result, "プログルストア新宿店");
}Playgroundで開くこの関数は、Rust公式ドキュメント「The Rust Programming Language」がジェネリクス・トレイト・ライフタイムの章の締めくくりに挙げている例と同じ形です。シグネチャ1行に、トレイト編で学んだ3つの要素がすべて入っています。
fn longest_with_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
// ^^ ライフタイムパラメータ(第22章)
// ^ 型引数(第17章)
where
T: Display, // トレイト境界(第20章)<'a, T>の順序
ライフタイムパラメータと型引数を両方宣言するときは、ライフタイムを先に書きます。<T, 'a>の順はコンパイルエラーです。
それぞれの役割
'aは問題01と同じく、戻り値がxとyのどちらから借りたものか分からないので、両方と結んでいます。T: Displayは第20章で学んだトレイト境界で、annを{ann}で出力するために必要です。テストでは&str・整数・自作のSaleと3種類の型を渡していますが、どれもDisplayを実装しているので同じ関数で受け取れます。SaleのDisplay実装は第19章で学んだ手動実装そのものです。
境界はwhere句ではなくfn longest_with_announcement<'a, T: Display>(...)のように書いても同じ意味です。
テストではprintln!が見えない
test_accepts_string_referenceは、&Stringを渡しても&strとして受け取れること(第8章で学んだ参照の自動変換)と、戻り値がshopの寿命の範囲で使えることを確認しています。なお、テスト実行時は成功したテストの標準出力が表示されないため、「お知らせ: …」の出力は画面に出ません。テストが失敗したときだけまとめて表示されます。