Kotlin入門系列:第二章 函數的定義與調用

1 在kotlin中創建集合

val set = hashSetOf(1, 7, 53) // 創建HashSet
val list = arrayListOf(1, 7, 53)	// 創建ArrayList
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three") // 創建HashMap,to是一個函數,比如1爲key,value爲one,具體函數後面說

// javaClass相當於java中的getClass()
// kotlin使用的是java的集合類    
println(set.javaClass)
println(list.javaClass)
println(map.javaClass)

輸出:
class java.util.HashSet
class java.util.ArrayList
class java.util.HashMap

2 讓函數更好調用

2.1 命令參數

java的集合都有一個默認的 toString 實現,但是它格式化的輸出是固定的

val list = listOf(1, 2, 3)
println(list)

輸出:
[1, 2, 3]

下面將使用kotlin的方式實現自定義的 toString 集合輸出格式

fun <T> joinToString(
	collection: Collection<T>, 
	separator: String, 
	prefix: String, 
	postfix: String): String {
	
	val result = StringBuilder(prefix)
	for ((index, element) in collection.withIndex()) {
		if (index > 0) result.append(seperator)
		result.append(element)
	}
	result.append(postfix)
	return result.toString()
}

val list = listOf(1, 2, 3)
// 輸出(1; 2; 3)
// 以下語句晦澀難懂
println(joinToString(list, "; ", "(", ")")

// kotlin可以在設置參數時命名參數,更明顯的查看參數表示的意義
// 即在傳遞參數時,可以給參數起個名字來表示,讓代碼有更好的可讀性,比如這裏的參數";"、"("、")"分別起名了separator、prefix和postfix
println(joinToString(list, separator = "; ", prefix = "(", postfix = ")"))

2.2 默認參數值

// 形參可以設置默認參數值
fun <T> joinToString(
collection: Collection<T>,
separator: String = "; ",
prefix: String = "(",
postfix: String = ")") {}

// 可以省略所有或部分參數的值,讓方法重載更加方便
val list = joinToString(list);

在kotlin轉java時不想一個個的寫重載方法參數,可以在方法添加註解 @JvmOverloads,編譯時會生成默認參數值的重載方法:

在kotlin時:

@JvmOverloads
fun <T> joinToString(Collection: Collection<T>, seperator: String, prefix: String, postfix: String) {}

在轉換到java時:

String joinToString(Collection<T> collection, String separator, String prefix, String postfix);

String joinToString(Collection<T> collection, String separator, String prefix);

String joinToString(Collection<T> collection, String separator);

String joinToString(Collection<T> collection);

2.3 消除靜態工具類:頂層函數和屬性

在java開發過程中總會用到一些工具類,有的是處理字符串有的是處理其他數據,往往這些方法都不會寫在某個功能模塊的類裏,而是寫在單獨的util包下提供xxxUtils工具類,這樣我們可以在處理相關數據的時候可以直接調用而不實例化。

在kotlin中有一種寫法叫頂層函數,這類函數一般定義在一個單獨的類文件中

// 假設該文件爲String.kt
// kotlin與java不同的地方在於,kotlin不需要函數、屬性、常量都要在class類中,可以直接將它們直接寫在文件上
// 寫在這些文件上的函數、屬性、常量都是static的,公開給所有模塊使用
@file:JvmName("StringFunctions") // 提供給Java調用時使用的名稱

package com.example.kotlin.strings

import java.lang.StringBuilder

fun <T> joinToString(
	collection: Collection<T>,
	separator: String = ", ",
	prefix: String = "",
	postfix: String = ""): String {
	val result = StringBuilder(prefix)
	for ((index, element) in collection.withIndex()) {
		if (index > 0) result.append(seperator)
		result.append(element)
	}
	result.append(postfix)
	return result.toString()
}

在java中使用時:

import com.example.kotlin.strings.StringFunctions;

StringFunctions.joinToString(...)
// 如果沒有kt文件沒有註明@file,要訪問頂層函數,是直接使用文件名訪問,比如StirngKt.joinToString(...)

頂層屬性

// 外部變量
var opCount = 0

fun performOperation() {
	opCount++;
}

fun reportOperationCount() {
	println("Operation performed $opCount times")
}

// 該變量會被存儲到一個靜態的字段中,val聲明的屬性是有一個getter的
// 如果想要聲明爲一個常量,可以使用const關鍵字
const val UNIX_LINE_SEPERATOR = "\n"
等價於java的
public static final String UNIX_LINE_SEPERATOR = "\n";

3 給別人的類添加方法:擴展函數和屬性

擴展函數,它就是一個類的成員函數,不過定義在類的外面。

// 計算一個字符串的最後一個字符
package strings

fun String.lastChar(): Char = this.get(this.length - 1)

你所要做的,就是把你要擴展的類或接口的名稱,放到即將添加的函數前面。這個類的名稱被稱爲接收者類型;用來調用這個擴展函數的那個對象,叫做接收者對象。

String.lastChar()的String爲接收者類型

this.get(this.length - 1)this爲接收者對象

// 顧名思義,擴展函數就是擴展一個類,在這個類的基礎上再添加新的函數。
// 比如這裏擴展了String類,在調用時將擴展函數中的this都替換成了傳遞給擴展函數的String對象,比如"kotlin",就像在String類的內部添加了這個函數一樣,下面代碼的效果類似於:
// fun lastChar(): Char = "Kotlin".length - 1
println("Kotlin".lastChar())

注意,擴展函數並不允許你打破它的封裝性。和在內部定義的方法不同的是,擴展函數不能訪問私有的或者是受保護的成員。

簡單說,擴展函數就是隻要類型相同,就可以將函數的接收者類型進行替換的操作,擴展函數是一個靜態方法,this就是指代這個接收者類型對象

3.1 導入和擴展函數

需要使用擴展函數時,需要顯式地導入

// 在strings包下的擴展函數lastChar()
import strings.lastChar
// import strings.*
// import strings.lastChar as last	// 修改導入的類或者函數名稱,使用as關鍵字,這裏把lastChar修改爲last

val c = "Kotlin".lastChar()
// val c = "Kotlin".last()	// 修改擴展函數名稱後使用

3.2 作爲擴展函數的工具函數

擴展函數無非就是靜態函數的一個高效語法糖,可以使用更具體的類型作爲接收者類型,而不是一個類。

fun <T> Collections<T>.joinToString(
	separator: String = ", ",
	prefix: String = "",
	postfix: String = ""): String {
	
	val result = StringBuilder(prefix)
	for ((index, element) in this.withIndex()) {
		if (index > 0) result.append(separator)
		result.append(element)
	}
	result.append(postfix)
	return result.toString()
}

val list = listOf(1, 2, 3)
printlin(list.joinToString(separator = "; ", prefix = "(", postfix = ")"))

具體類型的擴展函數

fun Collection<String>.join(
	separator: String = ", ",
	prefix: String = "",
	postfix: String = "") = joinToString(separator, prefix, postfix)

listOf("1", "2", "3").join()

3.3 擴展屬性

// 這裏必須定義getter函數,因爲沒有支持字段,因此沒有默認getter實現,同理也不能初始化因爲沒有地方存儲值
val String.lastChar: Char 
	get() = get(length - 1)

var StringBuilder.lastChar: Char
	get() = get(length - 1)
	set(value: Char) {
		this.setCharAt(length - 1, value)
	}

println("Kotlin".lastChar)
val sb = StringBuilder("Kotlin")
sb.lastChar = '!'	// setter
println(sb)

如果你嘗試給擴展屬性添加默認值,會出現編譯錯誤:擴展屬性不能有初始化器:

val MutableList<Int>.sumIsEven: Boolean = false
	get() = this.sum() % 2 == 0

爲什麼不能編譯通過?其實,這與擴展函數一樣,其本質也是對應java中的靜態方法。由於擴展沒有實際地將成員插入類中,因此對擴展屬性來說幕後字段是無效的。

public final class ExampleTestKt {
	// 擴展屬性實際上也是一個靜態方法
   public static final boolean getSumIsEven(@NotNull List $this$sumIsEven) {
      Intrinsics.checkParameterIsNotNull($this$sumIsEven, "$this$sumIsEven");
      return CollectionsKt.sumOfInt((Iterable)$this$sumIsEven) % 2 == 0;
   }
}

擴展屬性只能由顯式提供的getter和setter定義:

3.4 擴展函數的實現機制

擴展函數如此方便,會不會對性能產生影響?我們可以寫一個擴展函數然後將它編譯爲java查看:

fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
	val temp = this[fromIndex]
	this[fromIndex] = this[toIndex]
	this[toIndex] = temp
}

@Metadata(
   mv = {1, 1, 15},
   bv = {1, 0, 3},
   k = 2,
   d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u001a \u0010\u0000\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00030\u00022\u0006\u0010\u0004\u001a\u00020\u00032\u0006\u0010\u0005\u001a\u00020\u0003¨\u0006\u0006"},
   d2 = {"exchange", "", "", "", "fromIndex", "toIndex", "app_debug"}
)
public final class ExampleTestKt {
   public static final void exchange(@NotNull List $this$exchange, int fromIndex, int toIndex) {
      Intrinsics.checkParameterIsNotNull($this$exchange, "$this$exchange");
      int temp = ((Number)$this$exchange.get(fromIndex)).intValue();
      $this$exchange.set(fromIndex, $this$exchange.get(toIndex));
      $this$exchange.set(toIndex, temp);
   }
}

我們可以發現,擴展函數是一個靜態方法。靜態方法的特點:它獨立於該類的任何對象,且不依賴類的特定實例,被該類的所有實例共享。此外,被 public 修飾的靜態方法本質上也就是全局方法。

所以,擴展函數不會帶來額外的性能消耗。

3.5 擴展函數的作用域

既然擴展函數是全局的,一般情況下我們可以定義在包內,比如 com.example.extension 包下:

// Extension.kt
package com.example.extension

fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
	val tmp = this[fromIndex]
	this[fromIndex] = this[toIndex]
	this[toIndex] = tmp
}

但如果你嘗試將擴展函數放在一個類中管理,就會出現問題:

class Extension {
	// 即使添加了修飾符爲public也一樣無法訪問
	public fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
		val tmp = this[fromIndex]
		this[fromIndex] = this[toIndex]
		this[toIndex] = tmp
	}	
}

你會發現無法調用擴展函數 mutableList.exchange() 了,方法修飾爲 public 也一樣,我們可以看下編譯代碼:

public final class Extension {
   public final void exchange(@NotNull List $this$exchange, int fromIndex, int toIndex) {
      Intrinsics.checkParameterIsNotNull($this$exchange, "$this$exchange");
      int temp = ((Number)$this$exchange.get(fromIndex)).intValue();
      $this$exchange.set(fromIndex, $this$exchange.get(toIndex));
      $this$exchange.set(toIndex, temp);
   }
}

擴展函數變成類的方法了,只能提供給類或子類使用。

3.6 成員方法優先級總高於擴展函數

// 類中定義成員函數foo()
class Son {
	fun foo() = println("son called member foo")
}

// 定義擴展函數foo
fun Son.foo() = println("son called extension foo")

Son().foo() 

輸出:
son called member foo

當擴展函數和現有類的成員方法同時存在時,kotlin將會默認使用類的成員方法。

這麼設計其實是有意義的,在多人開發的時候,如果每個人都對 Son 擴展了 foo(),那就很容易造成混淆,對於第三方庫來說甚至是異常災難:我們把不應該更改的方法改變了。

3.7 標準庫中的擴展函數:run、let、also、takeIf

3.7.1 run

@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

run 是任何類型 T 的通用擴展函數,run 中執行了返回類型爲 R 的擴展函數 block,最終返回該擴展函數的結果。

run 擴展函數的作用域是獨立的:

fun testFoo() {
	val nickanme = "prefert"

	run {
		val nickname = "test"
		println(nickname)
	}
	println(nickname)
}

這個範圍函數本身似乎不是很有用。但相比範圍,還有一點不錯的是,它返回範圍內最後一個對象。

// 如果沒有登錄的話,顯示登錄對話框,否則顯示其他對話框
run {
	if (!islogin) loginDialog else getNewAccountDialog
}.show()

3.7.2 let

// let
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this) // 返回閉包結果
}

// apply
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this // 返回本身對象T
}

letapply 類似,唯一不同的是返回值:apply 返回的是原來的對象,而 let 返回的是閉包裏面的值。

let 的使用很多時候都和可空類型判斷結合處理:

data class Student(val age: Int)
class Kot {
	val student: Student? = getStudent()
	fun dealStudent() {
		// student不爲null的時候調用擴展函數let
		val result = student?.let {
			println(it.age)
			it.age // let返回閉包的結果
		}
	}
}

3.7.3 also

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

also 像是 letapply 函數的加強版。

class Kot {
	val student: Student? = getStudent()
	var age = 0
	fun dealStudent() {
		val result = student?.also { stu ->
			// this.age是外部類Kot的成員變量var age
			// stu是指定的名稱,默認會提供it使用
			this.age += stu.age
			println(this.age)
			println(stu.age)
			this.age
		}
	}
}

值得注意的是:如果使用 apply,由於它內部是一個擴展函數,this 將指向 studuent 本身而不是外部類 Kot,而且也無法調用到 Kot.age

3.7.4 takeIf

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (predicate(this)) this else null
}

如果我們不僅僅只想判空,還想加入條件,這時 let 可能就顯得有點不足,就可以使用 takeIf

 val result = student?.takeIf { it.age >= 18 }.let { ... }

takeIf 是接收滿足條件的情況下執行後續操作,還有相反的 takeUnless

3.8 擴展不是萬能的

kotlin是一種靜態類型語言,我們創建的每個對象不僅具有運行時,還具有編譯時類型。在使用擴展函數時,要清楚的瞭解靜態和動態調度之間的區別。

3.8.1 靜態與動態調度

以一個例子來說明什麼是靜態和動態調度。

class Base {
	public void fun() {
		System.out.println("I'm Base foo!");
	}
}
class Extended extends Base {
	@Override
	public void fun() {
		System.out.println("I'm Extended foo!");
	}
}
Base base = new Extended();
base.fun();

輸出:
I'm Extended foo!

我們聲明一個名爲 base 的變量,它具有編譯時類型 Base 和運行時類型 Extended。當我們調用時,base.foo() 將動態調度該方法,這意味着運行時類型 Extendedfun() 被調用。

當我們調用重載方法時,調度變爲靜態並且僅取決於編譯時類型。

void foo(Base base) {... }
void foo(Extended extended) { ... }
public static void main(String[] args) {
	Base base = new Extended();
	foo(base); // 即使base是Extended實例,但是會調用foo(Base base)
}

3.8.2 擴展函數始終靜態調度

擴展函數都有一個接收器(receiver),由於接收器實際上只是字節代碼中編譯方法的參數,因此你可以重載它,但不能覆蓋它。這可能是成員和擴展函數之間最重要的區別:前者是動態調度的,後者總是靜態調度的

open class Base
class Extended: Base()

fun Base.foo() = "I'm Base.foo!"
fun Extended.foo() = "I'm Extended foo!"

fun main(args: Array<String>) {
	val instance: Base = Extended()
	val instance2 = Extended()
	println(instance.foo()) // I'm Base.foo!
	println(instance2.foo()) // I'm Extended foo!
}

3.8.3 類中的擴展函數

如果我們在類的內部聲明擴展函數,那麼它將不是靜態的。如果該擴展函數加上 open 關鍵字,我們可以在子類中進行重寫。這是否意味着它將被動態調度?

當在類內部聲明擴展函數時,它同時具有調度接收器和擴展接收器。

  • 調度接收器(dispatch receiver):擴展被聲明爲成員時存在的一種特殊接收器,它表示聲明擴展名的類的實例

  • 擴展接收器(extension receiver):與kotlin擴展密切相關的接收器,表示我們爲其定義擴展的對象

// X是調度接收器,Y是擴展接收器
class X {
	fun Y.foo() = "I'm Y.foo"
}

如果將擴展函數聲明爲 open,則它的調度接收器只能是動態的,而擴展接收器總是在編譯時解析。

open class Base
class Extended : Base()

open class X {
	open fun Base.foo() {
		println("I'm Base.foo in X")
	}
	
	open fun Extended.foo() {
		println("I'm Extended.foo in X")
	}
}

class Y : X() {
	override fun Base.foo() {
		println("I'm Base.foo in Y")
	}

	override fun Extended.foo() {
		println("I'm Extended.foo in Y")
	}
}

X().deal(Base())    // I'm Base.foo in X
Y().deal(Base())    // I'm Base.foo in Y - dispatch receiver被動態處理
X().deal(Extended() // I'm Base.foo in X - extension receiver被靜態處理
Y().deal(Extended() // I'm Base.foo in Y 

可以發現 Extended 的擴展函數始終沒有被調用。決定兩個 Base 類擴展函數執行哪一個,直接因素是執行 deal() 的類的運行時類型。

擴展函數使用注意事項:

  • 如果該擴展函數是頂級函數或成員函數,則不能被覆蓋

  • 我們無法訪問其接收器的非公共屬性

  • 擴展接收器總是被靜態調度

3.8.4 被濫用的擴展函數

fun Context.loadImage(url: String, imageView: ImageView) {
	Glide.with(this)
		.load(url)
		.placeholder(R.mipmap.img_default)
		.error(R.mipmap.ic_error)
		.into(imageView)
}

ImageActivity.kt
this.loadImage(url, imageView)

上面的擴展函數其實是濫用的,實際上並沒有以任何方式擴展現有類。

Context 作爲上下文已經承擔了很多責任,基於 Context 擴展還很可能產生 ImageView 於傳入上下文週期不一致導致的問題。

正確做法是在 ImageView 進行擴展:

fun ImageView.loadImage(url: String) {
	Glide.with(this.context)
		.load(url)
		.placeholder(R.mipmap.img_default)
		.error(R.mipmap.ic_error)
		.into(this)
}

// 圖片請求框架擴展
object ImageLoader {
	fun with(context: Context, url: String, imageView: ImageView) {
		Glide.with(context)
		.load(url)
		.placeholder(R.mipmap.img_default)
		.error(R.mipmap.ic_error)
		.into(imageView)
	}
}

3.9 高階函數

高階函數指的是將函數用作一個函數的參數或者返回值的函數。

如下一個高階函數:

fun test(block: () -> Unit) {
	block()
}

test {
	...
}

一個高階函數格式如下:

高階函數名稱: [類型].(參數列表) -> 返回值
block: () -> Unit
block: (Int, Int) -> Int

3.9.1 將函數作爲參數的高階函數

public inline fun CharSequence.sumBy(selector: (Char) -> Int): Int {
	var sum: Int = 0
	// 遍歷字符串字符
	for (element in this) {
		sum += selector(element) // 高階函數的操作由外部處理
	}
	return sum
}

val str = "abc"
val sum = str.sumBy { 
	// 將字符轉爲int
	it.toInt() 
}
println(sum)

3.9.2 將函數作爲返回值的高階函數

fun <T> lock(lock: Lock, body: () -> T): T {
	lock.lock()
	try {
		return body() // 高階函數操作後返回類型T
	} finally {
		lock.unlock()
	}
}

val result = lock(lock) {
	// 不需要寫return關鍵詞
	sharedResource.operation()
}

3.9.3 自定義高階函數

fun operation(num1: Int, num2: Int, result: (Int, Int) -> Int) {
	return result(num1, num2) 
}

val plus = operation(10, 10) { num1, num2 ->
  	num1 + num2
}
val reduce = operation(10, 10) { num1, num2 ->
    num1 - num2
}
val ride = operation(10, 10) { num1, num2 ->
    num1 * num2
}
val except = operation(10, 10) { num1, num2 ->
    num1 / num2
}

3.9.4 run()和T.run()的區別

run()T.run() 兩個函數差不多,兩者之間的區別可以通過源碼查看:

public inline fun <T> run(block: () -> R): R {
	contract {
		callsInPlace(block, InvocationKind.EXACTLY_ONCE)
	}
	return block()
}

public inline fun <T, R> T.run(block: T.() -> R): R {
	contract {
		callsInPlace(block, InvocationKind.EXACTLY_ONCE)
	}
	return block()
}

可以發現 T.run() 的高階函數 block() 是一個擴展在 T 類型下的函數,說明這個高階函數可以使用當前對象 T 的上下文。當我們想要使用當前對象的上下文的時候,可以使用這個函數。

"kotlin".run {
	// 可以使用this對象,它代表對象"kotlin"
	println("length=${this.length}")
	println("first=${first()}")
	println("last=${last()}")
}

上面說明的擴展函數 runletalsotakeIf 都是有使用到高階函數。

4 處理集合:可變參數、中綴調用和庫支持

4.1 擴展java集合的API

val strings: List<String> = listOf("first", "second", "fourteenth")
strings.last()	// 輸出fourteenth

val numbers: Collection<Int> = setOf(1, 14, 2)
numbers.max()	// 輸出14

last和max都是擴展函數
fun <T> List<T>.last(): T { .. }
fun Collection<Int>.max(): Int { .. }

4.2 可變參數:讓函數支持任意數量的參數

val list = listOf(2, 3, 5, 7, 11)

// vararg是可變參數修飾符,相當於java的可變參數符號"..."
fun listOf<T>(vararg values: T): List<T> { .. }

// kotlin和java的另一個區別是,當需要傳遞的參數已經包裝在數組中時,調用該函數的語法
// 在java中,可以按原樣傳遞數組,而kotlin則要求你顯式地解包數組,以便每個數組元素在函數中能作爲單獨的參數來調用
// 這個功能被稱爲展開運算符,而使用的時候,不過是在對應的參數前面放一個*
fun main(args: Array<String>) {
	val list = listOf("args:", *args);	// 展開運算符展開數組內容
	println(list)
}

4.3 鍵值對的處理:中綴調用和解構聲明

4.3.1 中綴調用

// to是一種特殊的函數,稱爲中綴調用
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

// 下面是等價的
1.to("one")
1 to "one"

// 中綴調用可以與只有一個參數的函數一起使用,無論是普通的函數還是擴展函數
// 要允許使用中綴符號調用函數,需要使用infix修飾符標記
// Pair是kotlin標準庫中的類,用來表示一對元素
infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

4.3.2 解構聲明

我們想要從對象獲取數值存儲到臨時變量時經常會這樣做:

val b1 = Bird(20.0, 1, "blue")
val weight = b1.weight
val age = b1.age
val color = b1.color

kotlin爲我們提供瞭解構聲明,我們就可以這樣做:

val b1 = Bird(20.0, 1, "blue")
val (weight, age, color) = b1 
println("$weight, $age, $color")

但是kotlin對於數組的解構也有一定限制,在數組中它默認最多允許賦值5個變量。

解構內部的原理是通過 componentN 的方法來實現,N代表類中屬性的順序:

// 數據類Bird反編譯爲java代碼
public final double component1() { return this.weight; }
public final int component2() { return this.age; }
@NotNull
public final String component3() { return this.color; }

5 字符串和正則表達式的處理

kotlin的正則表達式和java相同

5.1 分割字符串

// 可以指定多個分隔符,且在java中"."是需要做轉義操作的而在kotlin不用
"12.345-6.A".split(".", "-")	// 輸出[12, 345, 6, A]

5.2 正則表達式和三重引號的字符串

// val path = "/Users/yole/kotlin-book/chapter.adoc"
fun parsePath(path: String) {
	val directory = path.substringBeforeLast("/")	// /Users/yole/kotlin-book
	val fullname = path.substringAfterLast("/")		// chapter.adoc
	
	val filename = fullname.substringBeforeLast(".")	// chapter
	val extension = fullname.substringAfterLast(".")	// adoc

	println("Dir:$directory, name:$filename, ext:$extension")
}

fun parsePath(path: String) {
	val regex = """(.+)/(.+)\.(.+)""".toRegex()	
	val matchResult = regex.matchEntire(path)
	if (matchResult != null) {
		val (directory, filename, extension) = matchResult.destructured
		println("Dir:$directory, name:$filename, ext:$extension")
	}
}

6 讓你的代碼更整潔:局部函數和擴展

在java中減少重複代碼時會使用 Extract Method抽取分解函數來實現重用代碼,但是這樣可能讓代碼更費解,因爲你以一個包含許多小方法的類告終,而且它們之間並沒有明確關係。

在kotlin提供了一個更整潔的方案:可以在函數中嵌套這些提取的函數。這樣既可以獲得所需的結構,也無需額外的語法開銷。

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
	if (user.name.isEmpty()) {
		throw IllegalArgumentException("Can't save user ${user.id}:empty name")
	}

	if (user.address.isEmpty() {
		throw IllegalArgumentException("Can't save user ${user.id}:empty address")
	}
	...
}

上面判斷 isEmpty() 的代碼是重複的,在kotlin中可以將驗證代碼放到局部函數中,可以擺脫重複,並保持清晰的代碼結構:

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
	// 在saveUser()方法中聲明一個validate()局部函數驗證有效性
	// 局部函數可以訪問所在函數中的所有參數和變量
	// 即validate()局部函數可以直接使用saveUser()函數的User參數
	fun validate(value: String, 
				 fieldName: String) {
		if (value.isEmpty()) {
			throw IllegalArgumentException("Can't save user ${user.id}:empty $fieldName")
		}
	} 

	// 調用局部函數驗證代碼有效性
	validate(user.name, "Name")
	validate(user.address, "Address")

	// 執行後續操作
	...
}

將上面代碼放到擴展函數中:

class User(val id: Int, val name: String, val address: String)

fun User.validateBeforeSave() {
	fun validate(value: String, fieldName: String) {
		if (value.isEmpty()) {
			throw IllegalArgumentException("Can't save user id $id, empty $fieldName")
		}
	}
	
	validate(name, "Name")
	validate(address, "Address")
}

fun saveUser(user: User) {
	user.validateBeforeSave()
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章