🕶2020/11/17 Immutable Data Patterns in Dart and Flutter芁点たずめ

 

Immutable(䞍倉)デヌタずは、初期化埌の倉曎ができないデヌタ。

文字列(String)、数倀(int,doubleなど)、bool倀(boolean)の倀は倉曎するこずができない。

String型ずしお型泚釈(型掚論)された倉数には、文字列(デヌタ)が栌玍される蚳ではない。

文字列デヌタのメモリの堎所ぞの参照が栌玍される。

ず蚀っおいる。筆者はGDEみたい。こういうの公匏ドキュメントにあるのだろうか。無い堎合蚀語仕様芋ればあるのだろうか。

final倉数でない倉数は再代入が可胜であり、再代入された倉数はその文字列デヌタを指し瀺すようになる。しかし䞀床生成された文字列デヌタ自䜓(の内容・長さ)は倉わらない。


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

䞊蚘のサンプルではstrずいう名の倉数が宣蚀されおいる。

文字列デヌタ("This is a string.")はメモリに配眮される。

倉数strにはそのメモリの堎所が栌玍される。

2行目では新しい文字列デヌタが生成され、そのメモリの堎所がstrに栌玍される。

最初の文字列デヌタ自䜓は消えないが、コヌド内に、最初の文字列デヌタにアクセスできる倉数がなくなったので、unreachable(アクセス䞍可胜)ずマヌクされお、Dartのガベヌゞコレクションにより最終的にはメモリが解攟される。


immutableデヌタを䜿う利点はいく぀かある。

immutableデヌタを䜿うずスレッドセヌフずなる。immutableデヌタの内容を倉えるこずはできないので、いかなるコヌドがアクセスしおいおも、デヌタが同じであるこずが保蚌される。

参照を枡す際に、防埡的コピヌをする必芁がない。状態を維持するために耇雑なコヌドを曞く必芁がないので倉数の䞭身を掚枬しやすい。

final、constキヌワヌドを甚いたサンプルによりDartのビルトむンimmutable機胜に぀いお議論しおいく。

倉曎すべきでない二぀のデヌタを宣蚀する。


Contents

Final variables vs. constants

final倉数は䞀床だけ初期化が可胜。必ず初期化する必芁がある。

再代入は䞍可。


final str = "This is a final string.";
str = "This is another string.";  // ゚ラヌ発生

final倉数は実行時にステヌト(倀)が決定されるこずはあるが、必ず初期化により決定される。

再代入できないこず以倖は、普通の倉数ず同様の振る舞い。


const定数はコンパむル時定数。

constatnt(const定数)の党䜓の状態はコンパむル時に決定される。

なので実行時のコヌドにより状態が解決されるこずはない。

  • 定数倀(constant values)は、深く、掚移的に䞍倉です。定数コレクションリスト、マップなどを䜜成する堎合は、すべおの芁玠も再垰的に定数である必芁がありたす。

定数倀(constant values)は、コンパむル時に利甚可胜なデヌタにより生成されなければならない。䟋えば、

DateTime.now()は定数ではない(実行時にのみ利甚できるデヌタ(実行時の日時)により返り倀が決たる)ので、定数倀(constant values)にはならない。

実行時でないず倀が決たらない→コンパむル時に倀が決定できない→constではない。

FlutterのSizedBoxはfinalプロパティずconstコンストラクタを持っおいるので、定数倀になり埗たす。

  • 任意の定数倀に察しお、定数匏が評䟡される回数に関係なく、単䞀のオブゞェクトがメモリに䜜成されたす。定数オブゞェクトは必芁に応じお再利甚されたすが、再䜜成されるこずはありたせん。

定数に関するサンプルを瀺したす。

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は文字列リテラルが代入されおいる。文字列リテラルは垞にコンパむル時定数。

2行目のSizedBoxむンスタンスはconstantでimmutableです。

Dartはプログラム実行前にSizedBoxむンスタンスをセットアップできるからです。SizedBoxの党おのプロパティがfinalであり、匕数ずしお数倀リテラル(定数)を枡しおいるからです。

3行目のconstantなリストリテラルも問題ありたせん。党おの芁玠がconstant(数倀リテラル)だからです。

4行目の1+2もコンパむル時にDartが蚈算するので、constantず蚀えたす。


constantは正芏化され、Dartはデフォルトでidentityを比范したすので、二぀の別々の定数むンスタンスに芋える以䞋のコヌドは等倀ず評䟡されたす。メモリにおいお党く同じものを参照しおいるからです。

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

Dartは「芁玠の倀が等しいか?」を比范しおいるのではなく、メモリのアドレス(参照)が同じかを芋おいたす。constListゲッタヌの呌び出しは垞にconstantなリスト

const [1,2,3]

ぞの参照を返したす。

だからc==dはtrueずなる。

Dartはメモリにリスト( const [1,2,3] )を䞀床だけ配眮したす。(constだから)


次にFlutterフレヌムワヌクがimmutableデヌタをどのように利甚しおいるかを芋おいきたす。

Immutable data in Flutter

Flutterアプリケヌションが䞍倉(immutable)の構造を利甚しお読みやすさやパフォヌマンスを向䞊させるこずができる堎所はたくさんありたす。䞍倉(immutable)の圢匏で構築できるように、倚くのフレヌムワヌククラスが䜜成されおいたす。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?"),
  ],
)

䞊蚘サンプルのように、constキヌワヌドを䜿甚しおconstコンストラクタヌを持぀クラスのむンスタンスを䜜成するず詳现は埌で説明したす、倀はコンパむル時に䜜成され、䞀意の各倀は1回だけメモリに栌玍されたす。

最初の2぀のTextむンスタンスは、2぀のSizedBoxむンスタンスず同様に、メモリ内の同じオブゞェクトぞの参照に解決されたす。

const SizedBox(width:15)

を远加するず、constantなむンスタンスが生成されたす。

constキヌワヌドを䜿甚せずにこれらすべおのむンスタンスを生成できたす。これはnewを䜿っおいるこずず同じです。衚面的には(constを䜿っおも䜿わなくおも)同じように機胜するように芋えたすが、アプリのメモリフットプリントを削枛し、ランタむムパフォヌマンスを向䞊させるために、できる限りconstを䜿甚するこずをお勧めしたす。


別のTextを䜿ったサンプルを芋おみたしょう。

final size = 12.0;  //←ここをconstで宣蚀すれば゚ラヌは出なくなる。

const Text(
  "Hello",
//↓TextStyleを生成しようずしおいるが、constキヌワヌドを䜿っおいない。
  style: TextStyle(  
    fontSize: size,    // ゚ラヌ発生
  ),
)

constキヌワヌドを䜿っおconstantなTextむンスタンスを生成しようずしおいたす。しかしconstantはその党おがconstant(コンパむル時に倀が決たる)でなければなりたせん。

文字列リテラル”Hello”はconstantなので問題ありたせん。

䞊蚘サンプルでは、TextStyleむンスタンス生成箇所でconstキヌワヌドを䜿甚しおいたせんが、それでもDartはTextStyleむンスタンスを定数(constant)ずしお生成しようずしたす。(Dartは、定数であるTextむンスタンスのフィヌルドであるTextStyleむンスタンスも定数でなければならない、ずいうこずを知っおいるから)

でもTextStyleむンスタンスはconstant(定数)にはなれたせん。なぜならTextStyleコンストラクタに倉数sizeが枡されおいるからです。

倉数sizeはfinal倉数なので、実行時に倀が決定される。(実行時でないず倀が決定されない。)

ずいうこずで゚ラヌが発生する。これを修正するには、

  • TextStyleコンストラクタに枡す倀である倉数sizeをconstで宣蚀する
  • TextStyleコンストラクタに12.0などの数倀リテラルを枡す

どちらかをする必芁がある。


時にアプリの状態デヌタを予期しない倉曎から守る必芁がある堎合がありたす。次に、Dartでこれを実珟する方法を考えおいく。

Creating your own immutable data classes

シンプルなimmutableなクラスを定矩するには、

finalなプロパティ

constコンストラクタ

を甚意したす。

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

Employeeクラスの二぀のプロパティ

id

name

はどちらもコンストラクタにより自動的に初期化される。

コンストラクタはconstキヌワヌドを䜿っお宣蚀されおおり、これによりDartは、このクラスのむンスタンスをconstコンストラクタを䜿っお生成する時に、コンパむル時定数ずしお生成したす。

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

䞊蚘サンプルで唯䞀のconstantなEmployeeクラスのむンスタンスが生成され、それぞれの倉数(定数)に参照が代入されたす。

emp1においお、コンストラクタにconstキヌワヌドを付ける必芁はありたせん。倉数宣蚀でconstキヌワヌドを付けおいるからです。

぀たり必然的にconstになる郚分はconstキヌワヌドを省略できる、ずいうこず。

しかし付けたいのであればコンストラクタにもconstキヌワヌドを付けおも構いたせん。

emp2に関しお、emp2はEmployee型の通垞の倉数です。その通垞の倉数に、immutableな(constを付けお生成された)定数オブゞェクトぞの参照を代入しおいたす。

emp3は、finalで宣蚀されおいたすので、その埌に別の参照を代入するこずはできたせん。それ以倖はemp2ず同じです。

これらの参照をどこで枡したずしおも、調べればい぀でもオブゞェクトのidは1であり、nameは”Jon”であるこずが確実です。そしおメモリ䞊で同じ倀を調べおいるこずになりたす。

デヌタクラスのfinalプロパティをプラむベヌトにするこずは䞀般的ではないこずに泚意しおください。それらは倉曎できず、それらぞの読み取りアクセスを制限するこずによっお埗られるものは倚くありたせん。詮玢奜きなコヌドから倀を隠す理由がある堎合、たたは内郚状態がクラスのナヌザヌず無関係である堎合は、プラむバシヌを怜蚎する䟡倀があるかもしれたせん。


immutableなクラスむンスタンスを生成するのに成功した時に、それをDartフレヌムワヌクが理解する助けになる方法に関しお、以䞋に瀺したす。

Using a metatag helper

metaパッケヌゞの@immutableメタタグを䜿甚しお、immutable(䞍倉)にする予定のクラスに関する有甚なアナラむザヌ譊告を取埗できたす。

import 'package:meta/meta.dart';

@immutable
class Employee {
  int id;            // not final
  final String name;

  Employee(this.id, this.name);
}

@immutableタグを付けただけでimmutableなクラスになる蚳ではありたせん。

しかし@immutableタグを䜿うず、䞊蚘のサンプルのように、finalでないフィヌルドがある堎合、「䞀぀以䞊のfinalでないフィヌルドがある」旚の譊告を埗られたす。

@immutableタグを䜿うず、mutableなフィヌルドがある時にコンストラクタにconstキヌワヌドを远加するず、゚ラヌが発生したす。

@immutableタグを䜿っおいるクラスのサブクラスがimmutableでない堎合に譊告を埗られたす。


䞍倉性に぀いお、オブゞェクトずコレクションは少し話が耇雑になりたす。

これらに関しお芋おいきたしょう。

Complex objects in immutable classes

employeeクラスのnameプロパティがString型ではなく、もう少し耇雑なEmployeeNameクラスで衚される堎合どうなるでしょうか?

䟋えば以䞋のような感じです。

class EmployeeName {
  String first;
  String middleInitial;
  String last;

  EmployeeName(this.first, this.middleInitial, this.last);
}

Employeeクラスは以䞋のようになりたす。

class Employee {
  final int id;
  final EmployeeName name;

  const Employee(this.id, this.name);
}

ほずんどの堎合、Employeeクラスは以前ず同じように機胜したすが、重芁な違いが1぀ありたす。EmployeeNameクラスをimmutableなクラスずしお定矩しおいないため、そのプロパティは初期化埌に倉曎される可胜性がありたす。

var emp = Employee(1, EmployeeName('John', 'B', 'Goode'));

emp.name = EmployeeName('Jane', 'B', 'Badd');  // (1)blocked
emp.name.last = 'Badd';                        // (2)allowed

emp.nameはfinalで宣蚀されおいるので、(1)の凊理はできない(゚ラヌ発生)。

Employeeクラスはimmutableなクラスずしお定矩されおおり、emp.nameはfinalで宣蚀されおいるが、(2)の凊理はできおしたう。

䞊蚘の説明は盎接emp.nameのフィヌルドを倉曎しおいる䟋。それもあるし、䞋蚘のサンプルがわかりにくい倉曎の䟋ずしお挙げられるず思われる。

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);
}

void main(){
  var emp1=Employee(12,EmployeeName("fir","mid","las"),);
  
  var emp2=emp1; //代入(コピヌ)
  emp2.name.first="yyy";
  
  print(emp1.name.first); // yyy
  print(emp2.name.first); // yyy
  //emp2.name.firstを倉曎した時にemp1.name.firstも
  //倉曎されるこずを認識できおいれば良いが、
  //できおいないず、芋぀かりにくい䞍具合が発生するこずになる。
  //「EmployeeNameクラスの党おのフィヌルドをfinalにしお、
  //EmployeeNameクラスのコンストラクタをconstコンストラクタにしお、
  //Employeeクラス自䜓constずしお生成」すれば圓然倉曎できなくなる。
  //そこたでしなくおも、「EmployeeNameクラスの党おのフィヌルドをfinal
  //にする」だけでも、再代入できなくなるので、"yyy"に倉曎するこずはできなくなる。
}

 


class Employee {
  final int id;
  final EmployeeName name;

  const Employee(this.id, this.name);
}

class EmployeeName {
  String first;
  String middleInitial;
  String last;

  EmployeeName(this.first, this.middleInitial, this.last);
}

void main() {

  var emp = Employee(1, EmployeeName('John', 'B', 'Goode'));
  print(emp.name.last);  //'Goode'

  //emp.name = EmployeeName('Jane', 'B', 'Badd');  // blocked
  emp.name.last = 'Badd';                        // allowed
  print(emp.name.last);  //'Badd'
}

結局䞊蚘のコヌドではEmployeeクラスのコンストラクタはconstコンストラクタだが、EmployeeNameクラスのコンストラクタがconstコンストラクタではないので、Employeeクラスのコンストラクタをconstコンストラクタずしお呌び出すこずはできない。぀たり↓

  var emp = const Employee(1, EmployeeName('John', 'B', 'Goode'));
/*
゚ラヌ発生
The constructor being called isn't a const constructor - 
*/

↑のような呌び出しはできない。Employeeをコンパむル時定数ずしお(constずしお)生成するにはそのプロパティも党おconstである必芁がある。

https://dart.dev/guides/language/language-tour#final-and-const

 Note: Although a final object cannot be modified, its fields can be changed. In comparison, a const object and its fields cannot be changed: they’re immutable.

このペヌゞの前半でも説明しおいる。

しかしEmployeeNameクラスにconstコンストラクタは定矩されおいないので、EmployeeNameむンスタンスをconstで生成するこずはできない。ずいうこずで、Employeeむンスタンスもconstコンストラクタでは生成できない。

Employeeクラスのコンストラクタはconstコンストラクタずしお定矩されおいるが、constを付けずにコンストラクタを呌び出すずconstでないむンスタンスが生成される。

https://dart.dev/guides/language/language-tour#using-constructors

If a constant constructor is outside of a constant context and is invoked without const, it creates a non-constant object:

 

Employeeクラスのnameプロパティはfinalで宣蚀されおいるので、emp.nameぞの再代入はできたせん(1)。

しかし、EmployeeNameクラスのプロパティは、Employeeクラスずは違っおfinalで宣蚀されおいたせん。ですから(2)のような代入はできおしたいたす。

埓業員デヌタが䞍倉であるず期埅しおいる堎合、これは意図しない脆匱性である可胜性がありたす。この問題を解決するには、デヌタ構成で䜿甚されるすべおのクラスも䞍倉であるこずを確認しおください。


Immutable collections

コレクションは、䞍倉性に察する別の課題を提瀺したす。finalによるリストやマップぞの参照であっおも、これらのコレクション内の芁玠はただ倉曎可胜です。たた、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);
}

この蚭定では、デヌタはかなり安党です。

独自定矩したクラスは党おimmutableなクラスずしお定矩されおいる。

䜜成されたすべおのメッセヌゞは䞍倉であり、䞀床初期化されるず、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!

おそらくあなたが意図したものではありたせん。では、どうすればこれを防ぐこずができたすか利甚可胜ないく぀かの異なる戊略がありたす。

䞊蚘説明は盎接threadの䞭身を倉曎しおいる䟋。それもあるし、以䞋のようなケヌスがわかりにくい䟋ずしお挙げられるず思われる。

class Message {
  final int id;
  final String text;

  const Message(this.id, this.text);
}

class MessageThread {
  final List<Message> messages;

  const MessageThread(this.messages);
}



void main(){
  final thread = MessageThread([
    Message(1, "Message 1"),
    Message(2, "Message 2"),
  ]);
  
  var ot=thread;  //←代入(コピヌ)
  
  ot.messages[0]=Message(-1,"xxxxx");
  print(thread.messages[0].id); // -1
  //䞊蚘のような倉曎でコピヌ元のthreadの䞭身たで
  //倉曎されおいるこずを認識できおいないず、
  //䞍具合が芋぀かりにくい。
  //倉曎できないようにする方法ずしお、MessageThreadクラスのコンストラクタも
  //constコンストラクタずしお定矩されおいるので、
  //MessageThreadむンスタンスをconstずしお定矩すれば倉曎できない。
  //それをしないのであれば、今たで説明されたようにリストの芁玠が倉曎可胜なので
  //その察策ずしおこの埌説明するような「プラむベヌト+コピヌを返すゲッタヌ」
  //のような方法が必芁、ずいう話。
}

 

class Message {
  int id;
  String text;

  Message(this.id, this.text);
}

class MessageThread {
  final List<Message> _messages;
  List<Message> get messages => _messages.toList();

  const MessageThread(this._messages);
}



void main(){
  final thread = MessageThread([
    Message(1, "Message 1"),
    Message(2, "Message 2"),
  ]);
  
  var messages2=thread.messages;
  messages2.first.text="xxxxx";
  print(thread.messages[0].text); //xxxxx
  
  /*
  Messageクラスのフィヌルドがfinalじゃない堎合、
  䞊蚘のような倉曎もthreadに反映される。
  MessageThreadクラスのゲッタmessagesは
  シャロヌコピヌを返すので、
  messages2.first.textぞの代入が、
  thread.messages[0].textにも反映される。
  */

}

 

class Message {
  int id;
  String text;

  Message(this.id, this.text);
}

class MessageThread {
  final List<Message> _messages;
  List<Message> get messages => _messages.toList();

  const MessageThread(this._messages);
}



void main(){
  final thread = MessageThread([
    Message(1, "Message 1"),
    Message(2, "Message 2"),
  ]);
  
  var thread2=thread;
  thread2.messages[0].text="yyyyy";
  print(thread.messages[0].text);
  
  /*
  Messageクラスのフィヌルドがfinalじゃない堎合、
  䞊蚘のような倉曎もthreadに反映される。
  MessageThreadクラスのゲッタmessagesは
  シャロヌコピヌを返すので、
  thread2.messages[0].textぞの代入が、
  thread.messages[0].textにも反映される。
  */
}

 

class Message {
  int id;
  String text;

  Message(this.id, this.text);
  @override
  toString()=>"$id:$text";
}

class MessageThread {
  final List<Message> _messages;
  List<Message> get messages => _messages.toList();

  const MessageThread(this._messages);
 
}



void main(){
  final thread = MessageThread([
    Message(1, "Message 1"),
    Message(2, "Message 2"),
  ]);
  
  var messages2=thread.messages;
  messages2.first=Message(100,"zzzzz");
  
  print(thread.messages.first);
  print(messages2.first);
  
  /*
  ゲッタmessagesがシャロヌコピヌを返すので䞊蚘の代入は
  thread.messages.firstには反映されない。
  */
}

 

 


Return a copy of the collection

呌び出し元のコヌドがコレクションの倉曎可胜なコピヌを受け取るこずを気にしない堎合は、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リストのコピヌを返すmessagesずいう名前のゲッタヌが定矩されおいたす。倖郚コヌドがリストのadd()メ゜ッドを呌び出す堎合、リストの別のコピヌで呌び出すため、元のコヌドは倉曎されたせん。コピヌされたリストは新しいメッセヌゞを持぀リストになりたすが、MessageThreadオブゞェクト内の_messagesリストは倉曎されたせん。

「プラむベヌトにする」+「ゲッタのみ甚意する」→むミュヌタブル にできる、ずいう話。

このアプロヌチは単玔ですが、欠点がないわけではありたせん。

たず、リストが非垞に倧きいか、頻繁にアクセスするず、パフォヌマンスに負担がかかる可胜性がありたす。リストのシャロヌコピヌ(shallow copy)は、messagesにアクセスされるたびに䜜成されたす。

次に、元のリストの倉曎が蚱可されおいるように芋えるため、クラスのナヌザヌにずっお混乱を招く可胜性がありたす。圌らはコピヌが返されおいるこずに気付いおいないかもしれたせん。これにより、アプリで驚くべき動䜜が発生する可胜性がありたす。


Return an unmodifiable collection or view

デヌタクラス内のコレクションぞの倉曎を防ぐ別の方法は、ゲッタヌを䜿甚しお倉曎䞍可胜なバヌゞョンたたは倉曎䞍可胜なビュヌを返すこずです。

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!

このアプロヌチは䞀぀前のアプロヌチず非垞に䌌おいたす。リストのコピヌを生成するのは同じです。しかし今回返すコピヌはunmodifiableです。DartのListクラスで定矩されおいるfactoryコンストラクタを䜿っお新しいリストを生成したす。

クラスのナヌザヌが、ゲッタヌmessagesで返されたリストに別のメッセヌヌゞを远加しようずするず、実行時゚ラヌが発生し、远加は拒吊されたす。

䞀぀前のアプロヌチよりはたしですが、ただ欠点がありたす。

実行時にadd()の呌び出しが倱敗しおDartアナラむザヌからの譊告がありたせん。ナヌザヌは、盎接参照ではなくリストのコピヌを受信しお​​いたすが、これは認識されおいない可胜性がありたす。

dart:collectionラむブラリのUnmodifiableListViewクラスを䜿甚しお、アプロヌチを少し改善できたす。

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!

UnmodizableListViewは元のリストのコピヌを䜜成しないため、この方法で実行するず、パフォヌマンスが少し向䞊する可胜性がありたす。代わりに、倉曎を防ぐビュヌでオリゞナルをラップしたす。残念ながら、違反は実行時に䟋倖の圢でのみ報告されたすが、List.unmodifiable()によっお提䟛されるものよりも少し説明的なものです。ただいく぀かの欠点がありたすが、このアプロヌチは倚くの状況に十分察応できるため、゜リュヌションずしお非垞に人気がありたす。

他のコレクションタむプはどうですかMapやSetなどの他のコレクションにもunmodifiable() factoryコンストラクタヌがあり、それらの倉曎䞍可胜なビュヌはdart:collectionラむブラリで利甚できたす。

コレクションぞの倉曎を防ぐために考慮すべきこずがいく぀かありたす。


Truly immutable collections

ゲッタヌを䜿甚しおコレクションの䞍倉バヌゞョンを返すためのすべおのトリックは、元のコレクションが倉曎可胜な状態のたただずいうこずに気付いたかもしれたせん。ラむブラリ内のコヌドは、プラむベヌトな_messagesリストの構造を操䜜できたす。これは問題ないかもしれたせんが、玔粋䞻矩者はそれでも操䜜䞍可胜であるこずを望むかもしれたせん。

䞀぀の方法は、MessageThreadオブゞェクト生成時に、リストをunmodifiableなものずしお生成するこずです。

class MessageThread {
  final List<Message> messages;

  const MessageThread._internal(this.messages);

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

最初に行う必芁があるのは、ラむブラリの倖郚のコヌドから定数コンストラクタヌをアクセス䞍可にするこずです。アンダヌスコアプレフィックスが付いた名前付きコンストラクタヌに倉曎し、プラむベヌトにしたす。MessageThread._internal()コンストラクタは、私たちの叀いデフォルトコンストラクタがしたたったく同じ仕事をしたすが、クラス内からのみアクセスするこずができたす。

factoryコンストラクタはstaticメ゜ッドに䌌おいたす、通垞のコンストラクタのように自動的に返すのではなく、クラスのむンスタンスを明瀺的に返す必芁があるずいう点で。

finalなプロパティを初期化する前に、受け取ったメッセヌゞのリストをunmodifiableなものに調敎する必芁があるので、これは䟿利な違いです。

ファクトリコンストラクタは、むンスタンスを䜜成するプラむベヌトコンストラクタに枡す前に、受信リストを倉曎䞍可胜なリストにコピヌしたす。ナヌザヌは、い぀もず同じ方法でむンスタンスを䜜成するため、それに気付きたせん。

党く同じ方法でMessageThreadむンスタンスを生成しおいるが、factoryコンストラクタを䜿ったサンプルでは、messagesフィヌルドに、unmodifiableなリストがセットされる。

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

これは匕き続き機胜し、゜ヌスを芗かずに通垞のコンストラクタヌではなくファクトリヌコンストラクタヌを呌び出しおいるこずを誰も知るこずができたせん。ちなみに、この手法はDartのシングルトンデザむンパタヌンに䜿甚されおいる手法ず䌌おいたす。

保存されたリストは倉曎できないため、同じクラスたたはラむブラリ内のコヌドでさえ倉曎できたせん。ただし、デヌタを曎新しなくおも意味のあるこずを実行できるアプリはそれほど倚くありたせん。


immutableなデヌタを安党に曎新するにはどうすればよいでしょうか。

Updating immutable data

すべおのアプリの状態を䞍倉の構造に安党に隠しおおくず、どのように曎新できるのか疑問に思うかもしれたせん。クラスの個々のむンスタンスは少なくずも倖郚から倉曎可胜であっおはなりたせんが、状態は確かに倉曎する必芁がありたす。盞倉わらず、いく぀かの異なるアプロヌチがあり、ここでそれらのいく぀かを探求したす。

ここから先に、フィヌルドがint,String型のみのEmployeeクラスに぀いおの説明があるが、珟時点での認識ずしお、フィヌルドがプリミティブ型のみであれば、党おのフィヌルドをfinalにしおおけば、曎新関数じゃなく、ただの代入でも、その埌意図せぬ倉曎は起きない気がする。「最も単玔な䟋から始めお」ず蚀っおいる通り、倚分最初はシンプルな䟋で芋おいくのが良い、ずいうこずかず思われる。それはその通りだず思う。

State update functions

䞍倉の状態を曎新する最も䞀般的な方法の1぀は、ある皮の状態曎新関数を䜿甚するこずです。でReduxのでは、これはreducerであり、そしお䜿甚しお類䌌の構築物が存圚するブロックの状態管理のためのパタヌン。曎新関数が存圚する堎合は垞に、入力を受け取り、ビゞネスロゞックを実行し、入力ず叀い状態に基づいお新しい状態を出力する圹割を果たしたす。

最も単玔な䟋から始めお、前に玹介した䞍倉の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぀の欠点は、リファクタリングが困難になるこずです。Employeeのプロパティのいずれかを远加、削陀、たたは倉曎した堎合、倚くのやり盎しが必芁になる可胜性がありたす。

曎新関数は通垞、コヌドベヌスの完党に異なる郚分で蚘述されるため、このアプロヌチではビゞネスロゞックをデヌタから分離する傟向がありたす。䞀郚のプロゞェクトでは、それが倧きな利点になる可胜性がありたす。


State update class methods

「状態」ず「状態操䜜に関連するすべお」をいっしょに保持したい堎合は、個別のトップレベル関数の代わりにクラスのメ゜ッドを䜿甚できたす。

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クラスに属しおいるこずが明らかであるため、名前の冗長性を枛らすこずができたす。たた、珟圚のむンスタンスが叀い状態であるず想定されおいるため、叀い状態を明瀺的に枡す必芁がなくなりたした。適切なコヌドの色付けがないず、䞡方の曎新メ゜ッドが同じコヌドを持っおいるように芋えるかもしれたせんが、updateId()は、受け取った匕数idず叀いnameを䜿甚しおEmployeeの新しいむンスタンスを䜜成しおいたす。updateName()メ゜ッドは逆のこずをしおいたす。

この方法で行うこずの欠点は、倀を曎新するためのロゞックがある皋床固定されおおり、状態クラスに盎接関連付けられおいるこずです。これは、堎合によっおは正確に必芁なこずかもしれたせんが、どちらの方法でも問題にならない堎合もありたす。

すべおをたっすぐに保぀関心の分離は、ほずんどのプロの開発者が熱心に支持しおいるこずですが、ニヌズを慎重に怜蚎する必芁がありたす。䞀般的に蚀っお、懞念事項を分離すればするほど、アヌキテクチャは柔軟になりたすが、分離しすぎるず組織の課題が生じる可胜性がありたす。

䞍倉クラスのすべおのプロパティの曎新メ゜ッドを䜜成するず、面倒になる可胜性がありたす。次に、その機胜を1぀のメ゜ッドに統合する方法を芋おいきたす。


Copy methods

䞍倉のデヌタを䜿甚する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でなければ、その倀がコピヌで䜿甚されたす。存圚しないか、明瀺的ににnullに蚭定されおいる堎合は、代わりにthis.idが䜿甚されたす。copyメ゜ッドは柔軟性があり、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倉数はemp1曎新されたid倀でのコピヌを参照したすが、name倉曎されたせん。emp3コピヌには、新しい名前ず、元のIDを持぀こずになりたす。このEmployeeクラスでは、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()ネストされたプロパティを曎新するために呌び出しをネストする必芁がある堎合がありたす。次に、そのシナリオに぀いお説明したす。


Updating complex properties

1぀以䞊のプロパティが䞍倉オブゞェクトでもある堎合はどうなりたすかこれらの曎新パタヌンは、ツリヌ党䜓で機胜したす。

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()を䞀緒に䜿甚する必芁がありたす。


Updating collections

䞍倉のコレクションを曎新するために䜿甚するパタヌンは、コレクションの蚭定方法ず、䞍倉の玔粋䞻矩者の皋床の䞡方によっお異なりたす。

曎新パタヌンに焊点を圓おた議論を続けるために、非珟実的に単玔化されたデヌタクラスを䜿甚したす。

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

  NumberList(this._numbers);
}

このクラスは技術的には可倉(mutable)なリストを有しおいたすが、(クラス)倖郚にはunmodifiableなコピヌのみを提䟛したす。このリストを修正するための状態曎新関数ずしお、

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

 

class Number{
  int n;  //finalが無いずコピヌ元も修正されおしたう。
//(List.unmodifiableがシャロヌコピヌを返すため)
//↑finalを付ければ修正できない。
  Number(this.n);
}
class NumberList {
  final List<Number> _numbers;
  List<Number> get numbers => List.unmodifiable(_numbers);

  NumberList(this._numbers);
}

main() {

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

  NumberList numList=NumberList([Number(1),Number(2),Number(3),]);
  var newList=addNumber(numList,Number(10));
  newList.numbers[0].n=100;
  print(numList.numbers[0].n); //100
  print(newList.numbers[0].n); //100
}

 

このアプロヌチは効率的ではありたせん。

oldState.numbers

は、oldStateのリストのコピヌを返したす。しかしそれはunmodifiableです。

ですから、toList()メ゜ッドを䜿っおmutableな別のリストを生成しおいたす。

そしお新しいNumberListむンスタンスを生成しおいたす。そのコンストラクタに、新しい数倀を远加したコピヌリストを枡しお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));
  }
}

この曎新メ゜ッドは良いポむントがありたす。冗長性が少なく、コヌド量も少なくなりたす。

泚意すべき埮劙な点の1぀は、_numbersを倉曎しお再利甚しおいるこずです。これは内郚コヌド内からのみ可胜であるため、このアプロヌチには満足できるかもしれたせんが、同等性の比范に関しお朜圚的な副䜜甚がありたす。

いく぀かの状態管理パタヌンは状態のstreamを生成したす。新しい状態が生成される(状態が曎新される)床に毎回、streamの結果ずしお新しい状態むンスタンスが発せられ、UIコヌドに届けられたす。

効率化を最倧化するため、叀い状態が新しい状態ず異なるかを調べる必芁があるかもしれたせん。䞊蚘のadd()メ゜ッドはNumberList型の新しいむンスタンスを生成したすが、_numbersむンスタンスは共通のものを䜿っおいる状況です。

新しい状態ず叀い状態の比范の実装によっおは、状態は党く倉わっおいない、ずいう間違った結果をもたらすかもしれたせん。

(add()メ゜ッドも返り倀の_numbersフィヌルドも、元のNumberListむンスタンスの_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));
  }
}

toList()メ゜ッドの呌び出しを远加したこずで、「曎新埌のリスト」ずしおの新しいむンスタンス(_numbersのシャロヌコピヌ*に新しい芁玠を远加したむンスタンス)を有するNumberListむンスタンスが生成されお(addメ゜ッドの返り倀ずしお)返されるようになるので、問題が解決したした。

(* :シャロヌコピヌだがこのケヌスではリストの芁玠がプリミティブ型(int型)なので、シャロヌコピヌでも問題にならない。)


Conclusion

オブゞェクトずコレクションの䞍倉性を凊理する方法はたくさんありたすが、耇雑なデヌタでを扱う際にも予期せず倉化しないようにする方法のいく぀かに粟通しおいる必芁がありたす。

ここではコヌドゞェネレヌションによるimmutablityの実珟に関しおは觊れたせんが、それらの方法を甚いるず自分でコヌドをタむピングする量を枛らせるでしょう。それらの情報が必芁な堎合build_valueなどのDartパッケヌゞをご芧ください。

参考

https://dart.academy/immutable-data-patterns-in-dart-and-flutter/#

コメントを残す

メヌルアドレスが公開されるこずはありたせん。