Génériques Dart

Si vous consultez la documentation API du type tableau de base List, vous découvrirez que son vrai type est List<E>. Les chevrons <...> marquent un type générique (type paramétré), c’est-à-dire un type doté de paramètres de type formels. Par convention, la grande majorité des variables de type portent un nom composé d’une seule lettre, par exemple E, T, S, K, V.

Pourquoi utiliser les génériques ?

  • Le rôle essentiel des génériques est de garantir la sécurité des types
  • Spécifier correctement les génériques permet de produire un code d’exécution plus performant ;
  • Les génériques réduisent considérablement la duplication de code.

Si vous voulez qu’une liste ne contienne que des chaînes de caractères, déclarez-la sous la forme List<String> : c’est une liste de chaînes.

Une fois définie comme liste exclusivement textuelle, toute tentative d’y insérer une valeur d’un type autre qu’une chaîne constitue une erreur.

// Erreur de vérification statique des types
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Erreur de compilation : 42 n’est pas une chaîne de caractèresLangage du code : 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); // Erreur de compilation : 42 n’est pas une chaîne de caractèresLangage du code : PHP (php)

Réduire la redondance du code

Imaginons que vous ayez besoin d’une interface abstraite pour un cache d’objets :

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}
Langage du code : JavaScript (javascript)

Plus tard, si vous avez besoin d’un cache ne stockant que des chaînes, vous devrez créer une nouvelle interface :

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}
Langage du code : Dart (dart)

Vous aurez ensuite besoin d’un cache dédié aux nombres… et vous devrez écrire sans cesse du code quasi identique.

Par exemple, vous écririez ceci :

abstract class IntCache {
  int getByKey(String key);
  void setByKey(String key, int value);
}Langage du code : Dart (dart)

Les génériques résolvent ce problème en une seule fois : il suffit de définir une interface générique avec paramètre de type :

abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}Langage du code : Dart (dart)

Ici, T est un type espace réservé, qui représente n’importe quel type concret fourni ultérieurement par le développeur.

Exemple concret

// Classe de base abstraite de cache générique
abstract class Cache<T> {
  T? getByKey(String key);
  void setByKey(String key, T value);
  void remove(String key);
}

// Implémentation du cache mémoire (basée sur 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. Exemple de cache de chaînes
  final Cache<String> strCache = MemoryCache<String>();
  strCache.setByKey("name", "Tom");
  print(strCache.getByKey("name")); // Tom

  // 2. Cache d’entiers int
  final Cache<int> intCache = MemoryCache<int>();
  intCache.setByKey("age", 26);
  print(intCache.getByKey("age")); // 26

  // 3. Cache d’objets personnalisés
  final Cache<User> userCache = MemoryCache<User>();
  userCache.setByKey("u1", User("Jack", 18));
  final User? user = userCache.getByKey("u1");
  print(user?.name); // Jack

  // Test de suppression
  userCache.remove("u1");
  print(userCache.getByKey("u1")); // null
}

// Classe d’entité personnalisée
class User {
  final String name;
  final int age;
  User(this.name, this.age);
}Langage du code : Dart (dart)

Utiliser les littéraux de collection génériques

Les littéraux List, Set et Map prennent tous en charge les génériques paramétrés :

  • List / Set : écrivez <Type> avant le crochet ouvrant
  • Map : écrivez <TypeClé, TypeValeur> avant l’accolade ouvrante

Exemples :

// Liste de chaînes
var names = <String>['Seth', 'Kathy', 'Lars'];

// Ensemble de chaînes (supprime les doublons)
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};

// Dictionnaire avec clés et valeurs de type chaîne
var pages = <String, String>{
  'index.html': 'Page d’accueil',
  'robots.txt': 'Fichier d’instruction pour robots d’indexation',
  'humans.txt': 'Fichier explicatif destiné aux développeurs humains',
};
Langage du code : PHP (php)

Exécuter l’exemple

void main() {
  // 1. List<String> Liste de chaînes : ordonnée, autorise les doublons, accessible par indice
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  print("=== Liste List ===");
  print(names); // [Seth, Kathy, Lars, Seth] doublons autorisés
  print(names[0]); // Récupérer le premier élément par indice : Seth
  names.add("Mike"); // Ajouter un élément à la fin
  print("Après ajout : $names");

  // 2. Set<String> Ensemble de chaînes : non ordonné, supprime automatiquement les doublons, pas d’indice
  var uniqueNames = <String>{'Seth', 'Kathy', 'Lars', 'Seth'};
  print("\n=== Ensemble Set (suppression auto des doublons) ===");
  print(uniqueNames); // {Seth, Kathy, Lars} le doublon Seth est supprimé
  print(uniqueNames.contains("Kathy")); // Vérifier l’existence : true
  uniqueNames.remove("Lars");
  print("Après suppression de Lars : $uniqueNames");

  // 3. Map<String, String> Dictionnaire clé-valeur : clés uniques, accès par clé
  var pages = <String, String>{
    'index.html': 'Page d’accueil',
    'robots.txt': 'Fichier d’instruction pour robots d’indexation',
    'humans.txt': 'Fichier explicatif destiné aux développeurs humains',
  };
  print("\n=== Dictionnaire Map ===");
  print(pages);
  print(pages['index.html']); // Récupérer une valeur par clé : Page d’accueil
  pages["about.html"] = "Page À propos"; // Ajouter une association clé-valeur
  print("Après ajout de about.html : $pages");

  // Exemples courants de parcours
  print("\n=== Parcourir une List ===");
  for (var name in names) {
    print("- $name");
  }

  print("\n=== Parcourir toutes les entrées d’une Map ===");
  for (var entry in pages.entries) {
    print("Fichier : ${entry.key} → Description : ${entry.value}");
  }
}Langage du code : Dart (dart)

Utiliser des paramètres génériques dans les constructeurs

Lors de l’appel d’un constructeur, spécifiez le type générique entre chevrons après le nom de la classe :

var nameSet = Set<String>.of(names);Langage du code : JavaScript (javascript)

Le code ci-dessous crée un dictionnaire avec des clés de type int et des valeurs de type View :

var views = SplayTreeMap<int, View>();
Langage du code : HTML, XML (xml)
import 'dart:collection';

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

  // SplayTreeMap nécessite dart:collection
  var views = SplayTreeMap<int, String>();
  views[5] = "Page paramètres";
  views[2] = "Page d’accueil";
  views[9] = "Mon espace";
  views[1] = "Écran de lancement";

  print("\nTreeMap ordonné :");
  for (var e in views.entries) {
    print("${e.key} : ${e.value}");
  }
}Langage du code : PHP (php)

Type d’exécution des collections génériques

Réification des types (reified)

Les génériques Dart sont réifiés : l’intégralité des informations de type générique est conservée à l’exécution.

Vous pouvez vérifier directement le type générique concret d’une collection pendant l’exécution :

void main() {
    var names = <String>[];
	names.addAll(['Seth', 'Kathy', 'Lars']);
	print(names is List<String>); // Affiche true
}Langage du code : PHP (php)

Comparaison complémentaire : les génériques Java utilisent l’effacement de type ; les paramètres génériques sont supprimés à l’exécution. En Java, on ne peut vérifier qu’un objet est une List, sans distinguer List<String> de List<int>.

* Restreindre le type des paramètres génériques

Limite (bound). Si vous n’avez pas encore étudié les classes, contentez-vous d’une compréhension générale de cette partie.

Lors de la définition d’un générique, vous pouvez contraindre le type transmis à n’être qu’une sous-classe d’une super-classe donnée. Cette contrainte s’appelle une limite de type et s’implémente grâce au mot-clé extends.

1 Restreindre aux types non nullables

Obliger le générique à accepter uniquement des types non nullables (la limite supérieure par défaut est Object? nullable) :

class Foo<T extends Object> {
  // Le type T transmis ne peut pas être nullable
}
Langage du code : JavaScript (javascript)

2 Limiter aux sous-classes d’une super-classe spécifique

Utilisez extends pour indiquer la classe de base ; vous pourrez alors appeler directement toutes les méthodes membres de cette classe :

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

class Extender extends SomeBaseClass {}
Langage du code : JavaScript (javascript)

Arguments de type autorisés : la super-classe elle-même, n’importe laquelle de ses sous-classes

var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();
Langage du code : HTML, XML (xml)

Si vous omettez le paramètre générique, c’est le type limite qui est utilisé par défaut :

var foo = Foo();
print(foo); // Affiche : Instance Foo<SomeBaseClass>
Langage du code : PHP (php)

Transmettre un type sans lien de parenté provoque une erreur statique de compilation :

var foo = Foo<Object>(); // Erreur de compilation : Object n’est pas une sous-classe de SomeBaseClass
Langage du code : JavaScript (javascript)

* Limite de type auto-référente (génériques à borne F)

Lors de la contrainte d’un générique, la limite peut faire référence à son propre paramètre de type pour créer une auto-contrainte, appelée borne F (F-bound).

Exemple standard : l’interface comparable Comparable<T>

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

// T doit implémenter Comparable<T>, ne peut être comparé qu’à des objets du même type
int compareAndOffset<T extends Comparable<T>>(T t1, T t2) =>
    t1.compareTo(t2) + 1;

class A implements Comparable<A> {
  @override
  int compareTo(A other) {
    // Implémenter la logique de comparaison
    return 0;
  }
}

int res = compareAndOffset(A(), A());
Langage du code : PHP (php)

La contrainte T extends Comparable<T> signifie : T ne peut être comparé qu’à des instances du même type que lui-même.

* Méthodes génériques

Les fonctions classiques et les méthodes de classe prennent également en charge les paramètres de type génériques :

T first<T>(List<T> ts) {
  // Logique de vérification préalable
  T tmp = ts[0];
  // Logique de traitement ultérieur
  return tmp;
}
Langage du code : PHP (php)

Exemple complet

T first<T>(List<T> ts) {
  if (ts.isEmpty) {
    throw ArgumentError("La liste ne peut pas être vide");
  }
  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
}Langage du code : Dart (dart)

Le paramètre générique <T> agit sur trois éléments :

  1. Type de retour de la fonction T
  2. Type du paramètre d’entrée List<T>
  3. Type de la variable locale T tmp

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *