Kotlin:泛型

点击查看泛型中文文档
点击查看泛型英文文档

简介

与 Java 类似,Kotlin 中的类也可以有类型参数:

class Box<T>(t: T) {
    var value = t
}

一般来说,要创建这样类的实例,我们需要提供类型参数:

val box: Box<Int> = Box<Int>(1)

但是如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径,允许省略类型参数:

val box = Box(1) // 1 具有类型 Int,所以编译器知道我们说的是 Box<Int>

型变

Java 类型系统中最棘手的部分之一是通配符类型(参见 Java Generics FAQ)。 而 Kotlin 中没有。 相反,它有两个其他的东西:声明处型变(declaration-site variance)与类型投影(type projections)。

首先,让我们思考为什么 Java 需要那些神秘的通配符。在 《Effective Java》第三版 解释了该问题——第 31 条:利用有限制通配符来提升 API 的灵活性。 首先,Java 中的泛型是不型变的,这意味着 List<String> 并不是 List<Object> 的子类型。 为什么这样? 如果 List 不是不型变的,它就没比 Java 的数组好到哪去,因为如下代码会通过编译然后导致运行时异常:

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!!此处的编译器错误让我们避免了之后的运行时异常
objs.add(1); // 这里我们把一个整数放入一个字符串列表
String s = strs.get(0); // !!! ClassCastException:无法将整数转换为字符串

因此,Java 禁止这样的事情以保证运行时的安全。但这样会有一些影响。例如,考虑 Collection 接口中的 addAll() 方法。该方法的签名应该是什么?
直觉上,我们会这样:

// Java
interface Collection<E> …… {
  void addAll(Collection<E> items);
}

但随后,我们就无法做到以下简单的事情(这是完全安全):

// Java
void copyAll(Collection<Object> to, Collection<String> from) {
  to.addAll(from);
  // !!!对于这种简单声明的 addAll 将不能编译:
  // Collection<String> 不是 Collection<Object> 的子类型
}

(在 Java 中,我们艰难地学到了这个教训,参见《Effective Java》第三版,第 28 条:列表优先于数组)

这就是为什么 addAll() 的实际签名是以下这样:

// Java
interface Collection<E> …… {
  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> 是 List<Object> 的一个超类

后者称为逆变性(contravariance),并且对于 List <? super String> 你只能调用接受 String 作为参数的方法 (例如,你可以调用 add(String) 或者 set(int, String)),当然如果调用函数返回 List<T> 中的 T,你得到的并非一个 String 而是一个 Object。

Joshua Bloch 称那些你只能从中读取的对象为生产者,并称那些你只能写入的对象为消费者。他建议:“为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型”,并提出了以下助记符:

PECS 代表生产者-Extends、消费者-Super(Producer-Extends, Consumer-Super)。

注意: 如果你使用一个生产者对象,如 List<? extends Foo>,在该对象上不允许调用 add() 或 set()。但这并不意味着该对象是不可变的:例如,没有什么阻止你调用 clear()从列表中删除所有元素,因为 clear() 根本无需任何参数。通配符(或其他类型的型变)保证的唯一的事情是类型安全。不可变性完全是另一回事。

声明处型变

假设有一个泛型接口 Source <T>,该接口中不存在任何以 T 作为参数的方法,只是方法返回 T 类型值:

// Java
interface Source<T> {
  T nextT();
}

那么,在 Source <Object> 类型的变量中存储 Source <String> 实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:

// Java
void demo(Source<String> strs) {
  Source<Object> objects = strs; // !!!在 Java 中不允许
  // ……
}

为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>,这是毫无意义的,因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。但编译器并不知道。

在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变:我们可以标注 Source 的类型参数 T 来确保它仅从 Source <T> 成员中返回(生产),并从不被消费。 为此,我们提供 out 修饰符:

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

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
    // ……
}

一般原则是:当一个类 C 的类型参数 T 被声明为 out 时,它就只能出现在 C 的成员的输出-位置,但回报是 C <Base> 可以安全地作为 C <Derived>的超类。

简而言之,他们说类 C 是在参数 T 上是协变的,或者说 T 是一个协变的类型参数。 你可以认为 C 是 T 的生产者,而不是 T 的消费者。

out修饰符称为型变注解,并且由于它在类型参数声明处提供,所以我们称之为声明处型变。 这与 Java 的使用处型变相反,其类型用途通配符使得类型协变。

另外除了 out,Kotlin 又补充了一个型变注释:in。它使得一个类型参数逆变:只可以被消费而不可以被生产。逆变类型的一个很好的例子是 Comparable:

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
    // 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量
    val y: Comparable<Double> = x // OK!
}

我们相信 in 和 out 两词是自解释的(因为它们已经在 C# 中成功使用很长时间了), 因此上面提到的助记符不是真正需要的,并且可以将其改写为更高的目标:

存在性(The Existential) 转换:消费者 in, 生产者 out

类型投影

使用处型变:类型投影

将类型参数 T 声明为 out 非常方便,并且能避免使用处子类型化的麻烦,但是有些类实际上不能限制为只返回 T! 一个很好的例子是 Array:

class Array<T>(val size: Int) {
    fun get(index: Int): T { …… }
    fun set(index: Int, value: T) { …… }
}

该类在 T 上既不能是协变的也不能是逆变的。这造成了一些不灵活性。考虑下述函数:

fun copy(from: Array<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

这个函数应该将项目从一个数组复制到另一个数组。让我们尝试在实践中应用它:

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" } 
copy(ints, any)
//   ^ 其类型为 Array<Int> 但此处期望 Array<Any>

这里我们遇到同样熟悉的问题:Array < T > 在 T 上是不型变的,因此 Array <Int> 和 Array <Any> 都不是另一个的子类型。为什么? 再次重复,因为 copy 可能做坏事,也就是说,例如它可能尝试写一个 String 到 from, 并且如果我们实际上传递一个 Int 的数组,一段时间后将会抛出一个 ClassCastException 异常。

那么,我们唯一要确保的是 copy() 不会做任何坏事。我们想阻止它写到 from,我们可以:

fun copy(from: Array<out Any>, to: Array<Any>) { …… }

这里发生的事情称为类型投影:我们说from不仅仅是一个数组,而是一个受限制的(投影的)数组:我们只可以调用返回类型为类型参数 T 的方法,如上,这意味着我们只能调用 get()。这就是我们的使用处型变的用法,并且是对应于 Java 的 Array<? extends Object>、 但使用更简单些的方式。

你也可以使用 in 投影一个类型:

fun fill(dest: Array<in 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> 而对于写值时等价于 Foo<in Nothing>。

如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为 interface Function <in T, out U>,我们可以想象以下星投影:

  • Function<*, String> 表示 Function<in Nothing, String>;
  • Function<Int, *> 表示 Function<Int, out Any?>;
  • Function<*, *> 表示 Function<in Nothing, out Any?>。

注意:星投影非常像 Java 的原始类型,但是安全。

泛型函数

不仅类可以有类型参数。函数也可以有。类型参数要放在函数名称之前:

fun <T> singletonList(item: T): List<T> {
    // ……
}

fun <T> T.basicToString(): String {  // 扩展函数
    // ……
}

要调用泛型函数,在调用处函数名之后指定类型参数即可:

val l = singletonList<Int>(1)

可以省略能够从上下文中推断出来的类型参数,所以以下示例同样适用:

val l = singletonList(1)

泛型约束

能够替换给定类型参数的所有可能类型的集合可以由泛型约束限制。

上界

最常见的约束类型是与 Java 的 extends 关键字对应的 上界:

fun <T : Comparable<T>> sort(list: List<T>) {  …… }

冒号之后指定的类型是上界:只有 Comparable<T> 的子类型可以替代 T。 例如:

sort(listOf(1, 2, 3)) // OK。Int 是 Comparable<Int> 的子类型
sort(listOf(HashMap<Int, String>())) // 错误:HashMap<Int, String> 不是 Comparable<HashMap<Int, String>> 的子类型

默认的上界(如果没有声明)是 Any?。在尖括号中只能指定一个上界。 如果同一类型参数需要多个上界,我们需要一个单独的 where-子句:

fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    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 Game<T> {
    public T save(T x) {}
    @NotNull
    public T load(@NotNull T x) {}
}

要成功重写Kotlin中的load()方法,你需要将T1声明为绝对非空的:

interface ArcadeGame<T1> : Game<T1> {
    override fun save(x: T1): T1
    // T1 is definitely non-nullable
    override fun load(x: T1 & Any): T1 & Any
}

当只使用Kotlin时,不太可能需要显式声明绝对不可空的类型,因为Kotlin的类型推断会为您处理这个问题。

类型擦除

Kotlin 为泛型声明用法执行的类型安全检测仅在编译期进行。 运行时泛型类型的实例不保留关于其类型实参的任何信息。 其类型信息称为被擦除。例如,Foo<Bar> 与 Foo<Baz?> 的实例都会被擦除为 Foo<>。

因此,并没有通用的方法在运行时检测一个泛型类型的实例是否通过指定类型参数所创建 ,并且编译器禁止这种 is 检测。

类型转换为带有具体类型参数的泛型类型,如 foo as List 无法在运行时检测。 当高级程序逻辑隐含了类型转换的类型安全而无法直接通过编译器推断时, 可以使用这种非受检类型转换。编译器会对非受检类型转换发出警告,并且在运行时只对非泛型部分检测(相当于 foo as List<
>)。

泛型函数调用的类型参数也同样只在编译期检测。在函数体内部, 类型参数不能用于类型检测,并且类型转换为类型参数(foo as T)也是非受检的。然而, 内联函数的具体化的类型参数会由调用处内联函数体中的类型实参所代入,因此可以用于类型检测与转换, 与上述泛型类型的实例具有相同限制。

类型参数的下划线操作符

提醒:类型参数的下划线操作符从Kotlin 1.7.0版本开始支持 ,点击查看Kotlin 1.7.0 的新特性
下划线操作符_可用于类型参数。当显式指定其他类型时,使用它自动推断参数的类型:

TestSomeClass.kt文件代码

class SomeImplementation : SomeClass<String>() {
    override fun execute(): String = "Test"
}

class SomeImplementation1 : SomeClass<String>(){
    override fun execute(): String {
        return "测试"
    }
}

class OtherImplementation : SomeClass<Int>() {
    override fun execute(): Int = 42
}

object Runner {
    inline fun <reified S: SomeClass<T>, 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 SomeClass<String>
    //T被推断为String,因为SomeImplementation是SomeClass<String>的派生

    val s = Runner.run<SomeImplementation, _>() //这里支持_,因为Kotlin版本是大于或等于1.7.0
//    assert(s == "Test")
    println("s = $s")

    // T is inferred as Int because OtherImplementation derives from SomeClass<Int>
    val n = Runner.run<OtherImplementation, _>() // 这里支持_,因为Kotlin版本是大于或等于1.7.0
//    assert(n == 42)
    println("n = $n")

}

运行结果
在这里插入图片描述

欢迎关注我的公众号,不定期推送优质的文章,
微信扫一扫下方二维码即可关注。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/454094.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot项目中出现不同端口跨域问题,如何解决?

方法一&#xff1a;比较繁琐&#xff0c;适合少量Controller控制器类 方法二 &#xff1a;需要写一个全局的配置文件即可 在如图所示的common目录下新建一个CorsConfig的class文件 具体代码展示&#xff1a; import org.springframework.context.annotation.Bean; import o…

淘宝客必看:微信公众号如何快速接入抖音本地团购并提高CPS收益

抖音已经成为社交媒体新的流量高地&#xff0c;抖音团购完全复刻了美团的模式&#xff0c;外卖、到店、吃喝玩乐全覆盖。很多淘宝客一直在寻找通过微信公众号实现营销变现的有效途径&#xff1f;是否对于如何将抖音团购转变为自己的佣金收入感到困惑&#xff1f;微信公众号接入…

Eslint + Prettier 配置

setting.json {"eslint.autoFixOnSave": true, // 保存文件时自动修复可修复的问题"eslint.validate": [// 定义哪些文件类型应该由 ESLint 验证"javascript","javascriptreact","vue" // 如果你正在使用 Vue&#xff0c;确…

基于单片机的GPS定位信息显示系统

基于单片机的GPS定位信息显示系统 摘 要 在当今信息时代的发展中&#xff0c;GPS全球定位系统是一个重要的组成部分&#xff0c;其具有精度很高、应用广泛、性能强大的特点&#xff0c;因此在实际生活中GPS全球定位显示系统被广泛运用于各类领域当中。它也是至今为止最好的定…

HarmonyOS 发送系统通知 基础通知

harmonyos中 提供了 各种不同功能的通知 来满足我们不同的业务需求 本文呢 我们先来看 最基本的通知 它的场景和使用方式 最基本的 是 当应用被切入后台 而应用内部发生了一些变化 就可以用通知的方式 来提醒用户 就比如 我们微信 被切到了后台 而这时 别人发消息过来了 那么…

2014

1,写出计算Ack(m,n)的递归算法 #include<iostream> using namespace std; int A(int m,int n){if(m0){return n1;}else if(m>0&&n0){return A(m-1,1);}else{return A(m-1,A(m,n-1));} }int main(){int m,n;cout<<"please input two number"&l…

【深度学习笔记】7_7 AdaDelta算法

注&#xff1a;本文为《动手学深度学习》开源内容&#xff0c;部分标注了个人理解&#xff0c;仅为个人学习记录&#xff0c;无抄袭搬运意图 7.7 AdaDelta算法 除了RMSProp算法以外&#xff0c;另一个常用优化算法AdaDelta算法也针对AdaGrad算法在迭代后期可能较难找到有用解的…

分享一个国内可用的AIGC网站,PC/手机端通用|免费无限制,支持Claude3 Claude2

背景 AIGC作为一种基于人工智能技术的自然语言处理工具&#xff0c;近期的热度直接沸腾&#x1f30b;。 作为一个AI爱好者&#xff0c;翻遍了各大基于AIGC的网站&#xff0c;终于找到一个免费&#xff01;免登陆&#xff01;手机电脑通用&#xff01;国内可直接对话的AIGC&am…

Aigtek:衡量基准电压源的技术指标有哪些

基准电压源在电子领域中扮演着至关重要的角色&#xff0c;它为各种应用提供了稳定、可靠的电压。为了确保电路的准确性和可靠性&#xff0c;工程师需要关注和评估基准电压源的技术指标。衡量基准电压源的技术指标有很多&#xff0c;下面将详细介绍其中几个重要的指标。 首先是输…

全面解析unityplayer.dll丢失的5种解决方法,轻松搞定dll丢失

在使用Unity开发的游戏或应用过程中&#xff0c;如果发现系统提示“找不到unityplayer.dll”文件&#xff0c;可能会引发一系列的问题与不便。这将直接导致游戏或应用程序无法正常启动运行&#xff0c;因为unityplayer.dll是Unity引擎的核心组件之一&#xff0c;负责处理关键的…

基于Web的论文管理系统设计

目 录 目 录 III 摘 要 V 关键词 V Abstract VI Key Word VI 第一章 绪论 6 1.1系统设计背景 1 1.2系统设计目的与意义 1 1.3国内外现状 2 1.4本文结构 3 第二章 需求分析 3 2.1系统需求分析 4 2.2系统角色设计 4 第三章 系统开发技术 4 3.1 PHP语言简介和特点 5 3.2 Mysql数据…

免费视频背景素材下载

找免费视频素材、背景就上这6个网站&#xff0c;高质量&#xff0c;无版权可商用。 1、菜鸟图库 https://www.sucai999.com/video.html?vNTYwNDUx 菜鸟图库虽然是个设计素材网站&#xff0c;但除了设计类素材之外还有很多视频、音频、办公类等素材&#xff0c;视频素材就有上…

5款可以免费使用的 UI 设计软件

在我们分享五个有用的原型工具之前&#xff0c;完成原型并优化界面。这是 UI 设计师的任务。UI 设计软件对设计师来说非常重要。UI 设计工具的使用是否直接影响最终结果&#xff0c;然后有人会问&#xff1a;UI 界面设计用什么软件&#xff1f;一些 UI 设计师和对 UI 设计感兴趣…

使用Docker管理linux容器

文章目录 一、使用docker管理镜像 二、使用docker管理容器 一、使用docker管理镜像 1、安装操作系统&#xff0c;我安装的是centOS 7 &#xff0c;因为centos7有着非常丰富的软件仓库&#xff0c;方便后续安装与docker相关的软件。 2、初始化设置&#xff0c; 关闭防火墙 关闭…

反爬虫技术:如何保护你的网站数据安全

在数字化时代&#xff0c;数据的价值日益凸显&#xff0c;而爬虫技术则成为了获取这些数据的重要手段之一。然而&#xff0c;对于网站运营者来说&#xff0c;非法爬虫不仅会导致数据泄露&#xff0c;还可能给网站带来巨大的流量压力和安全隐患。因此&#xff0c;本文将探讨如何…

Windows kafka 简单集群搭建

Windows kafka 简单集群搭建 文章目录 Windows kafka 简单集群搭建1.环境说明2.Zookeeper集群搭建2.1 ZooKeeper下载2.2 ZooKeeper安装2.2.1 解压zookeeper-3.4.8.tar.gz2.2.2 进入conf目录下&#xff0c;复制zoo_sample.cfg为zoo.cfg2.2.3 修改zoo.cfg文件2.2.4 生成myid文件2…

《LeetCode热题100》笔记题解思路技巧优化_Part_1

《LeetCode热题100》笔记&题解&思路&技巧&优化_Part_1 &#x1f60d;&#x1f60d;&#x1f60d; 相知&#x1f64c;&#x1f64c;&#x1f64c; 相识&#x1f622;&#x1f622;&#x1f622; 开始刷题哈希&#x1f7e2;1. 两数之和&#x1f7e1;2. 字母异位词…

JavaScript进阶3之参数按值传递、call,apply,bind和new的实现、继承的多种方式

JavaScript基础 参数按值传递按值传递共享传递 call、apply、bind和new的实现this软绑定硬绑定 call的实现第一步第二步第三步 apply的实现bind的实现返回函数的模拟实现传参的模拟实现构造函数效果的模拟实现构造函数效果的优化实现 new的实现初步实现 继承的多种方式&优缺…

能耗数据采集网关在钢铁企业的应用-天拓四方

能耗数据采集网关是一种集成多种传感器和数据通信技术的智能化设备&#xff0c;它能够实现对生产现场各类能耗数据的实时采集、存储和传输。通过网关设备&#xff0c;企业可以构建一个全面、高效的能源管理系统&#xff0c;对生产过程中的能源消耗进行实时监控和精准控制&#…

pcl弧度角度换算:rad2deg,deg2rad

角度弧度换算公式: 代码及结果在:cmath 中cos sin等常用函数的坑(弧度角度换算)-CSDN博客 pcl也有自带的rad2deg,deg2rad: 头文件 #include<pcl/common/angles.h> 代码如下 #include <iostream> #include<pcl/common/angles.h> int main() {vector<…