2020/1/6 Dart クラス(class)>>constructor

Dartの全てのオブジェクトはクラスのインスタンスであり、全てのクラスはObjectクラスから派生します。

Objectクラスを除く全てのクラスが一つのスーパークラスを持ちます(多重継承はできない。)が、それにも関わらず、クラスのボディは複数のクラス階層の中で再利用できます。これが「Mixinベースの継承」です。

  • クラスを修正する。
  • サブクラスを定義する。

上記のようなこと無しにクラスに機能を追加する方法にExtension methodがあります。

クラスの定義

基本

sample1-1

class SomeClass{
     //もっともシンプルなクラス
}

classキーワード、スペース、クラス名、中括弧と記述し、中括弧の中(ブロック内)にクラスの中身を実装します。



インスタンス変数を定義する

sample1-2

class Point {
  num x; // インスタンス変数xを宣言。初期値はnull
  num y; // インスタンス変数yを宣言。初期値はnull
  num z = 0; // インスタンス変数zを宣言。初期値は0
}

sample1-2ではx,y,zの三つのインスタンス変数(プロパティ)を宣言しています。インスタンス変数を初期化しないと、自動的にnullがセットされます。なので、sample1-2ではxとyは初期値nullになります。


sample1-3

class Point {
  num x;
  num y;
}

void main() {
  var point = Point();
  point.x = 4; // セッターを使ってxに4をセット。
  assert(point.x == 4); // ゲッターを使ってxの値を取得。
  assert(point.y == null); // yは初期化されておらず、値のセットもないのでnull
}

全てのインスタンス変数は自動的にゲッターメソッドが用意されます。

定数でない変数(non-final instance variable)は自動的にゲッターとセッターが用意されます。

sample1-3のPointクラスでは明示的にゲッター・セッターを定義していませんが、普通に値の取得(9,10行目)や値のセット(8行目)ができています。

インスタンス変数の宣言時に初期化すれば、インスタンス生成時(つまりコンストラクタやイニシャライザリストの実行前)に初期値がセットされます。


ゲッターとセッター(Getters and setters)

ゲッターとセッターは、オブジェクトのプロパティに対するアクセス(取得と代入)機能を提供する特別なメソッドです。

ゲッター・セッターの定義方法

sample1-4-1

 class Cont{
   String word;
   Cont(this.word);
   //↓ゲッターの定義
   String get greet{
     return "Hello,${word.toUpperCase()}. How do you do?";
   }
   //↓セッターの定義
   set greet(String str){
     word=str.toLowerCase();;
   }
 }

sample1-4ではクラスContの中にgreetという名のゲッター・セッターを定義しています。ゲッター・セッターの本体が一文の場合以下のようにアロー構文を用いて記述できます。

sample1-4-2

 class Cont{
   String word;
   Cont(this.word);
   /*
   //↓ゲッターの定義
   String get greet{
     return "Hello,${word.toUpperCase()}. How do you do?";
   }
   //↓セッターの定義
   set greet(String str){
     word=str;
   }
   */
   //↓ゲッター・セッター内が一文の場合アロー構文が使える。
   String get greet=>"Hello,${word.toUpperCase()}. How do you do?";
   set greet(String str) => word=str.toLowerCase();;
 }

 

sample1-5

void main(){
  var cont1=Cont("aaAaa");
  var cont2=Cont("bBBbb");
  var cont3=Cont("ccCCC");
  var cont4=Cont("dDDDd");
  var cont5=Cont("EEeee");
  
  print(cont1.word);
  print(cont2.word);
  print(cont3.word);
  print(cont4.word);
  print(cont5.word);
  print("********************");
  
  //ゲッターがない場合このように書かなければいけないところを、、、
  print("Hello,${cont1.word.toUpperCase()}. How are you?");
  print("Hello,${cont2.word.toUpperCase()}. How are you?");
  print("Hello,${cont3.word.toUpperCase()}. How are you?");
  print("Hello,${cont4.word.toUpperCase()}. How are you?");
  print("Hello,${cont5.word.toUpperCase()}. How are you?");
  print("********************");
  
  //ゲッターを用意すれば以下のような記述で済む
  print(cont1.greet);
  print(cont2.greet);
  print(cont3.greet);
  print(cont4.greet);
  print(cont5.greet);
  
  cont5.greet="testTESTteST";
  print(cont5.word);
  print(cont5.greet);
}
   
 class Cont{
   String word;
   Cont(this.word);
   /*
   //↓ゲッターの定義
   String get greet{
     return "Hello,${word.toUpperCase()}. How do you do?";
   }
   //↓セッターの定義
   set greet(String str){
     word=str.toLowerCase();
   }
   */
   //↓ゲッター・セッター内が一文の場合アロー構文が使える。
   String get greet=>"Hello,${word.toUpperCase()}. How do you do?";
   set greet(String str) => word=str.toLowerCase();
 }
aaAaa
bBBbb
ccCCC
dDDDd
EEeee
********************
Hello,AAAAA. How are you?
Hello,BBBBB. How are you?
Hello,CCCCC. How are you?
Hello,DDDDD. How are you?
Hello,EEEEE. How are you?
********************
Hello,AAAAA. How do you do?
Hello,BBBBB. How do you do?
Hello,CCCCC. How do you do?
Hello,DDDDD. How do you do?
Hello,EEEEE. How do you do?
testtesttest
Hello,TESTTESTTEST. How do you do?

 


コンストラクタ(Constructors)

コンストラクタの宣言は、基本的にクラス名と同名のコンストラクタ名で宣言します。(Named Constructorを除く)

もっとも一般的なコンストラクタはgenerative constructor(生成的コンストラクタ)で、新しいインスタンスを生成します。

sample2-1

class Point {
  num x, y;
  //↓生成的コンストラクタの定義
  Point(num x, num y) {
    // There's a better way to do this, stay tuned.
    this.x = x;
    this.y = y;
  }
}

thisはその時々のインスタンスを指します。

sample2-2

class Point {
  num x, y;

  Point(this.x, this.y);
}

簡易的な記述法としてsample2-2のような定義も可能です。


デフォルトコンストラクタ(Default Constructors)

コンストラクターを宣言しない場合、デフォルトのコンストラクターが提供されます。デフォルトのコンストラクターは引数がなく、スーパークラスの引数なしのコンストラクターを呼び出します。

class Person{
  String fN="default str";
  
  Person(){
    print("Person コンストラクタ");
  }
}

class Employee extends Person{
  //サブクラスでコンストラクタを定義していない。
  //↓
  //デフォルトコンストラクタが自動的に用意される。
  //↓
  //スーパークラス(Person)の引数無しコンストラクタを呼び出す。
}

void main(){

  Employee e1=Employee();
  print(e1.fN);
}

/*
Person コンストラクタ
default str
*/

 

 


コンストラクターは継承されません

サブクラスは、スーパークラスからコンストラクターを継承しません。コンストラクターを宣言しないサブクラスには、デフォルト(引数なし、名前なし)コンストラクターのみがあります。


名前付きコンストラクタ(Named Constructor)

名前付きコンストラクタを使うと複数のコンストラクタを用意でき、名前があることでよりわかりやすいコードを書くことができます。

sample2-3

void main(){
  var point0=Point.origin();//名前付きコンストラクタによるインスタンス生成
  print(point0.x);
  print(point0.y);
  var point1=Point(5,10);//生成的インストラクタによるインスタンス生成
  print(point1.x);
  print(point1.y);
}

class Point {
  num x, y;
  // ↓生成的コンストラクタの定義
  Point(this.x, this.y);

  // ↓名前付きコンストラクタの定義
  Point.origin() {
    x = 0;
    y = 0;
  }
}

//0
//0
//5
//10

「コンストラクタは継承されない」ということを思い出してください。つまりスーパークラスのnamed constructorはサブクラスに継承されない、ということです。スーパークラスで定義されているnamed constructorをサブクラスにも欲しい、という場合は、それをサブクラスで定義する必要があります。

class Person{
  String fN="default str";

  Person.named(){
    print("Person コンストラクタ(named)");
  }
  
}

class Employee extends Person{
  //サブクラスでコンストラクタを定義していない。
  //↓
  //デフォルトコンストラクタが自動的に用意される。
  //↓
  //スーパークラス(Person)の引数無しコンストラクタを呼び出す。
}

void main(){

  Employee e1=Employee();
  print(e1.fN);
}

//こうすると
//The superclass 'Person' doesn't have a zero argument constructor - line 10
//のエラーが発生。
//親クラスのコンストラクタは「引数無し(名前有り)」だが、エラーが出る。
//親クラスが「引数無し・名前無し」コンストラクタを持っている場合で尚且つ
//子クラスがコンストラクタを宣言しない場合に、
//子クラスにデフォルトコンストラクタ自動生成→親クラスの「引数無し(名前無し)」コンストラクタ
//を呼び出す、ということだと思われる。

 


Invoking a non-default superclass constructor

デフォルトでは、サブクラスのコンストラクタは「スーパークラスの名前無し・引数無しコンストラクタ」を呼び出します。スーパークラスのコンストラクタは(サブクラスの)コンストラクタのボディのはじめに呼び出されます。イニシャライザリストが使用されている場合、スーパークラスコンストラクタが実行される前にイニシャライザリストが実行されます。まとめると、実行の順序は以下の通りです。

  1. initializer list
  2. superclass’s no-arg constructor
  3. main class’s no-arg constructor

もしスーパークラスが「名前無し・引数無しコンストラクタ」を持っていない場合、手動でスーパークラスのコンストラクタの一つを呼び出す必要があります。コロン( : )の後ろに、コンストラクタボディの前に、スーパークラスコンストラクタを指定します。

class Person{
  String fN="default str";

  Person.named(){
    print("Person コンストラクタ(named)");
  }
  
}

class Employee extends Person{
  Employee():super.named(){
    
  }
  //サブクラスでコンストラクタを定義していない。
  //↓
  //デフォルトコンストラクタが自動的に用意される。
  //↓
  //スーパークラス(Person)の引数無しコンストラクタを呼び出す。
}

void main(){

  Employee e1=Employee();
  print(e1.fN);
}

/*
Person コンストラクタ(named)
default str
*/

//こうするとエラー出なくなる。
//上記の説明の通りだが、
//「スーパークラスが「名前無し・引数無しコンストラクタ」を持っていない場合、
//手動でスーパークラスのコンストラクタの一つを呼び出す必要がある。」
//その呼び出す方法は、「サブクラスコンストラクタのボディの前、コロンの後ろで呼び出す。」
//そのためには必然的にサブクラス(Employee)にコンストラクタを宣言する必要がある、
//ということだと思われる。
//つまり、スーパークラスが「名前無し・引数無しコンストラクタ」を持っていない場合は
//サブクラスはコンストラクタを宣言しないといけない(そしてそのはじめで
//スーパークラスのコンストラクタを手動で呼び出さないといけない)、ということ。

まとめる(場合分け)

  • スーパークラスに「名前無し引数無しコンストラクタ」がある場合

–>サブクラスのコンストラクタが無くてもOK

class Person {
  String firstName;

  Person(){
    print("Person コンストラクタ");
  }
  
  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  // Person does not have a default constructor;
  // you must call super.fromJson(data).
  /*
  Employee(){
    print("Employee コンストラクタ");
  }
  */
  /*
  Employee.fromJson(Map data) : super.fromJson(data) {
    print('in Employee');
  }
  */
}

main() {
  var emp = new Employee();
}
/*
Person コンストラクタ
*/

 


–>サブクラスのコンストラクタがある(名前無し引数無し)場合、スーパークラスのコンストラクタを手動で呼び出す必要はない(自動的に呼び出される)。

class Person {
  String firstName;

  Person(){
    print("Person コンストラクタ");
  }
  
  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  // Person does not have a default constructor;
  // you must call super.fromJson(data).
  
  Employee(){
    print("Employee コンストラクタ");
  }
  
  /*
  Employee.fromJson(Map data) : super.fromJson(data) {
    print('in Employee');
  }
  */
}

main() {
  var emp = new Employee();
}

/*
Person コンストラクタ
Employee コンストラクタ
*/

–>サブクラスのコンストラクタがある(名前有り引数無し)場合、スーパークラスのコンストラクタを手動で呼び出す必要はない(自動的に呼び出される)。

class Person {
  String firstName;

  Person(){
    print("Person コンストラクタ");
  }
  
  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  // Person does not have a default constructor;
  // you must call super.fromJson(data).
  /*
  Employee(){
    print("Employee コンストラクタ");
  }
  */
  
  Employee.fromJson(Map data){
    print('in Employee');
  }
  
}

main() {
  var emp = new Employee.fromJson(Map());
}

/*
Person コンストラクタ
in Employee
*/

  • スーパークラスに「名前無し引数無しコンストラクタ」が無い場合

–>サブクラスにコンストラクタ無いとエラー。

–>サブクラスにコンストラクタ(名前無し引数無し)がある場合、手動でスーパークラスのコンストラクタを一つ呼び出す必要あり。

class Person {
  String firstName;
/*
  Person(){
    print("Person コンストラクタ");
  }
  */
  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  // Person does not have a default constructor;
  // you must call super.fromJson(data).
  
  Employee():super.fromJson(Map()){
    print("Employee コンストラクタ");
  }
  
  /*
  Employee.fromJson(Map data){
    print('in Employee');
  }
  */
}

main() {
  var emp = new Employee();
}
/*
in Person
Employee コンストラクタ
*/

–>サブクラスにコンストラクタ(名前有り引数有り)がある場合、手動でスーパークラスのコンストラクタを一つ呼び出す必要あり。

上記のようにすればサブクラスのコンストラクタで受け取ったjson(map)を、スーパークラスのコンストラクタでフィールドにセットできた。


スーパークラスコンストラクタへ渡す引数はコンストラクタが実行される前に評価されるので、引数には関数呼び出しのような式を渡すこともできます。

class Employee extends Person {
  Employee() : super.fromJson(defaultData);
  // ···
}

warning:スーパークラスコンストラクタへの引数はthisにアクセスできません。例えば、引数はstatic methodを呼び出せますが、インスタンスメソッドを呼び出せません。


Initializer list

スーパークラスコンストラクタの呼び出しの近くで、(サブクラスの)コンストラクタのボディが実行される前にインスタンス変数を初期化することができます。イニシャライザはカンマ( , )で区切ります。

Point.fromJson(Map<String, double> json)
    : x = json['x'],            //←この部分が
      y = json['y'] {         //←イニシャライザリスト
  print('In Point.fromJson(): ($x, $y)');
}

 

warning : イニシャライザの右側でthisにアクセスすることはできません。


イニシャライザリストはfinalのフィールドを設定する時に便利です。次の例ではイニシャライザリストにより3つのfinalのフィールドを初期化します。

import 'dart:math';

class Point {
  final num x;
  final num y;
  final num distanceFromOrigin;

  Point(x, y)
      : x = x,
        y = y,
        distanceFromOrigin = sqrt(x * x + y * y);
}

main() {
  var p = new Point(2, 3);
  print(p.distanceFromOrigin);
}
/*
3.605551275463989
*/

Redirecting constructors

同じクラスの中で、他のコンストラクタにリダイレクトするためだけの目的のコンストラクタを用意することがあります。リダイレクトコンストラクタのボディは空です。コロン( : )の後ろにリダイレクトしたいコンストラクタの呼び出しを記述します。

class Point {
  double x, y;

  // The main constructor for this class.
  Point(this.x, this.y);

  // ↓メインのコンストラクタへリダイレクトするためのリダイレクトコンストラクタ
  Point.alongXAxis(double x) : this(x, 0);
}

Constant constructors

あるクラスが生成するインスタンスが不変であることがわかっている場合、そのインスタンスをコンパイル時定数として生成することができます。それをするためには、constantコンストラクタを定義し、全てのインスタンス変数をfinalにします。

class ImmutablePoint {
  static final ImmutablePoint origin =
      const ImmutablePoint(0, 0);

  final double x, y;  //←インスタンス変数をfinalにする

  const ImmutablePoint(this.x, this.y);  //←constコンストラクタを定義する
}

コンスタントコンストラクタは常に定数を生成する訳ではありません。詳細に関してはusing constructorのセクションをご覧ください。

↑const constructorしか定義されていないクラスのconst constructorをconstキーワード無しで呼び出した場合、コンスタントでないインスタンス(コンパイル時定数でないインスタンス)が生成されることを言っていると思われる。


Factory constructors

常に新しいインスタンスを生成する訳では無いコンストラクタを実装する時factoryキーワードを使用します。例えば、factoryコンストラクタはcacheからインスタンスを返すかもしれません。あるいは、サブタイプからインスタンスを返すかもしれません。factoryコンストラクタの別のユースケースとしては、イニシャライザリストのなかで扱えないロジックを使ってfinalな定数を初期化する場合があります。

下記のサンプルはfactoryコンストラクタであるLoggerは、chcheからオブジェクトを返します。factoryコンストラクタであるLogger.fromJsonはJSONオブジェクトからfinalな定数を初期化します。

class Logger {
  String name;
  bool mute = false;

  // _cache is library-private, thanks to
  // the _ in front of its name.
  static final Map<String, Logger> _cache =
      <String, Logger>{};

  
  
  factory Logger(String name) {
    return _cache.putIfAbsent(
        name, () => Logger._internal(name));
  }

  
  
  factory Logger.fromJson(Map<String, Object> json) {
    return Logger(json['name'].toString());
  }

  
  
  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}

void main(){
 
  Logger l1=Logger('first');  //←factoryコンストラクタ使用
  print(l1.name);
  Logger l2=Logger('second');  //←factoryコンストラクタ使用
  print(l2.name);
  Logger l3=Logger('first');  //←factoryコンストラクタ使用
  print(identical(l1,l3));  //←identical関数は、l1とl3が同一インスタンスの場合trueを返す。
  l3.name='third';
  print(l1.name);
}

/*
first
second
true
third
*/

Note:Factoryコンストラクタはthisにアクセスできません。

Loggerクラスの_cacheフィールドはstaticフィールドなので、全てのLoggerインスタンスに共通のフィールド。

factoryコンストラクタLogger( String : name )が実行されると、

–>引数のnameに対応するLoggerインスタンスが既に_cacheフィールドに存在する場合、そのインスタンスを返す。

–>引数のnameに対応するLoggerインスタンスが_chcheフィールドに存在しない場合、Logger._internal(name)を呼び出し、新たなインスタンスを生成。そして_cacheに追加する。

詳しくはputIfAbsent

 

 

 

参考

https://dart.dev/guides/language/language-tour#classes

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

 

コメントを残す

メールアドレスが公開されることはありません。