1. Giris
Ilk defe out ve in ile qarsilasanda cox adam bele dusunmek isteyir: “Aha, bu ele bir hiylali magiyadir ki, generics irsiyyet kimi islesin”. Amma duzgunu eksine dusunmekdir: out/in — tehlukesiz olmayan bir seyi etmenize imkan vermemek ucun mexanizmdir, hetta cox isteseniz de, hetta “men diqqetliyem” desenz de.
Tesevvur edin ki, bizim telim konsol tetbiqimiz ucun kicik bir modul hazirlayirsiniz (qoy bu, xerc izləyicisi olsun). Biz ele kod yazmaq isteyirik ki, hem Record (baza qeyd), hem ExpenseRecord (xerc), hem de IncomeRecord (gelir) ile islesin. Irsiyyeti artiq bilirik, ve “konteynerlerin” de uyusmasini isteyirik.
Problem ondadir ki, konteynerler ferqli olur. Bir qisim konteynerler “qutu” kimidir — biz oradan deyeri gotururuk. Digerleri ise “qebul edici/qutusu” kimidir — biz ora deyeri qoyuruq. Bəzileri ise “her ikisi”dir — ve adətən invariyantliq ve qadagalar da mehz orada baslayir.
Bugunku leksiyanin esas ideyasi: elan yerinde variance (declaration-site variance) odur ki, siz oz generic-tipinizi elan edende kompilyatora duz-uzune deyirsiniz: “Bax, men T-ni yalniz bele istifade edeceyem”. Kotlin buna inanir — ve evezinde daha cevik alt-tiplik (subtyping) icaze verir. Resmi senedlesmede bu declaration-site variance kimi tesvir olunur ve Source<out T> numunesinde gosterilir: eger tip heqiqeten yalniz T-ni “verirse”, onu out ile kovariant etmek olar.
2. Declaration-site variance: out/in nece elan edilir ve tip pozisiyalari nedir
Indi oz generic-tiplerimizi yazacagiq ve bu, demek olar ki, “giris hüquqlarinin sazlanmasi”na benzer: sanki siz ele bir API qurursunuz ki, tehlukeli emeliyyatlari evvelden qadagan edirsiniz.
Vacib məqam: out ve in tip parametrinin elan olundugu yerde yazilir, yəni sinif/interfeys/funksional tipin basliginda, meselen class Box<out T> ... veya class Sink<in T> .... Bu, “bir funksiyada birdəfəlik hiylə” deyil, butun tip ucun qaydadir:
- eger T out kimi elan olunubsa, Kotlin ciddi sekilde yoxlayacaq ki, T hec yerde “giris” kimi istifade olunmasin (metod parametrlerinde, setter-lerde və s.);
- eger T in kimi elan olunubsa, Kotlin yoxlayacaq ki, siz T tipli deyeri “deqiq tip” kimi bayira cixarmaqa calismayasiniz.
Niye bu qeder ciddi? Cunki eks halda variativlik runtime-da qirilardi (Java-da vaxtile massivlerle etdikleri kimi, ve haminin hele de azca cani agriyir). Kotlin bele secir: “daha yaxsi, kompilyasiya olunmayim, nəinki size tip fəlaketi yaratmaq sansi verim”.
Kompilyatorun T-nin harada “in”, harada “out” oldugunu nece hell etdiyini anlamaq faydalidir. Praktikada bu, deyerle ne bas verdiyidir: siz onu bayira verirsiniz, yoxsa iceri qebul edirsiniz.
Asagida — kicik “sifre vərəqi” cədvəli var. Bu, anlayisi əvəz etmir, amma kompilyator xətasina baxib “yenə nə istəyir?” deyəndə cox komek edir.
| T-nin rast gəldiyi yer | Bu T ne kimi… | Nümunə | out T olduqda icazəlidir | in T olduqda icazəlidir |
|---|---|---|---|---|
| Funksiyanin qaytardigi tip | out (bayira veririk) | |
he | xeyr |
| Funksiya parametri | in (iceri qebul edirik) | |
xeyr | he |
| val-xassesi, tipi T | out (getter) | |
he | xeyr |
| var-xassesi, tipi T | hem out, hem in | |
xeyr | xeyr (umumi halda) |
Burada vacib “umumi halda” qeydi var: bezen dizayni deyissəniz, qadaganı dolanmaq olar. Meselen, consumer dəyərleri Any? kimi saxlaya biler, cunki Any? T tipini ved etmir. Producer ise T-ni deyil, tip parametri ile bagli olmayan baska seyi qebul ede biler (meselen, “veziyyeti sifirla”).
3. out T: kovariyantliq ve producer
Indi out-u praktika uzerinde baxacagiq — bu, en “intuitiv” variant olacaq, cunki o, artiq islediyiniz List-e benzer. Bundan sonra adeten daha cox “beyni qarışdıran” in-i anlamaq asanlasacaq.
Nümunələr ucun mini-domen
Nümunələr “pisik ve it” haqqinda olmasin (baxmayaraq ki, pisikler ve itler elinden geleni edir), maliyye ucotu tetbiqimizde yasaya bilecek tiplere oxsar nese quraq:
open class Record(val id: Int, val comment: String)
class ExpenseRecord(
id: Int,
comment: String,
val amount: Int
) : Record(id, comment)
Burada ExpenseRecord — Record-un alt-tipidir. Demeli, ExpenseRecord-u Record gözlənilən yerde istifade etmek olar (bu, adî irsiyyetdir, bizde artiq var idi).
“Snapshot” kimi producer: Snapshot<out T>
Indi ele bir tip yaradaq ki, yalniz deyeri saxlasin ve qaytarsin. Bu, “hesablama neticesinin surəti”, “server cavabi”, “hazir deyer” kimi seylerə cox bənzəyir.
class Snapshot<out T>(private val value: T) {
fun get(): T = value
}
Diqqet edin: T burada yalniz out-pozisiyada istifade olunur — get()-den qaytarilir. Bu, out-un tələb etdiyı ve’din eynidir.
Alt-tipliyi yoxlayaq:
fun main() {
val expenseSnapshot: Snapshot<ExpenseRecord> =
Snapshot(ExpenseRecord(1, "Coffee", 250))
val recordSnapshot: Snapshot<Record> = expenseSnapshot
println(recordSnapshot.get().comment) // Coffee
}
Niye bu tehlukesizdir? Cunki recordSnapshot uzerinden iceri “sehv Record” qoya bilmirik. Biz yalniz oxuya bilirik, ve Snapshot<ExpenseRecord>-dan Record oxumaq tam normaldir: “xerc” hem de “qeyd”dir.
Mehz bu mentiq Kotlin-in read-only kolleksiyalarinin kovariant olmasinin esasindadir (onlar “deyer menbeyidir”). Senedlesme birbasa qeyd edir ki, read-only kolleksiyalar kovariantdir.
out nəyi qadagan edir: T-ni iceri qebul etmek olmaz
Indi isə Snapshot-u “yaxsilasdirmaq” isteyək: set(newValue: T) metodu elave edek — deyeri deyismek ucun.
class Snapshot<out T>(private var value: T) {
fun get(): T = value
// Bele olmaz — ideya numunesi, kompilyasiya olunmur:
// fun set(newValue: T) { value = newValue }
}
Kotlin bunu qadagan edecek (ve bu yaxsidir). Mentik belədir: eger Snapshot<ExpenseRecord>-u Snapshot<Record>-a menimsətmek olursa, sonra isə Snapshot<Record> tipli dəyişən vasitəsilə set(Record(...)) cagirmaq mümkün olsaydi, biz “adi Record”u, əslində ExpenseRecord saxlayan obyektin icine qoymus olardiq. Tip tehlukesizliyi dagilardi.
Demeli, out — cevikliyin qiymetidir: tip ierarxiyasinda “yuxari qaldirma” (upcast) alirsiniz, amma T-ni giris kimi tehlukesiz qebul etme imkanini itirirsiniz.
Tuzaq: var demek olar ki, her zaman out-u qirir
Yeni baslayanlar ucun en tez-tez olan surprizlerden biri: siz out T elan etmisiniz, sonra var value: T xassesini yazirsiniz ve birdən kompilyator narazi olur. Sebeb ondadir ki, var — hem getter, hem setter demekdir. Getter T qaytarir (out-pozisiya), setter ise T qebul edir (in-pozisiya). Amma out teleb edir ki, T yalniz out kimi istifade olunsun.
Ideya-numuneye baxin:
// Kompilyasiya olunmur — tuzaq kimi gosterilib:
// class Box<out T>(var value: T)
Eger sizə “producer” lazimdirse, adətən val (yalniz oxuma) edin və ya saxlamani iceri gizledib metodlarla verin.
4. in T: kontravariyantliq ve consumer
Eger out adeten “mentiqlidir” kimi hiss olunursa, in ilk baxisda “kompilyator istehza edir” kimi gorune biler. Indi bunu ele izah edek ki, kompilyator cani pis olan deyil, tip tehlukesizliyi klubunun girisinde duran muhafizeci kimi gorsensin.
Consumer ideyasi: biz yalniz T-ni qebul edirik
Ele bir tip quraq ki, qebul etsin T tipli dəyərləri, amma ozunden bayira T tipli hec ne vermesin.
Meselen, “formatter” (obyekti alir ve onu metne cevirir). Bu, bizim konsol tetbiqimiz ucun cox tetbiqidir: biz daim nese cap edirik.
class Formatter<in T>(private val toText: (T) -> String) {
fun format(value: T): String = toText(value)
}
Diqqet edin: T burada format(value: T) parametrinde durur — bu in-pozisiyadir. Hem də T funksiyanin girisinde (T) -> String dayanir, ve funksiyanin girisi de “in” kimi davranir.
Niye alt-tiplik “tersine cevrilir”
Indi en maraqli hissə: hansisi hansinin alt-tipidir?
Eger ExpenseRecord — Record-un alt-tipidirse, o zaman Formatter<Record> Formatter<ExpenseRecord>-un alt-tipidir, cuniki Formatter — consumer-dir (kontravariantdir).
Qəribə səslənir, amma mentiq sadədir: eger obyekt istənilən Record-u formatlaya bilirsə, o, mütləq ExpenseRecord-u da formatlaya bilir (cunki xerc — qeydın xüsusi halidir).
fun main() {
val recordFormatter: Formatter<Record> =
Formatter { r -> "#${r.id}: ${r.comment}" }
val expenseFormatter: Formatter<ExpenseRecord> = recordFormatter
val e = ExpenseRecord(10, "Taxi", 900)
println(expenseFormatter.format(e)) // #10: Taxi
}
Biz out ile muqayisede “əks istiqamətə” menimsetmə etdik. Bu normaldir: consumer ucun her sey “guzgu” kimidir.
in nəyi qadagan edir: T-ni deqiq tip kimi bayira vermek olmaz
Gelin ele bir consumer duzeldek ki, “son qəbul etdiyi elementi saxlasın” ve onu bayira qaytarsın:
class Sink<in T> {
// Qaytarishda T istəsəniz, kompilyasiya olunmayacaq ideya:
// private var last: T? = null
// fun put(x: T) { last = x }
// fun last(): T? = last
}
Niye olmaz? Cunki eger sizde Sink<Record> var ve onu Sink<ExpenseRecord> tipli deyisene menimsetmek olursa (bu in ile icazelidir), onda kimse dusunecek ki, last() ExpenseRecord? qaytaracaq. Amma real obyekt Sink<Record>-dur — ora istənilən Record qoyula bilerdi, mutleq xerc olmaq mecburiyyetinde deyil.
Buna gore Kotlin consumer-tiplerin qebul etmesine icaze verir, amma bayira deqiq T “ved etmeye” icaze vermir.
5. Kotlin-de out/in ile artiq harada rastlasmisdiq
Xos məqam: siz artiq variances-dan istifade etmisiniz, sadəcə bunu görməmisiniz. Kotlin calisir ki, “duzgun” variativlik standart kitabxanada hazir olsun ve sizin daim out/in-i əl ilə yazmaginiz gerekmesin.
List<out T> read-only producer kimi
Read-only List — kovariantdir, ona gore de List<ExpenseRecord>-u List<Record> kimi istifade etmek olar. Bu, yuxarida muzakire etdiyimiz producer mentiqidir. Senedlesmede bele deyilir: read-only kolleksiyalar kovariantdir.
Comparable<in T> consumer kimi
Comparable-da tip parametri kontravariantdir: Comparable<in T>. Senedlesmede numune bele qurulur ki, Comparable<Number>-u Comparable<Double>-a menimsetmek olar, cunki Number-u muqayise etmeyi bacaran obyekt Double-u da muqayise ede bilir.
Hətta muqayise riyaziyyatina derinden girmek istemeseniz de, formani yadda saxlamaq faydalidir: muqayise olunan tip compareTo(other: T)-nin girisinde durur, demeli bu consumer → in.
Funksiya tipləri: giris — in, cixis — out
Funksiya tipi (A) -> B variances ucun cox aydin modeldir: funksiya A tipli arqumenti istehlak edir ve B tipli netice istehsal edir. Ona gore giris in kimi, cixis ise out kimi davranir.
Praktiki fikir: eger sizde Record qebul ede bilen funksiya varsa, o avtomatik olaraq ExpenseRecord-u da qebul ede bilir (cunki xerc — qeyd dir). Buna gore bele funksiyani xerc emal edicisi gözlənilən yerde istifade etmek olar:
fun main() {
val handleRecord: (Record) -> String = { r -> "Handled: ${r.comment}" }
val handleExpense: (ExpenseRecord) -> String = handleRecord
println(handleExpense(ExpenseRecord(7, "Lunch", 600))) // Handled: Lunch
}
Eger indi “bu keçən leksiyadakı numuneye bənzəyir” diye dusundunuzse — bəli. Sadəcə indi gorursunuz ki, bu tesaduf deyil: eyni consumer/producer prinsipi, sadece funksiya formasinda.
6. Tetbiqi refaktorinq: melumat menbeyi ve cixis emalcisi
Bugunku movzu “boşluqda” qalmasin deye, gelin onu konsol tetbiqinin mentiqine səliqə ilə daxil edek. Biz “esrler ucun” arxitektura qurmuruq (bu ayrica fəndir), amma bir kicik, aydin sey edek: melumatların alinmasini cixisin formatlanmasindan ayiraq — out/in vasitəsilə.
Hesabat melumatlari producer-i: ReportSource<out T>
Hesabat ucun elementləri verən menbe yaradaq. Bu producer-dir, demeli out.
class ReportSource<out T>(private val items: List<T>) {
fun firstOrNull(): T? = items.firstOrNull()
}
Istifade edek:
fun main() {
val expenses = listOf(
ExpenseRecord(1, "Coffee", 250),
ExpenseRecord(2, "Book", 1200)
)
val expenseSource: ReportSource<ExpenseRecord> = ReportSource(expenses)
val recordSource: ReportSource<Record> = expenseSource
println(recordSource.firstOrNull()?.comment) // Coffee
}
Burada her sey mentiqlidir: xerc menbeyini qeyd menbeyi kimi istifade etmek olar, cunki o yalniz “verir”.
Cixis consumer-i: Printer<in T>
Indi T-ni qebul edib konsola cixaran “cap eden” obyekt duzeldək. Bu consumer-dir, demeli in.
class Printer<in T>(private val toLine: (T) -> String) {
fun print(value: T) {
println(toLine(value))
}
}
Tiplerin yerinə qoyulmasini yoxlayaq:
fun main() {
val printRecord = Printer<Record> { r -> "Record: ${r.comment}" }
val printExpense: Printer<ExpenseRecord> = printRecord
printExpense.print(ExpenseRecord(3, "Taxi", 900)) // Record: Taxi
}
“Istənilən Record” cap ede bilen obyekt, “yalniz ExpenseRecord” cap etmek lazim olan yerde uyğundur. Bu da in-in kontravariyantligidir.
Producer ve consumer-i bir funksiyada birlesdiririk
Indi ele funksiya yaza bilerik ki, “menbe” ve “printer” götürsün ve birinci elementi cap etsin. Burada out ve in-in eyni yerde nece “dostlasdigini” gormek faydalidir:
fun printFirst(
source: ReportSource<Record>,
printer: Printer<Record>
) {
val first = source.firstOrNull()
if (first != null) {
printer.print(first)
}
}
Indi ise tehlukesiz oldugu yerde “daha konkret” ve “daha umum” seyleri yerinə qoyaraq istifade edek:
fun main() {
val expenses = listOf(ExpenseRecord(1, "Coffee", 250))
val sourceExpenses = ReportSource(expenses) // ReportSource<ExpenseRecord>
val printerRecords = Printer<Record> { "-> ${it.comment}" }
printFirst(sourceExpenses, printerRecords) // -> Coffee
}
Bu niye umumen kompilyasiya olunur, axı printFirst ReportSource<Record> gozləyir, biz ise ReportSource<ExpenseRecord> veririk? Cunki ReportSource out kimi elan olunub, yəni kovariantdir. Printer niye kompilyasiya olunur? Cunki Printer in kimi elan olunub, yəni kontravariantdir.
7. out ve in dizayninda tipik sehvler
Sehv №1: out qoyub, sonra “normal mutable-konteyner” yaratmaga calismaq.
Cox yaygin vəziyyət: siz class Box<out T> yazirsiniz, sonra fun set(x: T) elave edirsiniz veya var value: T edirsiniz. Kompilyator narazidir ve ele gelir ki, “heyatinizi qirir”. Əslinde ise sizi mentiqi deşikden qoruyur: kovariant tip T-nin qebul edicisi kimi tehlukesiz istifade edile bilmez. Əgər sizə mutabellik lazimdirsa, ya tipi invariyant edin (class Box<T>), ya da rollari producer ve consumer kimi ayrica tiplere bolun.
Sehv №2: in qoyub, sonra T-ni deqiq tip kimi bayira qaytarmaq istemek.
Consumer tipde tez-tez “son qəbul olunan elementi qaytarmaq” arzusu yaranir. Amma tip kontravariant olanda, qaytarilan T tehlukeli ved olur: xaricden, iceri real daxil ola bilmis tipden daha konkret tip gozlenilecek. Belə hallarda ya daha umum bir sey qaytarin (Any?), ya da dizayni deyisin: consumer mecbur deyil ki, qəbul etdiyini “cixarsin”.
Sehv №3: dusunmek ki, out obyekti dəyişməz (immutable) edir.
out “immutable” demek deyil. O, “API-mda tip parametri yalniz cixisda istifade olunur” demekdir. Siz T olmadan daxili veziyyeti deyisen metodlara sahib ola bilersiniz (meselen, clear()), ve bununla yanaşı tip kovariant qalacaq. Variance yalniz tip tehlukesizliyini zemanet edir, mutasiyanin olmamasini yox — bunlar ferqli anlayislardir.
Sehv №4: kompilyasiya olunsun deye out veya in-i “tesadufi” secmek.
out ve in “tip xetalarinin dərmani” deyil, mena tesviridir. Guman etmeyi dayandirmaqin yaxşı yolu — her defe bu suali dilə getirmekdir: mənim tipim istehsal edir T-ni, yoxsa istehlak edir T-ni? Istehsal edirse (oxuyuruq, aliriq, qaytaririq) — out. Istehlak edirse (qebul edir, elave edir, girisi emal edir) — in. Her ikisidirsə — böyük ehtimalla invariyantliq olacaq ve dizayni başqa cür qurmaq lazım gələcək.
Sehv №5: təəccüblənmək ki, var həm out-u, həm də in-i qirir.
var x: T xassesi eyni anda hem T-ni verir (getter), hem de T-ni qebul edir (setter). Buna gore out T olan tipde setter-e gore qadagandir, in T olan tipde ise getter-e gore qadagandir. Eger siz dəyəri saxlamaq, amma variativliyi saxlamaq isteyirsinizse, adeten bele komek edir: out ucun val istifade edin, in ucun ise icerde T deyil, daha umum tip (Any?) saxlayin ve ya umumen saxlamayin ( “temiz emalci” edin).
GO TO FULL VERSION