Help us understand the problem. What is going on with this article?

【Dart】クイズ!extendsしたクラスのコンストラクタ実行順序!

問題

問題です。

次のコードの出力を予想してください。

// 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}); // これはエラー
}

このコード、一見すると、「ClassBClassAのプロパティを引き継いでいるのだから、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);
}

出力結果はこうなります。

答え
6
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);
}

おわり

間違いなどありましたらコメントでご指摘お願いします!!!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした