Menu
Coddy logo textTech

קווריאנטיות עם in ו-out

חלק מהיחידה תכנות מונחה עצמים במסלול ה-Kotlin של Coddy. שיעור 40 מתוך 57.

אפשר להעביר List<String> במקום שבו מצפים ל-List<Any>, כי List רק מוסיף למסור איברים. אי אפשר לעשות זאת עם MutableList<String>: קוד שמתייחס אליה כאל MutableList<Any> יכול להוסיף Int לרשימת מחרוזות:

fun describeAll(items: List<Any>) = items.joinToString { it::class.simpleName ?: "?" }
fun addNumber(items: MutableList<Any>) { items.add(42) }

בתוך main:

val words = listOf("a", "b")
println(describeAll(words))
val mixed = mutableListOf<Any>("x")
addNumber(mixed)
println(mixed)
// addNumber(mutableListOf("a"))   // שגיאה: MutableList<String> אינו MutableList<Any>

פלט:

String, String
[x, 42]

out T הופך מחלקה גנרית לקווריאנטית: היא רק מייצרת ערכים מסוג T כטיפוסי החזרה, ולעולם אינה מקבלת אותם. לכן Producer<Cat> נחשב ל-Producer<Animal>:

open class Animal(val name: String)
class Cat(name: String) : Animal(name)

class Producer<out T>(private val item: T) {
    fun get(): T = item
}

fun greet(p: Producer<Animal>) = "hello ${p.get().name}"

בתוך main:

val cats: Producer<Cat> = Producer(Cat("Tom"))
println(greet(cats))

פלט:

hello Tom

in T הופך אותה לקונטרה־וריאנטית: היא צורכת רק ערכי T, כפרמטרים. צרכן שמטפל בכל Animal יכול לשמש במקום צרכן של Cat:

open class Animal(val name: String)
class Cat(name: String) : Animal(name)

interface Feeder<in T> {
    fun feed(animal: T): String
}
class AnimalFeeder : Feeder<Animal> {
    override fun feed(animal: Animal) = "fed ${animal.name}"
}

fun feedCats(cats: List<Cat>, feeder: Feeder<Cat>) = cats.map { feeder.feed(it) }

בתוך main:

println(feedCats(listOf(Cat("Tom"), Cat("Kit")), AnimalFeeder()))

פלט:

[fed Tom, fed Kit]

המהדר אוכף את הכיוון: פרמטר טיפוס out אינו יכול להיות הטיפוס של פרמטר בשיטה, ופרמטר in אינו יכול להיות מוחזר. כלל האצבע הוא שמפיק משתמש ב־out, וצרכן משתמש ב־in. ספריית התקן פועלת באותו אופן: List<out E>, Comparator<in T>. ללא אף אחת ממילות המפתח, פרמטר טיפוס הוא אינווריאנטי, כמו ב־MutableList<E>, שגם קוראת וגם כותבת:

class Producer<out T>(private val item: T) {
    fun get(): T = item                  // תקין: T יוצא
    // fun set(x: T) { }                 // שגיאה: T הוגדר כ-out
}
interface Feeder<in T> {
    fun feed(animal: T): String          // תקין: T נכנס
    // fun last(): T                     // שגיאה: T הוגדר כ-in
}

אפשר לכתוב שונות גם במקום שבו משתמשים בטיפוס. MutableList<in Int> מקבלת כל רשימה ניתנת לשינוי שיכולה להכיל ערכי Int, כגון MutableList<Number>, ו-MutableList<out Number> מאפשרת קריאה בלבד:

fun fillWith(target: MutableList<in Int>, n: Int) {
    for (i in 1..n) target.add(i)
}
fun total(source: MutableList<out Number>) = source.sumOf { it.toDouble() }

בתוך main:

val numbers = mutableListOf<Number>(0.5)
fillWith(numbers, 3)
println(numbers)
println(total(mutableListOf(1, 2, 3)))

פלט:

[0.5, 1, 2, 3]
6.0
challenge icon

אתגר

קל

שני ממשקים מעבירים פריטים ממקום למקום: Source<out T> מפיק פריטים באמצעות next() (ומחזיר null כשהוא ריק), ו־Sink<in T> מקבל אותם באמצעות accept(item). הם מסופקים. כתבו את ListSource<T>(items), שמחזיר את הפריטים של הרשימה בזה אחר זה ואז את null; את Collector<T>, ששומר את הפריטים שהתקבלו ברשימת items לקריאה בלבד; ואת Counter, שהוא Sink<Any> שסופר את כל מה שהוא מקבל. לאחר מכן כתבו את pipe(from, vararg to), שמעביר כל פריט מהמקור לכל אחד מהכיורים ומחזיר את מספר הפריטים שהועברו.

הקוד המסופק מזרים שורה של מילים אל Collector<String> ושורה של מספרים אל Collector<Number>, ובשני המקרים גם אל Counter אחד. הדבר אפשרי בזכות out ו־in: ListSource<Int> הוא Source<Number>, ו־Sink<Any> הוא Sink<String>. הקוד מדפיס את הספירות, את שתי האוספים, את counted: 7 ואת after the end: null.

הקוד שלכם ייכתב בקבצים ListSource.kt, Collector.kt, Counter.kt, Pipe.kt, Source.kt ו־Sink.kt. הקובץ Main.kt מכיל את קוד הקלט/פלט המסופק, ואי אפשר לערוך אותו.

נסו בעצמכם

fun main() {
    // קוד הקלט/פלט שסופק: יש להשאיר אותו כפי שהוא
    val input = generateSequence(::readLine).toList()
    val words = ListSource(input[0].split(" "))
    val ints = ListSource(input[1].split(" ").map { it.toInt() })
    val counter = Counter()
    val allWords = Collector<String>()
    val numbers = Collector<Number>()
    println("words moved: " + pipe(words, allWords, counter))
    println("numbers moved: " + pipe(ints, numbers, counter))
    println(allWords.items)
    println(numbers.items)
    println("counted: ${counter.count}")
    println("after the end: ${words.next()}")
}
quiz iconבחנו את עצמכם

השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.

כל השיעורים ביחידה תכנות מונחה עצמים

תרגלו בעצמכם: Kotlin אונליין