Sous-classification

Kotlin supporte l'héritage de classe monoparentale - donc chaque classe (sauf la classe racine Any) a exactement une classe parente, appelée classe superclasse. Kotlin veut que vous réfléchissiez à la conception de votre classe pour vous assurer qu'il est effectivement sûr de... sous-classe il, donc les classes sont fermées par défaut et ne peuvent pas être héritées à moins que vous ne déclariez explicitement que la classe soit ouverte ou abstrait. Vous pouvez ensuite sous-classer à partir de cette classe en déclarant une nouvelle classe qui mentionne sa classe parente après un deux-points :

openclass MotorVehicle
class Car :MotorVehicle()

Les classes qui ne déclarent pas de superclasse héritent implicitement de Any. La sous-classe doit invoquer l'un des constructeurs de la classe de base, en passant soit des paramètres de son propre constructeur, soit des valeurs constantes :

openclassMotorVehicle(val maxSpeed: Double,val horsepowers: Int)classCar(val seatCount: Int,
    maxSpeed: Double
):MotorVehicle(maxSpeed,100)

La sous-classe hérite de de tous les membres qui existent dans sa superclasse - à la fois ceux qui sont directement définis dans la superclasse et ceux dont la superclasse a elle-même hérité. Dans cet exemple, Car contient les membres suivants :

  • seatCountqui est Carest une propriété propre
  • maxSpeed et horsepowersqui sont héritées de MotorVehicle
  • toString(), equals()et hashCode()qui sont hérités de Any

Notez que les termes "sous-classe" et "superclasse" peuvent couvrir plusieurs niveaux d'héritage - Car est une sous-classe de Anyet Any est la superclasse de tout. Si l'on veut se limiter à un seul niveau d'héritage, on dira "sous-classe directe" ou "superclasse directe".

Notez que nous n'utilisons pas val devant maxSpeed dans Car - en faisant cela, on aurait introduit une propriété distincte dans Car qui aurait fait de l'ombre à celle héritée de MotorVehicle. Tel qu'il est écrit, c'est juste un paramètre du constructeur que l'on transmet au superconstructeur.

private (et internal membres de superclasses d'autres modules) sont également hérités, mais ne sont pas directement accessibles : si la superclasse contient une propriété privée... foo qui est référencée par une fonction publique bar(), les instances de la sous-classe contiendront une propriété de type foo; elles ne peuvent pas l'utiliser directement, mais elles sont autorisées à appeler la fonction publique bar().

Lorsqu'une instance d'une sous-classe est construite, la "partie" de la super-classe est construite en premier (via le constructeur de la super-classe). Cela signifie que pendant l'exécution du constructeur d'une classe ouverte, il se pourrait que l'objet en cours de construction soit une instance d'une sous-classe, auquel cas les propriétés spécifiques à la sous-classe n'ont pas encore été initialisées. Pour cette raison, appeler une fonction ouverte à partir d'un constructeur est risqué : elle pourrait être surchargée dans la sous-classe, et si elle accède à des propriétés spécifiques à la sous-classe, celles-ci ne seront pas encore initialisées.

Surcharge de la fonction

Si une fonction membre ou une propriété est déclarée comme open, les sous-classes peuvent surcharger en fournissant une nouvelle implémentation. Disons que MotorVehicle déclare cette fonction :

openfundrive()="$horsepowers HP motor vehicle driving at $maxSpeed MPH"

Si Car ne fait rien, il héritera de cette fonction telle quelle, et il retournera un message avec les chevaux-vapeur et la vitesse maximale de la voiture. Si nous voulons un message spécifique à la voiture, Car peut remplacer la fonction en la redéclarant avec l'attribut override mot-clé :

overridefundrive()="$seatCount-seat car driving at $maxSpeed MPH"

La signature de la fonction surchargeante doit correspondre exactement à celle de la fonction surchargée, sauf que le type de retour dans la fonction surchargeante peut être un sous-type du type de retour de la fonction surchargée.

Si ce que la fonction surchargeante veut faire est une extension de ce que la fonction surchargeante a fait, vous pouvez appeler la fonction surchargeante via... super (soit avant, soit après, soit entre d'autres codes) :

overridefundrive()=super.drive()+" with $seatCount seats"

Interfaces

La règle du parent unique devient souvent trop limitative, car vous trouverez souvent des points communs entre les classes dans différentes branches d'une hiérarchie de classes. Ces points communs peuvent être exprimés en interfaces.

Une interface est essentiellement un contrat qu'une classe peut choisir de signer ; si elle le fait, la classe est obligée de fournir des implémentations des propriétés et des fonctions de l'interface. Cependant, une interface peut (mais ne le fait généralement pas) fournir une implémentation par défaut de certaines ou de toutes ses propriétés et fonctions. Si une propriété ou une fonction a une implémentation par défaut, la classe peut choisir de la surcharger, mais elle n'y est pas obligée. Voici une interface sans aucune implémentation par défaut :

interface Driveable {val maxSpeed: Double
    fundrive(): String
}

Nous pouvons choisir de laisser MotorVehicle implémenter cette interface, puisqu'elle possède les membres requis - mais nous devons maintenant marquer ces membres avec la balise overrideet nous pouvons supprimer open puisqu'une fonction surchargée est implicitement ouverte :

openclassMotorVehicle(overrideval maxSpeed: Double,val wheelCount: Int
): Driveable {overridefundrive()="Wroom!"}

Si nous devions introduire une autre classe Bicycle, qui ne devrait être ni une sous-classe ni une super-classe de MotorVehicle, nous pourrions quand même la faire implémenter Driveablepour autant que nous déclarions maxSpeed et drive dans Bicycle.

Les sous-classes d'une classe qui implémente une interface (dans ce cas, Car) sont également considérées comme implémentant l'interface.

Un symbole qui est déclaré à l'intérieur d'une interface doit normalement être public. Le seul autre modificateur de visibilité légal est private, qui ne peut être utilisé que si le corps de la fonction est fourni - cette fonction peut alors être appelée par chaque classe qui implémente l'interface, mais par personne d'autre.

Quant à savoir pourquoi vous voudriez créer une interface, autre que comme un rappel pour que vos classes implémentent certains membres, voir la section sur le polymorphisme.

Classes abstraites

Certaines superclasses sont très utiles en tant que mécanisme de regroupement pour les classes apparentées et pour fournir des fonctions partagées, mais sont si générales qu'elles ne sont pas utiles par elles-mêmes. MotorVehicle semble correspondre à cette description. Une telle classe devrait être déclarée abstraite, ce qui empêchera la classe d'être instanciée directement :

abstractclassMotorVehicle(val maxSpeed: Double,val wheelCount: Int)

Maintenant, vous ne pouvez plus dire val mv = MotorVehicle(100, 4).

Les classes abstraites sont implicitement ouvertes, car elles sont inutiles si elles n'ont pas de sous-classes concrètes.

Lorsqu'une classe abstraite implémente une ou plusieurs interfaces, elle n'est pas tenue de fournir les définitions des membres de ses interfaces (mais elle le peut si elle le souhaite). Elle doit quand même déclarer de tels membres, en utilisant abstract override et en ne fournissant aucun corps pour la fonction ou la propriété :

abstractoverrideval foo: String
abstractoverridefunbar(): Int

Être abstrait est la seule façon d'"échapper" à l'obligation d'implémenter les membres de vos interfaces, en déchargeant le travail sur vos sous-classes - si une sous-classe veut être concrète, elle doit implémenter tous les membres "manquants".

Polymorphisme

Le polymorphisme est la capacité de traiter les objets avec des traits similaires d'une manière commune. En Python, ceci est réalisé via le ducktyping: si x fait référence à un objet, vous pouvez appeler x.quack() tant que l'objet se trouve avoir la fonction quack() - rien d'autre n'a besoin d'être connu (ou plutôt, supposé) à propos de l'objet. C'est très flexible, mais aussi risqué : si la fonction x est un paramètre, chaque appelant de votre fonction doit savoir que l'objet qu'il lui passe doit avoir la fonction quack().quack(), et si quelqu'un se trompe, le programme explose au moment de l'exécution.

En Kotlin, le polymorphisme est réalisé via la hiérarchie des classes, de telle sorte qu'il est impossible de se retrouver dans une situation où une propriété ou une fonction est manquante. La règle de base est qu'une variable/propriété/paramètre dont le type déclaré est... A peut faire référence à une instance d'une classe B si et seulement si B est un sous-type de A. Cela signifie que soit , A doit être une classe et B doit être une sous-classe de Aou que A doit être une interface et B doit être une classe qui implémente cette interface ou être une sous-classe d'une classe qui le fait. Avec nos classes et interfaces des sections précédentes, nous pouvons définir ces fonctions :

funboast(mv: MotorVehicle)="My ${mv.wheelCount} wheel vehicle can drive at ${mv.maxSpeed} MPH!"funride(d: Driveable)="I'm riding my ${d.drive()}"

et les appeler comme ceci :

val car =Car(4,120)boast(car)ride(car)

Nous sommes autorisés à passer un Car à boast() parce que Car est une sous-classe de MotorVehicle. Nous sommes autorisés à passer un Car à ride() parce que Car implémente Driveable (grâce au fait d'être une sous-classe MotorVehicle). A l'intérieur de boast()nous sommes seulement autorisés à accéder aux membres du type de paramètre déclaré. MotorVehiclemême si nous sommes dans une situation où nous savons qu'il s'agit en réalité d'un paramètre de type Car (car il pourrait y avoir d'autres appelants qui passent un paramètre non Car). À l'intérieur de ride()nous sommes seulement autorisés à accéder aux membres du type de paramètre déclaré. Driveable. Cela garantit que chaque recherche de membre est sûre - le compilateur ne vous permet de passer que des objets dont il est garanti qu'ils ont les membres nécessaires. L'inconvénient est que vous serez parfois obligé de déclarer des interfaces ou des classes enveloppes "inutiles" afin qu'une fonction accepte des instances de différentes classes.

Avec les collections et les fonctions, le polymorphisme devient plus compliqué - voir la section sur les génériques.

Casting et test de type

Lorsque vous prenez une interface ou une classe ouverte comme paramètre, vous ne connaissez généralement pas le type réel du paramètre au moment de l'exécution, puisqu'il pourrait être une instance d'une sous-classe ou de n'importe quelle classe qui implémente l'interface. Il est possible de vérifier quel est le type exact, mais comme en Python, vous devriez généralement l'éviter et plutôt concevoir votre hiérarchie de classes de sorte que vous puissiez faire ce dont vous avez besoin en surchargeant correctement les fonctions ou les propriétés.

S'il n'y a pas de moyen agréable de contourner ce problème, et que vous avez besoin de prendre des actions spéciales basées sur le type de quelque chose ou d'accéder à des fonctions/propriétés qui n'existent que sur certaines classes, vous pouvez utiliser... is pour vérifier si le type réel d'un objet est une classe particulière ou une sous-classe de celle-ci (ou un implémenteur d'une interface). Lorsque cela est utilisé comme condition dans un ifle compilateur vous permettra d'effectuer des opérations spécifiques au type sur l'objet à l'intérieur de la balise if corps :

funfoo(x: Any){if(x is Person){println("${x.name}")// This wouldn't compile outside the if}}

Si vous voulez vérifier la présence de pas soit une instance d'un type, utilisez !is. On notera que null n'est jamais une instance d'un type non nul, mais elle est toujours une "instance" d'un type nul (même si, techniquement, elle n'est pas une instance, mais une absence d'instance quelconque).

Le compilateur ne vous laissera pas effectuer des vérifications qui ne peuvent pas réussir parce que le type déclaré de la variable est une classe qui se trouve sur une branche non liée de la hiérarchie des classes par rapport à la classe contre laquelle vous vérifiez - si le type déclaré de la variable x est MotorVehiclevous ne pouvez pas vérifier si x est un Person. Si le côté droit de is est une interface, Kotlin permettra au type du côté gauche d'être n'importe quelle interface ou classe ouverte, car il se pourrait qu'une certaine sous-classe de celle-ci implémente l'interface.

Si votre code est trop intelligent pour le compilateur, et que vous savez sans l'aide de is que x est une instance de Person mais que le compilateur ne le fait pas, vous pouvez couler votre valeur avec as:

val p = x as Person

Cela va générer un ClassCastException si l'objet n'est pas réellement une instance de Person ou de l'une de ses sous-classes. Si vous n'êtes pas sûr de ce que x est, mais que vous êtes heureux d'obtenir null si ce n'est pas un objet Personvous pouvez utiliser as?qui retournera null si le cast échoue. Notez que le type résultant est Person?:

val p = x as? Person

Vous pouvez également utiliser as pour effectuer un cast vers un type nullable. La différence entre ceci et le précédent as? est que celui-ci échouera si x est une instance non nulle d'un autre type que Person:

val p = x as Person?

Délégation

Si vous constatez qu'une interface que vous voulez qu'une classe implémente est déjà implémentée par une des propriétés de la classe, vous pouvez déléguer l'implémentation de cette interface à cette propriété avec by:

interface PowerSource {val horsepowers: Int
}classEngine(overrideval horsepowers: Int): PowerSource

openclassMotorVehicle(val engine: Engine): PowerSource by engine

Ceci implémentera automatiquement tous les membres de l'interface de PowerSource dans MotorVehicle en invoquant le même membre sur engine. Cela ne fonctionne que pour les propriétés qui sont déclarées dans le constructeur.

Propriétés déléguées

Disons que vous écrivez un ORM simple. Votre bibliothèque de base de données représente une ligne comme des instances d'une classe. Entity, avec des fonctions comme getString("name") et getLong("age") pour obtenir des valeurs typées à partir des colonnes données. Nous pourrions créer une classe wrapper typée comme ceci :

abstractclassDbModel(val entity: Entity)classPerson(val entity: Entity):DbModel(entity){val name = entity.getString("name")val age = entity.getLong("age")}

C'était facile, mais peut-être voudrions-nous faire du lazy-loading pour ne pas perdre de temps à extraire les champs qui ne seront pas utilisés (surtout si certains d'entre eux contiennent beaucoup de données dans un format qu'il est long d'analyser), et peut-être voudrions-nous un support pour les valeurs par défaut. Bien que nous puissions implémenter cette logique dans un fichier get() mais il faudrait la dupliquer dans chaque propriété. Alternativement, nous pourrions implémenter la logique dans un bloc séparé de type StringProperty séparée (notez que cet exemple simple n'est pas thread-safe) :

classStringProperty(privateval model: DbModel,privateval fieldName: String,privateval defaultValue: String?=null){privatevar _value: String?= defaultValue
    privatevar loaded =falseval value: String?get(){// Warning: This is not thread-safe!if(loaded)return _value
            if(model.entity.contains(fieldName)){
                _value = model.entity.getString(fieldName)}
            loaded =truereturn _value
        }}// In Personval name =StringProperty(this,"name","Unknown Name")

Malheureusement, l'utilisation de cette méthode nous obligerait à taper p.name.value chaque fois que nous voulons utiliser la propriété. Nous pourrions faire ce qui suit, mais ce n'est pas non plus génial car cela introduit une propriété supplémentaire :

// In Personprivateval _name =StringProperty(this,"name","Unknown Name")val name get()= _name.value

La solution est un propriété déléguée qui vous permet de spécifier le comportement de l'obtention et de la définition d'une propriété (quelque peu similaire à la mise en œuvre de la propriété déléguée __getattribute__() .__getattribute__()et __setattribute__() en Python, mais pour une seule propriété à la fois).

classDelegatedStringProperty(privateval fieldName: String,privateval defaultValue: String?=null){privatevar _value: String?=nullprivatevar loaded =falseoperatorfungetValue(thisRef: DbModel, property: KProperty<*>): String?{if(loaded)return _value
        if(thisRef.entity.contains(fieldName)){
            _value = thisRef.entity.getString(fieldName)}
        loaded =truereturn _value
    }}

La propriété déléguée peut être utilisée comme ceci pour déclarer une propriété en Person - notez l'utilisation de by au lieu de =:

val name byDelegatedStringProperty(this,"name","Unknown Name")

Maintenant, chaque fois que quelqu'un lit p.name, getValue() sera invoqué avec p comme thisRef et les métadonnées sur les name en tant que property. Puisque thisRef est un DbModelcette propriété déléguée ne peut être utilisée qu'à l'intérieur d'une DbModel .DbModelet de ses sous-classes.

Une belle propriété déléguée intégrée est lazyqui est une implémentation correctement threadsafe du modèle de chargement paresseux. L'expression lambda fournie ne sera évaluée qu'une seule fois, lors du premier accès à la propriété.

val name: String?by lazy {if(thisRef.entity.contains(fieldName)){
        thisRef.entity.getString(fieldName)}elsenull}

Classes scellées

Si vous voulez restreindre l'ensemble des sous-classes d'une classe de base, vous pouvez déclarer que la classe de base est... sealed (ce qui la rend également abstraite), auquel cas vous ne pouvez déclarer que des sous-classes dans le même fichier. Le compilateur connaît alors l'ensemble complet des sous-classes possibles, ce qui vous permettra de faire des analyses exhaustives de la classe de base. when exhaustive pour tous les sous-types possibles sans avoir besoin d'une expression else (et si vous ajoutez une autre sous-classe dans le futur et oubliez de mettre à jour la clause when, le compilateur vous le fera savoir).


← Précédent : les modificateurs de visibilité. Suivant : Objets et objets compagnons →

Ce document a été rédigé par Aasmund Eldhuset; il est la propriété de Khan Academy et fait l'objet d'une licence d'utilisation sous CC BY-NC-SA 3.0 US. Veuillez noter que ce produit ne fait pas partie de l'offre officielle de Khan Academy.