0Pricing
Scala for Backend Engineering & Functional Programming · Leçon

Variance : covariance et contravariance

Maîtrisez la covariance et la contravariance afin de gérer correctement les relations de sous-typage dans les types génériques.

Variance : covariance et contravariance est une leçon Scala for Backend Engineering & Functional Programming gratuite sur CoddyKit. Ceci est la leçon 2 sur 3. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage Scala for Backend Engineering & Functional Programming, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Scala for Backend Engineering & Functional Programming comprend 3 leçons au total.

Qu'est-ce que la variance des types ?

En Scala, lorsque vous disposez d'une hiérarchie de types (par exemple, Cat est un sous-type de Animal), comment les types génériques se comportent-ils ?

Une List[Cat] est-elle considérée comme un sous-type de List[Animal] ? Pas toujours par défaut !

La variance des types permet de définir comment les relations de sous-typage sont conservées (ou inversées) pour les types génériques. Elle est essentielle pour écrire du code flexible et sûr du point de vue des types.

Covariance : types « producteurs » (+T)

La covariance est indiquée en plaçant un + devant le paramètre de type (par exemple, trait Box[+T]).

  • Si A est un sous-type de B, alors Box[A] devient un sous-type de Box[B].
  • Considérez les types covariants comme des « producteurs » de T. Ils ne peuvent apparaître que dans des positions de sortie (comme les types de retour des méthodes).
  • Ainsi, si vous attendez une Box[Animal], vous pouvez fournir une Box[Cat], car elle « produit » quelque chose de plus spécifique (un Cat est un Animal).

La covariance en pratique

Essayez d'exécuter cet exemple de trait Producer covariant :

class Animal
class Cat extends Animal

trait Producer[+T] {
  def produce: T // Output position
}

class CatProducer extends Producer[Cat] {
  def produce: Cat = new Cat
}

object Main {
  def main(args: Array[String]): Unit = {
    val catProducer: Producer[Cat] = new CatProducer
    // Because Producer is covariant, Producer[Cat] is a subtype of Producer[Animal]
    val animalProducer: Producer[Animal] = catProducer 
    println("Assigned CatProducer to AnimalProducer.")
    println(s"Produced: ${animalProducer.produce.getClass.getSimpleName}")
  }
}

Quand utiliser la covariance

La covariance est sûre lorsque votre type générique ne « produit » que des valeurs de type T ou n'accepte jamais T comme argument.

  • Les collections immuables comme List[+T] en sont un excellent exemple. Vous pouvez considérer une List[Cat] comme une List[Animal], car vous ne faites qu'en « lire » des Animal (ou leurs sous-types).
  • Vous ne pouvez pas ajouter un Animal quelconque à une List[Cat] lorsqu'elle est typée comme List[Animal], ce qui préserve la sûreté des types.

Contravariance : types « consommateurs » (-T)

La contravariance est indiquée en plaçant un - devant le paramètre de type (par exemple, trait Consumer[-T]).

  • Si A est un sous-type de B, alors Consumer[B] devient un sous-type de Consumer[A].
  • Considérez les types contravariants comme des « consommateurs » de T. Ils ne peuvent apparaître que dans des positions d'entrée (comme les types des paramètres des méthodes).
  • Ainsi, si vous attendez un Consumer[Cat], vous pouvez fournir un Consumer[Animal], car il « consomme » quelque chose de plus général (il peut gérer n'importe quel Animal, y compris un Cat).

La contravariance en pratique

Essayez d'exécuter cet exemple de trait Consumer contravariant :

class Animal
class Cat extends Animal

trait Consumer[-T] {
  def consume(item: T): Unit // Input position
}

class AnimalConsumer extends Consumer[Animal] {
  def consume(item: Animal): Unit = {
    println(s"Consumed an animal: ${item.getClass.getSimpleName}")
  }
}

object Main {
  def main(args: Array[String]): Unit = {
    val animalConsumer: Consumer[Animal] = new AnimalConsumer
    // Because Consumer is contravariant, Consumer[Animal] is a subtype of Consumer[Cat]
    val catConsumer: Consumer[Cat] = animalConsumer 
    catConsumer.consume(new Cat)
    println("Assigned AnimalConsumer to CatConsumer.")
  }
}

Quand utiliser la contravariance

La contravariance est sûre lorsque votre type générique ne fait que « consommer » des valeurs de type T ou ne renvoie jamais T.

  • Les fonctions, et plus précisément le type de leur paramètre d'entrée, en sont un exemple courant. Si une fonction peut traiter n'importe quel Animal (Animal => Unit), elle peut certainement traiter un Cat. Ainsi, (Animal => Unit) est un sous-type de (Cat => Unit).
  • Cela offre davantage de flexibilité lorsque vous transmettez des fonctions comme arguments.

Invariance : le comportement par défaut

Si vous ne spécifiez ni + ni -, le paramètre de type est invariant. Il s'agit du comportement par défaut en Scala.

  • Box[A] est un sous-type de Box[B] uniquement si A et B sont exactement du même type.
  • Cela est souvent nécessaire pour les collections modifiables (par exemple, Array[T]) afin d'éviter les problèmes de sûreté des types, car vous pourriez à la fois lire et écrire différents sous-types.
class Food
class Apple extends Food

// Invariant Box
class Box[T](val item: T) {
  def getContent: T = item
}

object Main {
  def main(args: Array[String]): Unit = {
    val appleBox = new Box(new Apple)
    // The following line would cause a compile error:
    // val foodBox: Box[Food] = appleBox 
    println(s"An Apple Box contains: ${appleBox.getContent.getClass.getSimpleName}")
    println("Box[Apple] is NOT a subtype of Box[Food] (invariant).")
    println("The types must match exactly for invariant types.")
  }
}

Fonctions : covariance et contravariance

Les types de fonctions de Scala, Function1[-A, +B], combinent élégamment les deux formes de variance :

  • Le paramètre d'entrée A est contravariant (-A). Cela signifie qu'une fonction qui accepte un type plus général (par exemple, Animal) peut être utilisée là où l'on attend une fonction acceptant un type plus spécifique (par exemple, Cat).
  • Le type de retour B est covariant (+B). Cela signifie qu'une fonction qui renvoie un type plus spécifique (par exemple, Cat) peut être utilisée là où l'on attend une fonction renvoyant un type plus général (par exemple, Animal).
class Vehicle
class Car extends Vehicle

object Main {
  def main(args: Array[String]): Unit = {
    // Contravariance for input: (Vehicle => Unit) is a subtype of (Car => Unit)
    val printVehicle: Vehicle => Unit = (v: Vehicle) => println(s"Printing vehicle: ${v.getClass.getSimpleName}")
    val printCar: Car => Unit = printVehicle // OK: A general printer can print a specific car
    printCar(new Car)

    // Covariance for output: (() => Car) is a subtype of (() => Vehicle)
    val getCar: () => Car = () => new Car
    val getVehicle: () => Vehicle = getCar // OK: A specific producer can fulfill a general request
    println(s"Got vehicle: ${getVehicle().getClass.getSimpleName}")
  }
}

Vérification rapide : règles de variance

Considérez le trait suivant :

trait Handler[T] {
  def handle(item: T): Unit
}

Pour permettre l'utilisation de Handler[Animal] là où un Handler[Cat] est attendu (sachant que Cat étend Animal), quelle annotation de variance T doit-il avoir ?

Récapitulatif : maîtriser la variance

Vous maîtrisez désormais la variance en Scala ! Voici un bref récapitulatif :

  • Covariance (+T) : permet à Container[Subtype] d'être un sous-type de Container[Supertype]. Utile pour les types « producteurs » qui renvoient uniquement T.
  • Contravariance (-T) : permet à Container[Supertype] d'être un sous-type de Container[Subtype]. Utile pour les types « consommateurs » qui acceptent uniquement T en entrée.
  • Invariance : comportement par défaut. Les types doivent correspondre exactement.

Comprendre la variance vous aide à créer du code générique Scala plus flexible et plus sûr du point de vue des types !

Questions Fréquemment Posées

La leçon « Variance : covariance et contravariance » est-elle gratuite ?

Oui — le texte complet de « Variance : covariance et contravariance » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours Scala for Backend Engineering & Functional Programming, passe à CoddyKit PRO. Le cours Scala for Backend Engineering & Functional Programming comprend 3 leçons au total.

Qu'est-ce que j'apprendrai dans « Variance : covariance et contravariance » ?

Maîtrisez la covariance et la contravariance afin de gérer correctement les relations de sous-typage dans les types génériques. Tu pratiques Scala for Backend Engineering & Functional Programming avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer Scala for Backend Engineering & Functional Programming ?

Aucune expérience préalable n'est requise. Scala for Backend Engineering & Functional Programming sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 2 sur 3.

Combien de temps prend la leçon « Variance : covariance et contravariance » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon Scala for Backend Engineering & Functional Programming ?

Oui. Chaque leçon Scala for Backend Engineering & Functional Programming inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Génériques et paramètres de type
  2. Variance : covariance et contravariance
  3. Classes de types et implicites
← Retour à Scala for Backend Engineering & Functional Programming