Kotlin

Kotlin语法糖--其他小知识

2017-09-11  本文已影响161人  皮球二二

今儿起我们开始学习一下之前零零碎碎涉及到的一些知识

有时候我们把一个对象解构成很多变量,这样看起来就比较方便,就像这样

val (name, age) = aValue

这种就叫解构声明。一个解构声明可以同时创建多个变量,例如这里的name与age,然后我们可以独立使用它们

println("$name + $age")

一个解构声明会被编译成以下代码

println("${aValue.component1()} + ${aValue.component2()}")

其中的component1与component2函数是Kotlin中的约定原则之一,当然也可以用component3、component4等等
我们在实现解构声明的时候,componentN函数需要使用operator关键字进行标记,你可以写在扩展函数中或者类的内部函数中,就像这样

private operator fun A.component2(): Int {
    return age
}

class A {
    val name = "Hello"
    val age = 10
    operator fun component1(): String {
        return name;
    }
}

在数据类中使用也是相当广泛

data class DataB(val name1: String, val age1: Int)

class B {
    fun getDataB() : DataB {
        return DataB("World", 20)
    }
}

val (name1: String, age1: Int) = B().getDataB()
println("$name1 + $age1")

系统标准库也提供很多这样的扩展,比如对Map的迭代

@kotlin.internal.InlineOnly
public inline operator fun <K, V> Map.Entry<K, V>.component1(): K = key
@kotlin.internal.InlineOnly
public inline operator fun <K, V> Map.Entry<K, V>.component2(): V = value

使用的时候这样

val map: Map<String, String> = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3", "key4" to "value4")
for ((key, value) in map) {
    println("$key + $value")
}
val (_, age) = aValue

如果Lambda表达式具有Pair类型(或者Map.Entry以及任何其他具有相应componentN函数的类型)的参数,那么可以将他们放在括号中来引入多个新参数来取代单个新参数

a { 
    it -> it.name+" "+it.age
}
a {
    (name, age) -> name+" "+age
}
a {
    name+" "+age
}

与Java不同,Kotlin区分可变集合与不可变集合。这样有助于消除bug和设计良好的API
Kotlin的List<out T>类型是提供一个只读操作(如get、size)等的接口,可以修改List的是MutableList<T>。这一模式同样适用于Set<out T>/MutableSet<T>以及Map<K, out V>/MutableMap<K, V>
需要使用Kotlin标准库方法,如listOf、mutableListOf、setOf、mutableSetOf、mapOf、mutableMapOf
需要注意的是这些类型都是协变的,所以不可变类型子类集合可以直接赋值到父类集合中去。关于协变,在之前的泛型中已经专门说明了,此处不再赘述


可变集合不是协变的
只读集合是协变的

集合中有很多可用的扩展方法,你可以在使用过程中细细体会

区间表达式由具有操作符..形式的rangeTo函数辅以in和!in组成。区间是为任何可比较类型定义的,但对于整数原生类型,Kotlin对它有一个优化的实现

val i=2
if (i in 1..10) {
    println("查询到")
}

整形区间(IntRange、LongRange、CharRange)有一个额外的特性:他们可以用于迭代。编译器负责将其转换为类似Java的基于索引的for循环

for (j in 1..10) {
    println(j)
}

如果要倒序输出,则使用downTo函数

for (j in 10 downTo 1) {
    println(j)
}

如果想步进的话,直接用step函数

for (j in 1..10 step 2) {
    println(j)
}

如果要创建一个不包含结束元素的区间,使用until函数

for (j in 1 until 10) {
    println(j)
}

我们可以使用is操作符或者其否定形式!is来检查对象是否符合给定的类型

val obj: Any? = null
when (obj) {
    is String -> {

    }
    is Int -> {

    }
    else -> {

    }
}

在大多数情况下,不需要在Kotlin中使用显式转换操作符,因为编译器跟踪不可变值的is并检查,并在需要时自动自行安全的转换。我觉得Java在这点上,做的实在是太差了
来看看Java的类型转换

ArrayList<Object> objects=new ArrayList<>();
objects.add("123");
for (Object object : objects) {
    if (object instanceof String) {
        ((String) object).length();
    }
}

请注意,在已经判断过并且确定是String之后,Java依然要求将object强转为String,才能使用String的方法
再来看看Kotlin

var objs: MutableList<Any?> = mutableListOf()
objs.add("123")
for (obj in objs) {
    if (obj is String) {
        obj.length
    }
}

怎么样,感觉牛逼吧。编译期足够聪明,知道转换后类型是安全的
智能转换同样适用于when以及while表达式

Kotlin中不安全的转换由中缀操作符as完成。

val value = obj as String

但是在之前的那个集合中,如果add进去的是一个null,那么这样强转就会失败,转换操作符会抛出异常,所以我们就得这样

val value: String? = obj as String?

为了安全的转换,我们就是用中缀操作符as?,它在失败的时候返回null

val value: String? = obj as? String

需要注意的是,尽管as?的右边是一个非空类型的String,但是其转换结果是可空的

我们使用this表达式来表示当前的接收者:

  1. 在类的成员中,this指的是该类当前的对象
  2. 在扩展函数或者带接收者的函数字面值中,this表示在点左侧传递的接收者参数
  3. 如果this没有限定符,那么它指的是最内层包含它的作用域,如果要引用其他作用域中的this,那么请使用标签限定符this@lable,其中@lable是一个代指this来源的标签

请注意以下图片中高亮部分

C类对象
CIn类对象
a函数的接收者,一个Int
a函数的接收者,一个Int
sum函数的接收者
a函数的接收者,一个Int

Kotlin中有两种相等性的类型

  1. 引用相等(2个引用指向同一个对象,由===(以及其否定形式!==)操作判断)
  2. 结构相等(用equals()或者==(以及其否定形式!=)检查)

Kotlin旨在帮助我们从代码层面上消除NPE,类型系统区分一个引用是否可以容纳null还是不能容纳。例如String类型的常规变量是不能容纳null的,如果想要允许容纳,那么我们就得声明一个变量为可空字符串,写作String?


可空字符串

如果你直接访问可空类型字符串,会提示错误


不能直接访问
我们来看看如何访问可空类型

很传统

val length = if (b != null) b.length else 0
b?.length

如果b为非空,则返回b.length,否则返回null。这个表达式类型为Int?
如果只要对非空值执行某个操作,那么你可以尝试let

b?.let { 
    val length = b.length
}

这样在b为null的情况下,lambda表达式中的内容将不会执行

val length = b?.length ?: 0

如果?:左边的表达式不为null,那么Elvis操作符就返回左侧表达式的结果,反之返回右侧表达式的结果。这里如果b不为null,length值为b.length,否则为0
throw跟return在Kotlin中都是表达式,所以他们也可以出现在Elvis操作符右侧

fun check() : Int? {
    val b: String? = null
    val length = b?.length ?: return null
    val c: String? = null
    val length2 = c?.length ?: throw Exception()
    return length2
}

如果你确定你的值不会出现null,你可以写成这样

b!!.length

此时如果b出现null,那么就会出现NPE啦

注解的相关概念这里就不多说了,有疑问请查阅资料或者看看我之前的博客,我们直接谈怎么用吧
先来一段Java的注解声明

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface MyAnnotation {
    String name() default "Jack";
    int age();
}

用Kotlin翻译一下

@Target(AnnotationTarget.CLASS, AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.RUNTIME)
annotation class MyKAnnotation(val name: String = "Jack", val age: Int)

运行结果都是一样的


注解

使用的时候几乎没啥区别

@MyKAnnotation(name = "Hello", age = 20)
public class AnnotationClass {

    @MyKAnnotation(name = "WORLD", age = 30)
    String value="value";

    @MyKAnnotation(age = 10)
    public void function() {}
}

看看Kotlin的

@MyKAnnotation(name = "Hello", age = 20)
class KAnnotationClass @MyKAnnotation(name = "Hello KAnnotationClass", age = 20) constructor(val value_: String) {
    @MyKAnnotation(name = "WORLD", age = 30)
    val value = 10
    @MyKAnnotation(age = 10)
    fun function() {}
}

请注意,如果需要对类的主构造函数进行标注,则需要在构造函数声明中添加constructor关键字,并将注解添加在其前面
注解参数不可以有可空类型,因为JVM不支持将null作为注解属性的值进行存储
如果注解作为另一个注解的参数,则其名称前不需要加“@”字符作为前缀

@Target(AnnotationTarget.CLASS, AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.RUNTIME)
annotation class MyKAnnotation2(val kan: MyKAnnotation)

@MyKAnnotation2(kan = MyKAnnotation("old", 100))
class K2AnnotationClass @MyKAnnotation(name = "Hello KAnnotationClass", age = 20) constructor(@field:MyKAnnotation(name = "Hello KAnnotationClass", age = 20) val value_: String)

我们依然通过对比Java与Kotlin来学习,俗话说没对比就没伤害嘛
先看一个普通的类,一个私有方法加一个私有对象

class ReflectStudyA {
    private String aValue="123";
    private void toIntValue(String value) {
        System.out.println(Integer.parseInt(value));
    }
}

我想在代码中的任何地方调用toIntValue方法,并且将aValue的值作为入参。如果是Java,得这样写

try {
    Class reflectStudyAClass = Class.forName("com.renyu.kotlin.chapter9.ReflectStudyA");
    ReflectStudyA reflectStudyA = (ReflectStudyA) reflectStudyAClass.getDeclaredConstructor().newInstance();
    Field aValueField = reflectStudyAClass.getDeclaredField("aValue");
    aValueField.setAccessible(true);
    String aValue = (String) aValueField.get(reflectStudyA);
    Method method = reflectStudyAClass.getDeclaredMethod("toIntValue", new Class[] {String.class});
    method.setAccessible(true);
    method.invoke(reflectStudyA, aValue);
} catch (ClassNotFoundException e) {
    e.printStackTrace();
} catch (NoSuchFieldException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
} catch (NoSuchMethodException e) {
    e.printStackTrace();
} catch (InstantiationException e) {
    e.printStackTrace();
}

直接改成Kotlin,其实也没多大变化

val reflectStudyAClass = ReflectStudyA::class.java
var reflectStudyA: ReflectStudyA = reflectStudyAClass.getDeclaredConstructor().newInstance()
val aValueField = reflectStudyAClass.getDeclaredField("aValue")
aValueField.isAccessible = true
val aValue = aValueField.get(reflectStudyA)
val reflectStudyAMethod = reflectStudyAClass.getDeclaredMethod("toIntValue", String::class.java)
reflectStudyAMethod.isAccessible=true
reflectStudyAMethod.invoke(reflectStudyA, aValue)

要知道KClass与Java的Class是不一样的。最基本的反射功能是获取Kotlin类运行时的引用,要获取对静态已知的Kotlin类的引用,可以使用类字面值语法,比如MyClass::class。该引用是KClass类型的值,如果要在Kotlin编码中获得Java类的引用,就得在KClass实例上使用.java属性
对比一下KClass的写法

val reflectStudyAClass = ReflectStudyA::class
for (constructor in reflectStudyAClass.constructors) {
    var reflectStudyA: ReflectStudyA = constructor.javaConstructor!!.newInstance()
    for (declaredMemberProperty in reflectStudyAClass.declaredMemberProperties) {
        if (declaredMemberProperty.name == "aValue") {
            declaredMemberProperty.isAccessible = true
            val aValue = declaredMemberProperty.get(reflectStudyA)
            for (function in reflectStudyAClass.functions) {
                if (function.name == "toIntValue") {
                    function.isAccessible = true
                    function.javaMethod!!.invoke(reflectStudyA, aValue)
                }
            }
        }
    }
}

总得来说并没有变的多好,这个是因为我们使用的是Java中的对象。这次我们全部使用Kotlin来重写一遍

class ReflectStudyB {
    var aValue = "234"
    fun toIntValue(value: String) {
        println(value)
    }
}

我把private去掉了,然后你会发现眼前一亮

val reflectStudyBClass = ReflectStudyB::class
val reflectStudyB = reflectStudyBClass.createInstance()
val aValue_: KMutableProperty1<ReflectStudyB, String> = ReflectStudyB::aValue
val aValue = aValue_.get(reflectStudyB)
val toIntValueFun = ReflectStudyB::toIntValue
toIntValueFun.call(reflectStudyB, aValue)

这里有很多我们不明白的地方,没关系,我们一个个的来研究

一般情况下我们这样命名一个函数

fun abc(value: String) : Int {
    return value.toInt()
}

调用它也很自然的这样

abc("2")

但是我们也可以把它当做一个值传递,例如传给一个另函数,为此我们使用::操作符

fun getValue(funValue: (String) -> Int, value: String) {
    println(funValue(value))
}

arrays.forEach {
    getValue(::abc, it)
}

这里的::abc是函数类型(String) -> Int的一个值,所以直接传递到getValue方法中
如果我们需要使用类的成员函数或扩展函数,那么需要对这种写法限定,例如

fun String.bcd() : Int {
    return this.toInt()
}

这种情况下,我们需要使用String进行限定

arrays.forEach {
    getValue(String::bcd, it)
}

函数可以引用,同样属性也可以引用

var xValue = 3

::xValue.set(24)
println(::xValue.get())

表达式::xValue类型为KProperty<Int>,它允许我们使用get()读取它的值,或者使用name属性来获取属性名称。对于可变属性,类型则为KMutableProperty<Int>,并且有一个set()方法
如果要访问类的成员的属性,我们要这样限定

class ABC {
    var yValue = 3
}

val funValue = ABC::yValue
var abc = ABC()
funValue.set(abc, 12)
funValue.get(abc)
println(abc.yValue)

扩展属性其实也是差不多的

val String.x1Value: String
    get() = "Hello "+this

println(String::x1Value.get("author"))

构造函数可以像其他函数和属性那样引用,通过::操作符并添加类名来引用构造函数

class CDE {
    var value: String? = null

    constructor(value: String) {
        this.value = value
    }

    fun printValue() {
        println(value)
    }
}

通过这个方法引用构造函数

fun getCDE(cde: (String) -> CDE, value: String) : CDE {
    return cde(value)
}

看看引用部分

var cde: CDE = getCDE(::CDE, "aaa")
cde.printValue()
上一篇下一篇

猜你喜欢

热点阅读