Generiske stoffer i Kotlin

1. Oversigt

I denne artikel ser vi på generiske typer på Kotlin-sproget.

De ligner meget dem fra Java-sproget, men Kotlin-sprogskaberne forsøgte at gøre dem lidt mere intuitive og forståelige ved at introducere specielle nøgleord som ud og i.

2. Oprettelse af parametriserede klasser

Lad os sige, at vi vil oprette en parametreret klasse. Vi kan nemt gøre dette på Kotlin-sprog ved at bruge generiske typer:

class ParameterizedClass (private val value: A) {fun getValue (): A {return value}}

Vi kan oprette en forekomst af en sådan klasse ved at indstille en parametreret type eksplicit, når vi bruger konstruktøren:

val parameterizedClass = ParameterizedClass ("string-value") val res = parameterizedClass.getValue () assertTrue (res is String)

Heldigvis kan Kotlin udlede den generiske type fra parametertypen, så vi kan udelade det, når vi bruger konstruktøren:

val parameterizedClass = ParameterizedClass ("string-value") val res = parameterizedClass.getValue () assertTrue (res is String)

3. Kotlin ud og i Nøgleord

3.1. Det Ud Nøgleord

Lad os sige, at vi vil oprette en producentklasse, der producerer et resultat af en eller anden type T. Nogle gange; vi vil tildele den producerede værdi til en reference, der er af en supertype af typen T.

For at opnå det ved hjælp af Kotlin, vi er nødt til at brugeud nøgleord på den generiske type. Det betyder, at vi kan tildele denne henvisning til enhver af dens supertyper. Outværdien kan kun produceres af den givne klasse, men forbruges ikke:

class ParameterizedProducer (private val value: T) {fun get (): T {return value}}

Vi definerede en ParameterizedProducer klasse, der kan producere en værdi af typen T.

Næste; vi kan tildele en instans af ParameterizedProducer klasse til den reference, der er en supertype af den:

val parameterizedProducer = ParameterizedProducer ("string") val ref: ParameterizedProducer = parameterizedProducer assertTrue (ref er ParameterizedProducer)

Hvis typen T i ParameterizedProducer klasse vil ikke være ud type, vil det givne udsagn frembringe en kompilatorfejl.

3.2. Det i Nøgleord

Nogle gange har vi en modsat situation, hvilket betyder, at vi har en reference af typen T og vi vil være i stand til at tildele det til undertypen T.

Vi kan bruge i nøgleord på den generiske type, hvis vi vil tildele det til referencen for dets undertype. Det i nøgleord kan kun bruges på den parametertype, der forbruges, ikke produceres:

klasse ParameterizedConsumer {fun toString (værdi: T): String {return value.toString ()}}

Vi erklærer, at en toString () metode bruger kun en type værdi T.

Dernæst kan vi tildele en reference af typen Nummer til referencen af ​​dens undertype - Dobbelt:

val parameterizedConsumer = ParameterizedConsumer () val ref: ParameterizedConsumer = parameterizedConsumer assertTrue (ref is ParameterizedConsumer)

Hvis typen T i ParameterizedCounsumer vil ikke være i type, vil det givne udsagn frembringe en kompilatorfejl.

4. Skriv fremskrivninger

4.1. Kopier en Array of Subtypes til en Array of Supertypes

Lad os sige, at vi har en matrix af en eller anden type, og vi vil kopiere hele arrayet til arrayet af Nogen type. Det er en gyldig handling, men for at tillade kompilatoren at kompilere vores kode skal vi kommentere inputparameteren med ud nøgleord.

Dette lader kompilatoren vide, at inputargumentet kan være af enhver type, der er en undertype af Nogen:

sjov kopi (fra: Array, til: Array) {assert (from.size == to.size) for (i in from.indices) til [i] = fra [i]}

Hvis den fra parameter er ikke af ud nogen type, kan vi ikke videregive en matrix af et Int skriv som argument:

val ints: Array = arrayOf (1, 2, 3) val any: Array = arrayOfNulls (3) copy (ints, any) assertEquals (any [0], 1) assertEquals (any [1], 2) assertEquals (any [ 2], 3)

4.2. Tilføjelse af elementer af en undertype til en række af dens supertype

Lad os sige, at vi har følgende situation - vi har en række Nogen type, der er en supertype af Int og vi vil tilføje en Int element til denne matrix. Vi er nødt til at bruge i nøgleord som en type af destinationsarrayet for at lade kompilatoren vide, at vi kan kopiere Int værdi til denne matrix:

sjov udfyldning (dest: Array, værdi: Int) {dest [0] = værdi}

Derefter kan vi kopiere en værdi af Int skriv til matrixen af Nogen:

val objekter: Array = arrayOfNulls (1) fyld (objekter, 1) assertEquals (objekter [0], 1)

4.3. Stjerneprognoser

Der er situationer, hvor vi ikke er ligeglade med den specifikke type værdi. Lad os sige, at vi bare vil udskrive alle elementerne i en matrix, og det betyder ikke noget, hvilken type elementerne i denne matrix er.

For at opnå dette kan vi bruge en stjerneprojektion:

sjov printArray (matrix: Array) {array.forEach {println (it)}}

Derefter kan vi videregive en matrix af enhver type til printArray () metode:

val array = arrayOf (1,2,3) printArray (array)

Når vi bruger stjerneprojektionens referencetype, kan vi læse værdier fra den, men vi kan ikke skrive dem, fordi det vil forårsage en kompileringsfejl.

5. Generiske begrænsninger

Lad os sige, at vi vil sortere en række elementer, og at hver elementtype skal implementere en Sammenlignelig interface. Vi kan bruge de generiske begrænsninger til at specificere dette krav:

sjovt  sorter (liste: Liste): Liste {return list.sorted ()}

I det givne eksempel definerede vi, at alle elementer T behov for at gennemføre Sammenlignelig interface. Ellers, hvis vi forsøger at videregive en liste over elementer, der ikke implementerer denne grænseflade, vil det forårsage en kompilatorfejl.

Vi definerede en sortere funktion, der som argument tager en liste over elementer, der implementeres Sammenlignelig, så vi kan ringe til sorteret () metode på det. Lad os se på test case for denne metode:

val listOfInts = listOf (5,2,3,4,1) val sorted = sort (listOfInts) assertEquals (sorteret, listOf (1,2,3,4,5))

Vi kan let sende en liste over Ints fordi Int typen implementerer Sammenlignelig interface.

5.1. Flere øvre grænser

Med vinkelbeslagnotationen kan vi højst erklære en generisk øvre grænse. Hvis en typeparameter har brug for flere generiske øvre grænser, skal vi bruge separate hvor klausuler for den pågældende type parameter. For eksempel:

sjov sortering (xs: Liste) hvor T: CharSequence, T: Sammenlignelig {// sorterer samlingen på plads}

Som vist ovenfor er parameteren T skal implementere CharSequence og Sammenlignelig grænseflader på samme tid. På samme måde kan vi erklære klasser med flere generiske øvre grænser:

klasse StringCollection (xs: Liste) hvor T: CharSequence, T: Sammenlignelig {// udeladt}

6. Generics ved kørselstid

6.1. Skriv sletning

Som med Java er Kotlins generiske produkter det slettet ved kørselstid. Det er, en forekomst af en generisk klasse bevarer ikke dens typeparametre ved kørsel.

For eksempel, hvis vi opretter en Sæt og sæt et par strenge i det, ved runtime kan vi kun se det som en Sæt.

Lad os oprette to Sæt med to forskellige typeparametre:

val bøger: Set = setOf ("1984", "Brave new world") val primes: Set = setOf (2, 3, 11)

Ved kørsel skal typeoplysningerne for Sæt og Sæt vil blive slettet, og vi ser dem begge som almindelige Sæt. Så selvom det er perfekt muligt at finde ud af ved kørsel, er værdien en Sæt, vi kan ikke fortælle, om det er en Sæt af strenge, heltal eller noget andet: disse oplysninger er blevet slettet.

Så hvordan forhindrer Kotlins kompilator os i at tilføje en Ikke-streng ind i en Sæt? Eller når vi får et element fra en Sæt, hvordan ved det, at elementet er en Snor?

Svaret er simpelt. Compileren er den, der er ansvarlig for at slette typeoplysningerne men før det kender den faktisk bøger variabel indeholder Snor elementer.

Så hver gang vi får et element fra det, ville compileren kaste det til en Snor eller når vi tilføjer et element i det, ville compileren skrive check indgangen.

6.2. Reified Type Parameters

Lad os have det sjovere med generiske stoffer og oprette en udvidelsesfunktion til filtrering Kollektion elementer baseret på deres type:

sjov Iterable.filterIsInstance () = filter {det er T} Fejl: Kan ikke kontrollere for eksempel af slettet type: T

Det "det er T ” del for hvert indsamlingselement kontrollerer, om elementet er en type forekomst T, men da typeoplysningerne er blevet slettet under kørsel, kan vi ikke reflektere over typeparametre på denne måde.

Eller kan vi?

Reglen om sletning af typen gælder generelt, men der er et tilfælde, hvor vi kan undgå denne begrænsning: Inline-funktioner. Typeparametre for integrerede funktioner kan være reified, så vi kan henvise til disse typeparametre under kørsel.

Kroppen af ​​indbyggede funktioner er inline. Det vil sige, kompilatoren erstatter kroppen direkte til steder, hvor funktionen kaldes i stedet for den normale funktionsopkald.

Hvis vi erklærer den tidligere funktion som inline og marker typeparameteren som reified, så kan vi få adgang til generiske typeoplysninger ved kørsel:

inline sjov Iterable.filterIsInstance () = filter {det er T}

Inline reification fungerer som en charme:

>> val set = setOf ("1984", 2, 3, "Brave new world", 11) >> println (set.filterIsInstance ()) [2, 3, 11]

Lad os skrive et andet eksempel. Vi kender alle disse typiske SLF4j Logger definitioner:

klasse bruger {privat val log = LoggerFactory.getLogger (Bruger :: class.java) // ...}

Ved hjælp af reified inline-funktioner kan vi skrive mere elegant og mindre syntaks-skræmmende Logger definitioner:

inline sjov logger (): Logger = LoggerFactory.getLogger (T :: class.java)

Så kan vi skrive:

klasse bruger {privat val log = logger () // ...}

Dette giver os en renere mulighed for at implementere logning, på Kotlin-måde.

6.3. Dyb dybt ned i inline reification

Så hvad er så specielt ved integrerede funktioner, så typegenifikation kun fungerer med dem? Som vi ved kopierer Kotlins kompilator bytekoden for inline-funktioner til steder, hvor funktionen kaldes.

Da kompileren kender den nøjagtige parametertype på hvert opkaldssted, kan den erstatte den generiske typeparameter med de faktiske typereferencer.

For eksempel når vi skriver:

klasse bruger {privat val log = logger () // ...}

Når kompilatoren placerer linjen logger () funktionsopkald, det kender den faktiske generiske type parameter -Bruger. Så i stedet for at slette typeoplysninger, griber kompilatoren genoprettelsesmuligheden og reificerer den aktuelle typeparameter.

7. Konklusion

I denne artikel så vi på Kotlin Generic-typerne. Vi så, hvordan man bruger ud og i nøgleord korrekt. Vi brugte typeprojektioner og definerede en generisk metode, der bruger generiske begrænsninger.

Implementeringen af ​​alle disse eksempler og kodestykker findes i GitHub-projektet - dette er et Maven-projekt, så det skal være let at importere og køre, som det er.


$config[zx-auto] not found$config[zx-overlay] not found