Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

パターンと網羅性の検査

Rust では、パターンマッチングと束縛に、いくつかの非常に役立つ性質があります。 コンパイラは、束縛が行われるときにそれが反駁不能であることと、match アームが 網羅的であることを検査します。

パターンの有用性

有用性検査が答える中心的な問いは次のものです。 「この match 式において、その分岐は冗長か?」。 より正確には、すでに見たパターンのリストが与えられたときに、 ある新しいパターンが何らかの新しい値にマッチする可能性があるかを 計算することに帰着します。

たとえば、次の match 式では、 各パターンが、その上にあるパターンにマッチしなかった何かに マッチする可能性があるかを順に尋ねます。 ここでは、4 番目のパターンが 1 番目のパターンと冗長であることがわかります。 その分岐には「到達不能」の警告が出ます。 3 番目のパターンが有用かどうかは、 FooBar 以外のバリアントがあるかどうかに依存します。 最後に、ワイルドカードパターン(_)が その match 内のすべてのパターンのリストに対して有用かどうかを尋ねることで、 match 全体が網羅的かどうかを尋ねることができます。 ここでは _ が有用である((false, None) を捕捉する)ことがわかります。 したがって、この式には「網羅的でない match」エラーが出ます。

#![allow(unused)]
fn main() {
// x: (bool, Option<Foo>)
match x {
    (true, _) => {} // 1
    (false, Some(Foo::Bar)) => {} // 2
    (false, Some(_)) => {} // 3
    (true, None) => {} // 4
}
}

したがって、有用性は 2 つの目的で使われます。 到達不能コードの検出(これはユーザーにとって有用です)と、 match が網羅的であることの保証(これは健全性にとって重要です。 なぜなら match 式は値を返すことができるためです)。

どこで行われるか

この検査は、パターンを書ける場所ならどこでも行われます。match 式、if letlet else、 通常の let、関数引数です。

#![allow(unused)]
fn main() {
// `match`
// 有用性は到達不能な分岐を検出し、網羅的でない match を禁止できます。
match foo() {
    Ok(x) => x,
    Err(_) => panic!(),
}

// `if let`
// 有用性は到達不能な分岐を検出できます。
if let Some(x) = foo() {
    // ...
}

// `while let`
// 有用性は無限ループとデッドループを検出できます。
while let Some(x) = it.next() {
    // ...
}

// 分解 `let`
// 有用性は網羅的でないパターンを禁止できます。
let Foo::Bar(x, y) = foo();

// 関数引数の分解
// 有用性は網羅的でないパターンを禁止できます。
fn foo(Foo { x, y }: Foo) {
    // ...
}
}

アルゴリズム

網羅性検査は、MIR の構築前に check_match で実行されます。 これは rustc_pattern_analysis クレートに実装されており、 アルゴリズムの中核は usefulness モジュールにあります。 そのファイルには、アルゴリズムの詳細な説明が含まれています。

重要な概念

コンストラクターとフィールド

Pair(Some(0), true) において、Pair はその値のコンストラクターと呼ばれ、Some(0)true はそのフィールドです。マッチ可能なすべての値は、この方法で分解できます。 コンストラクターの例には、SomeNone(,)(2 タプルコンストラクター)、 Foo {..}(構造体 Foo のコンストラクター)、および 2(数値 2 のコンストラクター)があります。

各コンストラクターは固定数のフィールドを取ります。これはそのアリティと呼ばれます。Pair(,) の アリティは 2、Some のアリティは 1、None42 のアリティは 0 です。各型には既知の コンストラクター集合があります。一部の型には多くのコンストラクター(u64 など)があり、さらには 無限個のコンストラクター(&str&[T] など)を持つものもあります。

パターンも似ています。Pair(Some(_), _) はコンストラクター Pair と 2 つのフィールドを持ちます。 違いは、いくつかのパターン専用コンストラクターが追加されることです。具体的には、ワイルドカード _、 変数束縛、0..=10 のような整数範囲、[_, .., _] のような可変長スライスです。 or パターンは別に扱います。

さて、値 v がパターン p にマッチするかを検査するには、v のコンストラクターが p の コンストラクターにマッチするかを検査し、必要であればそれらのフィールドを再帰的に比較します。 代表的な例をいくつか示します。

  • matches!(v, _) := true
  • matches!((v0, v1), (p0, p1)) := matches!(v0, p0) && matches!(v1, p1)
  • matches!(Foo { a: v0, b: v1 }, Foo { a: p0, b: p1 }) := matches!(v0, p0) && matches!(v1, p1)
  • matches!(Ok(v0), Ok(p0)) := matches!(v0, p0)
  • matches!(Ok(v0), Err(p0)) := false(互換性のないバリアント)
  • matches!(v, 1..=100) := matches!(v, 1) || ... || matches!(v, 100)
  • matches!([v0], [p0, .., p1]) := false(互換性のない長さ)
  • matches!([v0, v1, v2], [p0, .., p1]) := matches!(v0, p0) && matches!(v2, p1)

この概念は、パターン解析において絶対的に中心となるものです。constructor モジュールは、 コンストラクターを抽出、列挙、操作するための関数を提供します。これは十分に有用な概念であるため、 その変種はコンパイラの他の場所にも見られます。たとえば、match 式の MIR lowering や いくつかの clippy lint などです。

コンストラクターのグループ化と分割

パターン専用コンストラクター(_、範囲、可変長スライス)は、それぞれ通常のコンストラクターの集合を表します。 たとえば、_: Option<T> は集合 {None, Some} を表し、[_, .., _] は アリティ >= 2 のスライスコンストラクターの無限集合 {[,], [,,], [,,,], …} を表します。

これらのコンストラクターを管理するために、可能な限りグループ化したまま保持します。たとえば次のようにします。

#![allow(unused)]
fn main() {
match (0, false) {
    (0 ..=100, true) => {}
    (50..=150, false) => {}
    (0 ..=200, _) => {}
}
}

この例では、01、..、49 はすべて同じアームにマッチするため、1 つのグループとして扱うことができます。 実際、この match で考慮する必要がある範囲は 0..5050..=100101..=150151..=200201.. だけです。同様に次の場合です。

#![allow(unused)]
fn main() {
enum Direction { North, South, East, West }
let wind = (Direction::North, 0u8);
match wind {
    (Direction::North, 50..) => {}
    (_, _) => {}
}
}

ここでは、North ではないすべてのコンストラクターを 1 つのグループとして扱えるため、扱うべきケースは North と、それ以外のすべてのもの、の 2 つだけになります。

これは「コンストラクター分割」と呼ばれ、網羅性検査を妥当な時間で実行するために不可欠です。

空の型が存在する場合の有用性と到達可能性

これはおそらく網羅性において最も微妙な側面です。完全に正確に言えば、match は 値に対して動作するのではなく、場所に対して動作します。特定の unsafe な状況では、場所が その型に対して有効なデータを含まないことがあり得ます。これは空の型に対して微妙な結果をもたらします。 次を見てください。

#![allow(unused)]
fn main() {
enum Void {}
let x: u8 = 0;
let ptr: *const Void = &x as *const u8 as *const Void;
unsafe {
    match *ptr {
        _ => println!("Reachable!"),
    }
}
}

この例では、ptr は無効なデータを持つ場所を指している有効なポインターです。_ パターンは 場所 *ptr の内容を見ないため、このコードは問題なく、アームが選択されます。言い換えると、 検査している場所の型が Void であるにもかかわらず、到達可能なアームがあります。一方で、 そのアームに束縛がある場合は次のようになります。

#![allow(unused)]
fn main() {
#[derive(Copy, Clone)]
enum Void {}
let x: u8 = 0;
let ptr: *const Void = &x as *const u8 as *const Void;
unsafe {
match *ptr {
    _a => println!("Unreachable!"),
}
}
}

ここでは、その束縛が *ptr という場所から型 Void の値をロードします。この例では、データが有効ではないため、 これは UB を引き起こします。一般の場合では、これは *ptr にあるデータの妥当性を表明します。 いずれにせよ、このアームが選択されることは決してありません。 最後に、空の match match *ptr {} について考えてみましょう。これが網羅的であると考えるなら、 *ptr に無効なデータがあることは無効です。言い換えると、空の match は意味論的には _a => ... match と同等です。明示性の観点から、アームがあるケースを好むため、 ユーザーに _a アームを削除するようには伝えません。言い換えると、_a アームは 到達不能ではあるものの、冗長ではありません。これが、lint が “unreachable” と言っているにもかかわらず、 到達不能なアームではなく冗長なアームに対して lint を行う理由です。

これらの考慮事項は、特定の場所、すなわち UB なしに非有効なデータを含み得る場所にのみ影響します。 それらは、ポインタのデリファレンス、参照のデリファレンス、union フィールドアクセスです。網羅性チェック中に、 与えられた場所が有効なデータを含むことが既知かどうかを追跡します。

以上を踏まえても、現在の網羅性チェックの実装は上記の考慮事項に従っていません。stable では、 空型はほとんどの場合、非空として扱われます。 exhaustive_patterns 機能は逆方向に誤っています。unsafe な状況で到達可能になり得るアームの省略を 許可してしまいます。never_patterns 実験的機能は、これを修正し、 パターン内の空型の正しい振る舞いを許可することを目指しています。