Kotlin 学习笔记(1)

"Kotlin Koans基础知识"

Posted by Undervoid on October 29, 2017

Kotlin koans

自己在学习Kotlin 的过程中, 经常去他的官网查阅资料, 其官方也推出了相应的学习语法练习的网站。Kotlin Koans 一共分为42 个内容, 分为6个模块。 每一个任务都有一系列的单元测试, 需要完成的任务就是码代码通过单元测试。 之后的一系列文章相当于对Kotlin koans 的语法做个简单的文章总结。 本片文章大概是从 Conventsions开始的,至于 Kotlin的基本类型我交给大家自己领悟和解决吧。

Introduction

下面将介绍下 Kotlin 几个基本的语法糖:

Fold

官方文档解释为:「Fold Accumulates value starting with initial value and applying operation from left to right to current accumulator value and each element. 」

Fold 方法就是给定初值和迭代方法, 然后通过迭代对集合中的每一个元素执行指定的操作并将操作的结果相累加。这个操作函数的两个参数就是累加结果和集合的元素。

    public inline fun <T, R> Iterable<T>.fold(initial: R, operation: (R, T) -> R): R {
        var accumulator = initial
        for (element in this) accumulator = operation(accumulator, element)
        return accumulator
    }

Map

官方文档解释为: 「Map Returns a list containing the results of applying the given transform function to each entry in the original map.」

Map 就是将指定的转换函数运用到原始集合的每一个元素,并返回到一个转换后的集合。

    inline fun <K, V, R> Map<out K, V>.map(
        transform: (Entry<K, V>) -> R
    ): List<R> (source)

Filter

官方文档解释为:「Filter Returns a list containing only elements matching the given predicate).」

Filter filter方法返回一个包含所有满足指定条件元素的列表。与之对应的还有filterNot,顾名思义就是返回一个包含所有不满足指定条件的元素列表。还有一个filterNotNull,返回所有不为null的元素列表。

    inline fun <K, V> Map<out K, V>.filter(
        predicate: (Entry<K, V>) -> Boolean
    ): Map<K, V> (source)

FlatMap

官方文档解释为: 「FlatMap Returns a single list of all elements yielded from results of transform, function being invoked on each entry of original map 」

FlatMap 就是对列表中的每一项根据指定的方法生成一个列表,最后将所有的列表拼接成一个列表返回。

    inline fun <K, V, R> Map<out K, V>.flatMap(
        transform: (Entry<K, V>) -> Iterable<R>
    ): List<R> (source)

Parition

官方文档解释为: 「Parition Splits the original collection into pair of lists, where first list contains elements for which predicate, yielded true, while second list contains elements for which predicate yielded false.」

Parition 其实就是一个分类方法的语法糖,方法会将原始的集合分成一对集合,这一对集合中第一个是满足指定条件的元素集合,第二个是不满足指定条件的集合。

    inline fun <T> Iterable<T>.partition(
        predicate: (T) -> Boolean
    ): Pair<List<T>, List<T>> (source)

这里的练习题提到了一个Destructuring Declarations, 中文翻译就是结构声明。 听起来真的高大上, 其实也的确是方便了用户对POJO类的使用,当然对于Kotlin User 来时就是dataclass的使用性能大大提升。

Kotlin 属性与域

简单的介绍下Kotlin的属性和域, 这也我初学Kotlin语法的困惑点之一。

属性的声明:

声明属性的完整语法如下,其中的初始化器(initializer), 取值方法(getter), 以及设值方法(setter)都是可选的。Kotlin 具有类型自动推断得特性,如果属性类型可以通过初始化器自动推断得到, 或者可以通过这个属性覆盖的基类成员属性推断得到, 则属性类型的声明也可以省略。

标准的初始化方法,需要弄清楚其中额几个概念

  • propertyName :变量名
  • PropertyType :变量类型,如果可以推断得到,可以省略
  • property_initializer :初始化器,它可以是一个固定值或一个表达式
  • getter :取值方法,访问器的一种
  • setter :设值方法,访问器的一种
    var <propertyName>: <PropertyType> [= <property_initializer>]
        [<getter>]
        [<setter>]

这里其实我们可以发现var 和 val 的区别,对于var 来说他可以改变值,所以他的初始化器,设值,取值方法都是允许同时存在的;而val 其实由于不可改变的特性导致不能有设置方法, 同时同时 getter 方法和 初始化器 不允许同时指定,因为只读变量只允许初始化一次。

使用 注解 和 可见度修饰符,如下注释 3 处,,如果不想在外界访问属性的 set/get 方法,可以使用可见度修饰符来避免外部访问。同样也可以在 set/get 方法前使用注解。

    open class ClsA {
        // 1
        var x: Int = 100
            get() = 100
            set(value) {
                field = value
            }

        // 2. 不允许有设值方法,同时get方法和初始化器不允许同时指定,因为只读变量只允许初始化一次
        val y: Int
            get() = 100
        // 这是不允许的,因为val只读的
        // set(value) {
        //    field = value
        // }

        // 3. 在set/get方法前使用可见度修饰符和注解
        var z: Int = 100
        @Inject get
        private set
    }
    eg:
    val param:ClsA = ClsA()
    // 编译错误,因为不允许访问z的设值方法
    param.z = 100

Backing Fields

Kotlin 我们可以直接使用属性名称对类的属性进行访问,但是实际上我们并没有直接访问该属性的引用,而是在编译时被转换成了 getter/setter 方法来进行访问,如注释中 1 处代码,这样我们既可以简化代码,又保持了类的封闭性。

Kotlin 的类中不允许拥有域(Field),也就是说你在类内部也无法直接使用属性的名称对属性进行访问,而是都会转换为 getter/getter 方法。因此当你在 set 方法中调用该属性为他赋值时又会调用 set 方法,导致 堆栈溢出,如实例中 2,这是我在开始写的时候遇到的问题。

    // 1 伪代码
    ClsA().x = 100 相当于 ClsA().setX(100)
    val a = ClsA.x 相当于 val a = ClsA().getX()
    // 2
    var xx: Int = 100
        set(value) {
        	// 再次调用 set 方法
            xx = value + 1
        }

但是自定义属性的访问器时不可避免的要使用 域变量,因此 Kotlin 提供了 Backing Fields 的特性,使用关键字 field 表示,field 标识符只允许在属性的访问器函数内使用,如下代码中,使用 field 即可真实的访问(不经过getter/setter) 变量 xx,完成赋值操作。

    var xx: Int = 100
        get() = field++
        set(value) {
            field = value - 1
        }

Backing Property

有时隐含的后端域属性不足以解决某些情况的问题,此时可以使用自定义的 Backing Property。如下实例中,Backing Property 就是另外定义一个属性 table 用来存储数据,而外界访问的属性 table 只是提供了一个访问器方法而已,本身已经不具备什么意义啦,将访问器的声明和数据的存储放在两个地方,就不会发生之前的冲突(我的理解😊)

    private var _table: Map<String, Int>? = null
    public val table: Map<String, Int>
        get() {
            if (_table == null) {
                _table = HashMap() // 类型参数可以自动推断得到, 不必指定
            }
            return _table ?: throw AssertionError("Set to null by another thread")
        }

编译器常数值

如果属性值在编译期间就能确定, 则可以使用 const 修饰符, 将属性标记为 编译期常数值(compile time constants). 这类属性必须满足以下所有条件:

  • 必须是顶级属性, 或者是一个 object 的成员
  • 值被初始化为 String 类型, 或基本类型(primitive type)
  • 不存在自定义的取值方法

lateinit延迟初始化属性

lateinit 关键字表示当前 变量 不会在声明时进行初始化操作,初始化操作会在后面进行,像是一种协议机制,告知编译器我会在后面使用该变量之前的恰当时机初始化该变量,不要进行警告⚠️。在一个 lateinit 属性被初始化之前访问它, 会抛出一个特别的异常,这个异常将会指明被访问的属性,以及它没有被初始化这一错误。

需要注意的是使用 lateinit 关键字有很多限制:

  • 必须是变量,即使用 var 关键字进行声明。
  • 不能修饰可为 null 的类型,比如 lateinit var str:String? 是编译不通过的。
  • 不能修饰基本数据类型。例如 Int,Float等

by lazy代理-属性延迟初始化

by lazy 是属性代理的基本运用,是经过简化后的属性代理,他为属性提供初始化方法。这里不扩展属性代理的相关问题。因为他只提供取值方法所以仅可以用在常量的初始化中,使用 by lazy 当属性被第一次访问时,就会触发初始化流程。

  • 只有常量,也就是使用 val才能使用 by lazy 延迟初始化
    val stuByLazy:Student by lazy {
        Student("name",11)
    }
    val mMyMsgTv:TextView by lazy {
        findViewById(R.id.mTestTv) as TextView
    }
    // 当常量被使用时才进行初始化
    logError(stuByLazy.myCls)
    mMyMsgTv.text="文本"

Kotlin lateinit vs. by lazy

  • lazy{} 只能用在val类型, lateinit 只能用在var类型
  • lateinit不能用在可空的属性上和java的基本类型上 然而lazy{}常被用在常用java 基本类型上
  • lateinit可以在任何位置初始化并且可以初始化多次。而lazy在第一次被调用时就被初始化,想要被改变只能重新定义
  • lateinit 有支持(反向)域(Backing Fields)

参考

Kotlin

Kotlin开发