請收下這些Kotlin開發必知必會的編碼實踐方式

語言: CN / TW / HK

theme: fancy

我正在參加「掘金·啟航計劃」

任何傻瓜都可以編寫計算機可以理解的代碼。優秀的程序員編寫出人類可以理解的代碼。— 馬丁·福勒

目前來説,作為Kotlin開發者想必對一些常見的比較優秀的編程實踐方式已經耳熟能詳了吧;下面讓我們,一起來鞏固下日常開發中常見的Kotlin編碼實踐方式,温故而知新,儘管有些知識點非常簡單,但請務必牢牢掌握,有些細節的東西也值得我們注意;本文筆者將從以下幾個方面進行鞏固複習,將配合大量示例代碼輔助説明,可能會略顯枯燥;

Kotlin常用編碼實踐方式.png

常量Constant實踐方式

首先簡單列舉下Kotlin中常用的常量類型,接下來我們從兩個方面來討論下Kotlin常量的實踐方式

  • 在頂層和伴隨對象中定義常量
  • Java中訪問Kotlin中的靜態對象

允許的常量類型

Kotlin 只允許定義基本類型(數字類型、字符和布爾值)String類型的常量

讓我們嘗試定義一個自定義類型的常量。首先,讓我們創建一個沒有任何邏輯的空類

class TestClass {}

然後我們在常量類型中去使用這個類

const val constantAtTopLevel = TestClass()

結果會發生什麼?當然是報錯啦,編譯器會提示以下錯誤,告訴我們只允許基本類型和String類型

Const 'val' has type 'SimpleClass'. Only primitives and String are allowed

Constant 頂層聲明

上述我們嘗試定義的自定義類型常量也是在頂層進行聲明的,值得注意的是注意 Kt 不會將文件與類匹配。在一個文件中,我們可以定義多個類。與 Java 不同,Kt不需要每個文件一個父類。每個文件可以有多個父類。

文件也可以有類之外的函數和變量。這些函數和變量可以直接訪問。

讓我們在剛才的文件中再創建一個常量:

const val CONSTANT_AT_TOP_LEVEL = "constant value"

現在,我們從另一個類中去訪問它

class ConstantAtTopLevelTest {    @Test    fun whenAccessingConstantAtTopLevel_thenItWorks() {        Assertions.assertThat(CONSTANT_AT_TOP_LEVEL).isEqualTo("constant value")   } }

事實上,常量可以從任何類訪問。如果我們將一個常量聲明為私有的, 它就只能被同一個文件中的其他類訪問。當我們想要在文件或整個應用程序中的類之間共享一些值的時候,頂級常量(top-level)是一個比較好的解決方案。 此外,當值與特定類無關時,使用頂級常量是一個很好的選擇。

侷限性

儘管聲明一個頂級變量非常簡單,但也需要注意下聲明該類常量時出現的一些限制。如上所述,定義在文件頂層的常量可以被同一文件中的任何類訪問,即便它是私有的。我們也不能限制該文件中特定類的可見性。因此可以得出的結論是,此類常量不與任何類相關聯

此外,對於頂級常量,編譯器會生成一個新類。為此,編譯器創建了一個類,其原始文件的名稱後綴為Kt。在上面的例子中,它是ConstantAtTopLevelTestKt,其中原始文件名是ConstantAtTopLevelTest.kt`

Constant 伴隨對象聲明

現在讓我們在伴隨對象中定義一個常量

class ConstantsBestPractices {    companion object {        const val CONSTANT_IN_COMPANION_OBJECT = "constant at in companion object"   } }

之後,讓我們從ConstantInCompanionObjectTest類訪問它

class ConstantInCompanionObjectTest { ​    @Test    fun whenAccessingConstantInCompanionObject_thenItWorks() {        Assertions.assertThat(CONSTANT_IN_COMPANION_OBJECT).isEqualTo("constant in companion object")   } }

這個時候該字段是屬於一個類的。所以當我們想將值與類相關聯時,在伴隨對象中定義常量是一個比較好的解決方案,我們通過類的上下文訪問它。

Java 中訪問的靜態對象

現在來看下Java代碼中的靜態對象的可訪問性。使用上述已經創建好的頂層常量和伴隨對象中的常量,我們新建一個AccessKotlinConstant Java 類

public class AccessKotlinConstant {    private String staticObjectFromTopLevel = ConstantPracticeKt.CONSTANT_AT_TOP_LEVEL;    private String staticObjectFromCompanion = ConstantsPractices.CONSTANT_IN_COMPANION_OBJECT; }

一方面,頂層聲明的常量可以從 Java 訪問,生成的類名後綴為Kt 另一方面,伴隨對象常量可以通過類名直接訪問

簡化我們的函數

如何避免 for 循環

在日常開發中,我們會經常用到For 循環,它是命令式編程的一個很好的結構。但是,如果有一個函數可以為你完成這項工作,那麼最好改用該函數,這樣能讓你的代碼變得簡潔易懂。下面就來談談For循環在一些特定環境下使用的替代的實踐方式

  • 使用repeat

//最好不要 fun  main () {  for (i in 0 until 10) {    println(i) } } ​ //可以這樣寫 fun  main () {  repeat(10) {    println(it) } }

  • 使用forEach

// DON'T fun  main () {  val list = listOf( 1 , 2 , 3 , 4 , 5 , 6 )  for (e in list) {    println(e) } } ​ // DO fun  main () {  listOf( 1 , 2 , 3 , 4 , 5 , 6 ).forEach {    println(it) } }

  • 使用Map

// DON'T fun  main () {  val list = listOf( 1 , 2 , 3 , 4 , 5 , 6 )  val newList = mutableListOf< Int ()  for (e in list) {    newList.add(e * e) } } ​ // DO fun  main () {  val list = listOf( 1 , 2 , 3 , 4 , 5 , 6 )  valnewList = list.map { it * it } }

…還有更多的功能可以用來消除對循環的需求,這裏就需要開發者在實際運用場景自行斟酌。

使用高階函數

所謂的高階函數,簡單來説就是使用函數作為參數或返回值的函數,上面使用的代碼可能不是最簡潔的寫法。我們可以通過將函數引用傳遞給高階函數來進一步縮短我們的代碼。下面簡單舉個栗子:

fun  main () {   val input = readLine()  input?.let {     val sentence = it.split( " " )    sentence.map(String::length).also(::println) } }

String::length傳遞類型的 lambda 函數(String) -> Int::println傳遞類型的 lambda 函數(Int) -> Unit

擴展值

如果開發者必須在代碼的多個位置使用相同的值,我們可以考慮使用擴展值,這樣可以有效避免代碼宂餘。

// 返回 int 的擴展值: // 第一個單斜槓的索引 private val String.hostEndIndex: Int    get () = indexOf( '/' , indexOf( "//" ) + 2 )    fun  main () {        val url = "http://jackytallow.com/@cybercoder.aj"        val host = url.substring( 0 , url.hostEndIndex).substringAfter( "//" )        val path = url.substring(url.hostEndIndex).substringBefore("?")   }

優化條件結構方法的返回

如果你有一個有條件地返回不同值的函數,而不是在條件結構的每一行中都有返回return,你可以將返回提取出來統一處理,這樣會簡潔一些。下面以斐波那契數列方法為例:

fun main () {  println(fibo(6)) } ​ fun fibo (n: Int) : Int {   return  when (n) {     0 -> 0    1 -> 1    else -> fibo(n - 1) + fibo(n - 2) } }

此外,我們還可以通過將函數代碼塊轉換為單行表達式函數來繼續改進這一點。

fun main () {  println(fibo(6)) } fun fibo (n: Int) : Int = when (n) {   0 -> 0  1 -> 1  else -> fibo(n - 1) + fibo(n - 2) }

靈活使用標準函數

Kotlin 中有 5 個主要的作用域函數可以利用:letalsoapply和。withrun,它們之間的區別相信大家已經非常熟悉了,下面分別談談它們日常開發中運用的基本場景,這些標準函數的出現旨在讓我們的代碼看起來更加優雅

let函數

let函數用比較官方的説法就是默認當前這個對象作為閉包的it參數,返回值為函數最後一行或者return

  • 通俗的來説,我們使用它來將一種對象類型轉換為另一種對象類型,比如説使用StringBuilder並計算其長度

    val stringBuilder = StringBuilder() val numberOfCharacters = stringBuilder.let {    it.append("這是一個轉換方法")    it.length }

  • let 函數也可以用於繞過可能的空類型。

fun  main () {   val age = readLine()?.toIntOrNull()   age?.let {    println( "你是$it歲" ); } ?: println( "輸入錯誤!" ); }

letalso的不同之處在於返回類型會發生變化

also函數

這接收一個對象並對其執行一些額外的任務。其實就是相當於給定一個對象,對該對象進行一些相關操作also返回它被調用的對象,所以當我們想在調用鏈上生成一些輔助邏輯時,使用also會很方便

fun  main () {  Person(    name = "JackyTallow" ,    age = 23 ,    gender = 'M'   ).also { println(it) } }

run函數

此函數與函數類似let,但這裏傳遞的是對象引用 是this而不是it,通常我們可以這麼理解,run與let的關聯方式和apply與also的關聯方式相同

  • 下面我們依舊使用StringBuilder並計算其長度,這裏我們使用run函數

val message = StringBuilder() val numberOfCharacters = message.run {    length }

  • 對於let,我們將對象實例稱為it,但在這裏,對象是lambda 內部的隱式this

同樣的,我們可以使用與let相同的方法來處理可空性:

val message: String? = "hello there!" val charactersInMessage = message?.run {    "value was not null: $this" } ?: "value was null"

apply函數

applyalso差不多,它會初始化一個對象,不同的是它有一個隱含的this,當你希望更改對象的屬性或行為時使用此函數,最後再返回這個對象。

fun  main () {   val me = Person().apply {    name = "JackyTallow"     age = 23     gender = 'M'   }  println(me) }

  • 值得注意的是,我們也可以用apply來構建builder模式的對象

data class Teacher(var id: Int = 0, var name: String = "", var surname: String = "") {    fun id(anId: Int): Teacher = apply { id = anId }    fun name(aName: String): Teacher = apply { name = aName }    fun surname(aSurname: String): Teacher = apply { surname = aSurname } } ​ val teacher = Teacher()   .id(1000)   .name("張三")   .surname("Spector")

with函數

當你想使用一個對象的某個屬性/多個屬性時使用這個函數。簡單來説,它只是apply函數的語法糖

fun  main () {   val me = with(Person()) {     name = "JackyTallow"     age = 23     gender = 'M'   }  println(me) }

另一種看待它的方式是在邏輯上將對給定對象的多個屬性調用方法進行分組,比如説我們的賬户驗證,或者相關賬户名稱驗證操作

with(bankAccount) {    checkAuthorization(...)    addPayee(...)    makePayment(...)   }

運算符重載和中綴函數

運算符重載

我們可以在Kotlin的官方文檔中找到運算符函數列表,在 Kotlin 中,+、- 和 * 等運算符鏈接到相應的函數,通過在你的類中提供這些函數,你可以在 DSL 中創建一些非常簡潔的處理語法,這些函數在我們的代碼中充當語法糖。下面只是簡單使用了下示例:

fun  main () {   val list = mutableListOf( 1 , 2 , 3 ) (list.puls(4).forEach(::println)) } ​ operator  fun  <T> MutableList <T>.plus (t: T ) : MutableList<T> {   val newList = mutableListOf<T>().apply { addAll( this@plus ) }  newList.add(t)   return newList }

上面是筆者簡單手寫了個plus函數,它是按照Kt源碼中自帶的plus函數的基礎上進行修改的。不要濫用此功能,一般來説,僅在你的特定DSL中執行此操作

中綴函數

Kotlin 允許在不使用句點和括號的情況下調用某些函數,這些就被稱之為中綴表示法,這樣使得代碼看起來更貼合自然語言,可以看到最常見的Map中的定義

map(  1 to "one",  2 to "two",  3 to "three" )

可以看到to特殊關鍵字就是一個利用中綴表示法並返回Pairto()方法

通用標準函數庫中的中綴函數

除了用於創建Pair 實例的 to() 函數之外,還有一些其他函數被定義為中綴。 例如,各種數字類——Byte、Short、IntLong—— 都定義了按位函數and()、or()、shl()、shr()、ushr()xor(), 允許更多可讀表達式:

val color = 0x123456 val red = (color and 0xff0000) shr 16 val green = (color and 0x00ff00) shr 8 val blue = (color and 0x0000ff) shr 0

  • Boolean類以類似的方式定義and()、or()xor( ) 邏輯函數:

if ((targetUser.isEnabled and !targetUser.isBlocked) or currentUser.admin) {    // Do something if the current user is an Admin, or the target user is active }

  • String類還將matchzip函數定義為中綴,允許一些易於閲讀的代碼

"Hello, World" matches "^Hello".toRegex()

在整個標準庫中還可以找到一些其他中綴函數的示例,以上這些應該是日常開發中最常見的

自定義簡單中綴函數

我們也可以編寫屬於自己的中綴函數,在為我們的應用程序編寫領域特定語言時,允許DSL 代碼更具可讀性。很多開源庫已經使用自定義函數並取得了很好的效果,比如説,mockito-kotlin庫定義了一些中綴函數—— doAnswerdoReturndoThrow—— 它們都是用於定義模擬行為

要想編寫自定義中綴函數,需要遵循以下三個規則:

  • 該函數要麼在 class類上定義,要麼是 class類的擴展方法

  • 該函數只接受一個參數

  • 該函數是使用infix關鍵字定義的

    下面筆者簡單定義一個斷言框架用來測試,在這其中定義自己的中綴函數

class Assertion<T>(private val target: T) {    infix fun isEqualTo(other: T) {        Assert.assertEquals(other, target)   } ​    infix fun isDifferentFrom(other: T) {        Assert.assertNotEquals(other, target)   } }

是不是看起來很簡單,通過使用infix關鍵字的存在我們可以編寫如下代碼

val result = Assertion(5) result isEqualTo 5 result isEqualTo 6 result isDifferentFrom 5

這樣是不是立馬讓這段代碼變得更加清晰起來,更容易理解

注意一下,中綴函數也可以編寫為現有類的擴展方法。這其實挺強大的,因為它允許我們擴充來自其他地方(包括標準庫)的現有類以滿足我們開發的需要。

例如,讓我們向字符串添加一個函數,以提取與給定正則表達式匹配的所有子字符串:

infix fun String.substringMatches(regex: Regex): List<String> {    return regex.findAll(this)       .map { it.value }       .toList() } ​ val matches = "a bc def" substringMatches ".*? ".toRegex()    Assert.assertEquals(listOf("a ", "bc "), matches)

中綴函數的出現使得我們的代碼變得更加清晰,更易於閲讀

yield函數運用

關於yiled() 函數,我們首先要知道它是一個在Kotlin Coroutines上下文中使用的掛起函數;如果條件允許的話,它會將當前協程調度程序的線程(或線程池)讓給其他協程運行

從日常開發角度出發,我們通常在構建序列和實現作業的協作式多任務處理兩個方面中會使用到yield() 函數,下面我們來動手實踐一下

構建序列

yiled() 最常見的用法之一就是用於構建序列,下面筆者將分別使用它來構建有限序列和無限序列,Let's go

有限序列

假設我們想要構建一個有限的元音序列。對於如此短的序列,我們可以使用多個語句來產生單個元音值。讓我們在Yield類中定義vowels() 函數

class Yield {    fun vowels() = sequence {        yield("a")        yield("e")        yield("i")        yield("o")        yield("u")   } }

現在我們調用這個vowels方法對此序列進行迭代iterator

val client = Yield() val vowelIterator = client.vowels().iterator() while (vowelIterator.hasNext()) {    println(vowelIterator.next()) }

在這個簡單的場景中,關於有限序列需要注意的一件重要事情,在調用vowelIteratornext() 方法之前,我們應該始終使用hasNext() 方法檢查序列中是否有下一個可用項

無限序列

使用yield() 的一個更實際的用例是構建無限序列。因此,讓我們用它來生成斐波那契數列的項。為了構建這樣一個序列,讓我們編寫一個fibonacci() 函數,它使用一個無限循環,在每次迭代中產生一個單項:

fun fibonacci() = sequence {    var terms = Pair(0, 1)    while (true) {        yield(terms.first)        terms = Pair(terms.second, terms.first + terms.second)   } }

接下來,讓我們通過對該序列使用迭代器來驗證序列的前五項:

val client = Yield() val fibonacciIterator = client.fibonacci().iterator() var count = 5 while (count > 0) {    println(fibonacciIterator.next())    count-- }

對於以上這個無限序列,可以放寬對迭代器的hasNext() 方法調用,因為我們可以保證獲得序列的下一個元素

協作式多任務處理

介紹已經説了yield() 是一個掛起函數,它允許當前調度的線程讓給另一個協程運行,而在協作式多任務系統中,一個任務會自願放棄以允許另一個作業執行,這也意味者,我們是不是可以使用yield() 實現協作式多任務處理

數字打印機

下面我們來實踐下,實現一個簡單的奇偶數字打印機,假設我們要打印低於特定閾值的所有數字

  • 使用AtomicInteger將當前值定義為0,並將閾值定義為常量整數值

val current = AtomicInteger(0) val threshold = 10

  • 定義一個numberPrinter() 函數來協調數字的打印,這裏筆者打算定義兩個不同的作業來分而治之,一個用於打印偶數,另一個用於打印奇數,所以為了確保我們一直等到所有低於閾值的數字都被打印出來,這裏將使用runBlocking

fun numberPrinter() = runBlocking {    val eventNumberPrinter = ...    val oddNumberPrinter = ... }

  • 接下來將打印數字的任務委託給兩個不同作業部分, 即evenNumberPrinteroddNumberPrinter

    • 首先先看看如何啟動eventNumberPointer作業:

      val evenNumberPrinter = launch {    while (current.get() < threshold) {        if (current.get() % 2 == 0) {            println("$current is even")            current.incrementAndGet()       }        yield()   } }

      可以看到非常直觀,僅在偶數的時候才打印當前值,然後使用了yield() 函數意味着,它需要明白與另一個可以打印奇數值的任務合作,所以它自願讓步了

    • 接下來,再看看oddNumberPrinter作業,除了只打印奇數的情況外,它本質上是相同的:

      val oddNumberPrinter = launch {    while (current.get() < threshold) {        if (current.get() % 2 != 0) {            println("$current is odd")            current.incrementAndGet()       }        yield()   } }

  • 最後,我們調用numberPrinter() 來打印數字,正如預期的那樣,我們能夠看到所有低於閾值的數字

0 is even 1 is odd 2 is even 3 is odd 4 is even 5 is odd 6 is even 7 is odd 8 is even 9 is odd

綜上我們通過構建序列和協作式多任務處理兩種方式對yield() 函數進行簡單實踐運用

參考