Dart Generics

Generics ermöglichen es Ihnen, beim Schreiben von Code keine konkreten Typen angeben zu müssen, sondern Typparameter als Platzhalter zu verwenden und die tatsächlichen Typen erst bei der Verwendung zu bestimmen.

Der Kernvorteil von Generics istTypsicherheitundCode-Wiederverwendung—Ein und derselbe Code funktioniert mit mehreren Typen, während der Compiler Typfehler für Sie prüft.


Warum Generics benötigt werden

Schauen wir uns zunächst einen Schmerzpunkt ohne Generics an.

Beispiel

Schreiben ohne Generics:

// Ohne Generics: kann nur Object speichern und benötigt beim Abrufen manuelles Casting
class IntBox {
  int value;
  IntBox(this.value);
}

class StringBox {
  String value;
  StringBox(this.value);
}

// Jedes Mal, wenn ein neuer Typ hinzugefügt wird, muss eine neue Box-Klasse geschrieben werden, was zu einer Menge dupliziertem Code führt

void main() {
  var intBox = IntBox(42);
  var stringBox = StringBox('EXAMPLE');

  print('Integer: ${intBox.value}');
  print('String: ${stringBox.value}');
}

Gibt es eine Möglichkeit, eine Box-Klasse zu schreiben, die sowohl int als auch String aufnehmen kann und dabei dennoch Typsicherheit zur Kompilierzeit garantiert?

Das ist das Problem, das Generics lösen sollen.


Generische Klasse

Eine generische Klasse deklariert Typparameter nach dem Klassennamen mit spitzen Klammern <> und verwendet diesen Typparameter innerhalb der Klasse.

Beispiel

// <T> ist ein Typparameter; T ist ein Platzhalter, der "einen beliebigen Typ" repräsentiert
// Konventionelle Benennung: T (Typ), E (Element), K (Schlüssel), V (Wert)
class Box<T> {
  T value;

  Box(this.value);

  T getValue() {
    return value;
  }

  void setValue(T newValue) {
    value = newValue;
  }

  void printValue() {
    print('Wert in Box: $value (Typ: ${value.runtimeType})');
  }
}

void main() {
  // Geben Sie einen konkreten Typ an, wenn Sie eine generische Klasse verwenden
  var intBox = Box<int>(42);
  var stringBox = Box<String>('BEISPIEL Dart-Tutorial');
  var doubleBox = Box<double>(3.14);

  intBox.printValue();
  stringBox.printValue();
  doubleBox.printValue();

  // Typsicherheit: Der folgende Code verursacht einen Kompilierzeitfehler
  // intBox.setValue('hello'); // Fehler: String kann nicht einem int zugewiesen werden

  // Typinferenz: Dart kann Typen aus Konstruktorargumenten ableiten
  var autoBox = Box('Automatisch als String abgeleitet');
  print('Abgeleiteter Typ: ${autoBox.value.runtimeType}');
}
Box 中的值: 42 (类型: int)
Box 中的值: EXAMPLE Dart 教程 (类型: String)
Box 中的值: 3.14 (类型: double)
推断类型: String

Mehrere Typparameter

Eine generische Klasse kann mehrere Typparameter haben.

Beispiel

// K steht für den Typ der Schlüssel, V steht für den Typ der Werte
class Pair<K, V> {
  K key;
  V value;

  Pair⟦⟧this(key.this, value.value);

  @override
  String toString() => 'Pair($key: $value)';
}

void main() {
  var pair1 = Pair<String, int>('example', 10);
  var pair2 = Pair('score, 95.5);  // Typinferenz

  print(pair1);
  print(pair2);
}
Pair(example: 10)
Pair(score: 95.5)

Generische Methoden

Zusätzlich zu generischen Klassen können auch Funktionen und Methoden Generics verwenden.

Beispiel

// Generische Funktion: Rückgabetyp und Parametertyp stimmen überein
// <T> vor dem Rückgabetyp deklarieren
T firstElement<T>(List<T> list) {
  if (list.isEmpty) {
    throw ArgumentError('Die Liste darf nicht leer sein');
  }
  return list[0];
}

// Generische Funktion: zwei Werte vertauschen
void swap<T>(List<T> list, int i, int j) {
  T temp = list[i];
  list[i] = list[j];
  list[j] = temp;
}

void main() {
  // Dart leitet den Typ beim Aufruf automatisch ab
  var names = ['example', 'Dart', 'Flutter'];
  print('Erster Name: ${firstElement(names)}');

  var scores = [95, 88, 72];
  print('Erste Punktzahl: ${firstElement(scores)}');

  // Elemente tauschen
  var items = ['A', 'B', 'C'];
  print('Vor dem Tausch: $items');
  swap(items, 0, 2);
  print('Nach dem Tausch: $items');
}
第一个名字: example
第一个分数: 95
交换前: [A, B, C]
交换后: [C, B, A]

Typbeschränkungen extends

Manchmal möchte man einen Typparameter auf Untertypen bestimmter Typen beschränken.

Mit dem Schlüsselwort extends können Sie Beschränkungen für generische Parameter festlegen.

Beispiel

// Definiere einen numerischen Rechner
// <T extends num> beschränkt T auf num oder dessen Unterklassen (int, double)
class Calculator<T extends num> {
  T a;
  T b;

  Calculator(this.a, this.b);

  // Da T darauf beschränkt ist, num zu erweitern, können die Methoden von num sicher verwendet werden.
  double add() => (a + b).toDouble();
  double subtract() => (a - b).toDouble();
  double multiply() => (a * b).toDouble();
  double divide() => a / b;

  void printOperations() {
    print('BEISPIEL-Rechner: $a und $b');
    print(' Addieren: ${add()}');
    print(' Subtrahieren: ${subtract()}');
    print(' Multiplizieren: ${multiply()}');
    print(' Dividieren: ${divide().toStringAsFixed(2)}');
  }
}

void main() {
  var intCalc = Calculator<int>(10, 3);
  intCalc.printOperations();

  var doubleCalc = Calculator<double>(3.5, 1.5);
  doubleCalc.printOperations();

  // Die folgende Zeile meldet einen Fehler: String ist keine Unterklasse von num.
  // var strCalc = Calculator<String>('a', 'b'); // Fehler!
}
EXAMPLE 计算器: 10 和 3
  加: 13.0
  减: 7.0
  乘: 30.0
  除: 3.33
EXAMPLE 计算器: 3.5 和 1.5
  加: 5.0
  减: 2.0
  乘: 5.25
  除: 2.33

Typbeschränkungen ermöglichen es Ihnen, die Flexibilität von Generics beizubehalten und gleichzeitig Methoden des eingeschränkten Typs zu verwenden. Zum Beispiel können Sie mit der obigen Beschränkung T extends num Operatoren wie +, -, *, / im Methodenkörper verwenden, weil der Typ num diese Operationen unterstützt.


Generische Sammlungen verwenden

Darts Kern-Sammlungsklassen (List, Set, Map) sind alle generisch.

Die Verwendung generischer Sammlungen bietet Typprüfung zur Kompilierungszeit.

Beispiel

void main() {
  // Elementtyp als String angeben
  List<String> names = ['example', 'Dart', 'Flutter'];
  // names.add(42); // Fehler: int kann nicht zu List<String> hinzugefügt werden

  // Schlüssel-Wert-Typen angeben
  Map<String, int> scores = {
    'example': 95,
    'Alice': 87,
  };
  // scores['Bob'] = 'Excellent'; // Fehler: String kann nicht einem int zugewiesen werden

  // Elementtyp des Set angeben
  Set<double> prices = {9.99, 19.99, 29.99};

  // Funktionale Operationen auf generischen Sammlungen
  List<int> numbers = [1, 2, 3, 4, 5, 6];

  // Typsicheres Filtern und Mapping
  List<int> evenNumbers = numbers.where((n) => n % 2 == 0).toList();
  List<String> labels = numbers.map((n) => 'EXAMPLE-$n').toList();

  print(Gerade Zahlen: $evenNumbers);
  print(Beschriftungen: $labels);

  // Verwende fold für typsichere Akkumulation
  int sum = numbers.fold<int>(0, (prev, n) => prev + n);
  print(Summe: $sum);
}
偶数: [2, 4, 6]
标签: [EXAMPLE-1, EXAMPLE-2, EXAMPLE-3, EXAMPLE-4, EXAMPLE-5, EXAMPLE-6]
总和: 21

Praktische Anwendung: Generisches Repository-Muster

Eine der klassischsten Anwendungen von Generics in der realen Entwicklung ist das "Repository-Muster".

Beispiel

// Definiere ein generisches Repository-Interface
abstract class Repository<T> {
  void add(T item);
  void remove(T item);
  T? findById(String id);
  List<T> getAll();
}

// User-Modell
class User {
  String id;
  String name;

  User(this.id, this.name);

  @override
  String toString() => 'User($id, $name)';
}

// Product-Modell
class Product {
  String id;
  String name;
  double price;

  Product(this.id, this.name, this.price);

  @override
  String toString() => 'Product($name, ¥$price)';
}

// Konkrete Implementierung: User-Repository
class UserRepository implements Repository<User> {
  final List<User> _users = [];

  @override
  void add(User user) => _users.add(user);

  @override
  void remove(User user) => _users.remove(user);

  @override
  User? findById(String id) {
    try {
      return _users.firstWhere((u) => u.id == id);
    } catch (_) {
      return null;
    }
  }

  @override
  List<User> getAll() => List.unmodifiable(_users);
}

// Konkrete Implementierung: Produkt-Repository (gleiche Schnittstelle, unterschiedliche Typen)
class ProductRepository implements Repository<Product> {
  final List<Product> _products = [];

  @override
  void add(Product product) => _products.add(product);

  @override
  void remove(Product product) => _products.remove(product);

  @override
  Product? findById(String id) {
    try {
      return _products.firstWhere((p) => p.id == id);
    } catch (_) {
      return null;
    }
  }

  @override
  List<Product> getAll() => List.unmodifiable(_products);
}

void main() {
  var userRepo = UserRepository();
  userRepo.add(User('u1', 'example'));
  userRepo.add(User('u2', 'Alice'));

  var productRepo = ProductRepository();
  productRepo.add(Product('p1', Dart-Tutorial, 29.99));
  productRepo.add(Product('p2', Flutter-Tutorial, 49.99));

  print(Benutzerliste: ${userRepo.getAll()});
  print(Produktliste: ${productRepo.getAll()});
  print(Benutzer u1 finden: ${userRepo.findById(u1')}');
  print(Produkt p2 finden: ${productRepo.findById(p2')}');
}
用户列表: [User(u1, example), User(u2, Alice)]
商品列表: [Product(Dart 教程, ¥29.99), Product(Flutter 教程, ¥49.99)]
查找用户 u1: User(u1, example)
查找商品 p2: Product(Flutter 教程, ¥49.99)

Das generische Repository-Muster ermöglicht es Ihnen, verschiedene Datentypen über eine einheitliche Schnittstelle zu verwalten, wodurch Typsicherheit gewährleistet und doppelter Code vermieden wird.

Weitere Erweiterungen