Dart3で導入されたクラス修飾子の役割

2023/12/08に公開

YUMEMI Flutter Advent Calendar 2023の8日目の記事です。

Dart3でいくつかのクラス修飾子が追加されました。
この記事では、Dartのクラス修飾子の役割についてまとめます。

mixin class

mixin classはmixinでありclassであるという特殊なクラスです。interfaceやbaseといった修飾子はclassの継承などの制限を行うものですが、mixin classはmixinとclassの2つの役割を持たせるためのものです。

Dart3以前はmixinでなくてもclassをwithで利用することでmixinとして利用可能でした。mixinとして利用されているclassを変更すると、そのclassをwithしているclassに思わぬ影響が発生することがありました。

mixin classはmixinとして利用されることを前提としているため、mixin classを変更してもwithしているclassに影響を与えることはありません。

mixinとして利用しているクラスに影響を与えないためにmixin classは以下の特徴があります。

Object以外のclassを継承することができない

mixin class A extends Foo {} // NG

mixin class A extends Object {} // OK
mixin class B extends A {} // NG

もしmixin classがObject以外のclassを継承することができると、親クラスの変更がmixin classをwithしているclassに意図せず影響を与える可能性があります。

factoryではないconstructorを持つことができない

mixin class A {
  A(this.text); // NG
  A(); // OK フィールドを初期化しないconstructorを持つ
  factory A.fromText(String text) { // OK
    final a = A();
    a.text = text;
    return a;
  }
  
  String text = '';
}

これはconstructorでフィールドを変更すると、mixin classをwithしているclassに意図せず影響を与える可能性があるためです。

interface class

interface classはlibraryの外部に対して純粋なインターフェースを宣言するための修飾子です。

以前はabstract classを利用してインターフェースを宣言していましたが、library外のクラスがextendsを利用して継承すると実装まで継承することになり、純粋なインターフェースとして公開することができませんでした。

interface classは以下の特徴があります。

library外でextends句で使用できない

library foo;
interface class A {
  void foo() {}
}
import 'foo.dart';
class B extends A {}  // NG
class B implements A {  // OK
  
  void foo() {}
}

interface classの実装をlibrary外のクラスで利用できず、interfaceのみを利用できます。

interface classのメソッドは実装を持つ

interfaceという名前から、interface classは実装を持たないと思われがちですが、実装を持つ必要があります。実装を持たなくて良いようにするにはabstractを利用します。

abstract interface class A {
  void foo();
}

base class

base classはinterface classとは逆にinterfaceの利用を禁止するための修飾子です。

もし以下のような実装が行われた場合、コンパイルエラーにはならないのですが、実行時にエラーが発生します。

library foo;
class A {
  void _privateFoo() {}
}

void foo(A a) {
  a._privateFoo();
}
import 'foo.dart';
class B implements A {}

void main() {
  foo(B()); // NoSuchMethodError (NoSuchMethodError: Class 'B' has no instance method '_privateFoo'. Receiver: Instance of 'B' Tried calling: _privateFoo())
}

これはAのinterfaceのみをBが利用して、プライベートメソッドの_privateFooを持っていないため、NoSuchMethodErrorが発生します。これを防ぐためにbase classを利用します。

base classは以下の特徴があります。

library外でimplements句で使用できない

library foo;
base class A {
  void foo() {}
}
import 'foo.dart';
class B implements A {}  // NG
final class B extends A {}  // OK
base class C extends A {}  // OK
sealed class D extends A {}  // OK

base classを継承するクラスはfinalかbase、もしくはsealedでなければなりません。これはbase classを継承したクラスをimplements句で利用すると、base classのinterfaceを利用することになり、base classのinterfaceはlibrary外で利用できないためです。

final class

final classはlibrary外での継承を禁止するための修飾子です。library内では子クラスにbase/final/sealedを付与することで継承が可能なので、swiftのfinal classとは異なります。

library foo;
final class A {}
final class B extends A {}  // OK
base class C extends A {}  // OK
sealed class D extends A {}  // OK
import 'foo.dart';
class E extends A {}  // NG

sealed class

sealed classはパターンマッチで網羅性チェックを行うための修飾子です。

library foo;
sealed class A {}
class B extends A {}
class C extends A {}

final string = switch (a) {
  B _ => 'B',
  C _ => 'C',
}

sealed classはlibrary外ではextends/implementsができません。ただし、sealed classのサブクラス(上記のBやC)はlibrary外でextends/implementsができま
す。

sealed classは以下の特徴があります。

複数の階層構造を作れる

sealed classは複数の階層構造を作ることができます。以下の例ではsealed class AのサブクラスとしてBとCがあり、sealed class CのサブクラスとしてDがあります。

library foo;
sealed class A {}
class B extends A {}
sealed class C extends A {}
class D extends C {}

まとめ

interfaceやbase、finalはlibraryの作成や利用に役に立つ修飾子です。多くの方はFlutterやサードパーティライブラリの利用時に知識が役に立つと思います。

一方mixinやsealedはFlutterアプリの実装に直接役に立つ修飾子です。使いこなすことでより良いコードを書くことができるようになると思います。

実際に利用してみると、Dart3で導入されたクラス修飾子の役割がより理解できると思います。

参考

Class modifiers for API maintainers

株式会社ゆめみ

Discussion