Dartのクラス修飾子まとめ

はじめに

普段abstractやmixinは使いますが、Dart3から登場した修飾子は使ったことがありません。むしろあまり知らないのでこれを気に調べてみました。

Dartの公式ドキュメントをまとめた記事です。

abstract

abstract class(抽象クラス)は具体的な振る舞い(実装)を必要としない場合に使用する。 またabstract classは抽象メソッドを定義することがほどんです。
そのためabstract classはインスタンス化できず、継承する前提のものです。ですのでインタフェースとして使うことが多いです。

abstract class Animal {
  void run();
}

// Error
final animal = Animal();

// OK
class Lion extends Animal {
  @override
  void run() {}
}

// OK
class Lion implements Animal {
  @override
  void run() {}
}

また変数を持たせたいなどの場合は下記です。

abstract class Vehicle {
  Vehicle(this.passenger);

  final int passenger;

  void run();
}

class Car extends Vehicle {
  Car(this.passenger) : super(passenger * 3);

  final int passenger;
}

abstract classをインスタンス化できるように見せたい場合はfactory constructorを使用すれば良いとのことです。

base

classやmixinの実装を継承するために base を使う。 base classはそれ自身のライブラリ以外での実装を禁止する。
実装されたprivate、メンバーは全てサブタイプに定義される。 base クラスを実装または拡張するクラスは、 base , final , sealed のいずれか。これは基底クラスを保証するため。

base class Animal {
  const Animal(this.age);
  
  final int age;
  
  void run() {
    print('run');
  }

  void _bark() {
    print('bark');
  }
}

// OK
final animal = Animal(3);

// OK
final Lion extends Animal {}

// NG
final Lion implements Animal {}

final Lion extends Animal {
  const Lion(super.age);

  void execute() {
    run();
    _bark();
  }
}

final lion = Lion(3);
// run, barkと出力される
lion.execute();

interface

インターフェースを使用したい場合に interface を使用する。

interface class Animal {
  void run() {
    print('run');
  }
}
// NG
final animal = Animal();

// NG
class Cat extends Animal {
  // ...
}

// OK
class Cat implements Animal {
  @override
  void run() {}
}

abstract interface

interface修飾子の最も一般的な使い方は、純粋なインターフェイスを定義すること。interface修飾子とabstract修飾子を組み合わせると、 abstract interface class になります。

interfaceクラスのように、他のライブラリーは純粋なinterfaceを実装することはできますが、継承することはできません。またabstractクラスと同様に、純粋インターフェースも抽象メンバを持つことができます。

final

現在のライブラリの外部にあるクラスからサブタイプにされるのを防ぐ。(Swiftのfinalに似ていますかね?) . 拡張されることがないので、安心して実装ができますね。

但し同じライブラリ内で拡張したり実装したりすることができるらしいです。
final修飾子はbaseの効果を包含するため、サブクラスもbase、final、sealedのいずれかをつけなければならない。

final class Animal {
  void run() {
    print('run');
  }
}
// OK
final animal = Animal();

// NG
class Cat extends Animal {
  // ...
}

// NG
class Cat implements Animal {
  @override
  void run() {}
}

sealed

既知の、列挙可能なサブタイプのセットを作るには、sealed修飾子を使う。 (Kotlinのsealedと似ていますかね?)
sealed修飾子は、クラスが自身のライブラリの外で拡張されたり実装されたりするのを防ぎます。シールドされたクラスは暗黙的に抽象化されます。

コンパイラーは、同じライブラリ内にしか存在しない直接のサブタイプが存在する可能性を認識します。このため、 switch がそのケースで考えられるすべてのサブタイプを網羅的に処理していない場合、コンパイラーは警告を出すことができます

sealed class Animal {}

class Lion extends Animal {}

class Cat implements Animal {}

class Dog extends Animal {}

// NG
final animal = Animal();

// OK
Animal lion = Lion();

String bark (Animal animal) {
  switch (animal) {
    Lion() => '',
    Cat() => '',
    Dog() => '',
  }
}

最後に

いかがでしたでしょうか?
正直 interface , abstract interface あたりがよくわかっていません。
修飾子は組み合わせて使用することも可能のようです。

https://dart.dev/language/modifier-reference#valid-combinations

今回は全ての修飾子を記載しましたが、次からは1つ1つの使い所を紹介できてらと思います。