I. Introduction▲
Quand nous utilisons Objective-C, nous n'accordons pas beaucoup d'importance à la notion de type. Nous utilisons principalement des types de base (entiers, pointeurs, et des définitions de types - typedefs -) ou des classes et leurs sous-classes. Swift apporte un système de types plus riche que nous allons détailler.
En Objective-C, nous utilisons souvent l'héritage : l'idée qu'une classe a toutes les fonctionnalités de sa classe mère et en ajoute, tel que :
class
Animal
{
func
feed
()
{
println
(
"nom"
)
}
}
class
Cat
:
Animal
{
func
meow
()
{
println
(
"miaou"
)
}
}
var
aCat
=
Cat
()
aCat
.
meow
()
aCat
.
feed
()
Nous savons qu'une instance de la classe Cat peut être passée partout où une instance d'Animal est attendue :
func
pet
(
animal
:
Animal
)
{
println
(
"caresser
\(
animal
)
"
)
}
pet
(
cat
)
Cette propriété n'est pas propre aux classes. C'est un concept plus général appelé sous-typage qui se manifeste sous toutes sortes de possibilités intéressantes. Par exemple, le type Array - qui est une classe dans la plupart des langages - est une structure en Swift. D'après le livre sur Swift, les structures ne permettent pas l'héritage. Même si la classe Array ne peut être dérivée, elle peut avoir des sous-types, car c'est un type générique. Prenez cette fonction :
func
feedAnimals
(
allTheBeasties
:[
Animal
])
{
for
animal
in
allTheBeasties
{
animal
.
feed
()
}
}
La question est : est-ce que cette fonction peut prendre un paramètre le type [Cat] ? Intuitivement, cela devrait fonctionner, car tous les Cat sont des Animal, et cela aurait donc du sens. Cependant, feedAnimal accepte seulement des sous-types d'[Animal]. Pour que ça fonctionne, Array doit changer de type suivant son contenu : si B est un sous-type du type A, [B] est un sous-type du type [A]— c'est exactement ce qui se passe en Swift.
Le comportement de ce sous-typage signifie que les tableaux (arrays) sont covariants avec leurs paramètres génériques. La covariance est une manière d'accepter plus de programmes comme étant valides, d'une façon qui a du sens. C'est raisonnable de permettre une instance de Cat, là où nous en attendions une d'Animal. De la même façon, il est raisonnable de permettre une instance d'Array de Cats là où nous en attendions une d'Array d'Animals. De même, passer un dictionnaire de type [String:Cat] où un dictionnaire de type [String:Animal] est attendu devrait aussi fonctionner.
Un exemple intéressant est celui des optionnels, parce les optionnels, si l'on enlève l'aspect sucre syntaxique, sont représentés comme une énumération :
enum
Optional
<
T
>
{
case
Some
(
T
)
case
None
}
Donc, quand une fonction attend un type Animal?, nous pouvons lui fournir un Cat? en toute sécurité et le compilateur va l'accepter :
func
findAnimal
(
maybeAnimal
:
Animal
?)
{
if
let
animal
=
maybeAnimal
{
println
(
"animal:
\(
animal
)
"
)
}
}
findAnimal
(
Cat
())
Ce code compile et s'exécute correctement, car le type Cat? est considéré comme un sous-type d'Animal?—c'est-à-dire, les optionnels sont covariants.
II. Les fonctions▲
Les classes, les structures et les énumérations sont, ce que nous identifions immédiatement comme des types provenant d'Objective-C. Mais en Swift, un autre type peut-être passé en paramètre : les fonctions. Ce sont des objets à part entière de première classe, et en tant que tels, ils ont des types. Les règles de sous-typage fonctions sont un rien plus complexes que celles des autres types, mais ne vous inquiétez pas, elles ont beaucoup de sens.
La plus simple façon de traiter des fonctions en paramètres et de considérer les fonctions comme invariantes : leurs types de déclarations sont fixes, et il n'est pas possible de les sous-typer. Intuitivement, cela n'a aucun sens. Si nous attendons une fonction qui renvoie une instance d'Animal, et que nous avons une fonction qui en renvoie une de Cat, nous devrions être satisfaits. Imposer une contrainte supplémentaire au type de retour d'une fonction serait trop simpliste. Le code suivant devrait fonctionner :
func
catFetcher
()
->
Cat
{
return
Cat
()
}
func
saveAnimalFetcher
(
fetcher
:()
->
Animal
)
{
// enregistrer le récupérateur pour une utilisation ultérieure
}
saveAnimalFetcher
(
catFetcher
)
Et il fonctionne. Donc, les fonctions sont covariantes, n'est-ce pas ? Pas si vite ! Les fonctions sans paramètre sont covariantes sur leur type de retour, mais qu'en est-il des fonctions avec paramètres ? Quelles sont les règles pour les paramètres de la fonction dans le code suivant :
func
convert
(
convertFunction
:
Cat
->
Animal
,
cat
:
Cat
)
->
Animal
{
return
convertFunction
(
cat
)
}
Encore une fois, la fonction sera covariante sur son type de retour : si quelque chose est attendu comme étant un Animal dans la fonction convert, elle devrait être satisfaite de recevoir une instance de Cat. Les règles sur les paramètres sont spéciales : considérez la fonction de conversion suivante :
class
Kitten
:
Cat
{}
func
raiseKitten
(
kitten
:
Kitten
)
->
Cat
{
// élever le chaton et le retourner, adulte
return
Cat
()
}
Si nous passons la fonction raiseKitten en paramètre de la fonction convert, il y aura une erreur, car ce qui est attendu est que la fonction convertFunction puisse accepter n'importe quel Cat, et raiseKitten n'accepte que des Kittens. Donc le sous-typage de paramètres n'est pas permis, et donc les fonctions ne sont pas covariantes pour leurs paramètres. Maintenant, considérez la fonction de conversion suivante :
func
identity
(
animal
:
Animal
)
->
Animal
{
return
animal
}
Est-ce que cette fonction devrait causer une erreur pour la fonction convert ? Non : convert a seulement besoin de pouvoir passer des instances de Cats, comme une instance de Cat est aussi une instance d'Animal, nous sommes corrects. En prenant du recul, nous réalisons que cela veut dire que le type : Animal -> Animal peut être utilisé là où nous attendons le type Cat -> Animal. En d'autres mots, Animal -> Animal est un sous-type de Cat -> Animal. Les sur-types sont acceptés comme paramètres, mais les sous-types non !
Ce comportement, acceptant un type qui peut être un sur-type du type déclaré, est ce qui est appelé contra-variance. La contra-variance de types est très rare en pratique, mais il est intéressant de se rappeler que quand les fonctions sont des objets à part entière, elles sont covariantes pour leur type de retour et contra-variantes pour leurs paramètres.
III. Limites▲
Ceci étant dit, pouvons-nous, nous, développeur, déclarer des types covariants, contra-variants ou invariants ? Pas à ce que j'ai pu observer. Pour l'instant, les règles sur la variance s'appliquent sur les types prédéfinis (tableaux et dictionnaires covariants, et les fonctions qui sont… ce qu'elles sont, etc.), mais les types personnalisés sont tous invariants. Par exemple, le code suivant ne compile pas :
struct
Shelter
<
T
>
{
let
inhabitants
:[
T
]
init
(
inhabitants
:[
T
])
{
self
.
inhabitants
=
inhabitants
;
}
}
func
countFurballs
(
shelter
:
Shelter
<
Animal
>)
{
println
(
"Furballs:
\(
shelter
.
inhabitants
.
count
)
"
)
}
var
catShelter
=
Shelter
(
inhabitants
:[
Cat
()])
countFurballs
(
catShelter
)
// lance une erreur de compilation
C'est parce que Shelter<Animal> n'est pas la même chose que Shelter<Cat>, et nous n'avons aucune façon de dire au compilateur que nous voulons qu'ils soient traités comme des sous-types, comme il le ferait quand il traite un tableau.
C'est un peu triste, à vrai dire, mais seulement dans le sens où c'est un nouveau jouet qui ne nous est pas rendu disponible. En fin de compte la plupart du temps, nous n'avons pas besoin de nous soucier de cela. Mais peut-être qu'un jour, dans le futur, nous serons capables de définir nos propres types génériques comme étant covariants et contra-variants en Swift.
IV. Remerciements Developpez▲
Nous remercions Alexandros Salazar de nous avoir aimablement autorisés à publier son article. Cet article est une traduction autorisée dont le texte original peut être trouvé sur nomothetis.svbtle.com. Nous remercions aussi Sirus64 pour sa traduction, bredelet pour sa relecture technique ainsi que ClaudeLELOUP pour sa relecture orthographique.