Système de type solide

Le système de type solide de Dart (Sound Type System)

Définition principale

Dart est un langage à typage statique solide sécurisé par un double mécanisme : vérifications statiques à la compilation et contrôles à l’exécution. À l’exécution, la valeur stockée dans une variable correspond toujours au type déclaré statiquement.

Les règles de typage sont appliquées strictement, mais les annotations de type sont facultatives : le compilateur déduit les types automatiquement.

L’analyse statique pendant la compilation intercepte la grande majorité des erreurs de type en amont.

Qu’est-ce que la solidité (Soundness)

Le code ne peut jamais entrer dans un état de type invalide. Si une expression a pour type statique String, elle ne produira que des chaînes de caractères à l’exécution, sans aucun autre type autorisé. Langages similaires : Java, C#.

  • Interception à la compilation : int a = "abc"; génère une erreur immédiate ;
  • Interception à l’exécution : le cast avec as lève une exception en cas d’incompatibilité de type.

Quatre principaux avantages

Les bugs de type apparaissent à la compilation, évitant les crashs en production difficiles à reproduire liés aux incompatibilités de types ;

Meilleure lisibilité du code : les types ne trompent pas le développeur sur la nature des données ;

Maintenance simplifiée : modifier un fragment de code déclenche des contrôles de type qui signalent toutes les portions affectées ;

Meilleure efficacité de compilation AOT : les informations de type permettent au compilateur de générer du code machine plus optimisé.

Exemples

Erreur statique provoquée par une collection dynamique

// Attend un paramètre de type List<int>
void printInts(List<int> a) => print(a);

void main() {
  // Aucun générique déclaré, déduit en List<dynamic>
  final list = [];
  list.add(1);
  list.add('2'); // Une liste dynamique accepte des types mixtes
  printInts(list); 
  // Erreur de compilation : List<dynamic> ne peut pas être affecté à List<int>
}Langage du code : Dart (dart)

Déclaration explicite des contraintes génériques

void printInts(List<int> a) => print(a);

void main() {
  final list = <int>[]; // Spécification explicite du générique int
  list.add(1);
  // list.add('2'); // Erreur de compilation, impossible d’insérer une chaîne
  list.add(2);
  printInts(list); // Passe sans problème les contrôles statiques
}Langage du code : Dart (dart)

Trois règles fondamentales

1 Lors de la redéfinition d’une méthode, le type de retour doit être le type d’origine ou l’un de ses sous-types (règle du producteur)

Les valeurs de retour des méthodes de la classe mère sont des producteurs : une sous-classe peut renvoyer un sous-type plus spécifique, mais pas un type sans lien.

Exemple

Cas valide

class Animal {
  Animal get parent => Animal();
}

class HoneyBadger extends Animal {
  @override
  HoneyBadger get parent => HoneyBadger(); // Sous-type, autorisé
}Langage du code : Dart (dart)

Cas invalide

class Root {}
class HoneyBadger extends Animal {
  @override
  Root get parent => Root(); // Type sans rapport, erreur de compilation
}Langage du code : Dart (dart)

2 Lors de la redéfinition d’une méthode, le type des paramètres doit être le type d’origine ou l’un de ses super-types

Les paramètres de méthode sont des consommateurs : une sous-classe ne peut qu’élargir le type des paramètres (vers un super-type), pas le restreindre à un sous-type, ce qui briserait la sûreté de typage.

Exemple valide (paramètre élargi à Object)

class Animal {
  void chase(Animal a) {}
}

class HoneyBadger extends Animal {
  @override
  void chase(Object a) {} // Object est super-type d’Animal, autorisé
}Langage du code : Dart (dart)

Exemple invalide (paramètre restreint à la sous-classe Mouse)

class Mouse extends Animal {}
class Cat extends Animal {
  @override
  void chase(Mouse a) {} // Restriction du type de paramètre, erreur statique
}

void main() {
  Animal cat = Cat();
  cat.chase(Alligator()); // Théoriquement autorisé, crée une faille de sécurité de type
}Langage du code : Dart (dart)

Règle 3 : Il est interdit d’affecter directement une collection générique dynamique à une collection générique figée

List<dynamic> et List<Dog> ne peuvent pas être affectés directement à List<Cat> et génèrent une erreur de compilation. Aucune conversion implicite n’existe entre des collections génériques différentes.

class Cat {}
class Dog {}

void main() {
  List<Cat> foo = <dynamic>[Dog()]; // Erreur statique à la compilation
  List<dynamic> bar = <dynamic>[Dog(), Cat()]; // Autorisé, liste dynamique acceptant des types variés
}Langage du code : Dart (dart)

Vérification de type à l’exécution

Les contrôles statiques ne couvrent pas tous les cas : des vérifications complémentaires s’exécutent à l’exécution et lèvent une exception en cas de désaccord de type.

Erreur à l’exécution lors d’un cast générique

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

void main() {
  List<Animal> animals = <Dog>[Dog()];
  List<Cat> cats = animals as List<Cat>; 
  // Exception à l’exécution : impossible de caster List<Dog> vers List<Cat>
}Langage du code : Dart (dart)

Cast descendant implicite depuis dynamic (vérifié à l’exécution)

Affecter une variable dynamic à un type concret déclenche une conversion implicite vérifiée à l’exécution ; une erreur survient si les types ne correspondent pas.

int assumeString(dynamic object) {
  String string = object; // Vérification à l’exécution qu’object est une chaîne
  return string.length;
}

void main() {
  assumeString("test"); // Fonctionne normalement
  assumeString(123);    // Erreur à l’exécution : int ne peut pas être converti en String
}Langage du code : Dart (dart)

Désactiver les casts implicites (mode typage strict)

Activez les conversions strictes dans analysis_options.yaml pour bloquer les casts descendants implicites depuis dynamic

analyzer:
  language:
    strict-casts: trueLangage du code : JavaScript (javascript)

Inférence de type (Type Inference)

Dart déduit automatiquement le type des variables, génériques et valeurs de retour de méthodes. Si les informations de type sont insuffisantes, le type par défaut est dynamic.

Inférence de variable simple

// Déduit automatiquement Map<String, Object>
var arguments = {'argA': 'hello', 'argB': 42};

var x = 3; // x est déduit en int
x = 4.0;   // Erreur, un int ne peut pas stocker un double

num y = 3; // Déclaration explicite num, accepte int et double
y = 4.0;   // AutoriséLangage du code : Dart (dart)

Inférence des constructeurs génériques / collections

List<int> listOfInt = []; // Déduit <int>[] automatiquement
var listOfDouble = [3.0]; // Déduit List<double>

var ints = listOfDouble.map((x) => x.toInt()); 
// x déduit double, rappel renvoie int → ints déduit Iterable<int>Langage du code : Dart (dart)

Système de type solide

Laisser un commentaire

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