Dartでfinalなメンバ変数を初期化する方法のまとめ
基本
これは特に問題ありません。
class User {
final String name = "test";
}
main () {
}
インスタンス生成時にメンバ変数を初期化したい
class User {
final String name;
}
main () {
}
これは問題があって、このまま実行すると「nameが初期化されていないよ」というエラーになります。
[koji:hello]$ dart final_constructor.dart
final_constructor.dart:2:18: Error: Final field 'name' is not initialized.
Try to initialize the field in the declaration or in every constructor.
final String name;
^^^^
[koji:hello]$
じゃあコンストラクタを書いて、そこで代入して初期化するか、と以下のようにシンプルに書いてもエラーになります。
class User {
final String name;
User(String name) {
this.name = name;
}
}
main () {
}
エラー内容は
[koji:hello]$ dart final_constructor.dart
final_constructor.dart:4:14: Error: Setter not found: 'name'.
this.name = name;
^^^^
final_constructor.dart:4:14: Error: The setter 'name' isn't defined for the class '#lib1::User'.
Try correcting the name to the name of an existing setter, or defining a setter or field named 'name'.
this.name = name;
^^^^
final_constructor.dart:2:18: Error: Final field 'name' is not initialized.
Try to initialize the field in the declaration or in every constructor.
final String name;
^^^^
[koji:hello]$
エラー内容を見ればなんとなく分かりますが、finalで宣言された変数に値を代入することなんてできないよ、というエラーになっています。 なんでsetterが無いんだ?という疑問を持ちましが、Dartはデフォルトでメンバ変数のsetterとgetterを生成してくれる、とのことです。
しかし、finalな変数の場合はsetterを用意してくれない、ということで、今回のエラーが発生しています。 じゃあどうやって初期化するんだよ、というのが本題です。
コンストラクタの引数で指定
Dartでは、コンストラクタの引数部分に、this
を付けてメンバ変数を書くことで、その変数を初期化することができます。
これはfinalで宣言されて値が初期化されていないメンバ変数でも有効です。
class User {
final String name;
User(this.name);
// User(this.name){} <--- こう書いてもOK
}
main () {
User u = User("test");
assert(u.name == "test");
}
Initlializer listで初期化
Dart独特の機能で、Initializerというものが有ります。(ドキュメント的にInitializerを複数指定できるよ、といったニュアンスで説明されているっぽくて、正式名称がよくわかりません。。。)
この方法でも上記と同様に、finalなメンバ変数に値を代入できます。
class User {
final String name;
User(String name) : this.name = name;
// User(String name): this.name = name{} <--- こう書いてもOK
}
main () {
User u = User("test");
assert(u.name == "test");
}
コンストラクタの動作の少し詳細
Dartは、自分でコンストラクタを指定しない場合、デフォルトで引数のないコンストラクタを生成します。 そしてもし継承関係の有るクラスの場合、サブクラスはまず親クラスの引数無しコンストラクタを呼んでから、自分のコンストラクタを呼ぶ、という動作になっています。 以下、分かりやすいように自分で引数無しコンストラクタを定義して動作を確認してみました。
class User {
User(){
print("User constructor");
}
}
class Admin extends User {
Admin() {
print("Admin constructor");
}
}
main(){
Admin admin = Admin();
}
実行結果は以下のとおりです。
[koji:hello]$ dart class_test1.dart
User constructor
Admin constructor
[koji:hello]$
確かに親クラスの引数無しコンストラクタが自動で呼び出されています。
しかし、もし自分で引数有りのコンストラクタを宣言すると、引数無しのコンストラクタは生成されなくなります。 そもそも、Dartは同名のコンストラクタの宣言を許可していないようです。(そのために名前付きコンストラクタ、という機能が有ります)
試しに以下のようにして実行してみます。
class User {
User(int a) {
print(a);
}
User() {
print("User constructor");
}
}
class Admin extends User {
Admin() {
print("Admin constructor");
}
}
main(){
Admin admin = Admin();
}
すると以下のようなエラーになり、すでに同名のUserが定義されているよ、と表示されています。
[koji:hello]$ dart class_test1.dart
class_test1.dart:5:5: Error: 'User' is already declared in this scope.
User() {
^^^^
class_test1.dart:2:5: Context: Previous declaration of 'User'.
User(int a) {
^^^^
class_test1.dart:10:5: Error: The superclass, 'User', has no unnamed constructor that takes no arguments.
Admin() {
^^^^^
[koji:hello]$
このことから、自分で引数の有るコンストラクタを定義すると、サブクラスのコンストラクタから呼び出される筈の引数無しコンストラクタがそもそも生成されない(そもそも同名のコンストラクタが生成できないので)、と言うことが分かりました。
なので、親クラスが引数無しコンストラクタを用意している場合、そのコンストラクタを明示的に呼ぶ必要が有ります。 では実際に試してみます。
class User {
final String name;
User(this.name) {
print("User constructor");
}
}
class Admin extends User {
Admin() {
super("test");
print("Admin constructor");
}
}
main(){
Admin admin = Admin();
}
一見大丈夫そうですが、実行するとエラーになります。
[koji:hello]$ dart class_test1.dart
class_test1.dart:9:9: Error: Can't use 'super' as an expression.
To delegate a constructor to a super constructor, put the super call as an initializer.
super("test");
^
class_test1.dart:8:5: Error: The superclass, 'User', has no unnamed constructor that takes no arguments.
Admin() {
^^^^^
[koji:hello]$
Initializerとしてsuperを呼び出してね、と言っています。 ココでやっとInitializerが必要な理由が見えてきました。 では実際にInitializerを使うように書き換えます。
class User {
final String name;
User(this.name) {
print("User constructor");
}
}
class Admin extends User {
Admin() : super("test") {
print("Admin constructor");
}
}
main(){
Admin admin = Admin();
assert(admin.name == "test");
}
これで完了です。 実行するとエラーもなくちゃんと正常に動作しました。
引数無しコンストラクタが有る場合には、まず親クラスのコンストラクタが呼ばれて、その後にサブクラスのコンストラクタが呼ばれていました。 つまり、Dartはまずは親クラスのコンストラクタを呼び出してから、サブクラスのコンストラクタが呼び出される必要が有るようです。 もしも、
Admin() {
super("test");
print("Admin constructor");
}
が実行できてしまうと、まずサブクラスのコンストラクタが実行できてしまっているので、たしかにこの呼び方が許可されないのは理解できますね。
このことからも、Initializer listは、このInitializer listが指定されているコンストラクタ自体が呼び出される前に、Initializer listで指定している処理(今回は親クラスのコンストラクタの呼び出し)が実行されて、それから初めてその後の処理(今回はサブクラスのコンストラクタ)が呼び出される、という動作になっている事が伺えます。
再度メンバ変数の初期化を考える
さて、どうやらInitializerはコンストラクタが動き始める前に、つまり対象クラスのインスタンスが生成される前に実行される、ということが分かりました。 そこで本題であるInitializerを使ったfinalなメンバ変数を思い出してみます。
User(String name) : this.name = name;
この書き方で普通にfinalなメンバ変数に代入ができました。 これはつまり、まだちゃんとUserクラスのインスタンスがコンストラクタによって生成される前なので、特殊な形としてfinalなメンバ変数に値を代入して初期化することが出来るんだな、と思われます。
まとめ
一回見てしまえば、特に難しい部分ではないかな、と思います。 Dartの言語ツアーを読んでいる最中ですが、ちょっとこのあたりの動作が気になったので確認しつつメモしました。
公開日:2018/12/12