Agence web » Actualités du digital » Qu’est-ce que la covariance et la contravariance dans la programmation? –

Qu’est-ce que la covariance et la contravariance dans la programmation? –

quest-ce-que-la-covariance-et-la-contravariance-dans-la-programmation-9842398

La covariance et la contravariance sont des termes qui décrivent comment un langage de programmation gère les sous-types. La variance d’un type détermine si ses sous-types peuvent être utilisés de manière interchangeable avec lui.

La variance est un concept qui peut sembler opaque jusqu’à ce qu’un exemple concret soit fourni. Considérons un type de base Animal avec un sous-type de Dog.

interface Animal {
    public function walk() : void;
}
 
interface Dog extends Animal {
    public function bark() : void;
}

Tous les «animaux» peuvent marcher, mais seuls les «chiens» peuvent aboyer. Voyons maintenant ce qui se passe lorsque cette hiérarchie d’objets est utilisée dans notre application.

Câblage des interfaces ensemble

Depuis chaque Animal peut marcher, nous pouvons créer une interface générique qui exerce tout Animal.

interface AnimalController {
 
    public function exercise(Animal $Animal) : void;
 
}

Le AnimalController a un exercise() méthode qui saisit le Animal interface.

interface DogRepository {
 
    public function getById(int $id) : Dog;
 
}

Maintenant nous avons un DogRepository avec une méthode qui est garantie de retourner un Dog.

Que se passe-t-il si nous essayons d’utiliser cette valeur avec le AnimalController?

$AnimalController -> exercise($DogRepository -> getById(1));

Ceci est autorisé dans les langues où les paramètres covariants sont pris en charge. AnimalController doit recevoir un Animal. Ce que nous passons est en fait un Dog, mais il satisfait toujours le Animal Contrat.

Ce type de relation est particulièrement important lorsque vous étendez des cours. Nous pourrions vouloir un générique AnimalRepository qui récupère n’importe quel animal sans ses détails d’espèce.

interface AnimalRepository {
 
    public function getById(int $id) : Animal;
 
}
 
interface DogRepository extends AnimalRepository {
 
    public function getById(int $id) : Dog;
 
}

DogRepository modifie le contrat de AnimalRepository– car les appelants recevront un Dog au lieu d’un Animal– mais ne le change pas fondamentalement. Il s’agit simplement d’être plus précis sur son type de retour. UNE Dog est toujours un Animal. Les types sont covariants, donc DogRepositoryLa définition de est acceptable.

Regarder la contravariance

Considérons maintenant l’exemple inverse. Il pourrait être souhaitable d’avoir un DogController, qui modifie la manière dont les «chiens» sont exercés. Logiquement, cela pourrait encore prolonger la AnimalController interface. Cependant, dans la pratique, la plupart des langues ne vous permettront pas de remplacer exercise() de la manière nécessaire.

interface AnimalController {
 
    public function exercise(Animal $Animal) : void;
 
}
 
interface DogController extends AnimalController {
 
    public function exercise(Dog $Dog) : void;
 
}

Dans cet exemple, DogController a précisé que exercise() n’accepte qu’un Dog. Cela est en conflit avec la définition en amont dans AnimalController, qui permet à n’importe quel «animal» d’être passé. Pour satisfaire le contrat, DogController doit, par conséquent, accepter également tout Animal.

À première vue, cela peut sembler déroutant et inutile. Le raisonnement derrière cette restriction devient plus clair lorsque vous écrivez contre AnimalController:

function exerciseAnimal(
    AnimalController $AnimalController,
    AnimalRepository $AnimalRepository,
    int $id) : void {
 
    $AnimalController -> exercise($AnimalRepository -> getById($id));
}

Le problème est que AnimalController pourrait être un AnimalController ou un DogController—Notre méthode ne permet pas de savoir quelle implémentation d’interface elle utilise. Ceci est dû aux mêmes règles de covariance qui étaient utiles auparavant.

Comme AnimalController force être un DogController, il y a maintenant un bogue d’exécution sérieux en attente de découverte. AnimalRepository renvoie toujours un Animal, donc si $AnimalController est une DogController, l’application va planter. Le Animal le type est trop vague pour passer au DogController exercise() méthode.

Il convient de noter que les langues qui prennent en charge la surcharge de méthode accepteraient DogController. La surcharge vous permet de définir plusieurs méthodes avec le même nom, à condition qu’elles aient des signatures (Ils ont différents paramètres et / ou types de retour.). DogController aurait un extra exercise() méthode qui n’acceptait que les «chiens». Cependant, il faudrait également mettre en œuvre la signature en amont acceptant tout «animal».

Gestion des problèmes de variance

Tout ce qui précède peut être résumé en disant que les types de retour de fonction sont autorisés à être covariant, alors que les types d’arguments doivent être contravariant. Cela signifie qu’une fonction peut renvoyer un type plus spécifique que celui défini par l’interface. Il peut également accepter un type plus abstrait comme argument (bien que la plupart des langages de programmation populaires ne l’implémentent pas).

Vous rencontrez le plus souvent des problèmes de variance lorsque vous travaillez avec des génériques et des collections. Dans ces scénarios, vous voulez souvent un AnimalCollection et un DogCollection. Devrait DogCollection étendre AnimalCollection?

Voici à quoi pourraient ressembler ces interfaces:

interface AnimalCollection {
    public function add(Animal $a) : void;
    public function getById(int $id) : Animal;
}
 
interface DogCollection extends AnimalCollection {
    public function add(Dog $d) : void;
    public function getById(int $id) : Dog;
}

Regardant d’abord getById(), Dog est un sous-type de Animal. Les types sont covariants et les types de retour covariants sont autorisés. Ceci est acceptable. Nous observons à nouveau le problème de la variance avec add() bien que-DogCollection doit permettre à tout Animal à ajouter afin de satisfaire le AnimalCollection Contrat.

Ce problème est généralement mieux résolu en rendant les collections immuables. Autorisez uniquement l’ajout de nouveaux éléments dans le constructeur de la collection. Vous pouvez ensuite éliminer le add() méthode tout à fait, faisant AnimalCollection un candidat valide pour DogCollection hériter de.

Autres formes de variance

Outre la covariance et la contravariance, vous pouvez également rencontrer les termes suivants:

  • Bivariant: Un système de types est bivariant si la covariance et la contravariance s’appliquent simultanément à une relation de type. La bivariance était utilisée par TypeScript pour ses paramètres avant TypeScript 2.6
  • Une variante: Les types sont variables si la covariance ou la contravariance s’applique.
  • Invariant: Tous les types qui ne sont pas des variantes.

Vous travaillerez généralement avec des types covariants ou contravariants. En termes d’héritage de classe, un type B est covariant avec un type A s’il étend A. Un type B est contravariant avec un type A si c’est l’ancêtre de B.

Conclusion

La variance est un concept qui explique les limites des systèmes de types. En général, il suffit de se rappeler que la covariance est acceptée dans les types de retour, alors que la contravariance est utilisée pour les paramètres.

Les règles de variance découlent du principe de substitution de Liskov. Cela indique que vous devriez pouvoir remplacer les instances d’une classe par des instances de ses sous-classes sans modifier aucune des propriétés du système plus large. Cela signifie que si le type B étend le type A, les instances de A peut être remplacé par des instances de B.

Utiliser notre exemple ci-dessus signifie que nous devons être en mesure de remplacer Animal avec Dog, ou AnimalController avec DogController. Ici, on voit encore pourquoi DogController ne peut pas remplacer exercise() d’accepter uniquement les chiens – nous ne pourrions plus les remplacer AnimalController avec DogController, en tant que consommateurs passant actuellement un Animal aurait maintenant besoin de fournir un Dog au lieu. La covariance et la contravariance appliquent la LSP et garantissent des normes de comportement cohérentes.

★★★★★