Dart Generika

Wenn Sie die API-Dokumentation des grundlegenden Array-Typs List einsehen, stellen Sie fest, dass sein eigentlicher Typ List<E> ist. Spitzklammern wie <...> kennzeichnen einen generischen Typ (parametrisierter Typ) – also einen Typ mit formalen Typ Parametern. Der Konvention nach werden nahezu alle Typvariablen mit einem einzelnen Buchstaben benannt, zum Beispiel E, T, S, K, V.

Warum Generika verwenden?

  • Der zentrale Vorteil von Generika ist die Gewährleistung von Typsicherheit
  • Korrekt spezifizierte Generika erzeugen optimierten Laufzeitcode;
  • Generika reduzieren Codeduplizierung drastisch.

Wenn eine Liste ausschließlich Zeichenketten speichern soll, deklarieren Sie sie als List<String> – dies ist eine Zeichenkettenliste.

Sobald eine Liste als zeichenkettenbasiert definiert ist, löst das Einfügen von Werten anderer Typen einen Fehler aus.

// Statische Typüberprüfung gibt Fehler aus
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Kompilierfehler: 42 ist keine ZeichenketteCode-Sprache: PHP (php)
D:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:5:12: Error: The argument type 'int' can't be assigned to the parameter type 'String'.
        names.add(42); // Kompilierfehler: 42 ist keine ZeichenketteCode-Sprache: PHP (php)

Beseitigung von Code-Redundanz

Angenommen, Sie benötigen eine abstrakte Schnittstelle für einen Objekt-Cache:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}
Code-Sprache: JavaScript (javascript)

Später, falls Sie einen Cache brauchen, der nur Zeichenketten speichert, müssen Sie eine separate Schnittstelle erstellen:

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}
Code-Sprache: Dart (dart)

Anschließend benötigen Sie spezielle Caches für Zahlen… Sie müssen nahezu identischen Code immer wieder schreiben.

Beispielsweise würden Sie folgenden Code verfassen:

abstract class IntCache {
  int getByKey(String key);
  void setByKey(String key, int value);
}Code-Sprache: Dart (dart)

Generika lösen dieses Problem mit einer einzigen Definition einer universellen Schnittstelle mit Typ-Parametern:

abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}Code-Sprache: Dart (dart)

Hier fungiert T als Typ-Platzhalter für beliebige konkrete Typen, die später vom Entwickler übergeben werden.

Praktisches Beispiel

// Generische abstrakte Cache-Basisklasse
abstract class Cache<T> {
  T? getByKey(String key);
  void setByKey(String key, T value);
  void remove(String key);
}

// Speichercache-Implementierung (basiert auf Map)
class MemoryCache<T> implements Cache<T> {
  final Map<String, T> _storage = {};

  @override
  T? getByKey(String key) {
    return _storage[key];
  }

  @Override
  void setByKey(String key, T value) {
    _storage[key] = value;
  }

  @Override
  void remove(String key) {
    _storage.remove(key);
  }
}

void main() {
  // 1. Beispiel Zeichenketten-Cache
  final Cache<String> strCache = MemoryCache<String>();
  strCache.setByKey("name", "Tom");
  print(strCache.getByKey("name")); // Tom

  // 2. Ganzzahl-Int-Cache
  final Cache<int> intCache = MemoryCache<int>();
  intCache.setByKey("age", 26);
  print(intCache.getByKey("age")); // 26

  // 3. Cache für benutzerdefinierte Objekte
  final Cache<User> userCache = MemoryCache<User>();
  userCache.setByKey("u1", User("Jack", 18));
  final User? user = userCache.getByKey("u1");
  print(user?.name); // Jack

  // Löschtest
  userCache.remove("u1");
  print(userCache.getByKey("u1")); // null
}

// Benutzerdefinierte Entitätsklasse
class User {
  final String name;
  final int age;
  User(this.name, this.age);
}Code-Sprache: Dart (dart)

Verwendung generischer Collection-Literale

List-, Set- und Map-Literale unterstützen alle parametrisierte Generika:

  • List / Set: Schreiben Sie <Typ> vor die öffnende eckige Klammer
  • Map: Schreiben Sie <Schlüsseltyp, Werttyp> vor die öffnende geschweifte Klammer

Beispiele:

// Zeichenkettenliste
var names = <String>['Seth', 'Kathy', 'Lars'];

// Zeichenkettenmenge (automatische Duplikatentfernung)
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};

// Wörterbuch mit Zeichenkettenschlüsseln und -werten
var pages = <String, String>{
  'index.html': 'Startseite',
  'robots.txt': 'Anweisungsdatei für Webcrawler',
  'humans.txt': 'Dokumentationsdatei für menschliche Entwickler',
};
Code-Sprache: PHP (php)

Ausführen des Beispielcodes

void main() {
  // 1. List<String> Zeichenkettenliste: geordnet, Duplikate erlaubt, zugreifbar über Index
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  print("=== List Liste ===");
  print(names); // [Seth, Kathy, Lars, Seth] Duplikate zulässig
  print(names[0]); // Erstes Element per Index abrufen: Seth
  names.add("Mike"); // Neues Element am Ende anhängen
  print("Nach Hinzufügen: $names");

  // 2. Set<String> Zeichenkettenmenge: ungeordnet, automatische Duplikatentfernung, kein Index
  var uniqueNames = <String>{'Seth', 'Kathy', 'Lars', 'Seth'};
  print("\n=== Set Menge (automatische Duplikatentfernung) ===");
  print(uniqueNames); // {Seth, Kathy, Lars} Doppeltes Seth wird automatisch gelöscht
  print(uniqueNames.contains("Kathy")); // Existenzprüfung: true
  uniqueNames.remove("Lars");
  print("Nach Löschen von Lars: $uniqueNames");

  // 3. Map<String, String> Schlüssel-Wert-Wörterbuch: eindeutige Schlüssel, Wertabruf per Schlüssel
  var pages = <String, String>{
    'index.html': 'Startseite',
    'robots.txt': 'Anweisungsdatei für Webcrawler',
    'humans.txt': 'Dokumentationsdatei für menschliche Entwickler',
  };
  print("\n=== Map Wörterbuch ===");
  print(pages);
  print(pages['index.html']); // Wert per Schlüssel abrufen: Startseite
  pages["about.html"] = "Über-Seite"; // Neues Schlüssel-Wert-Paar hinzufügen
  print("Nach Hinzufügen von about.html: $pages");

  // Häufige Iterationsbeispiele
  print("\n=== Iteriere über List ===");
  for (var name in names) {
    print("- $name");
  }

  print("\n=== Iteriere über alle Map-Einträge ===");
  for (var entry in pages.entries) {
    print("Datei: ${entry.key} → Beschreibung: ${entry.value}");
  }
}Code-Sprache: Dart (dart)

Generische Parameter in Konstruktoren

Beim Aufruf eines Konstruktors geben Sie den generischen Typ in Spitzklammern hinter dem Klassennamen an:

var nameSet = Set<String>.of(names);Code-Sprache: JavaScript (javascript)

Der folgende Code erstellt ein Wörterbuch mit int-Schlüsseln und Werten des Typs View:

var views = SplayTreeMap<int, View>();
Code-Sprache: HTML, XML (xml)
import 'dart:collection';

void main() {
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  var nameSet = Set<String>.of(names);
  print("Ursprüngliche Liste: $names");
  print("Duplikatfreies Set: $nameSet");

  // SplayTreeMap erfordert Import von dart:collection
  var views = SplayTreeMap<int, String>();
  views[5] = "Einstellungsseite";
  views[2] = "Startseite";
  views[9] = "Meine Seite";
  views[1] = "Splashscreen";

  print("\nSortierte TreeMap:");
  for (var e in views.entries) {
    print("${e.key} : ${e.value}");
  }
}Code-Sprache: PHP (php)

Laufzeittypen generischer Collections

Typ-Reifikation (reified)

Darts Generika sind reifiziert: Sämtliche generische Typinformationen bleiben zur Laufzeit erhalten.

Sie können den konkreten generischen Typ einer Collection zur Laufzeit direkt prüfen:

void main() {
    var names = <String>[];
	names.addAll(['Seth', 'Kathy', 'Lars']);
	print(names is List<String>); // Gibt true aus
}Code-Sprache: PHP (php)

Zusätzlicher Vergleich: Java nutzt Typ-Löschung (Type Erasure) für Generika; generische Parameter werden zur Laufzeit entfernt. In Java lässt sich nur prüfen, ob ein Objekt eine List ist – man kann nicht zwischen List<String> und List<int> unterscheiden.

* Einschränkung generischer Parametertypen

Bound (Typgrenze). Wenn Sie Klassen noch nicht gelernt haben, reicht ein grundlegendes Verständnis dieses Abschnitts aus.

Bei der Definition von Generika können Sie einschränken, dass übergebene Typen Unterklassen einer bestimmten Oberklasse sein müssen. Diese Einschränkung heißt Typgrenze und wird mit dem Schlüsselwort extends realisiert.

1 Einschränkung auf nicht-nullbare Typen

Erzwingen, dass das Generikum nur nicht-nullbare Typen akzeptiert (Standard-Obergrenze ist der nullbare Typ Object?):

class Foo<T extends Object> {
  // Der übergebene Typ T darf nicht nullbar sein
}
Code-Sprache: JavaScript (javascript)

2 Begrenzung auf Unterklassen einer festgelegten Oberklasse

Geben Sie mit extends eine Basisklasse an, dann können Sie alle Member-Methoden dieser Basisklasse direkt aufrufen:

class Foo<T extends SomeBaseClass> {
  String toString() => "Instanz Foo<$T>";
}

class Extender extends SomeBaseClass {}
Code-Sprache: JavaScript (javascript)

Gültige Typargumente: die Oberklasse selbst, beliebige ihrer Unterklassen

var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();
Code-Sprache: HTML, XML (xml)

Wird der generische Parameter weggelassen, wird standardmäßig der Grenztyp verwendet:

var foo = Foo();
print(foo); // Ausgabe: Instanz Foo<SomeBaseClass>
Code-Sprache: PHP (php)

Die Übergabe eines unzusammenhängenden Typs löst einen statischen Kompilierfehler aus:

var foo = Foo<Object>(); // Kompilierfehler: Object ist keine Unterklasse von SomeBaseClass
Code-Sprache: JavaScript (javascript)

* Selbstreferenzierende Typgrenzen (F-bound generische Typen)

Bei der Einschränkung von Generika kann die Grenze auf ihren eigenen Typparameter verweisen und eine Selbstbeschränkung bilden – dies wird F-bound genannt.

Standardbeispiel: Vergleichs-Schnittstelle Comparable<T>

abstract interface class Comparable<T> {
  int compareTo(T o);
}

// T muss Comparable<T> implementieren und kann nur mit Objekten desselben Typs verglichen werden
int compareAndOffset<T extends Comparable<T>>(T t1, T t2) =>
    t1.compareTo(t2) + 1;

class A implements Comparable<A> {
  @override
  int compareTo(A other) {
    // Vergleichslogik implementieren
    return 0;
  }
}

int res = compareAndOffset(A(), A());
Code-Sprache: PHP (php)

Die Einschränkung T extends Comparable<T> bedeutet: T kann nur mit Instanzen des identischen Typs verglichen werden.

* Generische Methoden

Normale Funktionen und Klassenmethoden unterstützen ebenfalls generische Typ-Parameter:

T first<T>(List<T> ts) {
  // Vorprüfungslogik
  T tmp = ts[0];
  // Nachverarbeitungslogik
  return tmp;
}
Code-Sprache: PHP (php)

Vollständiges Beispiel

T first<T>(List<T> ts) {
  if (ts.isEmpty) {
    throw ArgumentError("Die Liste darf nicht leer sein");
  }
  T tmp = ts[0];
  return tmp;
}

void main() {
  List<String> strList = ["apple", "banana", "orange"];
  String firstStr = first(strList);
  print(firstStr); //apple

  List<int> numList = [10, 20, 30];
  int firstNum = first(numList);
  print(firstNum);//10
}Code-Sprache: Dart (dart)

Der generische Parameter <T> wirkt an drei Stellen:

  1. Rückgabetyp der Funktion T
  2. Typ des Eingabeparameters List<T>
  3. Typ der lokalen Variable T tmp

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert