优雅的使用Kotlin

优雅的使用Kotlin

这篇文章并不会从零开始来教你怎么使用Kotlin,Kotlin对于笔者来说也是一个新概念,由于大势所趋(Google所有的官方Demo都开始用Kotlin,各大第三方Library也开始适配Kotlin),所以不会Koltin寸步难行呀~,经过一段时间的学习和练习,对Kotlin有了一点浅薄的理解,在此记录下来,希望能给大家带来帮助。

开始进入主题,我打算从下面几个方面来给大家总结一些使用技巧和个人的理解(不足之处还请大家多多指出)

  • 类(类、接口、数据类)
  • 函数(扩展函数)
  • DSL、扩展库和Anko
  • 协程

如何创建一个Kotlin类

我们先看看我们熟悉的Java如何创建一个普通的类:

class Player{
    private String name;
    public Player(String name){
        this.name = name;
    }
    
    public void play(){
        system.out.println(name+" start play");
    }
}

下面是Kotlin声明的类

class Player(val name :String){
    fun play(){
        println("$name start paly")
    }
}

看上去好像差别不是很大嘛,其实对于学过Java的人来说,Kotlin还是比较相近的,只需要了解一些新特性,上手还是很快的。

在上面的例子中,Kotlin的构造函数直接声明在类的后面,并且变量的声明也可以在函数的参数中声明,最后字符串的输出,直接使用${变量名}的形式输出

将上面的Kotlin代码反编译成Java代码如下:

public final class Player {
   @NotNull
   private final String name;

   public final void play() {
      String var1 = this.name + " start paly";
      System.out.println(var1);
   }

   @NotNull
   public final String getName() {
      return this.name;
   }

   public Player(@NotNull String name) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
   }
}

基本上一样,接下来我们就开始看看Kotlin怎样常见一个类,和我们Java的类有什么具体的区别。

构造函数

相信大家对Java的构造函数已经很熟悉了:

  • 多个构造函数,构造函数之间是重载的
  • 可以不声明构造函数,编译器会自动生成一个无参的构造函数

Kotlin中,一个类中可以有一个主构造函数和多个次构造函数。主构造函数就是声明在类头的构造函数,次构造函数必须委托主构造函数,文字太枯燥,举几个例子就一目了然了。

//这是没有省略所有关键字的类声明,当然如果没有可见性的修饰符(默认是public),可以省略看见性修饰符和constructor
class Player public constructor(val name :String){
    
    //如果想在构造函数中定一些变量或者创建对象,可以直接在类体中定义,也可以在init{...}代码块中去定义
    val id = "111" 
    
    init{
        println("$id 是在初始化的代码块中")
    }
   
    fun play(){
        println("$name start paly")
    }
    
    // 调用player("222").play()输出下面字符串:
    // 111 是在初始化的代码块中
    // 222 start paly
}


// 主构造函数和次构造函数并存
//初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块中的代码都会在次构造函数体之前执行。即使该类没有主构造函数,这种委托仍会隐式发生,并且仍会执行初始化块
class Player(val name: String) {
    val id = "111"

    init {
        println("$id 是在初始化的代码块中")
    }

    constructor(gender: String, name: String) : this("123") {
        println("我是 $gender 性")
    }

    fun play() {
        println("$name start paly")
    }

}


Kotlin构造函数小结:

  1. 可以有一个主构造函数和多个次构造函数
  2. 可以只有主构造函数和次构造函数
  3. 主、次构造函数同事存在的时候,次构造函数必须直接或者间接的委托到住构造函数
  4. 没有声明主构构造函数,会自动生成一个无参数的主构造函数,这点与java一样。
  5. 在主构造函数中你可以设置默认的值。

数据类

个人觉得Kotlin的数据类使用起来真的很方便,省去我们很多工作量(妈妈从此再也不担心我写数据类了~~)。

先用Java写一个数据类,方便我们后面做对比

public class User {
    private String id;
    private String name;
    private int gender;
    private String avatar;
    private int age;

    public User(String id) {
        this.id = id;
    }
    

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getGender() {
        return gender;
    }

    public void setGender(int gender) {
        this.gender = gender;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

可以看到声明一个数据类将近需要60多行的代码,还是在没有重写toString(),equals(),hashCode()等方法的前提下。

我们再来看看Kotlin如果实现一个对象类

data class User(val id: String, var name: String = "", var gender: Int = 0, var avatar: String = "", var age: Int = 0)

Kotlin只需要一行代码就搞定,是不是很优雅,优雅的背后是编译器为我们做了许多的事情,编译器会自动的从主构造函数中根据所有声明的属性生成以下的函数

  • equals()/hasCode()
  • toString()
  • copy()函数

实用小窍门
当我们使用定义好的类时,如果有些属性是不必要的,可以设置默认值,那么我们就可以按照以下的方式调用(拿上面的User做例子):

//Test.kt

class Test{
    
    fun main(args: Array<String>) {
        //姿势一,只赋值id属性
        User("111")
        //姿势二,打乱顺序,再也不用担心在参数居多的情况下,搞不清参数的含义了
         User(name = "liyachao", id = "222")
    }
}

在日常的工作中基本上熟悉上面两个知识点,就能很好的工作了,关于类还有

  • 枚举类 (和Java使用基本一样)
  • 密闭类 (sealed class)枚举类的扩展,每个枚举常量只存在一个实例,而密闭类的一个子类可以有多个实例,暂时还没有用到。
  • 嵌套类,与Java的静态内部类一样
  • 内部类,和Java一样,内部类持有一个外部类的对象引用
  • 匿名内部类。

函数

Kotlin在函数使用上增加很多的语法糖,我们可以方便、愉快的编写代码,也使得代码看起来非常简洁易懂,总是就是更加优雅

扩展函数

Kotlin允许在不改变已有类的情况下,为某个类添加新的函数,这个特性叫做扩展函数。有了这个特性,那么我们岂不是可以给任意的类添加我们想要的函数,再也不用添加XXXUtils或者XXXTool的类了,Kotlin使用了什么黑科技吗?让我们看看

假如我们要给Context添加一个toast的功能,可以这样写

fun Context.toast(str: String) = Toast.makeText(this, str, Toast.LENGTH_LONG).show()

// 当我们需要toast的时候,我们可以这样调用

context.toast("test")

是不是很神奇,我们通过Android Studio反编译看下.java文件是怎样的

//声明 toast方法的类
public final class TestKt {
   public static final void toast(@NotNull Context $receiver, @NotNull String str) {
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      Intrinsics.checkParameterIsNotNull(str, "str");
      Toast.makeText($receiver, (CharSequence)str, 1).show();
   }
}
// 调用toast的地方
TestKt.toast(context, "test");

从上面代码我们不难明白,其实Kotlin的扩展函数也是给我们生成一个工具类,在掉用的地方帮我们换成这个类的调用,原来如此的简单。

说到扩展函数,就不得不提的几个基础库的扩展函数,也叫做作用域函数,它们是:T.run、T.let、T.also、T.apply 以及run、width两个函数。对于这几个函数的使用刚开始的时候我还是很懵逼的,只是大概了解用法,以及带来的链式调用的便捷,但是它们怎么什么情况下使用,我还真是搞不明白,通常情况下都是想起拿个用哪个。但是这篇文章很好的解释了各个函数的用法和情况。

run

这是最简单的作用域,看下源代码,Kotlin是怎样定义这个方法的:

/**
 * Calls the specified function [block] and returns its result.
 * 其实就是提供了一个作用域的方法,返回函数block的结果
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

//举个例子,可以这样使用

fun test(){
    var mood = "I am sad"
    
    run {
        val mod = "I am happy"
        Toast.makeText($receiver, (CharSequence)str, 1)
    }.show()
}

在这个例子中看起来好像并没有什么卵用,但是其实还是有一些好处的(虽然我在实际开发中没有用到过)

  • 分隔开的作用域。
  • 可以根据作用域返回的结果进行接下来的操作

普通函数和扩展函数(with VS T.run)

还是先看下这两个函数的源码

/**
 * Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

从源码上看基本上是没有区别的,都是接受一个函数作为参数,然后返回这个函数的结果。它们之间唯一不同的在于with是一个普通函数,而T.run是一个扩展函数,那么问题来了,它们各自使用的优点是什么呢?举个例子

// 对于user的处理

fun test(){
    with(user){
        id = "222"
        name = "test"
    }
    
    user.run{
        id = "222"
        name = "test"
    }
    
    // 好像并没有什么区别,想想一下,如果user可能为null呢,该如何去实现
    
    with(user){
        this?.id = "222"
        this?.name = "test"
    }
    
    user?.run{
        id = "222"
        name = "test"
    }
}

第二种场景下,显然T.run扩展函数更好,因为我们可以在使用它之前对可空性进行检查。

this和it区别(T.run VS T.let)

如果我们对比T.run和T.let两个函数也是非常相似的,上源码

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

从源码中可以看出来,它们唯一的区别是接受的参数不一样。

  • T.run仅仅是被当做了block:T.()扩展函数的调用块。因此在其作用域内,T可以被this指代。在编码过程中,大多数情况下this是可以被省略的。
  • T.let将自己本身传递到函数block:(T)。因此这个类似于传递一个lambda表达式作为参数。它可以在函数作用域内部是用哪个it来指代,所以把这个称为传递it参数。

举个例子

user?.run{
    println("my name is $name)
}

user?.let{
    println("my name is $it.name")
}

从上面的例子来看,似乎T.run比T.let更加优越,但是T.let有一些微妙的优势(其实我觉得没什么卵用,都差不多,就看你喜欢用哪个)

  1. T.let函数提供了一种更清晰的区分方式去使用给定的变量函数/成员与外部类函数/成员。
  2. 例如当this作为函数的参数传递时,this不能被省略,并且it写起来比this更简洁,更清晰。
  3. T.let允许更好地命名已转换的已使用变量,即可以将it转换为其他有含义名称,而 T.run则不能,内部只能用this指代或者省略。

return this和return 函数结果(T.let VS T.also)

照例,我们先看下它们的源码

/**
 * Calls the specified function [block] with `this` value as its argument and returns `this` value.
 */
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

从上面源码可以看出来,它们唯一的不同就是T.let返回一个不同类型的值也就是函数执行的结果,而T.alse返回T类型本身

举个例子

val original = "abc"
// Evolve the value and send to the next chain
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // evolve it as parameter to send to next let
}.let {
    println("The reverse String is $it") // "cba"
    it.length  // can be evolve to other type
}.let {
    println("The length of the String is $it") // 3
}
// Wrong
// Same value is sent in the chain (printed answer is wrong)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // even if we evolve it, it is useless
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // even if we evolve it, it is useless
}.also {
    println("The length of the String is ${it}") // "abc"
}
// Corrected for also (i.e. manipulate as original string
// Same value is sent in the chain 
original.also {
    println("The original String is $it") // "abc"
}.also {
    println("The reverse String is ${it.reversed()}") // "cba"
}.also {
    println("The length of the String is ${it.length}") // 3
}

T.also似乎看上去没有意义,因为我们可以很轻易的将它们组合成为一个模块。仔细想想,T.also还是有一些好处的

  1. 它可以对一个对象提供非常清晰的区分过程,将其区分成更小的功能区域
  2. 在使用之前,可以进行强大的自我操作,创建一个链式调用的操作。

当把这个链式调用连接起来,就非常牛逼了。

// Normal approach
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}
// Improved approach
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

一行代码搞定,是不是很优雅

回顾所有的属性值

通过回顾这三个属性特征,我们可以非常清楚函数的行为。说明一下T.apply函数:

  1. 扩展函数
  2. 传递this作为参数
  3. 返回this(自己本身)

举个例子

// Normal approach
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}
// Improved approach
fun createInstance(args: Bundle) 
              = MyFragment().apply { arguments = args }

或者我们也可以让无链对象创建链式调用

// Normal approach
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}
// Improved approach, chaining
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }

说了这么多我们该怎么选用呢,通过一张图来说明
优雅的使用Kotlin_第1张图片

大概分析了一遍基础库中的最常用的扩展函数,熟练使用这些函数,能够让我们的代码看上去更加简练和易懂,最主要的还是要多用。

尾随闭包

大家可能对这个概念有点陌生,甚至没有听过这个词(我第一次听,也是从火火老师那边听的),其实很简单,上面我举了很多例子都是用到了尾随闭包,例如

fun test(){
    User("111").let{//这里就是一个尾随闭包
        
    }
}

尾随闭包是kotlin的一个语法,当函数的最后一个参数为函数的时候(或者接口,接口的话只能有一个函数),可以直接省略括号,直接函数名后跟着大括号即可,举例说明

fun test(){
    view.setOnClickListener{//因为onClick(v:View),只有一个参数,这里就直接省略了,可以直接用it来代替
        
    }
    //如果函数有多个参数
    view.setOnTouchListener{
        v,event -> //可以这样来写
    }
    
    //如果不用尾随闭包,那么写法基本和Java一样
    view..setOnClickListener(object :View.OnClickListener{
            override fun onClick(v: View?) {
                
            }
        })
}

是不是这样写,优雅了很多,也更加易读了。

inline(内联)、infix(中缀)、高阶函数

inline

Kotlin天生支持函数式编程,高阶函数和lambda是其一大特色.
使用高阶函数会带来一些运行时间效率的损失:每个函数都是一个对象,别切都会捕获一个闭包。即在那些函数体内会被访问的变量。内部分配(对于函数对象和类)和虚拟调用会引入运行时间开销。

使用inline修饰的函数,可以从编译器角度将函数的函数体复制到调用出实现内联

infix

中缀表达式是一种通用的算术或者逻辑公式表示方法,操作符以中缀形式处于操作数中间。中缀表达式允许我们使用一个单词或者字母来当做运算符用(实质上还是函数调用),忽略调用的点和圆括号

Kotlin的中缀表达式,需要满足以下条件

  1. 使用infix修饰
  2. 只有一个参数
  3. 参数不得接受可变数量的参数且不能有默认值
infix fun Int.add(i:Int):Int = this + i

infix fun Int.(i:Int):Int = this + i

fun main(args: Array<String>) {

    println(5 add 10)
    println(510)
}

由此可见,中缀表达式能让代码看起来更加接近自然语言。

DSL

DSL(domain specific language),即领域专用语言:专门解决某一特定问题的计算机语言,比如大家耳熟能详的SQL和正则表达式

那么Kotlin DSL 是什么呢?使用Kotlin语言呢开发的,解决特定领域问题,具备独特代码结构的API

Kitlin DSL的具体表现都有哪些呢?上面提到过的:扩展函数、中缀函数、lambda以及要讲的Anko Layout

DSL-扩展函数

想想一下这样的场景:我们需要给每一个点击事件添加埋点、需要判断是否连击(一般的按钮是不允许连击的),点击的时候需要进行动画。这种事件很像面向切面编程(AOP),在没有接触Kotlin的时候,大部分解决方案是用AspectJ或者Lancet来在编译时织入代码,进行hook函数。在Kotlin中我们使用扩展函数就可以实现这样的功能,我做了一个例子:

object TriggerTime {
    var triggerLastTime: Long = 0
    const val DELAY_TIME = 300
}

inline fun <T : View> T.clickWithAnimation(onClickListener: View.OnClickListener) = setOnClickListener {
    if (clickEnable) {
        it.animate().scaleX(0.8f).scaleY(0.8f).setDuration(80).withEndAction {
            it.animate().scaleX(1f).scaleY(1f).setDuration(80).start()
        }.start()
        onClickListener.onClick(it)
    }
}

inline fun <T : View> T.clickWithEvent(hashMap: HashMap<String, String>, noinline block: (T) -> Unit) {
    // hashmap 进行打点
    setOnClickListener {
        if (clickEnable)
            block.invoke(it as T)
    }
}

var <T : View> T.clickEnable: Boolean
    get() {
        var flag = false
        val currentClickTime = System.currentTimeMillis()
        if (currentClickTime - TriggerTime.triggerLastTime >= DELAY_TIME) {
            flag = true
        }
        TriggerTime.triggerLastTime = currentClickTime
        return flag
    }
    set(value) {

    }
    
//使用方法

fun test(view:View){
    view.clickWithEvent(hashMapOf("a" to "b")){
        //点击事件
    }
    
    view.clickWithAnimation{
        //点击事件
    }
}

上面的代码很简单,这里就不多做说明了,可以很方便的实现这一类的点击事件。

告别findViewById

使用Kotlin Android Extensions就可以访问布局XML的View,就像它们在布局中定义的属性一样,可以使用id的名称。Kotlin Android Extensions是Kotlin的一个插件。

将Kotlin Android Extensions集成到module中
只需要在module中的build.gradle上部添加apply plugin: 'kotlin-android-extensions'即可,十分方便,接下来举个例子,就知道怎样使用了

// 先定义一个普通的布局

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/test"/>

androidx.constraintlayout.widget.ConstraintLayout>
// 下面是activity
// 这句很重要,只有添加这句,才能直接使用id
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : FragmentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 直接使用xml 中的id 就可以直接使用view,很神奇吧
        test.visibility = View.VISIBLE
    }
}

是不是很优雅,再也不用写一大堆变量,然后findViewById,就可以直接使用,不仅要知其然,还要知其所以然。让我们看下反编译后的java代码

public final class MainActivity extends FragmentActivity {
   private HashMap _$_findViewCache;

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131296284);
      this.getWindow().setSoftInputMode(48);
      View var10000 = this._$_findCachedViewById(id.test);
      Intrinsics.checkExpressionValueIsNotNull(var10000, "test");
      var10000.setVisibility(0);
   }

   public View _$_findCachedViewById(int var1) {
      if (this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(var1);
      if (var2 == null) {
         var2 = this.findViewById(var1);
         this._$_findViewCache.put(var1, var2);
      }

      return var2;
   }

   public void _$_clearFindViewByIdCache() {
      if (this._$_findViewCache != null) {
         this._$_findViewCache.clear();
      }

   }
}

上面代码 不用我讲解,大家也一定明白其中原理了。

DSL-Anko布局

Anko是什么鬼?看下官方的解释

Anko is a Kotlin library which makes Android application development faster and easier. It makes your code clean and easy to read, and lets you forget about rough edges of the Android SDK for Java.

大概意思就是:是一款Kotlin的库,可以使Android开发过程更快和更简单,可以使代码更加简洁和易读,并且能让你不去关注复杂的Android SDK。

Anko大体包含四个方面:

  • Commons:intents、dialogs、logging…
  • Layouts:动态的Android布局
  • SQLite
  • Coroutines(协程):轻量级的线程

基本的操作流程可以去官网去看下。

怎样预览Anko Layout

首先需要下载插件
优雅的使用Kotlin_第2张图片

然后打开预览

优雅的使用Kotlin_第3张图片

可以将XML转换为Anko Layout(转过去后一大堆红,最好还是不要用)

优雅的使用Kotlin_第4张图片

怎么使用Anko layout举个例子

// 方法一
class TestUI : AnkoComponent<Activity> {
    override fun createView(ui: AnkoContext<Activity>): View = ui.apply {
        frameLayout {
            lparams(matchParent, matchParent)
            textView {
                text = "Hello Anko!!"
            }.lparams(wrapContent, wrapContent) {
                gravity = Gravity.CENTER
            }
        }
    }.view
}

// 方法二
object TestUI1 {
    fun createView(context: Context): View = context.UI {
        frameLayout {
            lparams(matchParent, matchParent)
            textView {
                text = "Hello Anko!!"
            }.lparams(wrapContent, wrapContent) {
                gravity = Gravity.CENTER
            }
        }
    }.view
}


// 使用

fun test(){
    //直接在activity的onCreate中使用,代替setContentView(view)
     TestUI().setContentView(activity)
     //得到一个view对象
    val view2 = TestUI1.create(context)
}

使用第一种方式的好处是可以预览Anko Layout,预览AnkoLayout,但是也不是实时的,需要你build一下,才能预览,个人感觉现在用处不大,看后面会不会优化一下,可以做到实时更新。

AnkoLayout 优缺点

当一个新的东西引进时,我们要思考,引进进的技术能给我们带来什么?可以让我们的开发变得更简单?优化app的性能?不能只为用新技术而用新技术。

ankoLayout的优点:
可以不通过xml来反射拿到view,直接在代码中new出来,相对于效率更高,我做过实验,一个简单的布局:xml方式需要15-20ms,AnkoLayout需要7-12ms,大概能减少30%的执行时间,如果是复杂的,效果很更明显,对于我们的布局优化是一个很不错的方式。

ankoLayout缺点:
需要学习,现在anko库支持的不是很好,不能实时可视化布局,比较没法

其他

双冒号(::)

刚开始用到这个符号是取class,在kotlin中是这样取一个类的class,而不是我们常用的XXX.class

System.out.print(Test::class.java)

刚开始的时候是懵逼的,根本不知道::是什么意思,只是简单理解,要获取一个类的class就使用这样的语法。随着对Kotlin的理解加深,也对::有了初步的理解。

Kotlin是支持函数作为参数(高阶函数),而::就是把一个方法当做一个参数,而且可以省略参数,举个例子

fun main() {
    foo2("xxx", this::bar2)     //right
    foo2("xxx", this::bar1)     //wrong, compile fail
}

fun bar1() {
    print("bar1")
}

fun foo2(content: String, body: (String) -> Unit) {
    body(content)
}

fun bar2(string: String) {
    print(string)
}

Kotlin委托

在委托模式中,当有两个对象参与参与同一个请求时,接受请求的对象将请求委托给另一个对象来处理。委托模式已经证明是实现继承的一个很好的代替方式。Kotlin中委托分为类委托属性委托,Kotlin官方库也封装一些常用的委托。

类委托

其实我们平常经常使用委托模式,例如view.setOnClickListener(this/*或者listener*/),这行代码是不是看上去异常的眼熟,对这就是给view设置点击监听事件,而this和listener就是一种委托模式,来咱们看下kotlin的委托模式

interface A {
    fun print()
}

val a = object : A {
    override fun print() {
        println("this a")
    }
}

fun main(args: Array<String>) {
    var aa = object :A by a{}

    aa.print()// print: this a
}

做个小结:

  1. 委托的关键词是by
  2. 委托可以通过重写方法控制委托的范围

属性委托

个人在思考中,感觉属性委比类委托使用场景要多,类委托还没想到比较好的使用场景。

系统为我们提供了一些常用的属性委托,包括

  1. lazy 延迟属性,只在访问的时候初始化属性值
  2. observable,属性的值变更时,会有回调
  3. notNull,如果使用该属性的时候为null,则会抛出IllegalStateException异常
  4. 把多个属性存储在一个map中,而不是每个窜在单独的字段中

延迟属性 lazy

延迟属性只提供了只读属性,并且提供了三种模式来处理线程安全:

  • LazyThreadSafetyMode.SYNCHRONIZED 线程同步
  • LazyThreadSafetyMode.PUBLICATION 多线程使用,无需同步
  • LazyThreadSafetyMode.NONE 单线程场景

看个例子

fun test(){
    val a:String by lazy{
        println("get value")
        "a"
    }
}

看下源码

public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
        when (mode) {
            LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
            LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
            LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
        }

//最简单的是UnsafeLazyImpl,单线程模式
internal class UnsafeLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer  //拿到初始化的lambda方法
    private var _value: Any? = UNINITIALIZED_VALUE //定义了一个非法值

    override val value: T
        get() {         //这行应该和上面一行连起来看,就是重写该属性的get()方法
            if (_value === UNINITIALIZED_VALUE) {
                _value = initializer!!() //初始化
                initializer = null
            }
            @Suppress("UNCHECKED_CAST")
            return _value as T //返回初始化后的值
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE // 这个是Lazy的方法,用于记录是否初始化结束。

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."  //重写toString方法

    private fun writeReplace(): Any = InitializedLazyImpl(value) //未找到方法作用
}

可观察属性 Observable

Delegates一共提供了三个委托方法notNullobservablevetoable

直接看例子

/**
在值发生变化的时候,会回调下面的方法
**/
var name: String by Delegates.observable("init") { prop, old, new ->
    println("old : $old")
    println("new : $new")
}


/**
vetoable委托对象可以对数值的变化进行控制。与observable唯一的不同是,参数B的返回值变为了Boolean,如果返回值为true则接受变化,如果为false则不接受变化。
**/
var age: Int by Delegates.vetoable(18) { prop, old, new ->
    println("old : $old")
    println("new : $new")
    val accept=new>16
    println("accept : $accept")
    accept //只接受>16的变更
}

自定义属性委托

有两种方式实现委托属性

  • 实现接口ReadOnlyProperty或者ReadWriteProperty,从字面就可以看出来,一个是只读的委托属性,一个是读写的委托属性,lazy就是只读的委托属性
  • 字符重定义 operator fun getValueoperator fun setValue

看个例子

// 第一种方式
var a: String by object : ReadWriteProperty<Any?, String> {
        var _value: String = "" //属性委托一般都需要一个间接对象进行数据读取与赋值
        
        override fun getValue(thisRef: Any?, property: KProperty<*>): String {
         
            print("in getValue")
            print("$thisRef")
            print("$property")
            return _value
        }
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {

            println("in setValue")
            _value = value
        }
    }


// 第二种方式
class ExtrasDelegate<out T>(private val extraName: String, private val defaultValue: T) {

    private var extra: T? = null

    operator fun getValue(thisRef: AppCompatActivity, property: KProperty<*>): T {
        extra = getExtra(extra, extraName, thisRef)
        return extra ?: defaultValue
    }
}


你可能感兴趣的:(Android)