欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Kotlin 泛型详解及简单实例

程序员文章站 2023-12-19 10:19:28
 kotlin 泛型详解 概述 一般类和函数,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的约束对代码的...

 kotlin 泛型详解

概述

一般类和函数,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的约束对代码的限制很大。而oop的多态采用了一种泛化的机制,在se 5种,java引用了泛型。泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

在kotlin中,依然可以使用泛型,解耦类与函数与所用类型之间的约束,甚至是使用方法都与java一致。

泛型类

声明一个泛型类

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

通常, 要创建这样一个类的实例, 我们需要指定类型参数:

val box: box<int> = box<int>(1)

但是, 如果类型参数可以通过推断得到, 比如, 通过构造器参数类型, 或通过其他手段推断得到, 此时允许省略类型参数:

val box = box(1) // 1 的类型为 int, 因此编译器知道我们创建的实例是 box<int> 类型

泛型函数

泛型函数与其所在的类是否是泛型没有关系。泛型函数使得该函数能够独立于其所在类而产生变化。在<thinking in java>有这么一句话:无论何时只要你能做到,你就应该尽量使用泛型方法,也就是说如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更明白。这种泛型使用思想,在kotlin中依然可以延续。

下面我们声明了一个泛型函数doprintln,当t是一个int类型时,打印其个位的值;如果t是string类型,将字母全部大写输出;如果是其他类型,打印“t is not int and string”。

fun main(args: array<string>) {
  val age = 23
  val name = "jone"
  val person = true

  doprintln(age) // 打印:3
  doprintln(name) // 打印:jone
  doprintln(person) // 打印:t is not int and string
}

fun <t> doprintln(content: t) {

  when (content) {
    is int -> println(content % 10) 
    is string -> println(content.touppercase())
    else -> println("t is not int and string")
  }
}

注:

  1. 类型参数放在函数名称之前。
  2. 如果在调用处明确地传入了类型参数, 那么类型参数应该放在函数名称 之后。如果不传入参数类型,编译器会根据传入的值自动推断参数类型。

擦除的神秘之处

下面我们先看一段代码:

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


fun main(args: array<string>) {
  var boxint = box<int>(10)
  var boxstring = box<string>("jone")

  println(boxint.javaclass) // 打印:class com.teaphy.generic.box
  println(boxstring.javaclass) // 打印:class com.teaphy.generic.box
}

现声明了一个泛型类box<t>,在不同的类型的类型在行为方面肯定不一样,但是在我们获取其所在类时,我们只是得到了“class com.teaphy.generic.box”。在这里我们不得不面对一个残酷的现实:在泛型内部,无法获得任何有关泛型参数类型的信息。

不管是java还是kotlin,泛型都是使用擦除来实现的,这意味着当你在使用泛型时,任务具体的类型信息都被擦除的,你唯一知道的就是你再使用一个对象。比如,box<string>和box<int>在运行时是想的类型,都是box的实例。在使用泛型时,具体类型信息的擦除是我们不不懂得不面对的,在kotlin中也为我们提供了一些可供参考的解决方案:

  • 类型协变
  • 类型投射
  • 泛型约束

类型协变

在类型声明时,使用协变注解修饰符(in或者out)。于这个注解出现在类型参数的声明处, 因此我们称之为声明处的类型变异。如果在使用泛型时,使用了该类型编译了会有什么效果呢?

假设我们有一个泛型接口source<in t, out r>, 其中t由协变注解in修饰,r由协变注解out修饰.

internal interface source<in t, out r> {
  fun mapt(t: t): unit
  fun nextr(): r
}
  • in t: 来确保source的成员函数只能消费t类型,而不能返回t类型
  • out r:来确保source的成员函数只能返回r类型,而不能消费r类型

从上面的解释中,我们可以清楚的知道了协变注解in和out的用意,其实际上是定义了类型参数在该类或者接口的用途,是用来消费的还是用来返回的,对其做了相应的限定。

类型投射

上面我们已经了解到了协变注解in和out的用意,下面我们将会用in和out,做一件有意义的事,看下面代码

fun copy(from: array<out string>, to: array<any>) {
  // ...
}

fun fill(dest: array<in string>, value: string) {
  // ...
}

对于copy函数中中,from的泛型参数使用了协变注解out修饰,意味着该参数不能在该函数中消费,也就是说在该函数中禁止对该参数进行任何操作。

对于fill函数中,dest的泛型参数使用了协变注解in修饰,array<in string>与java的 array<? super string> 相同, 也就是说, 你可以使用charsequence数组,或者 object 数组作为 fill() 函数的参数

这种声明在kotlin中称为类型投射(type projection),类型投射的主要用于对参数做了相对因的限定,避免了对该参数类的不安全操作。

星号投射

有些时候, 你可能想表示你并不知道类型参数的任何信息, 但是仍然希望能够安全地使用它. 这里所谓”安全地使用”是指, 对泛型类型定义一个类型投射, 要求这个泛型类型的所有的实体实例, 都是这个投射的子类型.

对于这个问题, kotlin 提供了一种语法, 称为 星号投射(star-projection):

  1. 假如类型定义为 foo<out t> , 其中 t 是一个协变的类型参数, 上界(upper bound)为 tupper ,foo<> 等价于 foo<out tupper> . 它表示, 当 t 未知时, 你可以安全地从 foo<> 中 读取tupper 类型的值.
  2. 假如类型定义为 foo<in t> , 其中 t 是一个反向协变的类型参数, foo<> 等价于 foo<innothing> . 它表示, 当 t 未知时, 你不能安全地向 foo<> 写入 任何东西.
  3. 假如类型定义为 foo<t> , 其中 t 是一个协变的类型参数, 上界(upper bound)为 tupper , 对于读取值的场合, foo<*> 等价于 foo<out tupper> , 对于写入值的场合, 等价于 foo<in nothing> .

如果一个泛型类型中存在多个类型参数, 那么每个类型参数都可以单独的投射. 比如, 如果类型定义为interface function<in t, out u> , 那么可以出现以下几种星号投射:

  1. function<*, string> , 代表 function<in nothing, string> ;
  2. function<int, *> , 代表 function<int, out any?> ;
  3. function<, > , 代表 function<in nothing, out any?> .

注意: 星号投射与 java 的原生类型(raw type)非常类似, 但可以安全使用

泛型约束

对于一个给定的类型参数, 所允许使用的类型, 可以通过泛型约束(generic constraint) 来限制。

上界

最常见的约束是 上界(upper bound):

fun <t : comparable<t>> sort(list: list<t>) {
  // ...
}

冒号之后指定的类型就是类型参数的 上界(upper bound): 对于类型参数 t , 只允许使用 comparable<t>的子类型. 比如:

sort(listof(1, 2, 3)) // 正确: int 是 comparable<int> 的子类型
sort(listof(hashmap<int, string>())) // 错误: hashmap<int, string> 不是
comparable<hashmap<int, string>> 的子类型

如果没有指定, 则默认使用的上界是 any? . 在定义类型参数的尖括号内, 只允许定义唯一一个上界. 如果同一个类型参数需要指定多个上界, 这时就需要使用单独的 where 子句:

fun <t> clonewhengreater(list: list<t>, threshold: t): list<t> where t : comparable,
t : cloneable {
  return list.filter { it > threshold }.map { it.clone() }
}

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

上一篇:

下一篇: