Generiske stoffer i Kotlin

1. Oversikt

I denne artikkelen ser vi på generiske typer på Kotlin-språket.

De ligner veldig på Java-språket, men Kotlin-språkskaperne prøvde å gjøre dem litt mer intuitive og forståelige ved å introdusere spesielle nøkkelord som ute og i.

2. Opprette parametrerte klasser

La oss si at vi ønsker å lage en parameterisert klasse. Vi kan enkelt gjøre dette på Kotlin-språk ved å bruke generiske typer:

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

Vi kan opprette en forekomst av en slik klasse ved å sette en parameterisert type eksplisitt når vi bruker konstruktøren:

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

Heldigvis kan Kotlin utlede den generiske typen fra parametertypen, slik at vi kan utelate det når vi bruker konstruktøren:

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

3. Kotlin ute og i Nøkkelord

3.1. De Ute Nøkkelord

La oss si at vi ønsker å lage en produsentklasse som skal produsere et resultat av noen type T. Noen ganger; vi vil tildele den produserte verdien til en referanse som er av en supertype av typen T.

For å oppnå det ved hjelp av Kotlin, vi trenger å brukeute søkeord på generisk type. Det betyr at vi kan tildele denne referansen til hvilken som helst av dens supertyper. Ut-verdien kan bare produseres av den gitte klassen, men ikke konsumeres:

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

Vi definerte en ParameterizedProducer klasse som kan produsere en verdi av typen T.

Neste; vi kan tilordne en forekomst av ParameterizedProducer klasse til referansen som er en supertype av den:

val parameterizedProducer = ParameterizedProducer ("string") val ref: ParameterizedProducer = parameterisedProducer assertTrue (ref is ParameterizedProducer)

Hvis typen T i ParameterizedProducer klasse vil ikke være ute type, vil den gitte utsagnet produsere en kompilatorfeil.

3.2. De i Nøkkelord

Noen ganger har vi en motsatt situasjon som betyr at vi har en referanse av typen T og vi vil være i stand til å tildele den til undertypen T.

Vi kan bruke i nøkkelord på den generiske typen hvis vi vil tildele det til referansen til subtypen. De i nøkkelord kan bare brukes på parametertypen som forbrukes, ikke produseres:

class ParameterizedConsumer {fun toString (value: T): String {return value.toString ()}}

Vi erklærer at a toString () metoden vil bare konsumere en verdi av typen T.

Deretter kan vi tildele en referanse av typen Nummer til referansen til subtypen - Dobbelt:

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

Hvis typen T i ParameterizedCounsumer vil ikke være i type, vil den gitte utsagnet gi en kompilatorfeil.

4. Skriv projeksjoner

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

La oss si at vi har en matrise av en eller annen type, og vi vil kopiere hele matrisen til matrisen av Noen type. Det er en gyldig operasjon, men for å tillate kompilatoren å kompilere koden vår, må vi kommentere inngangsparameteren med ute nøkkelord.

Dette lar kompilatoren vite at inngangsargumentet kan være av hvilken som helst type som er en undertype av Noen:

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

Hvis den fra parameter er ikke av ut Eventuelt type, vil vi ikke kunne passere en matrise av en Int skriv inn 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. Legge til elementer av en undertype til en rekke supertyper

La oss si at vi har følgende situasjon - vi har en rekke Noen type som er en supertype av Int og vi vil legge til et Int element til denne matrisen. Vi må bruke i nøkkelord som en type destinasjonsmatrise for å la kompilatoren vite at vi kan kopiere Int verdi til denne matrisen:

morsom fyll (dest: Array, verdi: Int) {dest [0] = verdi}

Deretter kan vi kopiere en verdi av Int skriv til matrisen av Noen:

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

4.3. Stjerneprojeksjoner

Det er situasjoner når vi ikke bryr oss om den spesifikke verdien. La oss si at vi bare vil skrive ut alle elementene i en matrise, og det spiller ingen rolle hva typen elementene i denne matrisen er.

For å oppnå det kan vi bruke en stjerneprojeksjon:

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

Deretter kan vi overføre en rekke alle typer til printArray () metode:

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

Når vi bruker stjerneprojeksjonsreferansetypen, kan vi lese verdier fra den, men vi kan ikke skrive dem fordi det vil forårsake en kompileringsfeil.

5. Generiske begrensninger

La oss si at vi vil sortere en rekke elementer, og hver elementtype skal implementere en Sammenlignelig grensesnitt. Vi kan bruke de generiske begrensningene til å spesifisere dette kravet:

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

I det gitte eksemplet definerte vi at alle elementene T nødvendig for å implementere Sammenlignelig grensesnitt. Ellers, hvis vi prøver å sende en liste over elementer som ikke implementerer dette grensesnittet, vil det føre til en kompilatorfeil.

Vi definerte en sortere funksjon som tar som et argument en liste over elementer som implementeres Sammenlignbar, slik at vi kan kalle sortert () metode på den. La oss se på testsaken for den metoden:

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

Vi kan enkelt sende en liste over Ints fordi det Int typen implementerer Sammenlignelig grensesnitt.

5.1. Flere øvre grenser

Med vinkelbrakettnotasjonen kan vi maksimalt erklære en generisk øvre grense. Hvis en typeparameter trenger flere generiske øvre grenser, bør vi bruke separate hvor klausuler for den spesifikke typeparameteren. For eksempel:

morsom sortering (xs: Liste) der T: CharSequence, T: Sammenlignbar {// sorterer samlingen på plass}

Som vist ovenfor, er parameteren T må implementere CharSequence og Sammenlignelig grensesnitt samtidig. På samme måte kan vi erklære klasser med flere generiske øvre grenser:

klasse StringCollection (xs: List) der T: CharSequence, T: Sammenlignbar {// utelatt}

6. Generics at Runtime

6.1. Skriv sletting

Som med Java, er Kotlins generiske medisiner det slettet ved kjøretid. Det er, en forekomst av en generisk klasse bevarer ikke typeparametrene under kjøretid.

For eksempel hvis vi oppretter en Sett og legg noen strenger i den, mens vi er i stand til å se den som en Sett.

La oss lage to Settene med to forskjellige typeparametere:

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

Ved kjøretid, typeinformasjon for Sett og Sett vil bli slettet, og vi ser dem begge som enkle Settene. Så selv om det er fullt mulig å finne ut ved kjøretid at verdien er en Sett, vi kan ikke fortelle om det er en Sett av strenger, heltall eller noe annet: at informasjonen er slettet.

Så hvordan forhindrer Kotlins kompilator oss i å legge til en Ikke-streng inn i en Sett? Eller når vi får et element fra en Sett, hvordan vet det at elementet er en String?

Svaret er enkelt. Kompilatoren er den som er ansvarlig for å slette typeinformasjonen men før det vet den faktisk bøker variabel inneholder String elementer.

Så hver gang vi får et element fra det, vil kompilatoren kaste det til en String eller når vi skal legge til et element i det, vil kompilatoren skrive sjekke inngangen.

6.2. Reified Type Parameters

La oss ha det mer moro med generiske stoffer og lage en utvidelsesfunksjon for å filtrere Samling elementer basert på deres type:

moro Iterable.filterIsInstance () = filter {det er T} Feil: Kan ikke sjekke for eksempel av slettet type: T

det er T ” del, for hvert innsamlingselement, sjekker om elementet er en forekomst av typen T, men siden typeinformasjonen er slettet ved kjøretid, kan vi ikke reflektere over typeparametere på denne måten.

Eller kan vi?

Typen slettingsregelen gjelder generelt, men det er ett tilfelle der vi kan unngå denne begrensningen: Inline-funksjoner. Type parametere for innebygde funksjoner kan være reifisert, slik at vi kan referere til disse typeparametrene ved kjøretid.

Kroppen av innebygde funksjoner er inline. Det vil si at kompilatoren erstatter kroppen direkte til steder der funksjonen kalles i stedet for normal funksjonsinnkalling.

Hvis vi erklærer forrige funksjon som på linje og merk typeparameteren som reifisert, så kan vi få tilgang til generisk typeinformasjon ved kjøretid:

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

Den innebygde reifikasjonen fungerer som en sjarm:

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

La oss skrive et annet eksempel. Vi er alle kjent med de typiske SLF4j Logger definisjoner:

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

Ved å bruke reified inline-funksjoner kan vi skrive mer elegant og mindre syntaksforferdende Logger definisjoner:

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

Så kan vi skrive:

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

Dette gir oss et renere alternativ for å implementere logging, på Kotlin-måten.

6.3. Dyp dykk ned i Inline Reification

Så, hva er så spesielt med innebygde funksjoner, slik at typefifisering bare fungerer med dem? Som vi vet kopierer Kotlins kompilator bytekoden til innebygde funksjoner til steder der funksjonen kalles.

Siden kompilatoren i hvert anropsside vet den nøyaktige parametertypen, kan den erstatte den generiske typeparameteren med de faktiske typereferansene.

For eksempel når vi skriver:

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

Når kompilatoren legger inn logger () funksjonsanrop, den kjenner den faktiske generiske typen parameter -Bruker. Så i stedet for å slette typeinformasjonen, benytter kompilatoren reifikasjonsmuligheten og bekrefter den faktiske typeparameteren.

7. Konklusjon

I denne artikkelen så vi på Kotlin Generic-typene. Vi så hvordan du bruker ute og i søkeord ordentlig. Vi brukte typeprojeksjoner og definerte en generisk metode som bruker generiske begrensninger.

Implementeringen av alle disse eksemplene og kodebitene finnes i GitHub-prosjektet - dette er et Maven-prosjekt, så det skal være enkelt å importere og kjøre som det er.


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