江苏廉政建设网站,广州短视频代运营公司,高端网站建设seo,山东省工程建设造价信息网站点击查看泛型中文文档 点击查看泛型英文文档
简介
与 Java 类似#xff0c;Kotlin 中的类也可以有类型参数#xff1a;
class BoxT(t: T) {var value t
}一般来说#xff0c;要创建这样类的实例#xff0c;我们需要提供类型参数#xff1a;
val box: Box…点击查看泛型中文文档 点击查看泛型英文文档
简介
与 Java 类似Kotlin 中的类也可以有类型参数
class BoxT(t: T) {var value t
}一般来说要创建这样类的实例我们需要提供类型参数
val box: BoxInt BoxInt(1)但是如果类型参数可以推断出来例如从构造函数的参数或者从其他途径允许省略类型参数
val box Box(1) // 1 具有类型 Int所以编译器知道我们说的是 BoxInt。型变 Java 类型系统中最棘手的部分之一是通配符类型参见 Java Generics FAQ。 而 Kotlin 中没有。 相反它有两个其他的东西声明处型变declaration-site variance与类型投影type projections。 首先让我们思考为什么 Java 需要那些神秘的通配符。在 《Effective Java》第三版 解释了该问题——第 31 条利用有限制通配符来提升 API 的灵活性。 首先Java 中的泛型是不型变的这意味着 ListString 并不是 ListObject 的子类型。 为什么这样 如果 List 不是不型变的它就没比 Java 的数组好到哪去因为如下代码会通过编译然后导致运行时异常 // Java
ListString strs new ArrayListString();
ListObject objs strs; // 此处的编译器错误让我们避免了之后的运行时异常
objs.add(1); // 这里我们把一个整数放入一个字符串列表
String s strs.get(0); // ClassCastException无法将整数转换为字符串因此Java 禁止这样的事情以保证运行时的安全。但这样会有一些影响。例如考虑 Collection 接口中的 addAll() 方法。该方法的签名应该是什么 直觉上我们会这样 // Java
interface CollectionE …… {void addAll(CollectionE items);
}但随后我们就无法做到以下简单的事情这是完全安全
// Java
void copyAll(CollectionObject to, CollectionString from) {to.addAll(from);// 对于这种简单声明的 addAll 将不能编译// CollectionString 不是 CollectionObject 的子类型
}在 Java 中我们艰难地学到了这个教训参见《Effective Java》第三版第 28 条列表优先于数组 这就是为什么 addAll() 的实际签名是以下这样 // Java
interface CollectionE …… {void addAll(Collection? extends E items);
}通配符类型参数 ? extends E 表示此方法接受 E 或者 E 的 一些子类型对象的集合而不只是 E 自身。 这意味着我们可以安全地从其中该集合中的元素是 E 的子类的实例读取 E但不能写入 因为我们不知道什么对象符合那个未知的 E 的子类型。 反过来该限制可以让Collection String表示为Collection? extends Object的子类型。 简而言之带 extends 限定上界的通配符类型使得类型是协变的covariant。 理解为什么这个技巧能够工作的关键相当简单如果只能从集合中获取元素那么使用 String 的集合 并且从其中读取 Object 也没问题 。反过来如果只能向集合中 放入 元素就可以用 Object 集合并向其中放入 String在 Java 中有 List? super String 是 ListObject 的一个超类。 后者称为逆变性contravariance并且对于 List ? super String 你只能调用接受 String 作为参数的方法 例如你可以调用 add(String) 或者 set(int, String)当然如果调用函数返回 ListT 中的 T你得到的并非一个 String 而是一个 Object。 Joshua Bloch 称那些你只能从中读取的对象为生产者并称那些你只能写入的对象为消费者。他建议“为了灵活性最大化在表示生产者或消费者的输入参数上使用通配符类型”并提出了以下助记符 PECS 代表生产者-Extends、消费者-SuperProducer-Extends, Consumer-Super。 注意 如果你使用一个生产者对象如 List? extends Foo在该对象上不允许调用 add() 或 set()。但这并不意味着该对象是不可变的例如没有什么阻止你调用 clear()从列表中删除所有元素因为 clear() 根本无需任何参数。通配符或其他类型的型变保证的唯一的事情是类型安全。不可变性完全是另一回事。 声明处型变 假设有一个泛型接口 Source T该接口中不存在任何以 T 作为参数的方法只是方法返回 T 类型值 // Java
interface SourceT {T nextT();
}那么在 Source Object 类型的变量中存储 Source String 实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点并且仍然禁止这样操作 // Java
void demo(SourceString strs) {SourceObject objects strs; // 在 Java 中不允许// ……
}为了修正这一点我们必须声明对象的类型为 Source? extends Object这是毫无意义的因为我们可以像以前一样在该对象上调用所有相同的方法所以更复杂的类型并没有带来价值。但编译器并不知道。 在 Kotlin 中有一种方法向编译器解释这种情况。这称为声明处型变我们可以标注 Source 的类型参数 T 来确保它仅从 Source T 成员中返回生产并从不被消费。 为此我们提供 out 修饰符 interface Sourceout T {fun nextT(): T
}fun demo(strs: SourceString) {val objects: SourceAny strs // 这个没问题因为 T 是一个 out-参数// ……
}一般原则是当一个类 C 的类型参数 T 被声明为 out 时它就只能出现在 C 的成员的输出-位置但回报是 C Base 可以安全地作为 C Derived的超类。 简而言之他们说类 C 是在参数 T 上是协变的或者说 T 是一个协变的类型参数。 你可以认为 C 是 T 的生产者而不是 T 的消费者。 out修饰符称为型变注解并且由于它在类型参数声明处提供所以我们称之为声明处型变。 这与 Java 的使用处型变相反其类型用途通配符使得类型协变。 另外除了 outKotlin 又补充了一个型变注释in。它使得一个类型参数逆变只可以被消费而不可以被生产。逆变类型的一个很好的例子是 Comparable interface Comparablein T {operator fun compareTo(other: T): Int
}fun demo(x: ComparableNumber) {x.compareTo(1.0) // 1.0 拥有类型 Double它是 Number 的子类型// 因此我们可以将 x 赋给类型为 Comparable Double 的变量val y: ComparableDouble x // OK
}我们相信 in 和 out 两词是自解释的因为它们已经在 C# 中成功使用很长时间了 因此上面提到的助记符不是真正需要的并且可以将其改写为更高的目标
存在性The Existential 转换消费者 in, 生产者 out
类型投影
使用处型变类型投影 将类型参数 T 声明为 out 非常方便并且能避免使用处子类型化的麻烦但是有些类实际上不能限制为只返回 T 一个很好的例子是 Array class ArrayT(val size: Int) {fun get(index: Int): T { …… }fun set(index: Int, value: T) { …… }
}该类在 T 上既不能是协变的也不能是逆变的。这造成了一些不灵活性。考虑下述函数
fun copy(from: ArrayAny, to: ArrayAny) {assert(from.size to.size)for (i in from.indices)to[i] from[i]
}这个函数应该将项目从一个数组复制到另一个数组。让我们尝试在实践中应用它
val ints: ArrayInt arrayOf(1, 2, 3)
val any ArrayAny(3) { }
copy(ints, any)
// ^ 其类型为 ArrayInt 但此处期望 ArrayAny这里我们遇到同样熟悉的问题Array T 在 T 上是不型变的因此 Array Int 和 Array Any 都不是另一个的子类型。为什么 再次重复因为 copy 可能做坏事也就是说例如它可能尝试写一个 String 到 from 并且如果我们实际上传递一个 Int 的数组一段时间后将会抛出一个 ClassCastException 异常。 那么我们唯一要确保的是 copy() 不会做任何坏事。我们想阻止它写到 from我们可以 fun copy(from: Arrayout Any, to: ArrayAny) { …… }这里发生的事情称为类型投影我们说from不仅仅是一个数组而是一个受限制的投影的数组我们只可以调用返回类型为类型参数 T 的方法如上这意味着我们只能调用 get()。这就是我们的使用处型变的用法并且是对应于 Java 的 Array? extends Object、 但使用更简单些的方式。 你也可以使用 in 投影一个类型 fun fill(dest: Arrayin String, value: String) { …… }Array 对应于 Java 的 Array? super String也就是说你可以传递一个 CharSequence 数组或一个 Object 数组给 fill() 函数。 星投影 有时你想说你对类型参数一无所知但仍然希望以安全的方式使用它。 这里的安全方式是定义泛型类型的这种投影该泛型类型的每个具体实例化将是该投影的子类型。 Kotlin 为此提供了所谓的星投影语法 对于 Foo out T : TUpper其中 T 是一个具有上界 TUpper 的协变类型参数Foo * 等价于 Foo out TUpper。 这意味着当 T 未知时你可以安全地从 Foo * 读取 TUpper 的值。对于 Foo in T其中 T 是一个逆变类型参数Foo * 等价于 Foo in Nothing 。 这意味着当 T 未知时没有什么可以以安全的方式写入 Foo * 。对于 Foo T : TUpper 其中 T 是一个具有上界 TUpper 的不型变类型参数Foo * 对于读取值时等价于 Foo out TUpper 而对于写值时等价于 Fooin Nothing。 如果泛型类型具有多个类型参数则每个类型参数都可以单独投影。 例如如果类型被声明为 interface Function in T, out U我们可以想象以下星投影
Function*, String 表示 Functionin Nothing, StringFunctionInt, * 表示 FunctionInt, out Any?Function*, * 表示 Functionin Nothing, out Any?。
注意星投影非常像 Java 的原始类型但是安全。
泛型函数
不仅类可以有类型参数。函数也可以有。类型参数要放在函数名称之前
fun T singletonList(item: T): ListT {// ……
}fun T T.basicToString(): String { // 扩展函数// ……
}要调用泛型函数在调用处函数名之后指定类型参数即可
val l singletonListInt(1)可以省略能够从上下文中推断出来的类型参数所以以下示例同样适用
val l singletonList(1)泛型约束
能够替换给定类型参数的所有可能类型的集合可以由泛型约束限制。
上界
最常见的约束类型是与 Java 的 extends 关键字对应的 上界
fun T : ComparableT sort(list: ListT) { …… }冒号之后指定的类型是上界只有 ComparableT 的子类型可以替代 T。 例如
sort(listOf(1, 2, 3)) // OK。Int 是 ComparableInt 的子类型
sort(listOf(HashMapInt, String())) // 错误HashMapInt, String 不是 ComparableHashMapInt, String 的子类型默认的上界如果没有声明是 Any?。在尖括号中只能指定一个上界。 如果同一类型参数需要多个上界我们需要一个单独的 where-子句
fun T copyWhenGreater(list: ListT, threshold: T): ListStringwhere T : CharSequence,T : ComparableT {return list.filter { it threshold }.map { it.toString() }
}所传递的类型必须同时满足 where 子句的所有条件。在上述示例中类型 T 必须既实现了 CharSequence 也实现了 Comparable。
绝对不可空的类型
提醒绝对不可空的类型从Kotlin 1.7.0版本开始支持 点击查看Kotlin 1.7.0 的新特性 为了使泛型Java类和接口的互操作性更容易Kotlin支持将泛型类型参数声明为绝对不可空的 要将泛型类型T声明为绝对不可空请使用 Any声明该类型。例如:T Any。 一个绝对不可空的类型必须有一个可空的上界。 声明绝对不可空类型的最常见用例是当您想要重写包含NotNull作为参数的Java方法时。例如考虑load()方法: import org.jetbrains.annotations.*;public interface GameT {public T save(T x) {}NotNullpublic T load(NotNull T x) {}
}要成功重写Kotlin中的load()方法你需要将T1声明为绝对非空的:
interface ArcadeGameT1 : GameT1 {override fun save(x: T1): T1// T1 is definitely non-nullableoverride fun load(x: T1 Any): T1 Any
}当只使用Kotlin时不太可能需要显式声明绝对不可空的类型因为Kotlin的类型推断会为您处理这个问题。 类型擦除 Kotlin 为泛型声明用法执行的类型安全检测仅在编译期进行。 运行时泛型类型的实例不保留关于其类型实参的任何信息。 其类型信息称为被擦除。例如FooBar 与 FooBaz? 的实例都会被擦除为 Foo。 因此并没有通用的方法在运行时检测一个泛型类型的实例是否通过指定类型参数所创建 并且编译器禁止这种 is 检测。 类型转换为带有具体类型参数的泛型类型如 foo as List 无法在运行时检测。 当高级程序逻辑隐含了类型转换的类型安全而无法直接通过编译器推断时 可以使用这种非受检类型转换。编译器会对非受检类型转换发出警告并且在运行时只对非泛型部分检测相当于 foo as List。 泛型函数调用的类型参数也同样只在编译期检测。在函数体内部 类型参数不能用于类型检测并且类型转换为类型参数foo as T也是非受检的。然而 内联函数的具体化的类型参数会由调用处内联函数体中的类型实参所代入因此可以用于类型检测与转换 与上述泛型类型的实例具有相同限制。 类型参数的下划线操作符
提醒类型参数的下划线操作符从Kotlin 1.7.0版本开始支持 点击查看Kotlin 1.7.0 的新特性 下划线操作符_可用于类型参数。当显式指定其他类型时使用它自动推断参数的类型:
TestSomeClass.kt文件代码
class SomeImplementation : SomeClassString() {override fun execute(): String Test
}class SomeImplementation1 : SomeClassString(){override fun execute(): String {return 测试}
}class OtherImplementation : SomeClassInt() {override fun execute(): Int 42
}object Runner {inline fun reified S: SomeClassT, T run() : T {return S::class.java.getDeclaredConstructor().newInstance().execute()}
}fun main() {//类型参数的下划线操作符仅在Kotlin 1.7.0 查看Kotlin 1.7.0 的新特性//https://kotlinlang.org/docs/whatsnew17.html#underscore-operator-for-type-arguments// T is inferred as String because SomeImplementation derives from SomeClassString//T被推断为String因为SomeImplementation是SomeClassString的派生val s Runner.runSomeImplementation, _() //这里支持_因为Kotlin版本是大于或等于1.7.0
// assert(s Test)println(s $s)// T is inferred as Int because OtherImplementation derives from SomeClassIntval n Runner.runOtherImplementation, _() // 这里支持_因为Kotlin版本是大于或等于1.7.0
// assert(n 42)println(n $n)}运行结果 欢迎关注我的公众号不定期推送优质的文章 微信扫一扫下方二维码即可关注。