我正在参与「启航方案」

任何傻瓜都可以编写核算机可以了解的代码。优异的程序员编写出人类可以了解的代码。— 马丁福勒

目前来说,作为Kotlin开发者想必对一些常见的比较优异的编程实践办法现已耳熟能详了吧;下面让咱们,一起来巩固下日常开发中常见的Kotlin编码实践办法,温故而知新,虽然有些知识点十分简略,但请务必牢牢把握,有些细节的东西也值得咱们留意;本文笔者将从以下几个方面进行巩固复习,将配合许多示例代码辅助说明,或许会略显单调;

请收下这些Kotlin开发必知必会的编码实践方式

常量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 = "https://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特别关键字便是一个运用中缀表明法并回来Pair<A, B> 的to()办法

通用规范函数库中的中缀函数

除了用于创立Pair<A, B> 实例的 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() 函数进行简略实践运用

参考

  • 一文吃透 Kotlin 中眼花缭乱的函数家族…
  • Kotlin best practises|kotlin guide
  • Kotlin 官方Docs