Dart 継承とポリモーフィズム

継承はオブジェクト指向プログラミングの三大特性の一つであり、あるクラスが別のクラスに基づいて拡張できるようにします。

この章では、Dart の extends 単一継承、メソッドオーバーライド、super キーワード、抽象クラスについて説明します。


extends 単一継承

Dart は単一継承のみをサポートしています——各クラスは直接の親クラスを1つだけ持つことができます。

ただし、継承チェーンは非常に長くできます。子クラスが親クラスを継承し、親クラスがさらに祖父クラスを継承する、というように階層的に伝わります。

例

// 親クラス(基底クラス、スーパークラス)
class Animal {
  String name;

  Animal(this.name);

  void eat() {
    print('$name は食べている');
  }

  void sleep() {
    print('$name は寝ている');
  }
}

// 子クラス:extends で Animal を継承
class Dog extends Animal {
  String breed;  // 品種

  // 子クラスのコンストラクタは親クラスのコンストラクタを呼び出す必要がある
  Dog(String name, this.breed) : super(name);

  // 子クラスで追加したメソッド
  void bark() {
    print('$name($breed)がワンワン吠えている!');
  }
}

void main() {
  var dog = Dog('ポチ', 'ゴールデンレトリバー');

  // 親クラスから継承したメソッドを呼び出す
  dog.eat();
  dog.sleep();

  // 子クラス自身のメソッドを呼び出す
  dog.bark();

  // 子クラスのオブジェクトは親クラスの型でもある
  Animal animal = dog;  // Dog は Animal なので、アップキャストできる
  animal.eat();
}
旺财はご飯を食べている。

子クラスは親クラスのすべての非プライベートなメンバ変数とメソッドにアクセスできます。

プライベートメンバ(アンダースコアで始まる)は継承されません。

Dart には public、protected、private キーワードはありません。アンダースコア(_)で始まるメンバはライブラリプライベート(library-private)であり、同じファイル内でのみアクセスできます。これは Java/C++ のアクセス制御とは異なります。


メソッドオーバーライド @override

子クラスは親クラスのメソッドをオーバーライド(override)して、独自の実装を提供できます。

@override アノテーションを使用してオーバーライドの意図を明確にし、コンパイラが正しくオーバーライドされているかをチェックしてくれます。

例

class Animal {
  String name;
  Animal(this.name);

  // 親クラスのメソッド
  void makeSound() {
    print('$name が何かの音を発した');
  }

  @override
  String toString() {
    return '動物: $name';
  }
}

class Cat extends Animal {
  Cat(String name) : super(name);

  // @override アノテーション:コンパイラに親クラスのメソッドをオーバーライドしていることを伝える
  @override
  void makeSound() {
    print('$name がニャーニャー鳴いている~');
  }
}

class Duck extends Animal {
  Duck(String name) : super(name);

  @override
  void makeSound() {
    print('$name がガーガー鳴いている!');
  }
}

void main() {
  var cat = Cat('ハナ');
  var duck = Duck('ドナルドダック');

  cat.makeSound();
  duck.makeSound();

  // ポリモーフィズム:同じメソッドでも、オブジェクトによって動作が異なる
  List<Animal> animals = [
    Cat('ミミ'),
    Duck('ダック'),
    Cat('マル'),
  ];

  print('--- サンプル 動物園 ---');
  for (var animal in animals) {
    animal.makeSound();  // 実行時には実際の型のメソッドが呼び出される
  }
}
小花 喵喵叫~
唐老鸭 嘎嘎叫!
--- サンプル 动物园 ---
咪咪 喵喵叫~
鸭鸭 嘎嘎叫!
球球 喵喵叫~

上記の例は次のことを示しています。ポリモーフィズム(Polymorphism):同じメソッド animal.makeSound() の呼び出しでも、実際に実行される動作はオブジェクトの実際の型によって決まります。

@override アノテーションを使用する利点は2つあります。1つはコードの意図が明確になること、もう1つは親クラスに同名のメソッドがない場合(例えばスペルミス)にコンパイラがエラーで知らせてくれることです。常に @override を付けることをお勧めします。


super キーワード

super は親クラスのオブジェクトを表し、子クラスは super を使って親クラスのコンストラクタ、メソッド、プロパティを呼び出します。

例

class Vehicle {
  String brand;
  int year;

  Vehicle(this.brand, this.year);

  void start() {
    print('$brand の車両が起動しました');
  }

  void displayInfo() {
    print('ブランド: $brand, 年式: $year');
  }
}

class ElectricCar extends Vehicle {
  int batteryLevel;

  ElectricCar(String brand, int year, this.batteryLevel)
      : super(brand, year);  // 親クラスのコンストラクタを呼び出す

  @override
  void start() {
    // 子クラスのメソッド内で親クラスのメソッドを呼び出す
    super.start();  // 先に親クラスの起動ロジックを実行する
    print('バッテリー残量: $batteryLevel%');
    print('電気モーターが静かに起動しました');
  }

  @override
  void displayInfo() {
    super.displayInfo();  // 先に親クラスの基本情報を表示する
    print('バッテリー残量: $batteryLevel%');  // その後に子クラス特有の情報を追加する
  }
}

void main() {
  var car = ElectricCar('サンプル EV', 2026, 85);
  car.start();
  print('---');
  car.displayInfo();
}
サンプル EV 车辆启动了
电池电量: 85%
电动机无声启动完成
---
品牌: サンプル EV, 年份: 2026
电池电量: 85%

super の使用場面のまとめ

場面構文説明
親クラスのコンストラクタを呼び出す: super(args)初期化リスト内で呼び出す必要がある
親クラスのメソッドを呼び出すsuper.methodName()子クラスのオーバーライドメソッド内で呼び出せる
親クラスのプロパティにアクセスするsuper.propertyName親クラスのメンバ変数にアクセスする

抽象クラス abstract

抽象クラスは直接インスタンス化できないクラスであり、インターフェースの仕様を定義するだけで、具体的なロジックは子クラスが実装します。

例

// abstract キーワードで抽象クラスを宣言
abstract class Shape {
  // 抽象メソッド:メソッド本体がなく、子クラスが実装する必要がある
  double getArea();
  double getPerimeter();

  // 抽象クラス内にも具体的なメソッドを定義できる
  void describe() {
    print('これは図形です、面積: ${getArea()}、周囲の長さ: ${getPerimeter()}');
  }
}

class Rectangle extends Shape {
  double width;
  double height;

  Rectangle(this.width, this.height);

  @override
  double getArea() => width * height;

  @override
  double getPerimeter() => 2 * (width + height);
}

class Circle extends Shape {
  double radius;

  Circle(this.radius);

  @override
  double getArea() => 3.14159 * radius * radius;

  @override
  double getPerimeter() => 2 * 3.14159 * radius;
}

void main() {
  // Shape shape = Shape(); // エラー!抽象クラスはインスタンス化できない

  var rect = Rectangle(10, 5);
  var circle = Circle(7);

  rect.describe();
  circle.describe();

  // 抽象クラスを型として使用
  List<Shape> shapes = [
    Rectangle(3, 4),
    Circle(5),
    Rectangle(6, 2),
  ];

  print('--- サンプル 形状統計 ---');
  double totalArea = 0;
  for (var shape in shapes) {
    totalArea += shape.getArea();
  }
  print('総面積: ${totalArea.toStringAsFixed(2)}');
}
这是一个形状,面积: 50.0, 周长: 30.0
这是一个形状,面积: 153.93791, 周长: 43.98226
--- サンプル 形状统计 ---
总面积: 108.54

抽象クラスの核心的な価値は「契約を定義すること」です。これはすべての子クラスに「これらのメソッドを実装しなければならない。そうでなければコンパイルは通りません」と伝えます。これにより、チームでの協力がより安全になります。実装者がメソッドを漏らすことがなく、呼び出し側はメソッドが必ず存在すると確信できます。

その他の拡張