#問題
問題です。
次のコードの出力を予想してください。
// xに値を代入した時に、代入した値をコンソールに出力するための関数。
int assign(int n) {
print(n);
return n;
}
// ClassBのインスタンスを作り、xというプロパティの値を出力します。
void main() {
print('the result is: ${ClassB().x}');
}
class ClassA {
ClassA() : x = assign(1) {
x = assign(2);
}
int x = assign(3);
}
class ClassB extends ClassA {
ClassB() : x = assign(4) {
x = assign(5);
}
@override
int x = assign(6);
}
xに値が代入されるたびに、その値がコンソールに出力されます。
その出力順序と、最終的にClassB
のインスタンスが保持しているx
の値を予想してくださいということです。
軽く行数稼ぎをしたら、すぐに答えを載せてしまいますね。
10...
9...
8...
7...
6...
5...
4...
3...
2...
1...
正解
正解は以下のようになります。
6
4
3
1
2
5
the result is: 5
もし「本当?」という方がいましたら、DartPadで確認していただくと良いと思います。
そういう確認がすぐできるのって便利で素晴らしいですね。
よくわかる解説
クラスClassB
のコンストラクタを呼んだ際の処理順序は
- プロパティの宣言に直接くっついている代入文
assign(6)
- 呼んだコンストラクタにくっついているイニシャライザ
assign(4)
- 呼んだコンストラクタの本文
assign(5)
となるのですが…ここでポイントです。
##イニシャライザリストの最後には、スーパークラスのコンストラクタが丁度一回呼ばれる
イニシャライザを(複数ある場合は上から順に)実行する際に、最後に必ずスーパークラスのコンストラクタが呼ばれます!
これは、明示的に呼んだ場合はそれでいいし、明示的に呼ばなかった場合はスーパークラスのデフォルトコンストラクタが自動的に呼ばれます。
もし、デフォルトコンストラクタが引数を取る場合、引数を与えながら明示的に呼ぶ必要があります。
明示的に書かないと、自動的にデフォルトコンストラクタを呼ぼうにも、引数に何を与えたら良いかわからないからですね。
void main() {
var classB = ClassB();
}
class ClassA {
ClassA(int x) {
print(x);
}
}
class ClassB extends ClassA {
ClassB() : super(3); // これがないとエラー
}
3
ということで、先程の問題は、ClassA
も含めると次の順序で実行されます。(直後にコードを再掲してあります)
- プロパティの宣言にある代入文
assign(6)
- 呼んだコンストラクタのイニシャライザ
assign(4)
- イニシャライザのラストとして、
ClassA
のコンストラクタを呼ぶ- プロパティの宣言にある代入文
assign(3)
- 呼んだコンストラクタのイニシャライザ
assign(1)
- (イニシャライザのラストとして、スーパークラスのコンストラクタを呼ぶ)
- 呼んだコンストラクタの本文
assign(2)
- プロパティの宣言にある代入文
- 呼んだコンストラクタの本文
assign(5)
// xに値を代入した時に、代入した値をコンソールに出力するための関数。
int assign(int n) {
print(n);
return n;
}
// ClassBのインスタンスを作り、xというプロパティの値を出力します。
void main() {
print('the result is: ${ClassB().x}');
}
class ClassA {
ClassA() : x = assign(1) { // 実行順4
x = assign(2); // 実行順5
}
int x = assign(3); // 実行順3
}
class ClassB extends ClassA {
ClassB() : x = assign(4) { // 実行順2
x = assign(5); // 実行順6
}
@override
int x = assign(6); // 実行順1
}
解説おわり!
#知っておくと良さそうなこと
##イニシャライザでスーパークラスのコンストラクタを2回呼ぶとエラー
同じコンストラクタでも異なるコンストラクタでも2回呼んだらエラーです。
(Dartは名前付きコンストラクタを使って複数のコンストラクタが作れる)
class ClassA {
ClassA.first();
ClassA.second();
}
class ClassB extends ClassA {
ClassB()
: super.first(), // ここがダメ
super.second();// ここがダメ
}
##イニシャライザで同じプロパティを2回初期化するとエラー
class ClassA {
ClassA()
: x = 1,
x = 2;
int x;
}
まあ、そりゃそう、という感じ?
##引数のthis.x
という書き方はイニシャライザでの初期化と等価
class ClassA {
ClassA.first({int x}) : x = x; // この2つは等価
ClassA.second({this.x}); // この2つは等価
ClassA.third({this.x}) : x = 1; // これはエラー
int x;
}
ClassA.third
は、xを2回初期化してることになるのでエラーです。
##スーパークラスのコンストラクタを呼ぶまで、スーパークラスのプロパティにはアクセスできない
class ClassA {
int x;
}
class ClassB extends ClassA {
ClassB({this.x}); // これはエラー
}
このコード、一見すると、「ClassB
はClassA
のプロパティを引き継いでいるのだから、x
を初期化することもできるはずだ。」と思うかもしれませんが、
この記事で説明した実行順序を理解すると、エラーになる理由も理解できます。
ClassB
のイニシャライザを呼んだ時点では、まだClassA
のコンストラクタを呼んでいませんので、プロパティx
も存在しません。
なのでエラーになります。
##オーバーライドした引数もスーパークラスでの初期化の影響を受ける
最初の問題と似ていますが、これはどんな出力になるでしょうか?
今回は順番よりも最終的なClassB().x
の値が重要です。
// xに値を代入した時に、代入した値をコンソールに出力するためのもの。
int assign(int n) {
print(n);
return n;
}
// ClassBのインスタンスを作り、xというプロパティの値を出力します。
void main() {
print('the result is: ${ClassB().x}');
}
class ClassA {
ClassA() : x = assign(1) {
x = assign(2);
}
int x = assign(3);
}
class ClassB extends ClassA {
@override
int x = assign(4);
}
出力結果はこうなります。
4
3
1
2
the result is: 2
なんと2
なんですね。
プロパティx
は、ClassB
内で新しく作ったから、ClassA
から影響を受けない、なんてことはないんですね!
ClassA
の影響を、受けます!
ClassB
内でx
の最終的な初期値を指定しようと思ったら、次のように、コンストラクタの本文内で書くと良いでしょう。
// xに値を代入した時に、代入した値をコンソールに出力するためのもの。
int assign(int n) {
print(n);
return n;
}
// ClassBのインスタンスを作り、xというプロパティの値を出力します。
void main() {
print('the result is: ${ClassB().x}');
}
class ClassA {
ClassA() : x = assign(1) {
x = assign(2);
}
int x = assign(3);
}
class ClassB extends ClassA {
ClassB() {
x = assign(4);
}
}
3
1
2
4
the result is: 4
プロパティをオーバーライドする必要がなくなりましたね。
ただ、ClassA
のコンストラクタが初期化用の引数を受け取るケースの方が多いでしょうから、そういうときはClassBのイニシャライザ(の最後)でスーパークラスのコンストラクタを呼ぶ方が普通かもしれません。
class ClassA{
ClassA({this.x});
int x;
}
class ClassB extends ClassA{
ClassB(): super(x: 4);
}
```
#おわり
間違いなどありましたらコメントでご指摘お願いします!!!