【入門編】Dartの「Object Patterns」でクラスのプロパティを分解する際の注意点 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!日々のDart/Flutter開発、お疲れ様です。
言語の進化を追いかけるのはワクワクしますよね。Dart 3で導入された「パターンマッチング(Pattern Matching)」は、コードの記述量を劇的に減らし、表現力を爆発的に高めてくれる最高の機能です。

今回は、そのパターンマッチングの中でも特に強力な「Object Patterns(オブジェクトパターン)」を取り上げます。クラスのプロパティをスマートに分解して取り出すテクニックですが、実はここに「知っていないと絶対にハマる、実行時の重大な落とし穴」があるんです。

ここをクリアすれば、あなたのDartのコードは一段と洗練され、バグの少ない堅牢なものになりますよ。さっそく本質へと飛び込んでいきましょう!

—

1. Object Patterns(オブジェクトパターン)の基本:そもそも何ができるの?

Dart 3のパターンマッチングは、単に値を比較するだけではありません。「オブジェクトの構造をその場で分解(Destructuring)して、中の値を取り出す」ことができます。

まずは、基本の形を見てみましょう。

class Point {
final int x;
final int y;

const Point(this.x, this.y);
}

void printCoordinates(Object obj) {
// ここがObject Pattern!
// objがPoint型であれば、自動的に型チェックしつつプロパティを分解する
if (obj case Point(x: var px, y: var py)) {
print(‘X座標: $px, Y座標: $py’);
} else {
print(‘Pointではありません’);
}
}

このコード、すごくスッキリしていて気持ちがいいですよね。
裏側で何が起きているかというと、コンパイラは `case Point(x: var px, y: var py)` を見たとき、「指定されたクラスの型チェック」と「プロパティ(フィールドまたはgetter)へのアクセス」を同時に行うコードを生成しています。

—

2. 【核心】Object Patternsは「プロパティ」ではなく「getter」を叩いている

さて、ここからが今回の本題です。
「クラスのプロパティを分解している」と聞くと、フィールド(変数)の値を直接ゴソッと抜き取っているようなイメージを持ちませんか?

実は、DartのObject Patternは、フィールドを直接見ているわけではありません。
オブジェクトパターンが裏側で行っているのは、ズバリ「該当する名前のgetterの呼び出し」です。

[Object Pattern: Point(x: var px)]
↓ 変換(コンパイル時)
[obj.x という getterを評価している!]

これを知っていると知らないとでは、コードの安全性に対する解釈がまったく変わってきます。なぜなら、getterである以上、そこには「ロジック(処理)」や「副作用」が存在し得るからです。

—

3. 副作用のあるプロパティを分解する際の「落とし穴」

では、この「getterを呼び出している」という特性が、どのような罠を生むのか具体的なコードで見てみましょう。

次の例をみてください。UIの状態管理やロガーなどでやりがちなアンチパターンです。

class UserSession {
int _accessCount = 0;

// アクセスされるたびに内部カウンターがインクリメントされ、
// ログ出力という「副作用」が発生するgetter
int get accessCount {
_accessCount++;
print(‘⚠️ [getter呼び出し] accessCount が参照されました! 現在の回数: $_accessCount’);
return _accessCount;
}

final String name;
UserSession(this.name);
}

void main() {
final session = UserSession(‘Alice’);

// パターンマッチングで分解してみます
if (session case UserSession(accessCount: 1)) {
print(‘初回アクセスです!’);
} else {
print(‘2回目以降のアクセスです。’);
}
}

このコードを実行すると、何が起きると思いますか?
「1回だけチェックしているんだから、ログは1回だけ出るはず」と思いますよね。

しかし、実際の出力結果はこうなります。

⚠️ [getter呼び出し] accessCount が参照されました! 現在の回数: 1
⚠️ [getter呼び出し] accessCount が参照されました! 現在の回数: 2
2回目以降のアクセスです。

……あれ? 「初回アクセスです!」と表示されるはずが、「2回目以降のアクセス」になってしまいました。しかも、getterが2回呼ばれています。

なぜこんなことが起きるのか?(Dart VMの視点)

これはバグではありません。パターンマッチングの評価プロセスの仕様によるものです。
複雑なパターン(例えば、複数のプロパティを同時にチェックする場合や、ガード条件、型の網羅性チェックなど)において、Dart VMやコンパイラは、安全性を担保するために同じgetterを複数回評価することがあります。

今回のケースでは、
1. `case UserSession(accessCount: 1)` にマッチするかどうかを判定するために `accessCount` のgetterが呼ばれる(この時点で内部カウンターが `1` から `2` に化ける)
2. 条件判定のプロセスや、マッチング失敗後のフォールバック処理の過程で、再び評価が走る

結果として、「パターンマッチの判定式を書いただけなのに、オブジェクトの状態が変わってしまった(副作用が起きた)」という最悪のバグが生まれるのです。

—

4. 現場で絶対に守るべき鉄則とベストプラクティス

他の言語(Kotlinの `when` や Rustの `match` など)から移行してきたエンジニアほど、この「パターンマッチングは純粋なデータ構造の分解である」という思い込みから、この罠にハマりがちです。

安全なDart開発を行うために、以下のルールを胸に刻んでおきましょう。

鉄則1:パターンマッチングの対象にするのは「純粋なデータ(Immutableなプロパティ)」に限定する

getterやメソッドの内部で、状態を変更したり(mutation)、外部APIを叩いたり、ログを吐いたりするような「副作用のあるプロパティ」をオブジェクトパターンのターゲットにしてはいけません。
オブジェクトパターンは、DTO(Data Transfer Object)や、状態を持たないフリーなイミュータブルクラスを分解するために使います。

鉄則2:副作用が懸念される場合は、事前にローカル変数へ退避させる

もし、何らかの理由でロジックを持つオブジェクトから値を取り出してパターンマッチしたい場合は、あらかじめ一度だけgetterを評価してローカル変数に代入し、その変数に対してパターンマッチングを行うのが正解です。

void safeCheck(UserSession session) {
// 一度だけgetterを安全に呼び出してローカル変数に保存!
final currentCount = session.accessCount;

// 保存した変数に対してパターンマッチ、または通常の条件分岐を行う
if (currentCount == 1) {
print(‘初回アクセスです!(安全)’);
} else {
print(‘2回目以降のアクセスです。(安全)’);
}
}

これなら、`session.accessCount` の呼び出しは意図通り確実に1回だけで済み、副作用に悩まされることもなくなります。

—

まとめ

今回はDart 3のObject Patternsの裏側と、副作用のあるプロパティを扱う際の落とし穴について解説しました。

  • Object Patternsの正体は「getterの呼び出し」である。
  • 複雑なパターンマッチングの中では、同じgetterが複数回評価されることがある。
  • 状態を変更するような副作用を持つgetterを直接パターンに組み込むと、予期せぬバグの温床になる。
  • 心配なときは、事前にローカル変数へ受けてから処理する。

言語の仕様やコンパイラの挙動を少し深く知るだけで、コードの信頼性は劇的に向上します。「なぜそう動くのか」を意識できるようになると、Dartを本当に使いこなせている証拠ですよ。

ここをクリアできれば、あなたのDartの基礎力はもうバッチリマスターできています!
明日からのコーディングに、ぜひこの知見を活かしてみてくださいね。それではまた!

タイトルとURLをコピーしました