三篇文章带你快速入门 Kotlin(下)
infix函数
我们前面介绍mapOf函数,A to B 这样的语法结构构建键值对。
val map = mapOf("apple" to 1, "banana" to 2, "orange" to 3, "pear" to 4, "grape" to 5)
这种语法结构的优点是可读性高,相比与调用一个函数,它更接近于使用英语的语法来编写程序。
to 并不是Kotlin语言中的一个关键字,之所以我们能够使用A to B 这样的语法结构
是因为Kotlin提供了一种高级的语法糖特性:infix函数
A to B 实际上是A.to(B)的写法
借助infix函数,我们可以使用一种更具可读性的语法来表达一段代码。
infix fun String.beginsWith(prefix: String) = startsWith(prefix)
这里给String类添加了一个beginsWith()函数,它用于判断一个字符串是否是以某个指定参数开头。而加上了infix关键字之后,beginsWith()函数就变成了一个infix函数,这样除了传统的函数调用方式之外,我们还可以用一种特殊的语法糖格式调用beginsWith()函数,如下所示:
if ("Hello Kotlin" beginsWith "Hello") {
// 处理具体的逻辑
}
infix函数有两个比较严格的限制:
- infix函数是不能定义成顶层函数的,它必须是某个类的程袁函数,可以使用扩展函数的方式将它定义到某个类中。
- infix函数必须接收且只能接收一个参数,参数类型是没有限制的。
我们还可以给Collection接口添加一个扩展函数:
infix fun <T> Collection<T>.has(element: T) = contains(element)
现在我们就可以使用如下的语法来判断集合中是否包括某个指定的元素:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
if (list has "Banana") {
// 处理具体的逻辑
}
模拟map to
infix fun <A, B> A.with(that: B): Pair<A, B> = Pair(this, that)
val map2 = mapOf("apple" with 1, "banana" with 2, "orange" with 3, "pear" with 4, "grape" with 5)
泛型的高级特性
对泛型进行实化
Java中的泛型是通过类型擦除机制来实现的,而Kotlin却允许将内联函数中的泛型进行实化。
要将某个泛型实化需要两个前提条件。首先,该函数必须是内联函数才行,也就是要用inline关键字来修饰该函数。其次,在声明泛型的地方必须加上reified关键字来表示该泛型要进行实化。示例代码如下:
inline fun <reified T> getGenericType() = T::class.java
然后我们就可以通过以下代码获得泛型的具体类型了:
val result1 = getGenericType<String>()
val result2 = getGenericType<Int>()
泛型实化的应用
泛型实化功能允许我们在泛型函数中获得泛型的实际类型,这也就使得类似于a is T
,T::class.java
这样的语法成为了可能。
例如启动一个Activity
val intent = Intent(this, TestActivity::class.java)
startActivtiy(intent)
我们可以将TestActivity::class.java这样的语法进行简化,利用泛型实化的功能
inline fun <reified T> startActivity(context: Context) {
val intent = Intent(context, T::class.java)
context.startActivity(intent)
}
启动Acitvity就可以这样写:
startActivity(TestActivity)(context)
我们在跳转的时候可能会附加一些参数:
val intent = Intent(this, TestActivity::class.java)
intent.putExtra("param1", "data")
intent.putExtra("param2", "123")
startActivity(intent)
经过刚才的封装,我们无法进行参数,我们利用高阶函数来扩展一下
inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {
val intent = Intent(context, T::class.java)
intent.block()
context.startActivity(intent)
}
我们增加一个函数类型参数,并且它的函数类型是定义在Intent类当中,最后可以在Lambda表达式中为Intent传递参数。
startActivity<TestActivity>(this){
putExtra("param1", "data")
putExtra("param2", "123")
}
泛型的协变
在开始学习协变和逆变之前,我们还得先了解一个约定。一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为in位置,而它的返回值是输出数据的地方,因此可以称它为out位置,如下图所示。
image.png协变的定义:假如定义了一个MyClass<T>的泛型类,其中A是B的子类型,同时MyClass<A>又是MyClass<B>的子类型,那么我们就可以称MyClass在T这个泛型上是协变的。
image.pngJava 上界通配符<? extends T>
Java 的协变通过上界通配符实现。
如果 Dog 是 Animal 的子类,但 List<Dog> 并不是 List<Animal> 的子类。
下面的代码会在编译时报错:
List<Animal> animals = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
animals = dogs; // incompatible types
而使用上界通配符之后,List<Dog> 变成了 List<? extends Animal> 的子类型。即 animals 变成了可以放入任何 Animal 及其子类的 List。
因此,下面的代码编译是正确的:
List<? extends Animal> animals = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
animals = dogs;
Kotlin 的关键词 out
上述代码改成 Kotlin 的代码:
fun main() {
var animals: List<Animal> = ArrayList()
val dogs = ArrayList<Dog>()
animals = dogs
}
其实,Kotlin 的 List 跟 Java 的 List 并不一样。
Kotlin 的 List 源码中使用了out
,out
相当于 Java 上界通配符。
public interface List<out E> : Collection<E> {
override val size: Int
override fun isEmpty(): Boolean
override fun contains(element: @UnsafeVariance E): Boolean
override fun iterator(): Iterator<E>
override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
public operator fun get(index: Int): E
public fun indexOf(element: @UnsafeVariance E): Int
public fun lastIndexOf(element: @UnsafeVariance E): Int
public fun listIterator(): ListIterator<E>
public fun listIterator(index: Int): ListIterator<E>
public fun subList(fromIndex: Int, toIndex: Int): List<E>
}
类的参数类型使用了out
之后,该参数只能出现在方法的返回类型。
观察如下代码,我们在泛型T的声明前面加上了一个out关键字。这就意味着现在T只能出现在out位置上,而不能出现在in位置上,同时也意味着SimpleData在泛型T上是协变的。
class SimpleData<out T>(val data: T?) {
fun get(): T? {
return data
}
}
泛型的逆变
逆变的定义:假如定义了一个MyClass<T>的泛型类,其中A是B的子类型,同时MyClass<B>又是MyClass<A>的子类型,那么我们就可以称MyClass在T这个泛型上是逆变的。
观察如下代码,我们在泛型T的声明前面加上了一个in关键字。这就意味着现在T只能出现在in位置上,而不能出现在out位置上,同时也意味着Transformer在泛型T上是逆变的。
interface Transformer<in T> {
fun transform(t: T): String
}
Java 下界通配符<? super T>
Java 的逆变通过下界通配符实现。
下面的代码因为是协变的,无法添加新的对象。编译器只能知道类型是 Animal 的子类,并不能确定具体类型是什么,因此无法验证类型的安全性。
List<? extends Animal> animals = new ArrayList<>();
animals.add(new Dog()); // compile error
使用下界通配符之后,代码编译通过:
List<? super Animal> animals = new ArrayList<>();
animals.add(new Dog());
PECS 原则即 Producer Extends,Consumer Super 。如果参数化类型是一个生产者,则使用 <? extends T>;如果它是一个消费者,则使用 <? super T>。
其中,生产者
表示频繁往外读取数据 T,而不从中添加数据。消费者
表示只往里插入数据 T,而不读取数据。
Kotlin 的关键词 in
in
相当于 Java 下界通配符。
abstract class Printer<in E> {
abstract fun print(value: E): Unit
}
class AnimalPrinter: Printer<Animal>() {
override fun print(animal: Animal) {
println("this is animal")
}
}
class DogPrinter : Printer<Dog>() {
override fun print(dog: Dog) {
println("this is dog")
}
}
fun main() {
val animalPrinter = AnimalPrinter()
animalPrinter.print(Animal())
val dogPrinter = DogPrinter()
dogPrinter.print(Dog())
}
类的参数类型使用了in
之后,该参数只能出现在方法的入参。
泛型的不变
默认情况下,Kotlin 中的泛型类是不变的。 这意味着它们既不是协变的也不是逆变的。
例如 MutableList,它可读可写,泛型没有使用in
、out
。
协程
协程的概念
协程和线程是有点类似的,可以简单地将它理解成一种轻量级的线程。
要知道,我们之前所学习的线程是非常重量级的,它需要依靠操作系统的调度才能实现不同线程之间的切换。而使用协程却可以仅在编程语言的层面就能实现不同协程之间的切换,从而大大提升了并发编程的运行效率。
它与jetpack aac mvvm 完美契合,官方推荐。
协程的基本用法
Kotlin并没有将协程纳入标准库的API当中,而是以依赖库的形式提供的。所以如果我们想要使用协程功能,需要先在app/build.gradle文件当中添加如下依赖库:
dependencies {
...
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8"
}
Global.launch函数
启动协程最简单的方式就是使用Global.launch函数,如下所示:
fun main(){
GlobalScope.launch {
println("协程启动运行")
}
}
GlobalScope.launch函数可以创建一个协程的作用域,这样传递给launch函数的代码块(Lambda表达式)就是在协程中运行的了。
runBlocking函数
runBlocking函数也可以用于启动一个协程,并且会保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。
fun main() {
runBlocking {
println("codes run in coroutine scope")
delay(1500)
println("codes run in coroutine scope finished")
}
}
launch函数
使用launch函数可以用于创建多个协程,如下所示:
fun main() {
runBlocking {
launch {
println("launch1")
delay(1000)
println("launch1 finished")
}
launch {
println("launch2")
delay(1000)
println("launch2 finished")
}
}
}
这里我们调用了两次launch函数,也就是创建了两个子协程。
开启10w协程
协程允许我们在单线程模式下模拟多线程编程的效果,代码执行时的挂起与恢复完全是由编程语言来控制的,和操作系统无关。
这种特性使得高并发程序的运行效率得到了极大的提升,试想一下,开启10万个线程完全是不可想象的事吧?而开启10万个协程就是完全可行的。
fun coroutine() {
val start = System.currentTimeMillis()
runBlocking {
repeat(100000) {
launch {
println(".")
}
}
}
val end = System.currentTimeMillis()
println(end - start)
}
suspend关键字
- 随着launch函数中的逻辑越来越复杂,可能你需要将部分代码提取到一个单独的函数中
- 这就产生了一个问题:我们在launch函数中编写的代码是拥有协程作用域的
- 但是提取到一个单独的函数中就没有协程作用域了
Kotlin提供了一个suspend关键字,使用它可讲任意函数声明成挂起函数,而挂起函数之间都是可以互相调用的
suspend fun printDot() {
println(".")
delay(1000)
}
coroutineScope函数
suspend关键字只能讲一个函数声明成挂起函数,是无法给它提供协程的作用域的。
例如要在printDot()函数 中调用launch函数,一定无法调用成功的。
因为launch函数要求必须在协程作用域当中才能调用。
我们使用coroutineScope函数即可解决。
coroutineScope函数可以在任何其他挂起函数中调用。它的特点是会继承外部的协程作用域并创建一个子作用域。
suspend fun printDot() = coroutineScope {
launch {
println(".")
delay(1000)
}
}
coroutineScope函数与runBlocking函数类似,它可以保证其作用域内的所有代码和子协程全部执行完之前眯会一直阻塞当前协程。
fun coroutine() {
runBlocking {
coroutineScope {
launch {
for (i in 1..10) {
println(i)
delay(1000)
}
}
}
println("coroutineScope end")
}
println("runBlocking end")
}
coroutineScope函数与runBlocking函数区别:
- coroutineScope只会阻塞当前协程,不影响其他协程,也不影响其他任何线程。不会有任何性能上的问题
- runBlocking函数会阻塞当前线程。可能会发生界面卡死的状况。
async函数
async函数必须在协程作用域当中才能调用,它会创建一个新的子协程返回一个Deferred对象,如果我们想要获取async函数代码块的执行结果,只需要调用Deferred对象的await()方法即可
代码如下所示:
fun coroutine() {
runBlocking {
val result = async {
5 + 5
}.await()
print(result)
}
}
调用await()方法时,如果代码块中的代码还没执行完,那么await()放会将当前协程阻塞住,直到可以获得async函数的执行结果。
fun coroutine() {
val time = measureTimeMillis {
runBlocking {
val result1 = async {
delay(1000)
5 + 5
}.await()
val result2 = async {
delay(4000)
4 + 5
}.await()
print("result1 $result1 result2 $result2")
}
}
println("time $time")
}
结果:为10result1 10 result2 9 time 5004。说明两个async确实是一种串行的关系,前一个执行完毕,后一个才执行。
我们做出修改为同时调用:
fun coroutine() {
val time = measureTimeMillis {
runBlocking {
val result1 = async {
delay(1000)
5 + 5
}
val result2 = async {
delay(3000)
4 + 5
}
print("result1 ${result1.await()} result2 ${result2.await()}")
}
}
println("async time $time")
}
结果:result1 10 result2 9 async time 3002。
我们只需要把await()方法在需要的时候调用即可,不必每次调用后立即使用。
withContext函数
withContext()函数是一个挂起函数,调用之后,会立即执行代码块中的代码,同时阻塞当前协程。执行完毕后,将最后一行的执行结果作为返回值返回。
基本相当于 val result = async{5+5}.await()
fun coroutine() {
runBlocking {
val result = withContext(Dispatchers.Default) {
5 + 5
}
println("withContext:" + result)
}
}
withContext()函数强制要求指定一个线程参数。
线程参数有以下3种:
- Dispatchers.Default 表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率。
- Dispatchers.IO 表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是阻塞和等待中,比如网络请求时,为了能够支持更高的并发数量。
- Dispatchers.Main 表示不会开启子线程,而是在Android主线程中执行代码。这个值只能在Android项目中使用。
我们刚才所学的协程作用域构建器中,除了coroutineScope函数之外。其他所有的函数都可以指定这样一个线程参数,只不过withContext()函数强制要求指定一个线程参数。
网友评论