【Kotlin】注解&反射&扩展

文章目录

注解

注解是将元数据附加到代码的方法。要声明注解,请将annotation修饰符放在类的前面:

kotlin 复制代码
annotation class Fancy

注解的附加属性可以通过用元注解标注注解类来指定:

  • @Target指定可以用该注解标注的元素的可能的类型(类、函数、属性、表达式等)
  • @Retention指定该注解是否存储在编译后的class文件中,以及它在运行时能否通过反射可见(默认都是true)
  • @Repeatable允许在单个元素上多次使用相同的该注解
  • @MustBeDocumented指定该注解是公有API的一部分,并且应该包含在生成的API文档中显示的类或方法的签名中
kotlin 复制代码
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
        AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class Fancy

用法

kotlin 复制代码
@Fancy class Foo {
    @Fancy fun baz(@Fancy foo: Int): Int {
        return (@Fancy 1)
    }
}

如果需要对类的主构造函数进行标注,则需要在构造函数声明中添加constructor关键字,并将注解添加到其前面:

kotlin 复制代码
class Foo @Inject constructor(dependency: MyDependency) {
    // ......
}

反射

反射是这样的一组语言和库功能,它允许在运行时自省你的程序的结构。
Kotlin让语言中的函数和属性做为一等公民、并对其自省(即在运行时获悉一个名称或者一个属性或函数的类型)与简单地使用函数式或响应式风格紧密相关。

Java平台上,使用反射功能所需的运行时组件作为单独的JAR文件(kotlin-reflect.jar)分发。这样做是为了减少不使用反射功能的应用程序所需的运行时库的大小。如果你需要使用反射,请确保该.jar文件添加到项目的classpath中。

类引用

最基本的反射功能是获取Kotlin类的运行时引用。要获取对静态已知的Kotlin类的引用,可以使用类字面值语法:

kotlin 复制代码
val c = MyClass::class

该引用是KClass类型的值。

通过使用对象作为接收者,可以用相同的::class语法获取指定对象的类的引用:

kotlin 复制代码
val widget: Widget = ......
assert(widget is GoodWidget) { "Bad widget: ${widget::class.qualifiedName}" }

当我们有一个命名函数声明如下:

kotlin 复制代码
fun isOdd(x: Int) = x % 2 != 0

我们可以很容易地直接调用它isOdd(5),但是我们也可以把它作为一个值传递。例如传给另一个函数。为此我们使用::操作符:

kotlin 复制代码
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // 输出 [1, 3]

扩展

扩展是kotlin中非常重要的一个特性,扩展函数允许我们在不改变已有类的情况下,为类添加新的函数。

扩展函数是指对类的方法进行扩展,写法和定义方法类似,但是要声明目标类,也就是对哪个类进行扩展,kotlin中称之为Top Level

扩展函数表现得就像是属于这个类的一样,而且我们可以使用this关键字和调用所有public方法。扩展函数定义形式:

kotlin 复制代码
fun receiverType.functionName(params){
    body
}
receiverType:表示函数的接收者,也就是函数扩展的对象
functionName:扩展函数的名称
params:扩展函数的参数,可以为NULL
kotlin 复制代码
//  对Context的扩展,增加了toast方法。为了更好的看到效果,我还加了一段log日志
fun Context.toast(msg : String){
    Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
    Log.d("text", "Toast msg : $msg")
}

// Activity类,由于所有Activity都是Context的子类,所以可以直接使用扩展的toast方法
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ......
        toast("hello, Extension")
    }
}

// 输出
Toast msg : hello, Extension

按照通常的做法,会写一个ToastUtils工具类,或者在BaseActivity中实现toast。但是使用扩展函数就会简单很多。

上面的例子就是在Context中添加新的方法,让我们以更简单的方式去显示toast,并且不用传入任何context参数,

可以被任何Context或者它的子类调用。我们可以在任何地方(例如一个工具类文件中)声明这个函数,然后在Activity中将它作为普通方法来直接调用。其中Context就是目标类Top Level,我们把它放到方法名前,用点.表示从属关系。在方法体中用关键字this对本体进行调用。和普通方法一样,如果有返回值,在方法后面跟上返回类型,我这里没有返回值,所以直接省略了。

扩展函数的原理: 通过查看对应的Java代码可以发现:

kotlin 复制代码
package com.st.stplayer.extension

fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
    val tmp = this[fromIndex]
    this[fromIndex] = this[toIndex]
    this[toIndex] = tmp
}
java 复制代码
package com.st.stplayer.extension;

import java.util.List;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

@Metadata(
   mv = {1, 4, 2},
   bv = {1, 0, 3},
   k = 2,
   d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u001a \u0010\u0000\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00030\u00022\u0006\u0010\u0004\u001a\u00020\u00032\u0006\u0010\u0005\u001a\u00020\u0003¨\u0006\u0006"},
   d2 = {"exchange", "", "", "", "fromIndex", "toIndex", "stplayer_debug"}
)
public final class ExtenndsKt {
   public static final void exchange(@NotNull List $this$exchange, int fromIndex, int toIndex) {
      Intrinsics.checkNotNullParameter($this$exchange, "$this$exchange");
      int tmp = ((Number)$this$exchange.get(fromIndex)).intValue();
      $this$exchange.set(fromIndex, $this$exchange.get(toIndex));
      $this$exchange.set(toIndex, tmp);
   }
}

通过上面的Java代码可以看出,我们可以将扩展函数理解为静态方法。而静态方法的特点是:它独立于该类的任何对象,且不依赖类的特定实例,

被该类的所有实例共享。此外,被public修饰的静态方法本质上也就是全局方法。所以扩展函数不会带来额外的性能消耗。

扩展函数的作用域

在平时写扩展时,我们通常会把扩展方法放到一个文件中,例如ViewExtends.kt:

kotlin 复制代码
package com.charon.ext

val View.isVisible: Boolean 
    get() = visibility == View.VISIBLE

fun View.toBitmap(): Bitmap? {
    clearFocus()
    isPress = false
    val willNotCache = willNotCacheDrawing()
    setWillNotCacheDrawing(false)
    // Reset the drawing cache background color to fully transparent
    // for the duration of this operation
    val color = drawingCacheBackgroundColor
    drawingCacheBackgroundColor = 0
    if (color != 0) destroyDrawingCache()
    buildDrawingCache()
    val cacheBitmap = drawingCache
    if (cacheBitmap == null) {
        Log.e("Views", "failed to get bitmap from $this", RuntimeException())
        return null
    }
    val bitmap = Bitmap.createBitmap(cacheBitmap)
    // Restore the view
    destroyDrawingCache()
    setWillNotCacheDrawing(willNotCache)
    drawingCacheBackgroundColor = color
    return bitmap
}

我们知道在同一个包内是可以直接调用exchange方法的。如果需要在其他包中调用,只需要import相应的方法即可,这与调用Java全局静态方法类似。

除此之外,实际开发时我们也可能会将扩展函数定义在一个Class内部统一管理:

kotlin 复制代码
class Extends {
    fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
        val tmp = this[fromIndex]
        this[fromIndex] = this[toIndex]
        this[toIndex] = tmp
    }
}

当扩展函数定义在Extends类内部时,情况就与之前不一样了,这个时候你会发现,之前的exchange方法无法调用了(之前调用位置在Extends类外部)。

借助IDEA我们可以查看到它对应的Java代码,这里展示关键部分:

java 复制代码
public static final class Extends {
    public final void exchange(@NotNull list $receiver, int fromIndex, int toIndex) {
        Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
        int tmp = ((Number)$receiver.get(fromIndex)).intValue();
        $receiver.set(fromIndex, $receiver.get(toIndex));
        $receiver.set(toIndex, Integer.valueOf(tmp));
    }
}

我们看到,exchange方法上已经没有static关键字的修饰了。所以当扩展方法在一个Class内部时,我们只能在该类和该类的子类中进行调用。

成员方法优先级总高于扩展函数
kotlin 复制代码
class Son {
    fun foo() = println("son called member foo")
}

它包含一个成员方法foo(),假如哪天心血来潮,想对这个方法做特殊实现,利用扩展函数可能会写出如下代码:

kotlin 复制代码
fun Son.foo() = println("son called extension foo")

object Test {
    @JvmStatic
    fun main(args: Array<String>) {
        Son().foo()
    }
}

在我们的预期中,我们希望调用的是扩展函数foo(),但是输出的结果为: son called member foo。这表明当扩展函数和现有类的成员方法同时存在时,

Kotlin将会默认使用类的成员方法。看起来似乎不够合理,并且很容易引发一些问题:我定义了新的方法,为什么还是调用了旧的方法?

但是换一个角度来思考,在多人开发的时候,如果每个人都对Son扩展了foo方法,是不是很容易造成混淆。对于第三方类库来说甚至是一场灾难:

我们把不应该更改的方法改变了。所以在使用时,我们必须注意:同名的类成员方法的优先级总高于扩展函数

被滥用的扩展函数

扩展函数在开发中为我们提供了非常多的便利,但是在实际应用中,我们可能会将这个特性滥用。例如ImageLoaderUtils这个加载网络图片为例:

kotlin 复制代码
fun Context.loadImage(url: String, imageView: ImageView) {
    GlideApp.with(this)
        .load(url)
        .placeholder(R.mipmap.img_default)
        .error(R.mipmap.ic_error)
        .into(imageView)
}

// 在ImageActivity.kt中使用
this.loadImage(url, imageView)

也许你在用的时候并没有感觉出什么奇怪的地方,但是实际上,我们并没有以任何方式扩展现有类。上述代码仅仅为了在函数调用的时候省去参数,这是一种滥用扩展机制的行为。

我们知道,Context作为"God Object",已经承担了很多责任。我们基于Context扩展,还很可能产生ImageView与传入上下文周期不一致导致的很多问题。

正确的做法应该是在ImageView上进行扩展:

kotlin 复制代码
fun ImageView.loadImage(url: String) {
    GlideApp.with(this.context)
        .load(url)
        .placeholder(R.mipmap.img_default)
        .error(R.mipmap.ic_error)
        .into(this)
}

这样在调用的时候,不仅省去了更多的参数,而且ImageView的生命周期也得到了保证。

在实际项目中,我们还需要考虑网络请求框架替换及维护的问题,一般会对图片的请求框架进行二次封装:

kotlin 复制代码
object ImageLoader {
    fun with(context: Context, url: String, imageView: ImageView) {
        GlideApp.with(context)
            .load(url)
            .placeholder(R.mipmap.img_default)
            .error(R.mipmap.ic_error)
            .into(imageView)
    }
}

所以,虽然扩展函数能够提供许多便利,我们还是应该注意在恰当的地方使用它,否则会造成不必要的麻烦。

扩展属性

扩展属性和扩展方法类似,是对目标类的属性进行扩展。扩展属性也会有setget方法,并且要求实现这两个方法,不然会提示编译错误。

因为扩展并不是在目标类上增加了这个属性,所以目标类其实是不持有这个属性的,我们通过getset对这个属性进行读写操作的时候也不能使用
field指代属性本体。可以使用this,依然表示的目标类。

kotlin 复制代码
// 扩展了一个属性paddingH
var View.panddingH : Int
    get() = (paddingLeft + paddingRight) / 2
    set(value) {
        setPadding(value, paddingTop, value, paddingBottom)
    }

// 设置值
text.panddingH = 100

View扩展了一个属性paddingH,并给属性增加了setget方法,然后可以在activity中通过textview调用。

扩展属性与扩展函数一样,其本质也是对应Java中的静态方法。由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。

扩展属性允许定义在类或者Kotlin文件中,但不允许定义在函数中。 因为扩展属性不能有初始化器,所以只能由显式提供的getter/setter定义,而且扩展属性只能被声明为val。

静态扩展

kotlin中的静态用关键字companion表示,但是它不是修饰属性或方法,而是定义一个方法块,在方法块中的所有方法和属性都是静态的,

这样就将静态部分统一包装了起来。静态部分的访问和java一致,直接使用类名+静态属性/方法名调用。

kotlin 复制代码
// 定义静态部分
class Extension {
    companion object{
        var name = "Extension"
    }
}

// 通过类名+属性名直接调用
toast("hello, ${Extension.name}")

// 输出
Toast msg : hello, Extension

静态的扩展和普通的扩展类似,但是在目标类要加上静态方法块的名称,所以如果我们要对一个静态部分扩展,就要先知道静态方法块的名称才行。

kotlin 复制代码
class Extension {
    companion object part{
        var name = "Extension"
    }
}

// part为静态方法块名称
fun Extension.part.upCase() : String{
    return name.toUpperCase()
}

// 调用一下
toast("hello, ${Extension.name}")
toast("hello, ${Extension.upCase()}")

//输出
Toast msg : hello, Extension
Toast msg : hello, EXTENSION

上面例子中,companion object一起是修饰关键字,part是方法块的名称。其中方法块名称part可以省略,如果省略的话,默认缺省名为
Companion

标准库中的扩展函数
  1. run

先看一下run方法,它是利用扩展实现的,定义如下:

kotlin 复制代码
public inline fun <T, R> T.run(block: T.() -> R): R = block()

简单来说,run是任何类型T的通用扩展函数,run中执行了返回类型为R的扩展函数block,最终返回该扩展函数的结果。

在run函数中我们拥有一个单独的作用域,能够重新定义一个nickName变量,并且它的作用域只存在于run函数中:

kotlin 复制代码
fun testFoo() {
    val nickName = "111"
    run {
        val nickName = "xxx"
        println(nickName) // xxx
    }
    println(nickName) // 111
}

这个范围函数本身似乎不是很有用。但是相比范围,还有一点不错的是,它返回范围内最后一个对象。

例如现在有这么一个场景:用户点击领取新人奖励的按钮,如果用户此时没有登陆则弹出loginDialog,

如果已经登录则弹出领取奖励的getNewAccountDialog。我们可以使用以下代码来处理这个逻辑:

kotlin 复制代码
run {
    if (!isLogin) loginDialog else getNewAccountDialog
}.show()
  1. apply

apply函数是这样的,调用某对象的apply函数,在函数范围内,可以任意调用该对象的任意方法,并返回该对象.

kotlin 复制代码
fun testApply() {
    ArrayList<String>().apply {
        add("testApply")
        add("testApply")
        add("testApply")
        println("this = " + this)
    }.let { println(it) }
}

// 运行结果
// this = [testApply, testApply, testApply]
// [testApply, testApply, testApply]

let函数和apply函数很像,唯一不同的是返回值。apply返回的是原来的对象,而let返回的是闭包里面的值。

  1. let

他的定义为:

kotlin 复制代码
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)

如果对象的值不为空,则允许执行这个方法。返回值是函数里面最后一行,或者指定return,与run一样,它同样限制了变量的作用域。

kotlin 复制代码
private var test: String? = null

private fun switchFragment(position: Int) {
    test?.let {
        LogUtil.e("@@@", "test is not null")
    }
}    

说到可能有人会觉得没什么用,用if判断下是不是空不就完了.

kotlin 复制代码
private var test: String? = null

private fun switchFragment(position: Int) {
//        test?.let {
//            LogUtil.e("@@@", "test is null")
//        }

    if (test == null) {
        LogUtil.e("@@@", "test is null")
    } else {
        LogUtil.e("@@@", "test is not null ${test}")
        check(test) // 报错
    }
}    

但是会报错:Smart cast to 'String' is impossible, beacuase 'test' is a mutable property that could have been changed by this time

  1. also

also是Kotlin 1.1版本中新加入的内容,它像是let和apply函数的加强版。

kotlin 复制代码
public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }

与apply一致,它的返回值是该函数的接受者。

kotlin 复制代码
class Kot {
    val student: Student? = getStu()
    var age = 0
    fun dealStu() {
        val result = student?.also { stu -> 
            this.age += stu.age                                    
            println(this.age)
            println(stu.age)
            this.age                                    
        }
    }
}

run、with、let、also、apply都是作用域函数,这些作用域函数如何使用以及区分呢?我们将从以下三个方面来区分它们:

  • 是否是扩展函数
  • 作用域函数的参数(this、it)
  • 作用域函数的返回值(调用本身、其他类型即最后一行)

首先我们来看一下 with 和 T.run,这两个函数非常的相似,他们的区别在于 with 是个普通函数,T.run 是个扩展函数,来看一下下面的例子:

kotlin 复制代码
val name: String? = null
with(name){
    val subName = name!!.substring(1,2)
}

// 使用之前可以检查它的可空性
name?.run { val subName = name.substring(1,2) }?:throw IllegalArgumentException("name must not be null")

在这个例子当中,name?.run 会更好一些,因为在使用之前可以检查它的可空性。

我们在来看一下 T.run 和 T.let,它们都是扩展函数,但是他们的参数不一样 T.run 的参数是 this, T.let 的参数是 it:

kotlin 复制代码
val name: String? = "hi-dhl.com"

// 参数是 this,可以省略不写
name?.run {
    println("The length  is ${this.length}  this 是可以省略的 ${length}")
}

// 参数 it
name?.let {
    println("The length  is  ${it.length}")
}

// 自定义参数名字
name?.let { str ->
    println("The length  is  ${str.length}")
}

在上面的例子中看似 T.run 会更好,因为 this 可以省略,调用更加的简洁,但是 T.let 允许我们自定义参数名字,使可读性更强,如果倾向可读性可以选择 T.let。

接下里我们来看一下 T.let 和 T.also 它们接受的参数都是 it, 但是它们的返回值是不同的 T.let 返回最后一行,T.also 返回调用本身:

kotlin 复制代码
var name = "hi-dhl"

// 返回调用本身
name = name.also {
    val result = 1 * 1
    "juejin"
}
println("name = ${name}") // name = hi-dhl

// 返回的最后一行
name = name.let {
    val result = 1 * 1
    "hi-dhl.com"
}
println("name = ${name}") // name = hi-dhl.com

从上面的例子来看 T.also 似乎没有什么意义,细想一下其实是非常有意义的,在使用之前可以进行自我操作,结合其他的函数,功能会更强大:

kotlin 复制代码
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

接下来看一下 T.apply 函数,T.apply 函数是一个扩展函数,返回值是它本身,并且接受的参数是 this:

kotlin 复制代码
// 普通方法
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}
// 改进方法
fun createInstance(args: Bundle) 
              = MyFragment().apply { arguments = args }
              
              
// 普通方法
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}
// 改进方法,链式调用
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }

以表格的形式汇总,更方便去理解

函数 是否是扩展函数 函数参数(this、it) 返回值(调用本身、最后一行)
with 不是 this 最后一行
T.run this 最后一行
T.let it 最后一行
T.also it 调用本身
T.apply this 调用本身

使用 T.also 函数交换两个变量

接下来演示的是使用 T.also 函数,实现一行代码交换两个变量?我们先来回顾一下 Java 的做法。

java 复制代码
int a = 1;
int b = 2;

// Java - 中间变量
int temp = a;
a = b;
b = temp;
System.out.println("a = "+a +" b = "+b); // a = 2 b = 1

// Java - 加减运算
a = a + b;
b = a - b;
a = a - b;
System.out.println("a = " + a + " b = " + b); // a = 2 b = 1
        
// Java - 位运算
a = a ^ b;
b = a ^ b;
a = a ^ b;
System.out.println("a = " + a + " b = " + b); // a = 2 b = 1

// Kotlin
a = b.also { b = a }
println("a = ${a} b = ${b}") // a = 2 b = 1

来一起分析 T.also 是如何做到的,其实这里用到了 T.also 函数的两个特点。

  • 调用 T.also 函数返回的是调用者本身。
  • 在使用之前可以进行自我操作。

也就是说 b.also { b = a } 会先将 a 的值 (1) 赋值给 b,此时 b 的值为 1,然后将 b 原始的值(2)赋值给 a,此时 a 的值为 2,实现交换两个变量的目的。

sNullOrEmpty | isNullOrBlank
kotlin 复制代码
public inline fun CharSequence?.isNullOrEmpty(): Boolean = this == null || this.length == 0

public inline fun CharSequence?.isNullOrBlank(): Boolean = this == null || this.isBlank()

// If we do not care about the possibility of only spaces...
if (number.isNullOrEmpty()) {
    // alert the user to fill in their number!
}

// when we need to block the user from inputting only spaces
if (name.isNullOrBlank()) {
    // alert the user to fill in their name!
}
with函数

with函数接收两个参数:第一个参数可以是一个任意类型的对象,第二个参数是一个Lambda表达式。with函数会在Lambda表达式中提供第一个参数对象的上下文,并使用Lambda表达式中的最后一行代码作为返回值返回。

with和apply这两个方法最大的作用就是可以让那个我们在写Lambda的时候,省略需要多次书写的对象名,默认用this关键字来指向它,this可以省略。

with是一个非常有用的函数,它包含在Kotlin的标准库中。它接收一个对象和一个扩展函数作为它的参数,然后使这个对象扩展这个函数。

这表示所有我们在括号中编写的代码都是作为对象(第一个参数)的一个扩展函数,我们可以就像作为this一样使用所有它的public方法和属性。

当我们针对同一个对象做很多操作的时候这个非常有利于简化代码。

kotlin 复制代码
fun testWith() {
    with(ArrayList<String>()) {
        add("testWith")
        add("testWith")
        add("testWith")
        println("this = " + this)
    }
}
// 运行结果
// this = [testWith, testWith, testWith]
repeat函数

repeat函数是一个单独的函数,定义如下:

kotlin 复制代码
/**
 * Executes the given function [action] specified number of [times].
 *
 * A zero-based index of current iteration is passed as a parameter to [action].
 */
@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {
    contract { callsInPlace(action) }

    for (index in 0..times - 1) {
        action(index)
    }
}

通过代码很容易理解,就是循环执行多少次block中内容。

kotlin 复制代码
fun main(args: Array<String>) {
    repeat(3) {
        println("Hello world")
    }
}

运行结果是:

kotlin 复制代码
Hello world
Hello world
Hello world

调度方式对扩展函数的影响

Kotlin是一种静态类型语言,我们创建的每个对象不仅具有运行时,还具有编译时类型。在使用扩展函数时,要清楚地了解静态和动态调度之间的区别。

静态与动态调度

先用一个Java的例子:

java 复制代码
class Base {
    public void fun() {
        System.out.println("I'm Base foo!");
    }
}
class Extended extends Base {
    @Override
    public void fun() {
        System.out.println("I'm Extended foo!");
    }
}
Base base = new Extended();
base.fun();

毫无疑问,因为重写了fun方法,所以最终肯定会执行I'm Extended foo!。

变量base具有编译时类型Base和运行时类型Extended。当我们调用时,base.foo()将动态调度该方法,这意味着运行时类型(Extended)的方法被调用。

当我们调用重载方法时,调度变为静态并且仅取决于编译时类型。

java 复制代码
void foo(Base base) {
    ...
}
void foo(Extended extended) {
    ...
}
public static void main(String[] args) {
    Base base = new Extended();
    foo(base);
}

在这种情况下,即使base本质上是Extended的实例,最终还是会执行Base的方法。

扩展函数始终静态调度

可能你会好奇,这和扩展有什么关系?我们知道,扩展函数都有一个接收器(receiver),由于接收器实际上只是字节代码中编译方法的参数,

因此你可以重载它。但不能覆盖它。这可能是成员和扩展函数之间最重要的区别:前者是动态调度的,后者总是静态调度的。

为了方便理解,举一个例子:

kotlin 复制代码
open class Base
class Extended: Base()
fun Base.foo() = "I'm Base.foo!"
fun Extended.foo() = "I'm Extended.foo!"
fun main(args: Array<String>) {
    val instance: Base = Extended()
    val instance2 = Extended()
    println(instance.foo())
    println(instance2.foo())
}
// 执行结果
I'm Base.foo!
I'm Extended.foo!

由于只考虑了编译时类型,第一个打印将调用Base.foo(),而第二个打印将调用Extended.foo()。

用扩展函数封装Utils

在Java中,我们习惯将常用的代码放到对应的工具类中,例如ToastUtils、NetworkUtils、ImageLoaderUtils等。

以NetworkUtils为例,该类中我们通常会放入Android经常需要使用的网络相关方法。比如,我们现在有一个判断手机网络是否可用的方法:

java 复制代码
public class NetworkUtils {
    public static boolean isMobileConnected(Context context) {
        if (context != null) {
            ConnectivityManager mConnectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo mMobileNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
            if (mMobilieNetworkInfo != null) {
                return mMobileNetworkInfo.isAvailable();
            }
        }
        return false;
    }
}

在需要调用的地方,我们通常会这样写:

java 复制代码
boolean isConnected = NetworkUtils.isMobileConnected(context);

虽然用起来比没有封装之前优雅了很多,但是每次都要传入context,造成的烦琐我们先不计较,重要是可能会让调用者忽视context

和mobileNetwork间的强关系。作为代码的使用者,我们更希望在调用时省略NetworkUtils类名,并且让isMobileConnected可以看起来像context的一个属性或方法。我们期望的是下面这样的使用方式:

java 复制代码
boolean isConnected = context.isMobileConnected();

由于Context是Android SDK自带的类,我们无法对其进行修改。在Java中目前只能通过继承Context新增静态成员方法来实现。

但是在Kotlin中,我们通过扩展函数就能简单的实现:

kotlin 复制代码
fun Context.isMobileConnected(): Boolean {
    val mNetworkInfo = connectivityManager.activeNetworkInfo
    if (mNetworkInfo != null) {
        return mNetworkInfo.isAvailable
    }
    return false
}

我们只需要将以上代码放入对应文件中即可。这时我们已经摆脱了类的束缚,使用方式如下:

kotlin 复制代码
val isConnected = context.isMobileConnected();

值得一提的是,在Android中对Context的生命周期需要进行很好的把控。这里我们应该使用ApplicationContext,

防止出现生命周期不一致导致的内存泄露或者其他问题。

运算符重载

Kotlin允许我们对所有的运算符(+ - * / % ++ --),以及其他关键字进行重载,从而拓展这些运算符与关键字的用法。

语法:运算符重载使用的是operator关键字,在指定函数的前面加上operator关键字,就可以实现运算符重载了。

指定函数,不同的运算符对应的重载函数是不同的,比如:+ 对应plus() 、 - 对应minus()

可以在类中,对同一运算符进行多次重载,来满足不同的需求。

运算符重载实际上是函数重载,本质上是对运算符函数的调用,从运算符到对应函数的映射过程由编译器完成,或者理解成是对已有的运算符赋予他们新的含义。

举例:

比如我们的+号,它的含义是两个数值相加: 1 + 1 = 2。 +号对应的函数名是plus。我们可以对+号这个函数进行重载,让它实现减法的效果。

下面我们实现一个Person数据类,然后重载Int的 + 号运算符,让一个Int对象可以直接和Person对象相加。返回的结果是这个Int对象减去Person对象的age的值。

kotlin 复制代码
data class Person(var name: String, var age: Int)
// 通过扩展的方式来实现运算符重载
operator fun Int.plus(b: Person): Int {
    return this - b.age
}
fun main() {
    val person1 = Person("A", 3)
    val testInt = 5
    println("result : ${testInt + person1}")  //输出结果=2
}

再比如,我们可以对Person数据类进行重载+号运算符,让Person对象可以直接调用+号来做一些函数操作。

kotlin 复制代码
data class Person(var name: String, var age: Int) {
    operator fun plus(other: Person): Person {
        return Person("${this.name} + ${other.name}", this.age + other.age)
    }
}
fun main() {
    val person1 = Person("A", 3)
    val person2 = Person("B", 4)
    val person3 = person1 + person2
    println("person3 = ${person3}") // person3 = Person(name=A + B, age=7)
}

一些场景运算符对应的函数名如下:

一元前缀操作符

表达式 翻译为
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()

算术运算符

表达式 翻译为
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)a.mod(b) (已弃用)
a..b a.rangeTo(b)

"In"操作符

表达式 翻译为
a in b b.contains(a)
a !in b !b.contains(a)

索引访问操作符

表达式 翻译为
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, ......, i_n] a.get(i_1, ......, i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ......, i_n] = b a.set(i_1, ......, i_n, b)

调用操作符

表达式 翻译为
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ......, i_n) a.invoke(i_1, ......, i_n)

相等与不等操作符

表达式 翻译为
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

比较操作符

表达式 翻译为
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0
函数引用

当我们有一个命名函数声明如下:

kotlin 复制代码
fun isOdd(x: Int) = x % 2 != 0

我们可以很容易地直接调用它(isOdd(5)),但是我们也可以把它作为一个值传递。例如传给另一个函数。

为此,我们使用::操作符:

kotlin 复制代码
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // 输出 [1, 3]

这里::isOdd是函数类型(Int) -> Boolean的一个值。

当上下文中已知函数期望的类型时::可以用于重载函数。

例如:

kotlin 复制代码
fun isOdd(x: Int) = x % 2 != 0
fun isOdd(s: String) = s == "brillig" || s == "slithy" || s == "tove"

val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // 引用到 isOdd(x: Int)

或者,你可以通过将方法引用存储在具有显式指定类型的变量中来提供必要的上下文:

kotlin 复制代码
val predicate: (String) -> Boolean = ::isOdd   // 引用到 isOdd(x: String)

如果我们需要使用类的成员函数或扩展函数,它需要是限定的。

例如String::toCharArray为类型String提供了一个扩展函数:String.() -> CharArray

属性引用

要把属性作为Kotlin中的一等对象来访问,我们也可以使用::运算符:

kotlin 复制代码
var x = 1

fun main(args: Array<String>) {
    println(::x.get()) // 输出 "1"
    ::x.set(2)
    println(x)         // 输出 "2"
}

表达式::x求值为KProperty<Int>类型的属性对象,它允许我们使用
get()读取它的值,或者使用name属性来获取属性名。更多信息请参见

关于KProperty类的文档。

对于可变属性,例如var y = 1::y返回KMutableProperty<Int>类型的一个值,

该类型有一个set()方法。

属性引用可以用在不需要参数的函数处:

kotlin 复制代码
val strs = listOf("a", "bc", "def")
println(strs.map(String::length)) // 输出 [1, 2, 3]

要访问属于类的成员的属性,我们这样限定它:

kotlin 复制代码
class A(val p: Int)

fun main(args: Array<String>) {
    val prop = A::p
    println(prop.get(A(1))) // 输出 "1"
}

Kotlin类型别名

类型别名为现有类型提供替代名称。

如果类型名称太长,你可以另外引入较短的名称,并使用新的名称替代原类型名。

它有助于缩短较长的泛型类型。

例如,通常缩减集合类型是很有吸引力的:

kotlin 复制代码
typealias NodeSet = Set<Network.Node>

typealias FileTable<K> = MutableMap<K, MutableList<File>>

你可以为函数类型提供另外的别名:

kotlin 复制代码
typealias MyHandler = (Int, String, Any) -> Unit

typealias Predicate<T> = (T) -> Boolean

你可以为内部类和嵌套类创建新名称:

kotlin 复制代码
class A {
    inner class Inner
}
class B {
    inner class Inner
}

typealias AInner = A.Inner
typealias BInner = B.Inner

类型别名不会引入新类型。

它们等效于相应的底层类型。

当你在代码中添加typealias Predicate<T>并使用Predicate<Int>时,Kotlin编译器总是把它扩展为(Int) -> Boolean

因此,当你需要泛型函数类型时,你可以传递该类型的变量,反之亦然:

kotlin 复制代码
typealias Predicate<T> = (T) -> Boolean

fun foo(p: Predicate<Int>) = p(42)

fun main(args: Array<String>) {
    val f: (Int) -> Boolean = { it > 0 }
    println(foo(f)) // 输出 "true"

    val p: Predicate<Int> = { it > 0 }
    println(listOf(1, -2).filter(p)) // 输出 "[1]"
}

文档

用来编写Kotlin代码文档的语言(相当于JavaJavaDoc)称为KDoc。本质上KDoc是将JavaDoc的块标签(block tags)语法(

扩展为支持Kotlin的特定构造)和Markdown的内联标记(inline markup)结合在一起。

生成文档

Kotlin的文档生成工具称为Dokka

DokkaGradleMavenAnt的插件,因此你可以将文档生成集成到你的构建过程中。

JavaDoc一样,KDoc注释也以/**开头、以*/结尾。注释的每一行可以以

星号开头,该星号不会当作注释内容的一部分。

按惯例来说,文档文本的第一段(到第一行空白行结束)是该元素的

总体描述,接下来的注释是详细描述。

每个块标签都以一个新行开始且以@字符开头。

以下是使用KDoc编写类文档的一个示例:

kotlin 复制代码
/**
 * 一组*成员*。
 *
 * 这个类没有有用的逻辑; 它只是一个文档示例。
 *
 * @param T 这个组中的成员的类型。
 * @property name 这个组的名称。
 * @constructor 创建一个空组。
 */
class Group<T>(val name: String) {
    /**
     * 将 [member] 添加到这个组。
     * @return 这个组的新大小。
     */
    fun add(member: T): Int { ...... }
}

KDoc目前支持以下块标签(block tags):

  • @param <名称>

    用于函数的值参数或者类、属性或函数的类型参数。

    为了更好地将参数名称与描述分开,如果你愿意,可以将参数的名称括在

    方括号中。因此,以下两种语法是等效的:

    kotlin 复制代码
    @param name 描述。
    @param[name] 描述。
  • @return

    用于函数的返回值。

  • @constructor

    用于类的主构造函数。

  • @receiver

    用于扩展函数的接收者。

  • @property <名称>

    用于类中具有指定名称的属性。这个标签可用于在

    主构造函数中声明的属性,当然直接在属性定义的前面放置doc注释会很别扭。

  • @throws <类>,@exception <类>

    用于方法可能抛出的异常。因为Kotlin没有受检异常,所以也没有期望所有可能的异常都写文档,但是当它会为类的用户提供有用的信息时,仍然可以使用这个标签。

  • @sample <标识符>

    将具有指定限定的名称的函数的主体嵌入到当前元素的文档中,以显示如何使用该元素的示例。

  • @see <标识符>

    将到指定类或方法的链接添加到文档的另请参见块。

  • @author

    指定要编写文档的元素的作者。

  • @since

    指定要编写文档的元素引入时的软件版本。

  • @suppress

    从生成的文档中排除元素。可用于不是模块的官方API的一部分但还是必须在对外可见的元素。

KDoc不支持@deprecated这个标签。作为替代,请使用@Deprecated注解。

泛型

通配符类型参数 ? extends E表示该方法接收E类型或者E类型的一些子类型,而不仅仅是E类型本身。

Kotlin抛弃了通配符类型这一概念,转而引入了生产者和消费者的概念,其中,生产者表示那些只能读取数据的对象,使用表达式out T标记。

消费者表示那些只能写入数据的对象,使用表达式in T标记。

为了便于理解,可以简单的将out T理解为? exnteds T,将in T理解为? super T

相关推荐
Love__Tay6 分钟前
【学习笔记】Python金融基础
开发语言·笔记·python·学习·金融
Lilith的AI学习日记20 分钟前
什么是预训练?深入解读大模型AI的“高考集训”
开发语言·人工智能·深度学习·神经网络·机器学习·ai编程
有风南来1 小时前
算术图片验证码(四则运算)+selenium
自动化测试·python·selenium·算术图片验证码·四则运算验证码·加减乘除图片验证码
wangjinjin1801 小时前
Python Excel 文件处理:openpyxl 与 pandas 库完全指南
开发语言·python
愚润求学1 小时前
【C++】类型转换
开发语言·c++
斯奕sky_small-BAD1 小时前
C++ if语句完全指南:从基础到工程实践
java·开发语言·php
Humbunklung1 小时前
Rust Floem UI 框架使用简介
开发语言·ui·rust
移动开发者1号1 小时前
Android 大文件分块上传实战:突破表单数据限制的完整方案
android·java·kotlin
移动开发者1号2 小时前
单线程模型中消息机制解析
android·kotlin
Yxh181377845542 小时前
抖去推--短视频矩阵系统源码开发
人工智能·python·矩阵