【入門編】Dartの型システムにおける共変性(Covariance)と反変性(Contravariance)の理解 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!FlutterやDartを使った開発を楽しんでいますか?

今回は、Dartの型システムにおける「共変性(Covariance)」と「反変性(Contravariance)」という、一見すると難解な言葉の裏にある本質についてお話ししますね。

「なんだか名前が難しそう……」と感じるかもしれませんが、ここをクリアすると、Dartのジェネリクスや関数型プログラミングで遭遇する「なぜか代入できない」「予期せぬ型エラーが出る」という謎がスッキリと解けるようになりますよ。

それでは、Dart VMの型チェックの裏側も少し覗きながら、一緒に本質をマスターしていきましょう!

—

1. 型の代入可能性(Subtyping)の基本イメージ

まずは、オブジェクト指向の基本である「スーパサブの関係(継承関係)」をおさらいしておきましょう。

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

ここでは `Dog` は `Animal` の一種(サブタイプ)ですよね。
変数への代入で考えると、これは直感的で分かりやすいはずです。

Animal myAnimal = Dog(); // OK: DogはAnimalの一種だから代入できる
// Dog myDog = Animal(); // ❌ NG: すべてのAnimalがDogとは限らないからエラー

この「より具体的な型(Dog)を、より抽象的な型(Animal)の場所に代入できる性質」を、型の世界では 共変(Covariant) と呼びます。

—

2. ジェネリクスにおける「共変性」の罠

さて、ここからが本題です。クラスやリストなどのジェネリクス(型引数を持つ構造)にこれを適用したとき、Dartの型システムはどう振る舞うでしょうか?

頭の体操として、次のコードを見てください。あなたならどう感じるでしょうか。

void main() {
List myDogs = [Dog(), Dog()];

// これって、コンパイルエラーになると思いますか?それとも通ると思いますか?
// List は List として扱えるでしょうか?
// List animals = myDogs; // ⚠️ さて、どうなるでしょう?
}

実は、Dart(および多くのモダンな言語)において、`List` を `List` の変数にそのまま代入することは、厳密な型安全性の観点からは危険を伴います。なぜなら、もしそれが許されると、次のような事故が起きるからです。

List myDogs = [Dog()];
List animals = myDogs; // 仮にこれが許されたとすると…

animals.add(Cat()); // List なので Cat を追加できるはず!
Dog myDog = myDogs[0]; // でも、中身は Cat が混ざっているので実行時エラー(TypeError)に!

Dartのスマートな解決策:リストは共変、でも…?

Javaなどの古い言語では、配列が完全に共変だったために上記のような実行時バグが多発しました。
しかし、Dartは安全です。Dartの `List` は実行時・コンパイル時にどのように扱われるでしょうか。

実は、Dartのジェネリクスはデフォルトでは不変(Invariant)、あるいは文脈によって挙動が変わりますが、メソッドの引数や戻り値の型において「共変性」がどのように作用するかを理解することが、Dartを極めるカギになります。

—

3. 関数型における「共変性」と「反変性」

クラスだけでなく、関数を引数に取る高階関数などを書くときにも、この概念は顔を出します。

関数の引数と戻り値における型のルールのイメージは、以下のようになります。

  • 戻り値の型は「共変(Covariant)」
  • `Animal` を返すはずの場所に、より具体的な `Dog` を返す関数を代入しても安全です(使う側から見れば `Animal` が返ってくることが保証されているため)。
  • 引数の型は「反変(Contravariance)」
  • ここが一番ハマりやすいポイントです!「`Dog` を受け取る関数」を期待されている場所に、「なんでも(`Animal`)受け取れる関数」を渡すのは安全ですが、その逆は危険です。

具体例で見てみましょう

void makeDogRun(void Function(Dog) dogAction) {
dogAction(Dog());
}

void handleAnimal(Animal animal) {
print(‘Handling animal’);
}

void main() {
// Dog を期待する場所に、Animal を受け取れる関数を渡す
// (反変のルールにより、これは安全かつ有効です)
makeDogRun(handleAnimal);
}

「`Dog` しか処理できないと聞いていたのに、どんな `Animal` でも処理できる優しい関数(`handleAnimal`)が来ちゃった!それなら `Dog` を渡されても余裕で処理できるね!」

これが、引数における反変性(Contravariance)の直感的なイメージです。逆にすると、「`Dog`専用の特殊な操作をする関数」のところに、何が来るか分からない一般の `Animal` を突っ込むことになるため、コンパイルエラーになります。

—

4. 現場で役立つ!`covariant` キーワードの活用法

Dartでクラスを継承してメソッドをオーバーライドする際、「親クラスでは抽象的な型を受け取っていたけれど、子クラスではより具体的な型で受け取りたい!」というケースによく遭遇します。

そんなとき、Dartの厳格な型チェッカーが「シグネチャが一致しない」と怒ることがあります。これを解決するのが `covariant` キーワードです。

class AnimalHouse {
void admit(Animal animal) {
print(‘Animal admitted’);
}
}

class DogHouse extends AnimalHouse {
// 親クラスの引数は Animal だが、DogHouse なので Dog に特化させたい!
@override
void admit(covariant Dog dog) { // ← covariant を付与する
print(‘Dog admitted: $dog’);
}
}

void main() {
AnimalHouse house = DogHouse();

// 多態性を通じた呼び出し
// コンパイル時には AnimalHouse として扱われているため Animal を渡せるが、
// 実際の中身は DogHouse なので Dog が渡されることを期待している。
house.admit(Cat()); // ⚠️ 実行時エラーになる可能性があるため注意が必要です!
}

💡 シニアからのアドバイス

`covariant` キーワードは非常に強力ですが、「実行時型安全性の保証をプログラマの責任に委ねる」という側面を持っています。
むやみやたらに使うと、予期せぬ `TypeError` を引き起こす原因になります。「本当にこの型に絞り込む必要があるか?」をアーキテクチャの視点から一度立ち止まって考えるようにしてくださいね。

—

まとめ

ここまでのポイントをギュッと凝縮して振り返ってみましょう!

1. 共変(Covariance):

  • 「~の一種である」という関係をそのまま維持する性質(例:戻り値の型)。

2. 反変(Contravariance):

  • 型の階層が逆転する性質(例:関数の引数の型)。

3. Dartにおける実践:

  • 安全な設計を心がけつつ、どうしても必要な場面では `covariant` キーワードを使ってコンパイラに意図を伝える。

型システムは、私たち開発者を縛る窮屈な檻ではなく、「未来のバグからコードを守ってくれる最強の守護神」です。この共変性と反変性の仕組みを頭の片隅に置いておくだけで、複雑なジェネリクスやコールバック地獄に直面したときも、迷わずエレガントなコードが書けるようになりますよ。

ここをクリアできれば、あなたのDartの基礎力はもうバッチリマスターできています!明日からのコーディングに、ぜひこの知見を活かしてみてくださいね。

タイトルとURLをコピーしました