怎么样编写地道的Kotlin代码

Posted ihrthk

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了怎么样编写地道的Kotlin代码相关的知识,希望对你有一定的参考价值。

怎么样编写地道的Kotlin代码

今天我打算谈论一下怎么编写地道的(Idimo)Kotlin代码,『地道』意思就符合Kotlin代码的习惯。我今天要讲的是如何摆脱Java编码习惯,开启你的Kotlin的编码风格。

好,我准备了一个代码Demo。

1.类的声明Bean(String name, int age)

这是用Java声明了一个Bean,它包含两个域name和age

public class Bean 
    private final String name;
    private final int age;

    public Bean(String name, int age) 
        this.name = name;
        this.age = age;
    

    public String getName() 
        return name;
    

    public int getAge() 
        return age;
    

对应的Kotlin代码应该这样写

class Bean2 
    private var name: String
    private var age: Int

    constructor(name: String, age: Int) 
        this.name = name
        this.age = age
    

    fun getName(): String 
        return name
    

    fun getAge(): Int 
        return age
    

我们中许多人是通过Java编程语言建立的知识来编程的。Kotlin是受到多种语言的启发,包括Java语言,所以这么写是可以正常工作的。但我们『绝对不应该』这么写Kotlin代码。

那么你是实际上应该做的是把所有的不必要的东西。我想说的是这个类,有两个属性,仅此而已。(这个类有一个主构造器,两个属性参数)

class Bean3(val name: String, val age: Int)

Kotlin演示了声明一个类是如此的简单。

2.数据类data class

看下这个代码,它表示解析一个名字,把它的firstName和lastName放到容器里。因为我没有办法,在一个函数里返回两个值,我不得不把它打包放进一个对象里。

fun parseName(name: String): List<String> 
    val space = name.indexOf(" ")
    return listOf(
        name.substring(0, space),
        name.substring(space + 1)
    )


fun main(args: Array<String>) 
    val name = parseName("Jane Doe")
    val first = name[0]
    val last = name[1]

    println("$first $last")
    if (name == parseName("Jane Doe")) 
        println("Equals is work")
    

我在这里滥用List容器,然后笨拙地把firstName和lastName都放进容器里。
事实上,即使用任何编程语言,你不应该这么写代码。

但是有一些现实的原因,我们可能会这样做。那就是:声明一个类的成本是很高的!

你需要找到一个合适package,然后创建一个文件,最后在里面写很多代码。

在Kotlin中,你不在需要这样做。你仅仅通过这样一行代码,
声明一个FullName类,它有first和last两个属性就可以了。

class FullName(val first: String, val last: String)

写一个类是简单的,节约了你很多时间,也节约了你的脑力劳动。
你可以在一个文件里声明多个类,它也不不会花费你任何东西。

我现在运行这个代码,发现它们是不相等的,因为我没有重写equals方法和hashCode方法。它是如此繁琐的,在Kotlin中,你真的不需要这样做。
因为你可能知道有一种叫做数据类的东西,在class前面加一个data就可以了。
编译器会帮我们生成equals和hashCode方法(还有copy和toString方法)。

写一个类是麻烦的这样的想法,在Kotlin是需要改变一下了。

3.属性

我们已经谈论了类,现在我们讲属性,然后是讲函数。

private var prop: String = "..."
fun getProp() = prop
fun setProp(v: String) 
    println("New value $v")
    prop = v

刚才我已经给你展示是了,用一行代码生成get和set方法。也就是在Kotlin中不需要写getXXX和setXXX方法。

如果你想要自定义seter方法,你也不需要为此单独声明一个函数,因为你可以使用『属性语法』。代码如下:

private var prop: String = "..."
    set(value) 
        println("New value $value")
        field = value
    

在这个自定义里面,你有filed,但是不需要域去存储,你也不用声明额外的名字,是的,就是这么简单直接!

4.属性委托

看一下这个代码:

private var _os: String? = null
val os: String
    get() 
        if (_os == null) 
            println("Computing...")
            _os = "android 12"
        
        return _os!!
    

这里已经有一些合理的逻辑,我有两个属性,一个是可空私有,另一个是不可空公有。

我第一次访问时,我检查它是否为空,然后计算一个值并写入其中,然后我输出它。

因此,这是什么呢?是的,这就是属性懒加载。

大家在Java语言中,写过很多这样的代码,这样的代码是很无聊的,这就是为什么Kotlin有一种机制叫做属性委托。所以委托属性让你摆脱这些的重复懒加载的逻辑。

代码如下:

val os: String by lazy 
    println("Computing...")
    "Android 12"

这个属性不是简单的初始化。这是委托给这个lazy。在第一次访问时,这个
Lambda将被执行。然后这个值会被储存起来,以便下次访问会使用。

这个lazy不是关键字,它仅仅是一个库函数,你可以定义你自己的。

5.自定义属性委托

这里有一种像观察者的属性,有些东西被修改,你将会被通知到。
你可以使用库函数,也可以自己实现。在这里使用了一个delegates.observable的库函数去完成这个工作。

var observeMe by Delegates.observable("a")  p, old, new ->
    println("$p.name goes $old -> $new")


fun main(args: Array<String>) 
    println("Observable property:")
    observeMe = "bb"
    observeMe = "ccc"
    observeMe = "dddd"

但是如果你喜欢,你不想这么写代码,你可以自己实现一个。

自定义属性委托,就是声明一个类它封装了你的属性的逻辑,比如geter方法和setter方法。

你现在可以简单地引用这个类在许多属性中,获取您的业务逻辑数据库访问,以及各种验证。

你可以把这个做成一个库,重用到在你的项目上。

这里举一个库例子:
https://github.com/MarcinMoskala/PreferenceHolder

var canEatPie: Boolean by bindToPreferenceField(true)

你实际上应该这么做,我确定你能够从中收益的。

6.函数

现在,我们开始讨论一下函数,函数是非常重要的,因此看一下下面这个代码:

class StringUtil 

    fun getFirstWorld(s: String, separator: String): String 
        val index = s.indexOf(separator)
        return if (index < 0) s else s.substring(0, index)
    

    fun getFirstWorld(s: String) = getFirstWorld(s, " ")


fun main(args: Array<String>) 
    println(StringUtil().getFirstWorld("Jane Doe"))

这个代码是非常糟糕的,请不要在Kotlin中这样写代码。

这是受我们Java编程习惯的启发,我们不得不把所有的东西都放进class里,因此就有了StringUtil。
你们写过有自己的StringUtil吗?oh,如果没有的话,那一定是一个新项目。

但是在Kotlin中,这里有点不同。你不需要使用一个类把这些方法或者属性包裹起来。

  1. 首先,Kotlin类没有静态,因此为了使用这些函数,我们不得不每次都new一个StringUtil对象。我不想每次都new一个对象,所以类转换为对象。这个代码有了一点提高。
  2. 事实上,我不需要一个对象,通过一个类就可以调用这个函数。在Kotlion中,我们有顶层函数。现在已经很像Kotlin代码了。
  3. 但是它不是很好的Kotlin代码,因为这里有两个重载。所以getFirstWord假设要解析字符串,找到第一个空格,然后取第一个单词,然后返回它。但是如果分隔符不是空格,而是逗号什么的?所以,这里有全功能的版本。我想表达的是这里只是一个默认值。在Java中,我们不得不使用重载来模拟。在Kotlin中,直接指定默认值就好了。当你有很多默认参数不同的值,比如多个布尔值,等等。你可以只使用命名参数语法来表示您实际需要的。并且剩余其他的继续使用默认值。现在代码更少了,有更强的表现力。
  4. 现在的代码,介于Java风格和Kotlin风格中间的状态,因为,这个函数的范围应该是String,而不是全局的。最好是把这个函数,放进String类里面去。Oh不,你不能控制String,你无法把任何东西放进这个类里。并且,你真的应该保持String类的api最小化。但是我真想要String有一个函数叫做getFirstWorld,那怎么办呢?在Kotlin中,有扩展函数去完成。这个只是在语法上给String添加了额外的函数,实际上你没有改变JDK里面String类的源码。我现在手动演示一下,它是怎么工作的!因为,我有一个接受者类型String,我不在需要这个参数了。我可以说这个点这里,或者省略所有在左边这些。现在我能使用它了。
  5. 同时我也能扩展属性来去做。事实上,用扩展属性来做会非常好。

就是这样,因此,扩展函数和扩展属性,是非常重要的。这不仅仅是方便,它可以让你保持你类的API最小化,并且充满无限想象。

现在你看下Kotlin的String类,它仅有5个函数,相比较Java一屏又一屏的方法,简洁的太多了。并且所有的工具函数可以都可以是扩展,可以把这些函数放在不同的库里面,这样就把String类也做到了模块化。

这样做是对于工具类api设计是非常重要的。

7.遍历树

现在看一下这个代码:在这里,我编写的一个非常典型遍历树的代码。我有个容器和叶子元素。容器可以嵌套其他的,而叶子元素持有文字。我要取出这个结构中所有的的文本。

因此我定义三个类,Element,Container和Text。Container可以包含很多Element,而Text可以持有文本。

现在,我要遍历这个。所以我使用扩展函数。我用的是顶级函数,我前面说过的。
这都是正确的,但是我不喜欢这个代码,为什么呢?

fun main(args: Array<String>) 
    val root = Container(
        Text("a"),
        Container(
            Text("b"),
            Container(
                Text("c"),
                Text("d")
            ),
            Text("e")
        ),
        Text("f")
    )
    println(root.extractText())


abstract class Element

class Container(vararg val children: Element) : Element()
class Text(val text: String) : Element()


fun Element.extractText(): String 
    return extractText(this, StringBuilder()).toString()


fun extractText(e: Element, sb: StringBuilder): StringBuilder 
    if (e is Text) 
        val text = e as Text
        sb.append(text.text)
     else if (e is Container) 
        val container = e as Container
        for (child in container.children) 
            extractText(child, sb)
        
     else 
        throw Exception("Unrecognized element:$e")
    
    return sb

  1. 这里,为了遍历层次结构,我需要递归。我需要传递stringBuilder沿着堆栈向下。
  2. 然后是顶层函数,这个不是在函数内部之外的任何地方都需要。所以我想
    就是把它放进里面去。

把这两个问题修改好,就是这样的。

fun Element.extractText(): String 
    val sb = StringBuilder()
    fun extractText(e: Element): StringBuilder 
        if (e is Text) 
            val text = e as Text
            sb.append(text.text)
         else if (e is Container) 
            val container = e as Container
            for (child in container.children) 
                extractText(child)
            
         else 
            throw Exception("Unrecognized element:$e")
        
        return sb
    
    extractText(this)
    return sb.toString()

现在有一点提高,你看见哪些灰色的代码了吗?哪些是无用的。

fun Element.extractText(): String 
    val sb = StringBuilder()
    fun extractText(e: Element): StringBuilder 
        if (e is Text) 
            sb.append(e.text)
         else if (e is Container) 
            e.children.forEach(::extractText)
         else 
            throw Exception("Unrecognized element:$e")
        
        return sb
    
    extractText(this)
    return sb.toString()

我们if else 替换成when


fun Element.extractText(): String 
    val sb = StringBuilder()
    fun extractText(e: Element): StringBuilder 
        when (e) 
            is Text -> 
                sb.append(e.text)
            
            is Container -> 
                e.children.forEach(::extractText)
            
            else -> 
                throw Exception("Unrecognized element:$e")
            
        
        return sb
    
    extractText(this)
    return sb.toString()

使用密封类,把when里面的else去掉

fun Element.extractText(): String 
    val sb = StringBuilder()
    fun extractText(e: Element): StringBuilder 
        when (e) 
            is Text -> sb.append(e.text)
            is Container -> e.children.forEach(::extractText)
        
        return sb
    
    extractText(this)
    return sb.toString()

我们用Kotlin写同样逻辑,代码更少,逻辑逻辑更清晰,表现力更强。

现在,局部函数,扩展函数、顶级函数、默认参数——使用这些。
它们将使您的代码更好。

8.表达式

看一下,下面这些例子表达方式写起来就像在脑海中旧的习惯一样。我们会尝试变换让他们变得更好。

class Example(val a: Int, val b: String?, val c: Boolean)

fun main(args: Array<String>) 
    var ex = Example(1, null, true)

    val a = ex.a
    val b = ex.b
    val c = ex.c
    println("a = " + a + ", b = " + b + ", c = " + c)

    val map = HashMap<String, Int>()
    map.put("k1", 1)
    map.put("k2", 2)
    map.put("k3", 3)

    for (e in map.entries) 
        val key = e.key
        val value = e.value
        println(key + " -> " + value)
    

    var s: String
    if (System.currentTimeMillis() % 2L == 0L) 
        println("Yal!")
        s = "Luck!"
     else 
        s = "Not this time"
    
    println(s)


fun test(e: Example): String 
    when (e.a) 
        1 -> return "Odd"
        3 -> return "Odd"
        5 -> return "Odd"
        2 -> return "Even"
        4 -> return "Even"
        6 -> return "Even"
        else -> return "Too big"
    


fun test2(str: String?): String? 
    println(str!!.length)

    if (str != null) 
        str.forEach  it ->
            println(it)
        
    

    if (str == null) return null
    if (str == null) throw RuntimeException("Exception")
    if (str == null) error("error")

    return ""

第一件事这里要说的是var。我不能说永远不要使用var。var是有用的。可变变量可以用于许多事情上。但是它不是被鼓励的。如果你需要var,你需要一个很好的理由。
这里没有好的理由使用val,这是肯定的。

val ex = Example(1, null, true)

这里,让我们看看这三个。它是重复,重复是丑陋的。重复是容易出错,特别是当这不是单一的名字,但被许多事情所束缚。因此,我想要摆脱这种重复。这里我可以用with函数解决。它有个陌生的东西,这是一个建筑结构。在Kotlin中,它是一个函数。在这里,我们可以得到摆脱所有与ex有关的事情,就像这样。

with(ex)
    val a = a
    val b = b
    val c = c
    println("a = " + a + ", b = " + b + ", c = " + c)

但现在看起来是更愚蠢的,对吧?我只是在分配相同的变量。不要这样做。现在我打印了一行,字符串加某物,字符串加某物,字符串加东西。这是尴尬的。现在大多数语言都有『string interpolation』,Kotlin也有。使用字符串插值,它是很好。

with(ex)
    println("a = $a, b = $b, c = $c")

现在,我在创建了一个map。我可以把它变的好一点,通过使用我的索引操作符。

val map = HashMap<String, Int>()
map["k1"] = 1
map["k2"] = 2
map["k3"] = 3

但是我可以使用builder function让它更好一点,因次我能做的是用Pairs替代所有的key和value。

val map = mapOf(Pair("k1", 1), Pair("k2", 2), Pair("k2", 2))

是的,所以map可以由Pair去构建。而map只是一系列的由键到值组成的Pair。但实际上,Pair在这里是有点多余。所以我们通常用to function去替代。它不是一个内置操作符,只是一个库函数。这里,这就是如何你创建一个Map。

val map = mapOf("k1" to 1,"k2" to 2,"k3" to 3 )

下面关于if语句的语法,是我非常讨厌的,因为它的声明和赋值是分开的。所以我很喜欢在Kotlin这样做。所以如果这样做的,你会发现还有很多这样的事情,实际上就是用『表达式』代替『语句』。这是对于C语言家族来说很陌生。我们习惯于把声明和赋值进行分开。你把表达式赋值给变量和写语句来指定一个值。

var s: String
if (System.currentTimeMillis() % 2L == 0L) 
    println("Yal!")
    s = "Luck!"
 else 
    s = "Not this time"

println(s)

当然,这里你就不用使用一个var,你不用另外写一行,你可以在一行指定的值。因此,表达式是更好的。顺便说一下,这个表达式的结果是语句块中的最后一项。

val s = if (System.currentTimeMillis() % 2L == 0L) 
    println("Yal!")
    "Luck!"
 else 
    "Not this time"

看下这个when的例子,when不像Java的switch那样呆板固执,它是很强大的,也很重要的。你也可以这么做像这样写代码,对吧?

fun test(e: Example): String 
    when (e.a) 
        1 -> return "Odd"
        3 -> return "Odd"
        5 -> return "Odd"
        2 -> return "Even"
        4 -> return "Even"
        6 -> return "Even"
        else -> return "Too big"
    

但是这里如果只有一个返回值会更好一些,像这样

fun test(e: Example): String 
    return when (e.a) 
        1 -> "Odd"
        3 -> "Odd"
        5 -> "Odd"
        2 -> "Even"
        4 -> "Even"
        6 -> "Even"
        else -> "Too big"
    

你也不必重复这些case,你可以把他们合在一起。像这样

fun test(e: Example): String 
    return when (e.a) 
        1, 3, 5 -> "Odd"
        in setOf(2, 4, 6) -> "Even"
        else -> "Too big"
    

是的,所以这个可以进一步简化。再一次,你在努力消除干扰。

fun test(e: Example) = when (e.a) 
    1, 3, 5 -> "Odd"
    in setOf(2, 4, 6) -> "Even"
    else -> "Too big"

顺便说一下,如果你想检查奇偶数,不要像我这样做。这只是为了演示目的。

9.函数类型

让我们来看一些函数风格。

fun main() 
    val numbers = (1..100).toList()

    val list = mutableListOf<String>()
    for (it in numbers) 
        if (it % 16 == 0) 
            list.add("0x" + it.toString(16))
        
    

    println(list)

    repeat(6) 
        println(it)
    


fun repeat(times: Int, body: (Int) -> Unit) 
    for (index in 0 until times) 
        body(index)
    

所以人们经常将Kotlin称为函数式语言。事实上,我不这么认为。我认为Kotlin是一个支持函数式风格的多范式语言。

你用Kotlin不用一定要写函数风格的代码,但它通常这这样做会更好的。

fun main() 
    val list = (1..100).toList()
        .filter 
            it % 16 == 0
        .map 
            "0x" + it.toString(16)
        
    println(list)

    repeat(6) 
        println(it)
    


fun repeat(times: Int, body: (Int) -> Unit) 
    for (index in 0 until times) 
        body(index)
    

以上是关于怎么样编写地道的Kotlin代码的主要内容,如果未能解决你的问题,请参考以下文章

怎么样编写地道的Kotlin代码

怎么样编写地道的Kotlin代码

怎么样编写地道的Kotlin代码

Python 工匠:编写地道循环的两个建议

Android Studio里面编写Kotlin代码的时候,怎么设置将类或者函数的第一个大括号默认位置另起一行?

Kotlin 代码在编译时会“缩小”吗?