Menu
CoddyTech

Tính biến thiên với in và out

Thuộc phần Lập trình hướng đối tượng trong hành trình Kotlin của Coddy. Bài học 40 trên 57.

Có thể truyền một List<String> vào nơi cần List<Any>, vì List chỉ đưa các phần tử (element) ra ngoài. Không thể làm vậy với MutableList<String>: code xem nó như MutableList<Any> có thể thêm một Int vào danh sách chuỗi:

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

Bên trong main:

val words = listOf("a", "b")
println(describeAll(words))
val mixed = mutableListOf<Any>("x")
addNumber(mixed)
println(mixed)
// addNumber(mutableListOf("a"))   // lỗi: MutableList<String> không phải là MutableList<Any>

Đầu ra:

String, String
[x, 42]

out T khiến một lớp generic đồng biến: lớp này chỉ tạo ra các giá trị T làm kiểu trả về và không bao giờ nhận chúng làm đầu vào. Khi đó, Producer<Cat> được xem là 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}"

Bên trong main:

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

Đầu ra:

hello Tom

in T khiến kiểu này trở thành nghịch biến: nó chỉ nhận các giá trị T làm tham số. Một bộ nhận có thể xử lý mọi Animal có thể thay thế bộ nhận 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) }

Bên trong main:

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

Đầu ra:

[fed Tom, fed Kit]

Trình biên dịch kiểm soát chiều: tham số kiểu out không thể là kiểu của tham số phương thức, còn tham số kiểu in không thể là kiểu trả về. Quy tắc chung là bên cung cấp dùng out, bên tiếp nhận dùng in. Thư viện chuẩn cũng làm như vậy: List<out E>, Comparator<in T>. Nếu không có từ khóa nào trong hai từ khóa này, tham số kiểu sẽ bất biến, như trong MutableList<E>, vốn vừa đọc vừa ghi:

class Producer<out T>(private val item: T) {
    fun get(): T = item                  // Hợp lệ: T là đầu ra
    // fun set(x: T) { }                 // lỗi: T được khai báo là out
}
interface Feeder<in T> {
    fun feed(animal: T): String          // Hợp lệ: T là đầu vào
    // fun last(): T                     // lỗi: T được khai báo là in
}

Tính biến thiên cũng có thể được viết ở nơi dùng kiểu dữ liệu. MutableList<in Int> chấp nhận mọi danh sách có thể thay đổi có thể chứa giá trị Int, chẳng hạn như MutableList<Number>, còn MutableList<out Number> chỉ cho phép đọc:

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() }

Bên trong main:

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

Đầu ra:

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

Thử thách

Dễ

Hai giao diện di chuyển các phần tử: Source<out T> tạo ra các phần tử bằng next() (trả về null khi danh sách trống), còn Sink<in T> tiếp nhận chúng bằng accept(item). Hai giao diện này đã được cung cấp. Hãy viết ListSource<T>(items), trả về lần lượt từng phần tử trong danh sách rồi trả về null; Collector<T>, lưu các phần tử được tiếp nhận trong danh sách items chỉ đọc; và Counter, một Sink<Any> đếm mọi thứ nó tiếp nhận. Sau đó, hãy viết pipe(from, vararg to), hàm này chuyển từng phần tử của nguồn vào tất cả các sink và trả về số phần tử đã chuyển.

Code được cung cấp chuyển một dòng từ gồm các từ vào Collector<String> và một dòng số vào Collector<Number>, đồng thời chuyển cả hai dòng vào cùng một Counter. Điều đó hoạt động nhờ out và in: ListSource<Int> là một Source<Number>, còn Sink<Any> là một Sink<String>. Code in ra số lượng, cả hai bộ sưu tập, counted: 7 và after the end: null.

Code của bạn nằm trong các tệp ListSource.kt, Collector.kt, Counter.kt, Pipe.kt, Source.kt và Sink.kt. Main.kt chứa code đầu vào/đầu ra được cung cấp và bạn không thể chỉnh sửa tệp này.

Tự làm thử

fun main() {
    // Đoạn code đầu vào/đầu ra có sẵn: giữ nguyên
    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 iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Playground Kotlin