写了这么久 Kotlin,你真的理解 in、out 和 * 吗?

各位 Kotlin 爱好者们,早上好。

今天我们深入研究一下 Kotlin 泛型。

跟其他语言一样,Kotlin 通过泛型来处理不同的类型。你可以定义适用于不同类型的类、方法和属性,让代码更容易复用,同时保留类型安全。

我们可以把泛型理解成一个能装不同东西的盒子:水果、玩具、书都能装,不需要为每种东西单独设计一个盒子。泛型容器和函数也是如此,它们能处理不同类型的数据,还能在程序运行前帮你发现类型错误。

这篇文章从简单例子开始,逐步介绍 Kotlin 开发者需要了解的泛型基础。

各代码块按独立示例阅读,重复的类名、函数名和 main 不需要合并到同一个文件;健康动物示例会复用前一个示例中的 Animal。

什么是泛型

泛型允许类、接口、方法和属性使用稍后才确定的类型。需要处理多种数据类型,又希望保留类型安全时,就可以用它。

例如:你需要一个容器,能存放整数、字符串或自定义对象。与其为每种类型写一个类,不如用泛型定义一个可复用的容器类。

在 Kotlin 中,同一个泛型类、方法或属性可以处理不同类型的数据,减少重复代码。

基本语法

Kotlin 泛型类和泛型函数的基本写法如下:

kotlin 复制代码
class Box<T>(val content: T)

fun <T> printContent(content: T) {
    println(content)
}

这里的 T 是类型参数,使用类或函数时,可以用具体类型替换它。

下面定义一个简单的泛型类 Box:

kotlin 复制代码
class Box<T>(val value: T) {
    fun getContent(): T {
        return value
    }
}

fun main() {
    val intBox = Box(10)
    val stringBox = Box("Hello")
    println(intBox.getContent()) // Output: 10
    println(stringBox.getContent()) // Output: Hello
}

这个 Box 可以存放整数、字符串,也可以存放其他类型的内容。同一份类定义可以用于不同数据类型。

例如,购物车系统可以用这样的 Box 存放不同类型的商品,包括电子产品、书籍和食品。

泛型的好处

类型安全:编译器会检查类型,降低运行时出现 ClassCastException 的风险。

代码复用:用同一个类或函数处理不同类型,不需要重复实现。

类型转换与性能:泛型可以减少调用方手写的类型转换,但不能据此保证性能更好。JVM 泛型存在类型擦除,编译器仍可能插入类型转换,基本类型还可能涉及装箱。

可读性:相比直接使用 Any,泛型能更清楚地表达类型之间的关系和代码意图。

泛型函数

泛型也可以用于函数,让同一个函数处理不同类型的数据。

例如:你需要打印任意列表中的元素。整数列表、字符串列表等都可以交给同一个泛型函数,不必分别实现。

kotlin 复制代码
fun <T> printList(items: List<T>) {
    for (item in items) {
        println(item)
    }
}

fun main() {
    val intList = listOf(1, 2, 3)
    val stringList = listOf("a", "b", "c")
    
    println("Printing integer list:")
    printList(intList) // Output: 1, 2, 3
    
    println("Printing string list:")
    printList(stringList) // Output: a, b, c
    
    // Works with custom types too
    data class User(val id: Int, val name: String)
    val userList = listOf(
        User(1, "Alice"),
        User(2, "Bob"),
        User(3, "Charlie")
    )
    
    println("Printing user list:")
    printList(userList) // Output: User(id=1, name=Alice), User(id=2, name=Bob), User(id=3, name=Charlie)
}

printList 可以接收不同元素类型的列表并打印内容,包括自定义类型的列表。

进阶用法

泛型函数还可以实现转换和筛选等操作:

kotlin 复制代码
// Transform a list of one type to another type
fun <T, R> List<T>.transformList(transform: (T) -> R): List<R> {
    return this.map { transform(it) }
}

// Find elements in a list that match a condition
fun <T> List<T>.findElements(predicate: (T) -> Boolean): List<T> {
    return this.filter { predicate(it) }
}

fun main() {
    val numberList = listOf(1, 2, 3, 4, 5)
    
    // Square each number
    val squaredNumbers = numberList.transformList { it * it }
    println("Squared numbers: $squaredNumbers") // Output: [1, 4, 9, 16, 25]
    
    // Find even numbers
    val evenNumbers = numberList.findElements { it % 2 == 0 }
    println("Even numbers: $evenNumbers") // Output: [2, 4]
    
    // Works with any type
    data class Product(val name: String, val price: Double)
    val products = listOf(
        Product("Laptop", 1200.0),
        Product("Phone", 800.0),
        Product("Headphones", 150.0)
    )
    
    // Transform products to their names
    val productNames = products.transformList { it.name }
    println("Product names: $productNames") // Output: [Laptop, Phone, Headphones]
    
    // Find products under $500
    val affordableProducts = products.findElements { it.price < 500.0 }
    println("Affordable products: $affordableProducts") // Output: [Product(name=Headphones, price=150.0)]
}

默认值处理

Kotlin 允许函数参数声明默认值,泛型函数也一样。下面的例子展示的是两种提供回退值的方式:由调用方传入,或根据类型选择;它们没有在参数声明中使用 = 默认值。

使用场景:从数据库读取一个值,如果没有找到,就返回默认值。用泛型可以让这套逻辑适用于不同类型的数据。

提供回退值的泛型函数示例:

kotlin 复制代码
// Version 1: Explicit fallback value
fun <T> getValueOrDefault(value: T?, defaultValue: T): T {
    return value ?: defaultValue
}

// Version 2: More advanced approach with type-specific defaults
inline fun <reified T> getValueOrDefault(value: T?): T {
    return value ?: when (T::class) {
        String::class -> "" as T
        Int::class -> 0 as T
        Boolean::class -> false as T
        Double::class -> 0.0 as T
        List::class -> emptyList<Any>() as T
        Map::class -> emptyMap<Any, Any>() as T
        else -> throw NotImplementedError("No default value for type ${T::class.simpleName}")
    }
}

fun main() {
    // Version 1: Using explicit defaults
    println(getValueOrDefault("Hello", "Default")) // Output: Hello
    println(getValueOrDefault(null, "Default"))    // Output: Default
    
    // Version 2: Using type-specific defaults (explicit type shown here)
    println(getValueOrDefault<String>("Hello"))    // Output: Hello
    println(getValueOrDefault<String>(null))       // Output: "" (empty string)
    println(getValueOrDefault<Int>(null))          // Output: 0
    println(getValueOrDefault<Boolean>(null))      // Output: false
    
    // Custom types with Version 1
    data class User(val name: String)
    val defaultUser = User("Anonymous")
    println(getValueOrDefault(null, defaultUser))  // Output: User(name=Anonymous)
}

这个例子给出了两个版本:

版本一:调用方显式传入默认值,可以用于任意类型。

版本二:使用 reified 类型参数,根据类型自动选择默认值。

版本一每次调用都要传入默认值,例如 getValueOrDefault("Hello", "Default")。

版本二不需要传入默认值。示例显式指定了类型;如果编译器能从实参或上下文推断出 T,也可以省略类型实参。

你可以根据需要选择:显式提供默认值,或由函数根据类型选择默认值。第二种写法只支持 when 中列出的类型,其余类型在值为 null 时会抛出异常。

第二种写法只是有限类型的示例,不能当作通用默认值工厂。T::class 不包含 List<String> 等类型的元素类型信息,emptyList() 和 emptyMap() 返回的也是只读集合。Kotlin/JVM 上,可变与只读集合映射到相同的 Java 接口,仅判断 T::class 无法区分它们;例如请求 MutableList<Int> 的默认值时,这里的转换会失败。需要可变集合或其他类型时,应使用版本一,显式提供符合 T 的回退值。

这里要区分两个问题:

普通泛型函数可以接收一个 T 类型的回退值;参数默认值也可以是表达式或函数调用,并不要求必须是具体常量。

如果要在函数体中使用 T::class 判断类型,则需要 inline 和 reified。

不变性(Invariance)

不变性(Invariance)指的是:即使两个类型参数之间存在子类型关系,使用它们的同一个泛型类型也不会因此产生子类型关系。

Kotlin 自定义泛型类型默认是不变的。

例如:如果容器类型是不变的,那么存放字符串的容器就不能直接传给接收 Container<Any> 的函数。这种限制用于保证类型安全。

这里需要区分具体声明:Kotlin 的只读 List 声明了协变,List<String> 可以赋给 List<Any>;MutableList 则是不变的。

kotlin 复制代码
class Container<T>(val value: T)

fun main() {
    val stringContainer: Container<String> = Container("Hello")
    val anyContainer: Container<Any> = stringContainer // Error: Type mismatch
    val anyContainerCorrect: Container<Any> = Container<Any>("Hello")
    println(anyContainerCorrect.value) // Output: Hello
}

Container<String> 不是 Container<Any> 的子类型,这体现了自定义泛型类型默认的不变性。上面的错误赋值是故意保留的反例,运行其余代码前需要注释掉这一行。

型变(Variance)

Kotlin 使用 out、in 和星投影 * 来表达泛型的型变与投影。

型变是指:类型参数之间的子类型关系,如何影响包含这些参数的泛型类型之间的关系。

1. out:协变

协变会保留类型参数之间的子类型关系。例如,Apple 是 Fruit 的子类型,那么协变类型 Producer<Apple> 也是 Producer<Fruit> 的子类型。

Kotlin 用 out 声明这种关系。

例如,你有一个 Apple 列表,需要将它传给接收只读 List<Fruit> 的方法。

kotlin 复制代码
open class Fruit

class Apple : Fruit()

class FruitProducer<out T : Fruit>(private val fruit: T) {
    fun produce(): T {
        return fruit
    }
}

fun main() {
    val appleProducer: FruitProducer<Apple> = FruitProducer(Apple())
    val fruitProducer: FruitProducer<Fruit> = appleProducer
    println(fruitProducer.produce()) // Prints an Apple instance; exact text varies
}

FruitProducer<out T> 声明了协变,因此可以把 FruitProducer<Apple> 赋给 FruitProducer<Fruit>。

这个很好理解,对于生产者(向外产生值),假设我规定了只能给你水果(fruitProducer),那么无论我给你苹果(appleProducer),还是梨子,都是合理的。

处理集合和类型层级时,这种关系很有用。

2. in:逆变

逆变会反转类型参数之间的子类型关系。

例如,Apple 是 Fruit 的子类型,那么逆变类型 Consumer<Fruit> 是 Consumer<Apple> 的子类型(倒反天罡)。

Kotlin 用 in 声明这种关系,常用于消费输入值的类型。

例如:一个消费者能处理所有 Fruit,当然也能处理 Apple。

逆变允许你把这个 Fruit 消费者用在要求 Apple 消费者的位置。单纯给接收 Fruit 的函数传入 Apple,本身只需要普通的子类型关系。

kotlin 复制代码
open class Fruit

class Apple : Fruit()

class FruitConsumer<in T : Fruit> {
    fun consume(fruit: T) {
        println("Consuming a fruit")
    }
}

fun main() {
    val fruitConsumer: FruitConsumer<Fruit> = FruitConsumer()  // FruitConsumer can consume any Fruit
    val appleConsumer: FruitConsumer<Apple> = fruitConsumer    // Contravariance allows this

    appleConsumer.consume(Apple())  // Output: Consuming a fruit
}

FruitConsumer<in T> 允许把 FruitConsumer<Fruit> 赋给 FruitConsumer<Apple>。这样,能处理一般类型的消费者也可以用于只要求处理某个子类型的场景。

用上面这个例子来讲,我说我可以处理水果(fruitConsumer),那么肯定也能处理苹果呀(appleConsumer)!

如果还不好理解,可以想象一个 MutableList<Fruit>:既然能添加水果,那么也能添加苹果。你可以把它赋给 MutableList<in Apple>,通过这个引用添加 Apple,但读取元素时只能得到 Any?。

同时使用 out 和 in

下面通过接口,把协变和逆变放在同一个例子中:

例如,系统既需要生产不同类型的对象,也需要消费它们:工厂生产 Fruit,商店消费这些对象。

kotlin 复制代码
open class Fruit

class Apple : Fruit()

interface Source<out T> {
    fun next(): T
}

interface Sink<in T> {
    fun accept(item: T)
}

fun <T> feedSink(item: T, sink: Sink<T>) {
    sink.accept(item)
}

fun main() {
    val stringSource: Source<String> = object : Source<String> {
        override fun next(): String = "Kotlin"
    }
    val anySource: Source<Any> = stringSource
    println(anySource.next()) // Output: Kotlin

    val fruitSink: Sink<Fruit> = object : Sink<Fruit> {
        override fun accept(item: Fruit) {
            println("Consuming a fruit")
        }
    }
    val appleSink: Sink<Apple> = fruitSink

    feedSink(Apple(), appleSink)
}

在这个例子中:

Source<out T> 是协变的,因此 Source<String> 可以赋给 Source<Any>。

Sink<in T> 是逆变的,因此 Sink<Fruit> 可以赋给 Sink<Apple>。

Kotlin 中,生产者常用协变,消费者常用逆变。可以这样记:

类只返回(生产)T 类型的值时,使用 out。

类只接收(消费)T 类型的值时,使用 in。

类既生产又消费 T 类型的值时,通常不声明型变。

这个记法与 Java 中的 PECS 原则相对应。不过,Java 的通配符用于使用处,前面的 Kotlin 接口是在声明处指定型变:

Producer Extends, Consumer Super

  • Producer (生产者)→ Extends :如果泛型对象主要用于生产(提供)数据,使用 ? extends T。
  • Consumer (消费者)→ Super :如果泛型对象主要用于消费(接收)数据,使用 ? super T。

理解这些关系,有助于设计类型安全、方便调用的 API,尤其是在处理集合和类型层级时。

理解 where 子句

where 可以为类型参数设置多个约束,也就是要求一个类型同时具备几种能力。

where 子句是什么

假设你需要一个类型,它既是 Animal,又能参与比较,以便找出年龄最大的动物。where 可以同时表达这两个要求。

下面的函数从列表中找出年龄最大的动物。比较年龄时使用 compareTo,避免原文直接相减可能出现的整数溢出:

kotlin 复制代码
// Base animal class with age
open class Animal(val age: Int)

// Dog class that extends Animal and can be compared
class Dog(age: Int) : Animal(age), Comparable<Dog> {
    override fun compareTo(other: Dog): Int {
        return this.age.compareTo(other.age)
    }
}

// Function to find the oldest animal
// The "where" clause requires T to be BOTH:
// 1. A type of Animal AND
// 2. Something that can be compared
fun <T> findOldestAnimal(animals: List<T>): T where T : Animal, T : Comparable<T> {
    return animals.maxByOrNull { it.age } ?: throw IllegalArgumentException("List is empty")
}

fun main() {
    val dogs = listOf(Dog(5), Dog(3), Dog(9), Dog(7))
    val oldestDog = findOldestAnimal(dogs)
    println("The oldest dog is ${oldestDog.age} years old.") // Output: The oldest dog is 9 years old.
}

接下来的 Healthy 接口会增加 healthScore 属性,用来判断动物是否健康。

HealthyDog 会继承 Animal,实现用于年龄比较的 Comparable<HealthyDog>,并实现提供健康评分的 Healthy。

findOldestHealthyAnimal 会通过 where 要求 T 同时满足 Animal、Comparable<T> 和 Healthy 三个约束,以便先按健康评分筛选,再找出年龄最大的动物。

例如:找出年龄最大的健康动物

kotlin 复制代码
// Health interface with a health score
interface Healthy {
    val healthScore: Int
}

// HealthyDog that is an Animal, can be compared, and has health info
class HealthyDog(age: Int, override val healthScore: Int) : Animal(age), Comparable<HealthyDog>, Healthy {
    override fun compareTo(other: HealthyDog): Int {
        return this.age.compareTo(other.age)
    }
}

// Function requiring THREE conditions:
// 1. Must be an Animal AND
// 2. Must be comparable AND
// 3. Must have health information
fun <T> findOldestHealthyAnimal(animals: List<T>): T where T : Animal, T : Comparable<T>, T : Healthy {
    // Only consider animals with good health
    return animals.filter { it.healthScore > 50 }
        .maxByOrNull { it.age } ?: throw IllegalArgumentException("No healthy animals found")
}

fun main() {
    val healthyDogs = listOf(
        HealthyDog(5, 70),
        HealthyDog(3, 80),
        HealthyDog(9, 60),
        HealthyDog(7, 90)
    )
    val oldestHealthyDog = findOldestHealthyAnimal(healthyDogs)
    println("The oldest healthy dog is ${oldestHealthyDog.age} years old with a health score of ${oldestHealthyDog.healthScore}.")
    // Output: The oldest healthy dog is 9 years old with a health score of 60.
}

Healthy 接口提供 healthScore 属性,作为判断动物健康状况的依据。

HealthyDog 继承 Animal,实现 Comparable<HealthyDog> 来比较年龄,同时实现 Healthy 来提供健康评分。

findOldestHealthyAnimal 通过 where 要求 T 同时满足这三个约束,然后筛选健康评分大于 50 的动物,再找出年龄最大的那个。这里按 age 调用 maxByOrNull,并没有使用 compareTo;Comparable<T> 在这两个示例中是演示多重约束,并非函数实现所必需。

星投影

不知道确切的类型实参时,可以使用星投影 *,在保留类型安全的前提下处理未知类型。

例如:处理集合或 API 时,具体类型实参未知,或者当前操作不需要关心它,就可以使用星投影。

kotlin 复制代码
fun printList(list: List<*>) {
    for (item in list) {
        println(item)
    }
}

fun main() {
    val intList: List<Int> = listOf(1, 2, 3)
    val stringList: List<String> = listOf("a", "b", "c")
    printList(intList) // Output: 1, 2, 3
    printList(stringList) // Output: a, b, c
}

List<*> 表示元素类型未知的列表,可以接收不同元素类型的列表。只需要处理集合,而不关心元素的确切类型时,这种写法很方便。

什么时候使用星投影

星投影适合以下场景:

不需要类型信息:当前操作不依赖元素的具体类型。

与旧代码交互:对接使用原始类型或通配符的 Java 代码。

处理异构集合:集合中可能包含多种类型的元素。

如果你觉得 List<*> 和 List<Any> 差不多,需要先区分可空性:List<*> 读出的元素是 Any?,可能为 null;List<Any> 的元素则是非空的。对只读 List 而言,List<*> 与 List<Any?> 的读取能力相同;下面用 MutableList 看星投影对写入的限制。

星投影的限制

星投影也会限制你能执行的操作:

kotlin 复制代码
fun addToList(list: MutableList<*>, item: Any) {
    // This doesn't compile!
    // list.add(item)
    
    // We cannot add an element of an unknown type; clear() is still allowed
    println("The list contains ${list.size} items")
}

对于上面的 MutableList<*>,通常可以:

读取元素,得到 Any? 类型的值。

调用返回 Any? 的方法。

调用不依赖具体元素类型的方法。

但不能:

通过 add 或 set 写入元素,因为不知道列表实际接受什么类型。

调用要求你提供未知元素类型值的方法。

星投影并不意味着集合不可修改,例如仍可调用 clear()。对其他带上界的泛型类型,读取结果应按其上界理解,不一定都是 Any?。

更复杂的例子

kotlin 复制代码
// Function to count elements that match a specific condition
fun countElements(collection: Collection<*>, predicate: (Any?) -> Boolean): Int {
    var count = 0
    for (item in collection) {
        if (predicate(item)) {
            count++
        }
    }
    return count
}

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    val strings = listOf("apple", "banana", "cherry", "date")
    
    // Count even numbers
    val evenCount = countElements(numbers) { it is Int && it % 2 == 0 }
    println("Number of even integers: $evenCount") // Output: 2
    
    // Count strings starting with 'a'
    val aCount = countElements(strings) { it is String && it.startsWith('a') }
    println("Number of strings starting with 'a': $aCount") // Output: 1
}

这个计数函数通过星投影接收不同类型的集合,再用运行时类型检查(is)为每个元素应用条件。这里把谓词参数写成 Any?,直接传入元素,避免原文的 item!! 在遇到 null 时抛出异常。

星投影让你可以处理不同类型的数据,同时保留对未知类型所需的安全限制。

泛型约束

有时,你需要限制泛型类或函数允许使用哪些类型。可以为类型参数定义上界,让传入的类型必须满足相应要求。

使用场景:一个数学函数只适用于数值类型。可以用泛型约束,确保调用方传入 Int、Float、Double 等合适的类型。

kotlin 复制代码
fun <T : Number> sum(a: T, b: T): Double {
    return a.toDouble() + b.toDouble()
}

fun main() {
    println(sum(3, 4)) // Output: 7.0
    println(sum(3.5, 4.2)) // Output: 7.7
    // println(sum("3", "4")) // Error: Type inference failed
}

T : Number 将 T 限定为 Number 的子类型,因此 sum 只能接收数值类型,不能传入字符串等类型。结果统一转为 Double,大整数转换和浮点运算可能损失精度,这个函数并不保证精确求和。

加上这个约束后:

可以使用 Int、Double、Float、Long 等类型。

不能使用 String、Boolean 等非数值类型。

函数体中可以调用 Number 的方法,例如 toDouble()。

如果使用 where,可以为同一个类型参数指定多个约束。

例如:某个排序算法要求元素既能比较,又能克隆。多个约束可以确保只有同时满足这些要求的类型才能传入。

kotlin 复制代码
fun <T> performOperation(item: T) where T : Comparable<T>, T : Cloneable {
    // Compare the item with itself (just as an example)
    if (item.compareTo(item) == 0) {
        println("Item is equal to itself")
    }
    
    // Cloneable.clone() is protected, so this function cannot call it directly
}

fun main() {
    // This would work if String implemented Cloneable (it doesn't in reality)
    // performOperation("Hello")
    
    // For demonstration, let's create a class that meets both constraints
    class ComparableAndCloneable(val value: Int) : Comparable<ComparableAndCloneable>, Cloneable {
        override fun compareTo(other: ComparableAndCloneable): Int {
            return value.compareTo(other.value)
        }
        
        public override fun clone(): Any {
            return ComparableAndCloneable(value)
        }
    }
    
    performOperation(ComparableAndCloneable(10)) // Works because it satisfies both constraints
}

这里的 T 必须同时满足 Comparable<T> 和 Cloneable,因此 performOperation 只接受符合这两个条件的类型。

注意,Kotlin/JVM 的 Cloneable.clone() 是受保护成员,仅有这个上界不能让普通泛型函数直接调用它;强制转换成 Java 的 Cloneable 也不能解决这个问题。示例类通过公开重写 clone() 提供可调用的实现,但在泛型函数中还需要能公开表达该操作的接口或具体类型。

常见的约束类型

类约束:<T : SomeClass>。

接口约束:<T : SomeInterface>。

多个约束:<T> where T : Interface1, T : Interface2。

可空类型约束:<T : Any?> 允许可空类型,而 <T : Any> 只允许非空类型。不写上界时,默认上界就是 Any?。

泛型约束可以保留泛型的复用能力,同时让允许的类型和可执行的操作更明确。

为泛型类型参数设置多个上界

where 可以为同一个泛型类型参数设置多个上界,让它同时满足多个接口或父类约束。

使用场景:构建一个模拟系统,对象需要同时具备移动、绘制等行为。多个上界可以确保只接收具备这些能力的对象。

kotlin 复制代码
interface Vehicle {
    fun drive()
}

interface Electric {
    fun chargeBattery()
}
class Tesla : Vehicle, Electric {
    override fun drive() {
        println("Driving Tesla")
    }
    override fun chargeBattery() {
        println("Charging Tesla's battery")
    }
}
fun <T> operateVehicle(vehicle: T) where T : Vehicle, T : Electric {
    vehicle.drive()
    vehicle.chargeBattery()
}
fun main() {
    val tesla = Tesla()
    operateVehicle(tesla) // Output: Driving Tesla \n Charging Tesla's battery
    
    // This would cause a compile-time error:
    class GasCar : Vehicle {
        override fun drive() {
            println("Driving gas car")
        }
    }
    // operateVehicle(GasCar()) // Error: Type argument is not within bounds
}

operateVehicle 只接受同时实现 Vehicle 和 Electric 的类型。通过 where 指定这两个上界,函数就能调用驾驶和充电操作。

例如:同时约束类和接口。

kotlin 复制代码
abstract class Machine {
    abstract fun turnOn()
    abstract fun turnOff()
}

interface Programmable {
    fun program(task: String)
}
class Robot : Machine(), Programmable {
    override fun turnOn() {
        println("Robot powering on")
    }
    
    override fun turnOff() {
        println("Robot shutting down")
    }
    
    override fun program(task: String) {
        println("Robot programmed to: $task")
    }
}
fun <T> operateMachine(machine: T) where T : Machine, T : Programmable {
    machine.turnOn()
    machine.program("Perform assembly tasks")
    machine.turnOff()
}
fun main() {
    val robot = Robot()
    operateMachine(robot)
    // Output:
    // Robot powering on
    // Robot programmed to: Perform assembly tasks
    // Robot shutting down
}

总结

泛型初看可能有些绕,但它能让代码复用于不同类型,并保留类型检查。本文介绍了 Kotlin 泛型的基础。后面,我会再发一篇文章,讲解更多的实战案例。

相关推荐
小七在进步2 小时前
C++内存管理:new/delete vs malloc/free深度解析
android·java·c++
geovindu15 小时前
sql:Search-Optimized SQL Queries using mysql 9.0
android·数据库·sql·mysql·数据库开发·数据库架构
知之则吱吱17 小时前
Android 开发者验证机制迎来新变化:核心要求、实施时间与开发者应对指南
android·google
墨天梦18 小时前
D09_Retrofit接口与数据转换
android·kotlin·retrofit
电商API_1800790524719 小时前
闲鱼商品列表API接口商品详情API接口请求示例(含一份真实响应数据示例)
android·数据挖掘·网络爬虫·跨境电商·反向海淘
我命由我1234519 小时前
Android 广播 - 组件通信
android·java·开发语言·android studio·android jetpack·android-studio·android runtime
阿俊-全栈开发19 小时前
LikeShop 单商户标准版商品管理二开:分类、SKU 规格与库存联动实现
android·likeshop·likeshop开源商城
用户2181697049301 天前
Kotlin 语法二
android