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:
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
// 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
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
// <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
// <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
// 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
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