blog

DartとFlutterにおける不変のデータパターン

イミュータブルデータとは、初期化後に変更できないデータのことです。イミュータブルデータはDart言語のいたるところにあります。実際、基本的な変数型のほとんどはこのように動作します。例えば、文字列、数値...

Mar 27, 2020 · 19 min. read
シェア

イミュータブル・データとは、初期化後に変更できないデータのことです。イミュータブル・データはDart言語のいたるところにあります。実際、基本的な変数型のほとんどはこの方法で動作します。例えば、文字列、数値、ブーリアンなどは、一度作成されると二度と変更できません。文字列変数は文字列データそのものを格納するのではなく、単に文字列データがメモリ上のどこにあるかを示す参照です。非終端型で表される文字列への参照は再割り当てが可能で、新しい文字列データを指すことができますが、一度文字列が作成されると、文字列データ自体は内容も長さも変更されません:

var str = "This is a string.";
str = "This is another string.";

このコードでは、strという名前の文字列変数を宣言しています。 文字列を構成する文字データへのカンマを反転した間に、データをメモリに配置し、そのアドレスへのメモリ内の文字列をstrに格納します。

2行目は全く新しい文字列を作成し、その文字列のメモリ位置への参照をstr変数に再代入し、最初の文字列への参照を上書きします。しかし、元の文字列は変更されません。コード内でその文字列を使用しない場合、その文字列はアクセス不能としてマークされ、最終的にDartのゴミ収集器がその文字列が占有しているメモリを回収します。

イミュータブル・データ・パターンを使用することには多くの利点があります。 イミュータブル・データ・パターンは本質的にスレッドセーフであり、一度作成されたデータは内容を変更することができません。このデータへの参照を渡すことで、このデータを非常に安全に使用することができます。また、このデータを渡す際に、誤って変更されないようにするための保護コピーについて考える必要もありません。プロジェクトでイミュータブル・データを使用すると、データの一貫性やデータ・セキュリティの心配がなくなるため、開発がよりシンプルで簡単になります。

Dart の組み込みデータ不変機能の説明では、まず最後の const キーワードについて見ていきます。

この記事のコードはDart 2.8.4とFlutter 1.17.5でテストされています。

I. Final宣言とConst宣言における定数

初心者にとって、Dartのfinalキーワードとconstキーワードを区別するのは簡単ではないかもしれません。finalとconstで定数を宣言する場合は、その違いを理解し、どのような場面で使用できるのかを知っておくことが重要です。

finalと宣言された変数は一度だけ初期化することができ、一度値で初期化されると再代入することはできません:

final str = "This is a final string.";
str = "This is another string."; // error

final を使って宣言された変数 str は、一度初期化されると dart によって変更することができなくなります。final 型の変数は、最終的な値を決定するコードの操作に依存することができますが、その代入は初期化時に行わなければなりません。再代入が禁止されていることを除けば、final 変数はあらゆる点で通常の変数と同様です。

Dartの定数はコンパイル時の定数で、constを使用して変更します。定数の値や状態はコンパイル時に決定されます。定数の値はコードの実行に依存しません。アプリケーションが実行されると、定数はフリーズし、再び変更することはできません。

Dartの定数には、主に3つのプロパティがあります:

  • 定数の不変性には依存関係があります

例えば、定数型のデータ構造を作成したい場合、そのデータ構造内のすべての要素は定数でなければなりません。

  • 定数値はコンパイル時に決定する必要があります。

    例えば、DateTime.now()は定数として宣言できません。なぜなら、実行時にのみ利用可能なデータに依存して自身を生成するからです。

    FlutterのSizedBoxのプロパティはすべてfinal型で、コンストラクタはconstant型です。Dartコンパイラーはコンパイル時に簡単な数学演算や文字列の連結を行うことができます。

  • 定数は再作成できません。

    与えられた定数値に対して、その定数式が何度計算されても、メモリ上にオブジェクトが作成されます。定数オブジェクトは必要に応じて再利用できますが、再作成されることはありません。定数の例

const str = "This is a constant string.";
const SizedBox(width: 10); // a constant object
const [1, 2, 3]; // a constant collection
1 + 2; // a constant expression

str 定数には文字列が代入されますが、これは常にコンパイル時の定数です。

SizedBox のすべてのプロパティは内部的に final 型であり、パラメータの値で初期化されるためです。

各要素が定数である限り、定数データのコレクションも可能です。

式 1 + 2 はコード実行中に Dart コンパイラによって計算されるため、定数と見なすこともできます。

定数は共有されるため、Dartのデータ比較は同じオブジェクトかどうかを比較するのがデフォルトですが、一見独立した2つの定数インスタンス間の比較は、メモリ内のまったく同じオブジェクトを参照するため等しくなります:

List<int> get list => [1, 2, 3];
List<int> get constList => const [1, 2, 3];
var a = list;
var b = list;
var c = constList;
var d = constList;
print(a == b); // false
print(c == d); // true

a,b,c,dはすべて同じ集合を指していますが、dartは集合の要素ではなく、集合への参照を比較するため、const宣言された定数のみが等しいとみなされます。 get constListを呼び出すたびに集合が返されますが、集合がconst宣言されているということは、dartがコレクションをメモリに一度だけ初期化し、毎回同じ参照を返すことを意味します。はコレクションを一度だけメモリに初期化し、毎回同じ参照を返します。

次に、Flutterフレームワークがどのようにイミュータブルデータを利用しているかを調べます。

Flutterにおける不変データ

Flutterアプリケーションはプログラムの可読性やパフォーマンスを向上させるために多くの場所でデータ不変モデルを使うことができます。Flutterフレームワークの多くのクラスはイミュータブルモデルで作成されるように設計されています;2つの一般的な例はSizedBoxとTextです:

Row(
 children: <Widget>[
 const Text("Hello"),
 const SizedBox(width: 10),
 const Text("Hello"),
 const SizedBox(width: 10),
 const Text("Can you hear me?"),
 ],
)

行には 5 つの子要素があります。const キーワードを使用して const コンストラクタを持つクラスのインスタンスを作成すると、コンパイル時にこれらの値が作成され、一意の値が 1 回だけメモリに格納されます。最初の 2 つの Text インスタンスは、2 つの SizedBox インスタンスと同様に、メモリ内で同じオブジェクトへの参照に解決されます。const SizedBox(width: 15) が追加される場合、定数の別のインスタンスがその新しい値のために作成されます。

constの代わりにnewキーワードを使用してインスタンスを作成することもでき、最終的な結果は同じですが、アプリケーションの実行にかかるメモリの量を減らしたい場合や、アプリケーションのパフォーマンスを向上させたい場合は、constを使用した方がよいでしょう。

別のテキストの例を見てみましょう:

final size = 12.0;
const Text(
 "Hello",
 style: TextStyle(
 fontSize: size, // error
 ),
)

このコードは多くの知識を必要とします。

Textの定数インスタンスを作成しようとしていますが、定数の内部メンバも定数でなければならないことに注意してください。文字列テキスト "hello "は、constキーワードが省略されていても動作します。同様に、DartはTextStyleを定数として作成しようとします。TextStyleが定数Textインスタンスの一部であるためには定数でなければならないことを知っているからです。同様に、DartはTextStyleを定数として作成しようとします。なぜなら、TextStyleは定数のTextインスタンスの一部であるためには定数でなければならないことを知っているからです。しかし、TextStyleは変数のサイズに依存するため、ここでは定数になることはできず、実行時まで値を持ちません。ここでコンパイラーは、このコード行に問題があることを教えてくれます。これを解決するには、sizeを定数参照に置き換えるか、12.0のような数値を使う必要があります。もちろん、sizeが先にfinalで宣言されていても、同じエラーが報告されます。

プログラムの状態を表すデータへの偶発的な変更を防ぐ必要がある場合があります。

III.独自の不変データクラスの作成

コンストラクタを const で宣言し、変数を final で修飾すれば、不変クラスを作るのも簡単です。

class Employee {
 final int id;
 final String name;
 
 const Employee(this.id, this.name);
}

Employee クラスには 2 つの属性があり、いずれも final と宣言され、コンストラクタによって自動的に初期化されます。コンストラクタは const キーワードを使用して、このクラスをコンパイル時定数としてインスタンス化できることを Dart に伝えます:

const emp1 = Employee(1, "Jon");
var emp2 = const Employee(1, "Jon");
final emp3 = const Employee(1, "Jon");

ここでは、Employeeの定数インスタンスが1つだけ作成され、各変数への参照が代入されています。なぜなら、constを使用して変数参照を宣言したことは、このコンストラクタがconstによって変更されることを意味するからです。

この emp2 変数は Employee 型の通常の変数ですが、不変の定数オブジェクトへの参照が代入されています。 emp3 変数はどちらも新しい参照が代入されないため、emp2 変数と同等です。これらの2つの変数で何をしようが、どのように渡そうが、オブジェクトのidは1であり、オブジェクト内部の名前は "Jon "であり、メモリに移動して変数を調べれば、常に同じであることがわかります。

データ・クラスでfinal型のプロパティをprivateと宣言するのは一般的ではないことに注意してください。これらのプロパティは変更することができませんし、一般的に読み取りアクセスを制限することは意味がありません。もちろん、他のコードからこれらのプロパティへのアクセスをブロックしたい真の理由がある場合や、クラスが内部状態に関してユーザーに依存していない場合は、private宣言の使用を検討することができます。

データ不変クラスをうまく作成できた場合、Dartでデータ不変クラスをうまく作成できたことを理解するのに役立つものはありますか?続きを読む

、注釈の使用

metaパッケージ内で@imutableアノテーションを使用すると、不変を宣言したいクラスを分析し、警告を出すのに役立ちます。

import 'package:meta/meta.dart';
@immutable
class Employee {
 int id; // not final
 final String name;
 Employee(this.id, this.name);
}

immutableアノテーションはクラスを不変にするわけではありませんが、この例では1つ以上のフィールドがfinal型でないという警告が表示されます。コンストラクタを const で変更しても、その属性がミュータブルである場合も警告が表示されます。クラスを@immutableで変更しても、そのサブクラスがimmutableでない場合も警告が表示されます。プロパティタイプの中には、オブジェクトやコレクションなど、イミュータブルと宣言するとさらに複雑になるものがあります。次のセクションでは、これらの問題に対処する方法について説明します。不変クラス内の複雑なオブジェクト 従業員の名前が、文字列よりも複雑なオブジェクトで表現されている場合はどうなるでしょうか?例を示します:

class EmployeeName {
 String first;
 String middleInitial;
 String last;
 EmployeeName(this.first, this.middleInitial, this.last);
}

だから社員は今、こんな感じです:

class Employee {
 final int id;
 final EmployeeName name;
 const Employee(this.id, this.name);
}

ほとんどの場合、Employee クラスは以前と同じように使用されますが、1 つだけ大きな違いがあります。EmployeeName はまだ不変クラスとして定義されていないため、初期化後にプロパティが変更される可能性があります:


var emp = Employee(1, EmployeeName('John', 'B', 'Goode'));
emp.name = EmployeeName('Jane', 'B', 'Badd'); // blocked
emp.name.last = 'Badd'; // allowed

Employee の name 属性は final 型であるため、Dart はその再割り当てを禁止しています。ただし、EmployeeName の属性は同様に保護されていないため、そのデータを変更することは可能です。従業員データを不変にすることを意図している場合、これは意図しない脆弱性になる可能性があります。この問題を回避するには、使用するすべてのクラスが不変であることを確認してください。

不変データセット

コレクションは、データの不変性という課題ももたらします。リストやマップがfianlで変更されたとしても、コレクションの要素は変更可能です。さらに、Dartのリストとマップは本質的に変更可能な複雑なオブジェクトなので、要素の追加、削除、並び替えをサポートします。チャットメッセージデータを使った簡単な例を考えてみましょう:

class Message {
 final int id;
 final String text;
 const Message(this.id, this.text);
}
class MessageThread {
 final List<Message> messages;
 const MessageThread(this.messages);
}

このようなクラス宣言があれば、データは比較的安全です。同様に、一度 MessageThread が作成されると、メッセージリスト内の要素を変更したり置き換えたりすることはできません。しかし、リストコレクションは外部のコードによって操作することができます:

final thread = MessageThread([
 Message(1, "Message 1"),
 Message(2, "Message 2"),
]);
thread.messages.first.id = 10; // blocked
thread.messages.add(Message(3, "Message 3")); // Uh-oh. This works!

あなたが意図したものとは違うかもしれません。では、どうすればこれを防ぐことができるのでしょうか?いくつかの方法があります。

このコレクションのコピーを返します。

コレクションの変更可能なコピーを使用しても構わない場合は、Dart ゲッターを使用して、クラスの外からアクセスするたびにマスターリストのコピーを返すことができます:

class MessageThread {
 final List<Message> _messages;
 List<Message> get messages => _messages.toList();
 const MessageThread(this._messages);
}
thread.messages.add(Message(3, "Message 3")); // new list

この MessageThread クラスでは、実際のメッセージ・リストはプライベートです。messages は getter と呼ばれるゲッターを定義し、_messages リストのコピーを返します。外部コードがリストの add() メソッドを呼び出すと、リストの別のコピーに対して実行されるため、元のテーブルが変更されることはありません。返されたコピーには新しいメッセージデータが追加されますが、MessageThread オブジェクトのリストは変更されません。

この方法は単純ですが、欠点がないわけではありません。まず、リストが大きかったり、頻繁にアクセスされたりすると、リストの浅いコピーを作成するためにメッセージへのアクセスが行われないため、パフォーマンスに問題が生じます。第二に、クラスのユーザが元のリストを変更できるかのように見えるため、ユーザを混乱させる可能性があります。また、リターン・ペアがコピーであることに気づかず、予期せぬことが起こる可能性もあります。

を変更できないことを求めるコレクションまたはビューを返します。

データクラス内のコレクションへの変更を防ぐもうひとつの方法は、 変更不可能なバージョンや変更不可能なビューを返すゲッターを使うことです:

class MessageThread {
 final List<Message> _messages;
 List<Message> get messages => List.unmodifiable(_messages);
 const MessageThread(this._messages);
}
thread.messages.add(Message(3, "Message 3")); // exception!

このメソッドは、前に説明したメソッドとよく似ています。まだコレクションのコピーを返しますが、返されるコピーは不変です。ファクトリー・メソッドのコンストラクタを使用して、新しいコレクションを返します。ここで、ユーザがリストのコピーに新しいメッセージを追加しようとすると、実行時に例外がスローされ、変更はブロックされます。このアプローチはより良いですが、まだいくつかの欠点があります。コンパイラは add() メソッドの呼び出しが実行時に失敗することを警告しませんし、ユーザは直接参照ではなくコピーを使用していることを明示的に知らされません。

import 'dart:collection';
class MessageThread {
 final List<Message> _messages;
 UnmodifiableListView<Message> get messages =>
 UnmodifiableListView<Message>(_messages);
 const MessageThread(this._messages);
}
thread.messages.add(Message(3, "Message 3")); // exception!

UnmodifiableListViewは元のコレクションのコピーを作成しないので、この方法で実行する方が良いかもしれません。なぜなら、UnmodifiableListViewは元のコレクションのコピーを作成しないからです。残念ながら、この方法も実行時にしか機能しません。まだいくつかの欠点がありますが、この方法は多くの状況に対する解決策として十分です。

他のコレクションタイプはどうですか?他のコレクションにも unmodifiable() ファクトリーコンストラクタがあり、dart:collection ライブラリは対応する unmodifiable ビューを提供します。

コレクションの変更を防ごうとする場合、他にも考慮すべきことがいくつかあります。

本当に不変のデータセット

getterを使用すると元のデータのコピーが返されることにお気づきかもしれませんが、 元のデータはまだイミュータブルではありません。private _messages を使用することで、データの安全性を高めることができますが、バージン開発者の中には、生のデータをイミュータブルにしたいだけという人もいるでしょう。これを実現するひとつの方法は、MessageThread オブジェクトを作成する際に、コレクションやビューのイミュータブルバージョンを作成することです:

class MessageThread {
 final List<Message> messages;
 const MessageThread._internal(this.messages);
 factory MessageThread(List<Message> messages) {
 return MessageThread._internal(List.unmodifiable(messages));
 }
}

最初にすべきことは、定数コンストラクタをライブラリの外のコードから隠すことです。このコンストラクタをアンダースコアのプレフィックスを持つ名前付きコンストラクタに変更して、プライベートなコンストラクタにします。MessageThread._internal() コンストラクタは、古いデフォルト コンストラクタとまったく同じ働きをしますが、内部コードからのみアクセスできます。デフォルトの public コンストラクタは、ファクトリメソッドのコンストラクタに変更されます。ファクトリは通常のコンストラクタのように自動的にクラスのインスタンスを返すのではなく、明示的にクラスのインスタンスを返す必要があるからです。なぜなら、イニシャライザが最終属性として使用する前に、受信メッセージのリストを調整する必要があるからです。ファクトリーコンストラクタは受信リストを変更不可能なリストにコピーし、インスタンスを生成するプライベートコンストラクタに渡します。インスタンスは以前と同じ方法で作成されるため、ユーザーには賢明な選択はありません:

final thread = MessageThread([
 Message(1, "Message 1"),
 Message(2, "Message 2"),
]);

このようなコードでも動作しますし、ソースコードを見なくても、通常のコンストラクタの代わりにファクトリーコンストラクタを呼び出していることは誰にもわかりません。ところで、この解決策はDartのシングルトンパターンに少し似ています。これで、保存されたリストコレクションは不変になり、同じクラスやライブラリのコードでさえ変更できなくなります。しかし、ほとんどのアプリケーションでは、その機能が意味を持つようになる前にデータの更新を検証することは理にかなっています。

III.不変データの更新

アプリケーションの状態をすべてイミュータブルな構造体に安全に保存した後、それをどのように更新するかについて悩むかもしれません。クラスの個々のインスタンスは変更可能であるべきではありませんが、データの状態は間違いなく変更する必要があります。これを実現する方法はいくつかありますが、ここではそのうちのいくつかを紹介します。

、トップレベルの関数を使用

イミュータブルな状態を更新する最も一般的な方法の一つは、ある種の状態更新関数を使うことです。Reduxの内部ではreducerが使用され、同様にBLoCパターンを使用する場合、内部には同様の構造があります。状態更新機能がどこに実装されているかにかかわらず、通常は入力を受け取り、ビジネスロジックを実行し、入力と古い状態に基づいて新しい状態を出力します。最も単純な例から始めて、前述の不変 Employee の状態更新機能を見てみましょう。これらの関数は Employee クラスの一部ではないことに注意してください:

class Employee {
 final int id;
 final String name;
 const Employee(this.id, this.name);
}
Employee updateEmployeeId(Employee oldState, int id) {
 return Employee(id, oldState.name);
}
Employee updateEmployeeName(Employee oldState, String name) {
 return Employee(oldState.id, name);
}

これは最も単純な方法の1つで、サポートされている更新のみが完了するようにするのに便利です。基本的に、各関数は以前の従業員の状態を参照し、そのデータと新しいデータを使用してまったく新しいインスタンスを構築し、呼び出し元に返します。こうすることで、単純な変数の更新でも同じだけのサンプルコードが必要になります。

この方法のもう 1 つの欠点は、リファクタリングが難しいことです。Employee の属性を追加、削除、または変更するには、多くの場所を変更する必要があります。

通常、更新機能はコードベースのまったく別の場所に記述されるため、このアプローチではビジネスロジックをデータから切り離すことができます。プロジェクトによっては、これが利点になることもあります。

クラスメソッド

ステートクラスでステートを管理したい場合は、トップレベルの関数を使用するのではなく、クラスメソッドを使用します。

class Employee {
 final int id;
 final String name;
 const Employee(this.id, this.name);
 
 Employee updateId(int id) {
 return Employee(id, name);
 }
 Employee updateName(String name) {
 return Employee(id, name);
 }
}

この方法を使用すると、各更新メソッドが Employee クラスに属することが明確になるため、長い名前付けの一部を省略できます。同様に、現在のインスタンスが古い状態であると仮定するため、古い状態を明示的に渡す必要がなくなります。コードをよく見ないと、両方の update メソッドが同じコードを持っているように見えるかもしれませんが、updateId() は渡された id パラメータと old を使用して新しい Employee インスタンス名を作成しています。

この方法の欠点は、値を更新するロジックがある程度固定され、ステート・クラスに直接結びついていることです。場合によっては、これはまさにあなたが望むことかもしれません。

不変クラスの各プロパティに対して更新メソッドを作成するのは面倒です。次に、関数をひとつのメソッドにまとめる方法を検討します。

コピー

DartやFlutterのプロジェクトでよく使われる、不変のデータを使う方法は、クラスにcopyWithメソッドを追加することです。そうすることで、どのような戦略を使ってもシンプルで統一性のあるものになります:

class Employee {
 final int id;
 final String name;
 const Employee(this.id, this.name);
 Employee copyWith({int id, String name}) {
 return Employee(
 id ?? this.id, 
 name ?? this.name,
 );
 }
}

この copyWith() メソッドは、通常、既定値のない名前付きオプション引数を使用します。この return 文では、Dart の if null 演算子 ?を使用して、従業員のコピーが各属性の新しい値を取得すべきか、または既存の状態の値を保持すべきかを判断します。メソッドが値 id を受け取った場合、それは null ではないので、その値はレプリカで使用されます。id が存在しないか、明示的に null に設定されている場合は、this.id が使用されます。コピー・メソッドは非常に柔軟で、1 回の呼び出しで任意の数のプロパティを更新できます。

copyWith() の使用例:

final emp1 = Employee(1, "Bob");
final emp2 = emp1.copyWith(id: 3);
final emp3 = emp1.copyWith(name: "Jim");
final emp4 = emp1.copyWith(id: 3, name: "Jim");

このコードを実行した後、emp2変数は更新されたid値を持つemp1のコピーを参照しますが、名前は変更されません。emp4のコピー操作は、各値を置き換えるので、完全に新しいオブジェクトを作成するのと同じです。状態更新関数やメソッドは、copyWith()を利用して、コードを非常に単純化することができるタスクを実行することができます:

Employee updateEmployeeId(Employee oldState, int id) { return oldState.copyWith(id: id); } Employee updateEmployeeName(Employee oldState, String name) { return oldState.copyWith(name: name); }

copyWith() メソッドは単に新しいオブジェクトを作成するためのラッパーなので、ここで状態更新関数を使用するのは少しやりすぎだと思うかもしれません。多くの場合、外部コードにコピー関数を直接使用させても問題ありません。イミュータブル・クラスのプロパティもイミュータブルである場合、ネストされたプロパティを更新するために copyWith() の呼び出しをネストする必要があるかもしれません。この状況については、次に説明します。

複雑なオブジェクトのプロパティの更新

プロパティの1つ以上がイミュータブル・オブジェクトでもある場合はどうしますか?それぞれの依存クラスに対して CopyWith メソッドを実装する必要があります。

class EmployeeName {
 final String first;
 final String last;
 const EmployeeName({this.first, this.last});
 EmployeeName copyWith({String first, String last}) {
 return EmployeeName(
 first: first ?? this.first,
 last: last ?? this.last,
 );
 }
}
class Employee {
 final int id;
 final EmployeeName name;
 const Employee(this.id, this.name);
 Employee copyWith({int id, EmployeeName name}) {
 return Employee(
 id: id ?? this.id,
 name: name ?? this.name,
 );
 }
}

ここで、Employee には EmployeeName 型のプロパティがあり、両方のクラスは不変で、copyWith() によって更新を促すメソッドを持っています。この設定を使用して、従業員の姓を更新する必要がある場合は、以下のように実行できます:

final updatedEmp = oldEmp.copyWith(
 name: oldEmp.name.copyWith(last: "Smith"),
);

ご覧のように、従業員の姓を更新するには、両方のバージョンの copyWith() を同時に使用する必要があります。

、コレクションの更新

不変コレクションをどのように更新するかは、コレクションをどのようにセットアップするか、そしてどこまで更新するかによって決まります。説明を簡単にするために、単純なデータクラスを使用します:

class NumberList {
 final List<int> _numbers;
 List<int> get numbers => List.unmodifiable(_numbers);
 NumberList(this._numbers);
}

このクラスは変更可能なリストを持っていますが、変更不可能なワン・コピー・レプリカだけを外部に公開しています。このリストを更新するには、以下のメソッドを使用します:

NumberList addNumber(NumberList oldState, int number) {
 final list = oldState.numbers.toList();
 return NumberList(list..add(number));
}

この方法はあまり効率的ではありません。oldState.numbers式はoldStateリストのコピーを提供しますが、変更できないため、toList()を使用して変更可能な別のコピーを作成する必要があります。次に、新しいNumberListを作成し、新しい数値を追加したリストのコピーを渡します。Dartのカスケード演算子を使用すると、...コンストラクタに追加する前にリストへの追加を実行します。更新メソッドを試すこともできます:

class NumberList {
 final List<int> _numbers;
 List<int> get numbers => List.unmodifiable(_numbers);
 NumberList(this._numbers);
 NumberList add(int number) {
 return NumberList(_numbers..add(number));
 }
}

この方法には多くの利点があります。実装はそれほど複雑ではなく、コードも少なくて済みます。注意すべき点としては、_numbersのクラスの再利用です。これはクラスの内部コードで実装することができ、このアプローチで満足できるかもしれませんが、等しい比較に対して副作用があるかもしれません。

状態管理パターンの中には、状態ストリームを生成するものがあります。新しい状態が作成されるたびに、新しい状態のインスタンスがストリームに供給され、リスニングしている UI コードに渡されます。最大限の効率を得るには、新しく受け取った状態が前の状態と実際に異なるかどうかをチェックすることができます。 add() 上記のコードは、_numbersの新しいインスタンスではなく、NumberListの新しいインスタンスを作成します。等号比較がどのように実装されているかによって、比較コードは、_numbersに格納されているリスト参照が変更されることがないため、同じ状態を生成し続けると誤解する可能性があります。numbersは決して変化しません。このような理由や他の理由から、変更のたびにリストを再作成したいと考える人もいます:

class NumberList {
 final List<int> _numbers;
 List<int> get numbers => List.unmodifiable(_numbers);
 NumberList(this._numbers);
 NumberList add(int number) {
 return NumberList(_numbers.toList()..add(number));
 }
}

この問題は、_numbers のコピーを作成し、そのコピーに新しい値を追加して、更新された新しいリストを含む NumberList の新しいインスタンスを返す toList() を追加することで解決します。

まとめ

オブジェクトやコレクションの不変量を処理する方法は数多くあり、Dart で複雑なデータが誤って変更されるのを防ぐ方法のいくつかについては、もうお分かりのはずです。コードのデモはありませんが、もっと詳しく学びたい場合は、build_valueなどのDartパッケージの内容をチェックしてみてください。

Read next

例外とエラー例外は、ほとんどの人が犯している間違いである。

1、はじめに ExceptionとErrorはThrowableクラスから継承され、JavaではThrowable型のインスタンスだけが投げたりキャッチしたりできる、例外処理の基本的な仕組みです。この2つの違いは何ですか?

Mar 27, 2020 · 3 min read