請收下這些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() 函式進行簡單實踐運用

參考