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 Tomin 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.0Thử 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()}")
}
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
1Lớp và đối tượng
Làm việc với tệpĐịnh nghĩa lớpHàm khởi tạo chínhKhối init và kiểm tra tính hợp lệHàm khởi tạo phụÔn tập: thẻ thư viện4Kế thừa
Lớp mở và lớp conGhi đè thành viênsuper và hàm khởi tạoLớp trừu tượngÔn tập: Danh bạ nhân viên2Thuộc tính và tính đóng gói
Trình truy cập tùy chỉnhBộ sửa đổi phạm vi truy cậpSetter riêng tưlazy và lateinitÔn tập: tài khoản ngân hàng5Giao diện
Khai báo giao diệnPhương thức mặc địnhNhiều giao diệnGiao diện như kiểu dữ liệuÔn tập: Phương thức thanh toán8Kiểu tổng quát
Hàm tổng quátLớp tổng quátRàng buộc kiểuTính biến thiên với in và outÔn tập: Hàng đợi ưu tiênTự luyện tập: Playground Kotlin