Dart-Vererbung und Polymorphismus

Vererbung ist eines der drei Hauptmerkmale der objektorientierten Programmierung. Sie ermöglicht es, eine Klasse basierend auf einer anderen Klasse zu erweitern.

Dieses Kapitel stellt Darts Einfachvererbung mit extends, das Überschreiben von Methoden, das Schlüsselwort super und abstrakte Klassen vor.


extends Einfachvererbung

Dart unterstützt nur Einfachvererbung—jede Klasse kann nur eine direkte Elternklasse haben.

Doch die Vererbungskette kann lang sein: Eine Unterklasse erbt von einer Elternklasse, die Elternklasse erbt von einer Großelternklasse und so weiter.

Beispiel

// Elternklasse (Basisklasse, Oberklasse)
class Animal {
  String name;

  Animal(this.name);

  void eat() {
    print('$name isst');
  }

  void sleep() {
    print('$name schläft');
  }
}

// Unterklasse: verwendet extends, um Animal zu erben
class Dog extends Animal {
  String breed;  // Rasse

  // Unterklassenkonstruktor muss den Elternklassenkonstruktor aufrufen
  Dog(String name, this.breed) : super(name);

  // Von der Unterklasse neu hinzugefügte Methoden
  void bark() {
    print('$name ($breed) bellt!');
  }
}

void main() {
  var dog = Dog('Wangcai', 'Golden Retriever');

  // Eine von der Elternklasse geerbte Methode aufrufen
  dog.eat();
  dog.sleep();

  // Die eigene Methode der Unterklasse aufrufen
  dog.bark();

  // Ein Unterklassenobjekt ist auch vom Typ der Elternklasse
  Animal animal = dog;  // Dog ist ein Animal, daher kann es upgecastet werden
  animal.eat();
}
Wangcai is eating.
Wangcai is sleeping.
Wangcai (Golden Retriever) is barking!
Wangcai is eating.

Eine Unterklasse kann auf alle nicht-privaten Mitgliedsvariablen und Methoden in der Elternklasse zugreifen.

Private Mitglieder (beginnend mit einem Unterstrich) werden nicht vererbt.

Dart hat keine Schlüsselwörter public, protected oder private. Mitglieder, die mit einem Unterstrich (_) beginnen, sind bibliotheksprivat und es kann nur innerhalb derselben Datei auf sie zugegriffen werden. Dies unterscheidet sich von der Zugriffskontrolle in Java/C++.


Methodenüberschreibung @override

Eine Unterklasse kann die Methoden der Elternklasse überschreiben, um eine eigene Implementierung bereitzustellen.

Die Verwendung der @override-Annotation drückt die Absicht zum Überschreiben klar aus, und der Compiler hilft Ihnen zu prüfen, ob das Überschreiben korrekt ist.

Beispiel

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

  // Methode der Elternklasse
  void makeSound() {
    print('$name made some sounds');
  }

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

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

  // @override-Annotation: teilt dem Compiler mit, dass ich eine Methode der Elternklasse überschreibe
  @override
  void makeSound() {
    print('$name miaut~');
  }
}

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

  @override
  void makeSound() {
    print('$name quakt!');
  }
}

void main() {
  var cat = Cat('Xiaohua');
  var duck = Duck('Donald Duck');

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

  // Polymorphismus: Die gleiche Methode verhält sich bei verschiedenen Objekten unterschiedlich
  List<Animal> animals = [
    Cat('Mimi'),
    Duck('Yaya'),
    Cat('Qiuqiu'),
  ];

  print('--- BEISPIEL Zoo ---');
  for (var animal in animals) {
    animal.makeSound();  // Zur Laufzeit wird die Methode des tatsächlichen Typs aufgerufen
  }
}
小花 喵喵叫~
唐老鸭 嘎嘎叫!
--- EXAMPLE 动物园 ---
咪咪 喵喵叫~
鸭鸭 嘎嘎叫!
球球 喵喵叫~

Das obige Beispiel demonstriertPolymorphismus: Beim selben Methodenaufruf animal.makeSound() hängt das tatsächlich ausgeführte Verhalten vom realen Typ des Objekts ab.

Die Verwendung der @override-Annotation hat zwei Vorteile: Erstens wird die Absicht des Codes klarer; zweitens meldet der Compiler einen Fehler, wenn die Elternklasse keine Methode mit demselben Namen hat (z. B. ein Tippfehler). Es wird empfohlen, @override immer hinzuzufügen.


super-Schlüsselwort

super repräsentiert das Objekt der Elternklasse. Eine Unterklasse verwendet super, um den Konstruktor, die Methoden und die Eigenschaften der Elternklasse aufzurufen.

Beispiel

class Vehicle {
  String brand;
  int year;

  Vehicle(this.brand, this.year);

  void start() {
    print('$brand Fahrzeug gestartet');
  }

  void displayInfo() {
    print('Marke: $brand, Jahr: $year');
  }
}

class ElectricCar extends Vehicle {
  int batteryLevel;

  ElectricCar(String brand, int year, this.batteryLevel)
      : super(brand, year);  // Konstruktor der Elternklasse aufrufen

  @override
  void start() {
    // Eine Methode der Elternklasse aus einer Methode der Unterklasse aufrufen
    super.start();  // Zuerst die Startlogik der Elternklasse ausführen
    print('Batteriestand: $batteryLevel%');
    print('Elektromotor lautlos gestartet');
  }

  @override
  void displayInfo() {
    super.displayInfo();  // Zuerst die Basisinformationen der Elternklasse anzeigen
    print('Batteriestand: $batteryLevel%');  // Dann die spezifischen Informationen der Unterklasse ergänzen
  }
}

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

Zusammenfassung der Anwendungsszenarien von super

SzenarienSyntaxBeschreibung
Aufruf des Konstruktors der Elternklasse: super(args)Muss in der Initialisierungsliste aufgerufen werden
Aufrufen von Methoden der Elternklassesuper.methodName()Kann in überschriebenen Methoden in der Unterklasse aufgerufen werden
Zugriff auf Eigenschaften der Elternklassesuper.propertyNameZugriff auf Mitgliedsvariablen der Elternklasse

Abstrakte Klasse abstract

Eine abstrakte Klasse ist eine Klasse, die nicht direkt instanziiert werden kann; sie definiert nur die Schnittstellenspezifikation, während die konkrete Logik von Unterklassen implementiert wird.

Beispiel

// Das Schlüsselwort abstract deklariert eine abstrakte Klasse
abstract class Shape {
  // Abstrakte Methode: kein Methodenkörper, Unterklassen müssen sie implementieren
  double getArea();
  double getPerimeter();

  // Abstrakte Klassen können auch konkrete Methoden haben
  void describe() {
    print('Dies ist eine Form, Fläche: ${getArea()}, Umfang: ${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(); // Fehler! Abstrakte Klassen können nicht instanziiert werden

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

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

  // Verwendung einer abstrakten Klasse als Typ
  List<Shape> shapes = [
    Rechteck(3, 4),
    Circle(5),
    Rechteck(6, 2),
  ];

  print('--- BEISPIEL Shape-Statistiken ---');
  double totalArea = 0;
  for (var shape in shapes) {
    totalArea += shape.getArea();
  }
  print('Gesamtfläche: ${totalArea.toStringAsFixed(2)}');
}
这是一个形状,面积: 50.0, 周长: 30.0
这是一个形状,面积: 153.93791, 周长: 43.98226
--- EXAMPLE 形状统计 ---
总面积: 108.54

Der Kernwert einer abstrakten Klasse ist „einen Vertrag zu definieren“. Sie teilt allen Unterklassen mit: Du musst diese Methoden implementieren, sonst schlägt die Kompilierung fehl. Das macht die Zusammenarbeit im Team sicherer – Implementierer werden keine Methoden verpassen, und Aufrufer können sicher sein, dass die Methoden definitiv existieren.

Weitere Erweiterungen