okuzawatsの日記

モバイルアプリケーション開発の沼💀

[Dart] 基本的な文法

書いている人

モバイルアプリケーションアーキテクトとして働いています。モバイルアプリケーションのアーキテクチャ、自動テスト、CI/CDに興味があります。


Dartの基本的な文法についてまとめます。

コンストラクタ

Dartにおけるコンストラクタの最も原始的な構文は以下のように書けます。コンストラクタの引数としてString型のパラメータnameを受け取り、コンストラクタの本体の中でメンバ変数に代入しています。

class Dog {
  String name;

  Dog(String name) {
    this.name = name;
  }
}

以下の構文では、コンストラクタの引数として受け取ったパラメータを直接メンバ変数に代入しています。この構文では、メンバ変数をfinalにすることができます。

class Dog {
  final String name;

  Dog(this.name);
}

上記の構文を用いた場合にも、コンストラクタ本体を定義することができます。

class Dog {
  final String name;

  Dog(this.name) {
    print(name);
  }
}

Dog dog = Dog('pochi'); // => pochi

また、this.nameの部分を{}で囲うと、コンストラクタ呼び出しを名前付き引数で行うことができます。

class Dog {
  final String name;

  Dog({this.name});
}

Dog dog = Dog(name: 'pochi');

Dartでは、コンストラクタのオーバーロードができません。その代わりに、Dartでは名前付きコンストラクタを定義することができます。

class Dog {
  String name;

  Dog(String name) {
    this.name = name;
  }

  Dog.noName() {
    name = 'no name';
  }
}

Dog noName = Dog.noName();
print(noName.name); // => no name

なお、上記の例において、プライマリコンストラクタではthisを用いてメンバ変数にアクセスし、名前付きコンストラクタではthisを用いずにメンバ変数にアクセスしている理由は、前者では名前の隠蔽が発生し、後者では発生しないためです。

アクセス修飾子

Dartにはアクセス修飾子がありません。その代わり、メンバの名前で可視性を与えることができます。メンバの名前が_から始まるものは、いわゆるprivateな可視性となります。それ以外の名前は、いわゆるpublicな可視性となります。

例えば、以下のクラスのメンバ変数_nameはprivateなメンバ変数となり、同一ライブラリ内からのみアクセス可能です(Dart - Using Access Modifiers (Private & Public) - Woolha)。

// cat.dart
class Cat {
  String _name;

  Cat(String name) {
    _name = name;
  }
}

同一ライブラリ内からのみアクセス可能と言うのは、例えば以下のように同一ファイル内にクラスを定義した場合は、他クラスのprivateなメンバにアクセスすることができます。

// cat.dart
class Cat {
  String _name;
  String _keeper;

  Cat(String name) {
    _name = name;

    Human human = Human();
    _keeper = human._name;
  }
}

class Human {
  String _name = 'human';
}

可視性の範囲外からは、privateなメンバにはアクセスできません。

// main.dart
void main(List<String> arguments) {
  Cat cat = Cat('tama');

  //cat._name; // 不可
}

privateなメソッドも、メソッド名を_から始めて定義します。

class Cat {
  // 略
  void _toilet() {
    // do something here
  }
}

setterは隠蔽し、getterだけを公開したい時は、以下のように別途getterを定義します。

class Cat {
  String _name;
  String get name => _name;
  // 略
}

拡張メソッド(後述)もprivateにすることが可能です。privateな拡張メソッドは、拡張メソッドを使いたい場所を限定したい時に使います。

extension CatExtension on Cat {
  Dog _toDog() {
    return Poodle(name: this.name);
  }
}

継承

Dartでは、extendsキーワードを用いてクラスの継承を行うことができます。単一継承のみ可能で、多重継承はできません。

以下の例では、抽象クラスDogを継承して、Poodleクラスを実装しています。抽象クラスを定義するためのキーワードはabstractです。

abstract class Dog {
  final String name;

  Dog({this.name});
}

class Poodle extends Dog {
  Poodle({name}) : super(name: name);
}

Dog poodle = Poodle(name: 'pochi');

抽象クラスでは、抽象メソッドを定義することができます。抽象メソッドは、メソッドのシグネチャのみ定義し、メソッド本体を持たないメソッドです。具象クラスの中では定義できません。また、抽象メソッドは、具象クラスの中で必ずオーバーライドしなければなりません。メソッドをオーバーライドするには、対象となるメソッドに@overrideアノテーションを付与します。

abstract class Dog {
  // 略
  void bark(); // 抽象メソッド
}

class Poodle extends Dog {
  // 略
  @override
  void bark() {
    print('bow wow!');
  }
}

また抽象クラスにおいては、メソッドの実装を持つことも可能です。この場合、具象クラスにおいてメソッドをオーバーライドする必要は必ずしもありませんが、オーバーライドすることも可能です。この時、必要に応じてsuper.メソッド名()で継承元のメソッドを呼び出すことが可能です。

abstract class Dog {
  // 略
  void run() {
    print('running...');
  }
}

class Poodle extends Dog {
  // 略
  @override
  void run() {
    super.run();
    print('it is a poodle...');
  }
}

メソッドだけでなく、メンバ変数についてもオーバーライドすることが可能です。なお、メンバ変数を抽象的に定義する方法はわかりませんでした。Dart 2.8.2では実装されていないものと思われます。

abstract class Dog {
  // 略
  final String size = 'medium';
}

class Poodle extends Dog {
  // 略
  @override
  final String size = 'large';
}

クラスやメソッドのオーバーライドを禁止する機能についても、おそらくDart 2.8.2では実装されていないです。

拡張メソッド

Dart 2.7以降、Dartでは型に拡張メソッドを定義して、機能を追加することができます。構文は以下です(Extension methods | Dart)。

extension <extension name> on <type> {
  (<member definition>)*
}

以下の例ではint型に拡張メソッドとしてtoTenTimes()を定義しています。

extension IntExtension on int {
  int toTenTimes() {
    return this * 10;
  }
}

int型に対して、あたかもint型に元からtoTenTimes()というメソッドが定義されていたかのようにtoTenTimes()を呼び出すことが可能になります。ただし、インポートは必要です。拡張メソッドの中では、thisで対象のクラスのインスタンスにアクセスすることができます。

int answer = 42.toTenTimes();
print(answer); // => 420

拡張について名前を定義することができます。上の例では、IntExtensionという名前で拡張をしています。拡張に名前を定義している理由は、「APIがコンフリクトした時に備えて拡張は名前を持っている(extensions have names, which can be helpful if an API conflict arises)Extension methods | Dart)」とのことです。

ラムダ

Dartでは以下の形式でラムダを書くことができます。

(int i) {
  return i + 1;
};

変数succに上記のラムダをバインドします。

int Function(int) succ = (int i) {
  return i + 1;
};

バインドした変数を経由してラムダを実行します。

print(succ(0)); // => 1
print(succ(100)); // => 101

以下の形式は上記の形式の糖衣構文です。

int Function(int) succ2 = (int i) => i + 1;

print(succ2(0)); // => 1
print(succ2(100)); // => 101

ラムダを渡して実行してみます。

int calculate(int Function(int) func, int input) {
  return func.call(input);
}

print(calculate(succ, 42)); // => 43

当然、名前をつけないラムダを渡すこともできます。

print(calculate((int i) => i + 1 , 42)); // => 43

identicalを用いた参照の等価性の評価

identicalを用いて、参照の等価性を評価することができます。identicalはdart:coreに含まれています。

bool identical(
  Object a,
  Object b
)

以下の例では、2つの変数の参照が等しいため、identicalはtrueを返します。

Dog dog = Dog(name: 'pochi');

bool result = identical(dog, dog);
print(result); // => true

以下の例では、2つの変数の参照が異なるため、identicalはfalseを返します。

Dog dog = Dog(name: 'pochi');
Cat cat = Cat(name: 'tama');

bool result = identical(dog, cat);
print(result); // => false

変数が異なっても、参照が等価であればidenticalはtrueを返します。

Dog dog1 = Dog(name: 'pochi');
Dog dog2 = dog1;

bool result = identical(dog1, dog2);
print(result); // => true

identicalが返すbool値は、==の結果とは異なります(以下の例では==オペレーターをoverrideしてnameが等しければ==がtrueを返すようにしています)。

Dog dog1 = Dog(name: 'pochi');
Dog dog2 = Dog(name: 'pochi');

bool result = identical(dog1, dog2);
print(result); // => false
print(dog1 == dog2); // => true