先日の Flutter3.10.0 のリリースに合わせて、Dart3 がリリースされ、待望だった records
, patterns
, sealed class
などが実装されました。
中でも patterns
は適用できる場所が多く使われ方も多様なので、特に Dart の書き方に大きな影響を与える変更だと思います。
この記事では便利な反面複雑でとっつきづらい patterns
をできるだけわかりやすく紹介します。
サンプルコードは 公式のドキュメント を引用しています。 原典が気になる人はそちらを参照してください。
Dart の expressions
と statements
このセクションは
patterns
の説明とは直接関係があるわけではないですが、 先に読んでもらえるとpatterns
の後の説明がより腑に落ちやすくなると思います。
プログラミング言語における形式的な構造のルールを構文と呼びます。例えば Dart では関数の構文は以下のような形であると決まっています。
<返り値の型※> <関数名>(<引数>) async※ {<関数の本体>}
<返り値の型※> <関数名>(<引数>) async※ => <関数の返り値>
// ※は省略可
これはいわば Dart のコンパイラとプログラマの間の約束事であり、このルールに則っていることがコードを書く上での最低条件になります。
Dart にはたくさんの構文がありますが、中でも代表的なものは 式(expressions) と 文(statements) です。
式は大雑把に言ってしまうと「値を返すもの」です。例えば以下のようなものは式にあたります。
- 関数呼び出し (
someObject.toString()
など) - 数値や文字列などのリテラル (
10
,"Hello World"
など) - List や Map などのリテラル (
[1, 2, 3]
,{name: "Hello"}
など) - 三項演算子 (
someCondition ? "Some text" : "Some other text"
など) - …etc
それに対して文は、「値を返さないが特定の処理を行うもの」です。例えば以下のようなものは文にあたります。
- if 文 (
if (someCondition) {} else {}
など) - for 文 (
for (final value in list) {}
など) - return 文 (
return someValue;
など) - throw 文 (
throw someError;
など)
普段プログラマが書いている Dart のコードの大部分は基本的に式か文のどちらかにあたります。
特に関数のボディ内にかけるのは必ず式か文のどちらかです。例えば enum A {}
は式でも文でもないので関数のボディ内には書けません。
例外的に switch
は、Dart3 から式としても文としても使うことができます。
式として使うか文として使うかで全然書き方が違うので、きちんと別物として区別しておく必要があります。
patterns
とは何か
patterns
は Dart に新しく追加された構文の種類です。
式っぽく動く patterns
も文っぽく動く patterns
もあるので混同しがちですが、
あくまで patterns
は patterns
なので全くの別物として理解する方が混乱しないと思います。
大きく分けて 2 つの機能があり、それが マッチング と 値の分解 です。
マッチング
switch (number) {
// Constant pattern matches if 1 == number.
case 1:
print('one');
}
マッチングの代表的な例は case pattern
です。特定の条件に合致するかどうかを検証することができます。
これ以外にもさまざまな条件をマッチングに用いることができます。
値の分解
var numList = [1, 2, 3];
// List pattern [a, b, c] destructures the three elements from numList...
var [a, b, c] = numList;
// ...and assigns them to new variables.
print(a + b + c);
patterns
を使うことで、List
や record
などの特定のオブジェクトを分解しつつ、値を変数に取り出すことができます。
マッチングと値の分解は組み合わせて使うこともでき、「マッチングした値を分解して変数に代入する」「分解した値をマッチングに利用する」などの操作も可能です。
基本的に patterns
の使い方はこの二つですが、いくつかの種類と組み合わせ方法があるので、それぞれサンプルコードを見ながら確認していきましょう。
patterns
はどこで使えるか
変数宣言
// Declares new variables a, b, and c.
var (a, [b, c]) = ('str', [1, 2]);
変数宣言の左辺に patterns
を使うことで、record や List に含まれる値を取り出して別々の変数に入れることができます。
あくまで変数宣言の左辺に使えるだけで、いきなり patterns
を書けるわけではないため、以下のようなコードはエラーになります。
(var a, [var b, var c]) = ('str', [1, 2]);
変数代入
var (a, b) = ('left', 'right');
(b, a) = (a, b); // Swap.
print('$a $b'); // Prints "right left".
すでに宣言した変数へ値の代入をすることができます。
Switch 式と Switch 文
switch (obj) {
// Matches if 1 == obj.
case 1:
print('one');
// Matches if the value of obj is between the constant values of 'first' and 'last'.
case >= first && <= last:
print('in range');
// Matches if obj is a record with two fields, then assigns the fields to 'a' and 'b'.
case (var a, var b):
print('a = $a, b = $b');
default:
}
switch
は式と文どちらとして使っても patterns
を使うことができます。
後述するすべての種類の patterns
が使えるので、マッチングの使い方としてはこの switch
との併用がメインになると思います。
if-case
文
if (pair case [int x, int y]) return Point(x, y);
if 文にも patterns
を使うことのできる if-case
という文法が追加されました。一つの patterns
にマッチングさせたい時に switch 文よりもシンプルに使えそうです。
同じく collection-if
の中でも patterns
を使うことができます。
var nav = ['Home', 'Furniture', 'Plants', if (login case 'Manager') 'Inventory'];
ちなみに三項演算子では patterns
を使うことができないようです 。
Irrefutable pattern
と Refutable pattern
マッチングが失敗することを許容するかどうかで、 patterns
は 2 種類に分けることができます。
変数宣言や変数の代入など、そもそもマッチングが失敗することが文法的に許されない patterns
のことを Irrefutable
、
switch
や if-case
など、マッチングが失敗しても処理を続ける(もしくは単にスキップする)ことのできる patterns
のことを Refutable
と呼びます。
Irrefutable
で無効な patterns
を書くと基本的にコンパイルエラーになりますが、
型のキャストや Null チェックなど一部の patterns
に関してはランタイムでエラーを投げる仕様になっているので注意が必要です。
patterns
の種類
公式のドキュメントでは 15 種類の patterns
が紹介されているので、順番に見ていきましょう。
Logical-or
通常の if 文と同様に ||
で or
を表現できます。
var isPrimary = switch (color) {
Color.red || Color.yellow || Color.blue => true,
_ => false
};
||
で繋がれているのは patterns
なので複雑な組み合わせにも対応しています。。
final isRed = switch (color) {
Color.red || Color(:final code) when code == '#F00' => true,
_ => false
};
Logical-and
通常の if 文と同様に &&
で and
を表現できます。 ||
よりも優先して評価されます。
switch ((1, 2)) {
// Error, both subpatterns attempt to bind 'b'.
case (var a, var b) && (var b, var c): // ...
}
Parenthesized
()
をつけると ||
を &&
より優先して評価することができます。
// ...
x || y && z => 'matches true',
(x || y) && z => 'matches false',
// ...
Relational
数値などの比較ができます。比較演算子の左辺が省略されたような書き方です。
String asciiCharType(int char) {
const space = 32;
const zero = 48;
const nine = 57;
return switch (char) {
< space => 'control',
== space => 'space',
> space && < zero => 'punctuation',
>= zero && <= nine => 'digit',
_ => ''
};
}
Cast
指定した型にキャストできるときだけマッチングします。
下のサンプルコードで使われている変数宣言の patterns
は Irrefutable
なので、キャストに失敗した場合はエラーが throw
されます。
(num, Object) record = (1, 's');
var (i as int, s as String) = record;
switch 文や if-case 文などのマッチングの場合は後述する Variable patterns
と Wildcard patterns
の組み合わせで同様のことが可能です。
switch (record) {
case (int _, String _):
print('First field is int and second is String.');
}
Variable
マッチングした場合にだけ使える変数を宣言することができます。
通常の変数宣言と同様に var
final
などのキーワード以外にも型を使った宣言が可能です。型を使った宣言をしたときに型が合わなかった場合はマッチングしません。
switch ((1, 2)) {
// 'var a' and 'var b' are variable patterns that bind to 1 and 2, respectively.
case (var a, var b): // ...
// 'a' and 'b' are in scope in the case body.
}
when
キーワードを使うことで、マッチングした Variable
を使ってさらに絞り込みをすることができます。
switch ((1, 2)) {
case (var a, var b) when a.isOdd: // ...
}
Identifier
patterns
の中で最も複雑でややこしいのがこの Identifier
です。どこで使われるかによって意味合いが大きく変わります。
- 変数宣言の中で使われるとき
var (a, b) = (1, 2);
- 変数代入の中で使われるとき
int a;
int b;
(a, b) = (1, 2);
- マッチングの中で使われるとき
const c = 1;
switch (2) {
case c:
print('match $c');
default:
print('no match'); // Prints "no match".
}
- ワイルドカードとして使うとき
いづれのタイミングでも _
を使うことができます。
1.
の変数宣言の中で使われるときの Identifier pattern
は Variable pattern
と混同しがちなので注意が必要です。
例えば
var (a, b) = (1, 2);
このようなコードを書くと、 patterns
として扱われるのは (a, b)
部分のみですが、
switch ((1, 2)) {
case (var a, var b):
...
}
こちらの場合は (var a, var b)
全体が patterns
として扱われます。
前者のコードは Identififer pattern
と通常の変数宣言の組み合わせであり、後者のコードは Variable pattern
です。
以下のようなコードは通らないので気をつけましょう
(var a, var b) = (1, 2);
switch ((1, 2)) {
case var (a, b):
...
}
Null-check
Variable patterns
でマッチングした変数に ?
をつけることで null チェックをすることができます。
Variable patterns
でマッチングした変数が null だった場合はマッチングしません。
String? maybeString = 'nullable with base type String';
switch (maybeString) {
case var s?:
// 's' has type non-nullable String here.
}
Null-assert
Null-check
と同様に null チェックをすることができます。
Variable patterns
でマッチングした変数が null だった場合はエラーが throw されます
List<String?> row = ['user', null];
switch (row) {
case ['user', var name!]:
// 'name' is a non-nullable string here.
}
Constant
定数を使ったマッチングです。
switch (number) {
// Matches if 1 == number.
case 1: // ...
}
List
List の中身を分解してマッチングさせることができます。
List の長さが違う場合はマッチングしませんが、長さが不明な場合でも次の Rest element
を使うことで、「0 番目の要素だけ」などにマッチングさせることができます。
switch (obj) {
// Matches if obj is a list with two elements.
case [a, b]: // ...
}
Rest element
...
を List の patterns
に使うことで、明示的に書かなかった残りの部分にマッチングさせることができます。
var [a, b, ..., c, d] = [1, 2, 3, 4, 5, 6, 7];
// Prints "1 2 6 7".
print('$a $b $c $d');
...
の前に変数名を入れることで、残りの部分にマッチングした値の List をとることができるようになります。
var [a, b, ...rest, c, d] = [1, 2, 3, 4, 5, 6, 7];
// Prints "1 2 [3, 4, 5] 6 7".
print('$a $b $rest $c $d');
Map
List
と同様に Map
の中身をマッチングさせる事ができます。 List
とは対照的に、すべての要素が明示されている必要はありません。
if (json case {'user': [String name, int age]}) {
print('User $name is $age years old.');
}
Record
Dart3 から導入された record
にも patterns
を使う事ができます。
var (name, age) = ('Bob', 20);
var (myString: foo, myNumber: bar) = (myString: 'string', myNumber: 1);
名前付き record
にマッチングするときには <マッチングしたいフィールド名>: <マッチングした値を入れる変数名>
の形でマッチングさせることができます。
ただし<マッチングしたいフィールド名>
と <マッチングした値を入れる変数名>
が同名の場合、前者を省略することが可能です。
注意点として、 文法の都合上 :
は省略できません。
var (myString: myString, myNumber: myNumber) = (myString: 'string', myNumber: 1);
// ↓ こう書ける
var (:myString, :myNumber) = (myString: 'string', myNumber: 1);
Variable pattern
の場合も同様に省略が可能です。
switch ((myString: 'string', myNumber: 1)) {
case (myString: final myString, myNumber: final myNumber):
...
}
// ↓ こう書ける
switch ((myString: 'string', myNumber: 1)) {
case (:final myString, :final myNumber):
...
}
Object
クラスのインスタンスに対してマッチングすることができます。フィールドに関しても record
と同様に変数として取り出すことが可能です。
switch (shape) {
// Matches if shape is of type Rect, and then against the properties of Rect.
case Rect(width: var w, height: var h): // ...
}
Wildcard
Variable pattern
か Identifier pattern
で、マッチングはさせたいけど変数として使うわけじゃないものに関しては _
を使うことで任意の値にマッチングさせつつ変数宣言はスキップすることができます。
var list = [1, 2, 3];
var [_, two, _] = list;
実用例
patterns
を使うと便利なケースをいくつか紹介します。
Future.wait
の書き換え
これまでの Future.wait
を使った Future
の並列化では各 Future の型情報が抜け落ちてしまっていましたが、
final stringFuture = Future.value('Hello');
final numberFuture = Future.value(1);
final results = await Future.wait([stringFuture, numerFuture]);
final stringResult = results[0]; // ← Object 型
final numberResult = results[1]; // ← Object 型
patterns
を record
と組み合わせて使うことで型情報を保ったまま並列化をすることができます。
final stringFuture = Future.value('Hello');
final numberFuture = Future.value(1);
final (stringResult, numberResult) = await (stringFuture, numerFuture).wait;
json のバリデーション
Dart3のPattern使うと左のコードが右に書き換えられるのかなりすごい pic.twitter.com/JimzdIuAJu
— 糸目 (@itometeam) May 18, 2023
まとめ
patterns
は初見だと読み書きに少し戸惑いますが、使いこなせるととても便利なので、実用例に挙げたものを少しずつ使ってみるところからでも試してみてください!