Dart3で導入されたクラス修飾子の役割
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で導入されたクラス修飾子の役割がより理解できると思います。
Discussion