--- type: slide --- # Effective Kotlin - Speaker: 禎志 (Rex) - Chapter 1: Safety --- ## Item 1: Limit mutability - *"狀態持有"* 是一把雙刃劍,why? ```kotlin= class BankAccount { var balance = 0.0 private set fun deposit(depositAmount: Double) { balance += depositAmount } @Throws(InsufficientFunds::class) fun withdraw(withdrawAmount: Double) { if (balance < withdrawAmount) { throw InsufficientFunds() } balance -= withdrawAmount } } class InsufficientFunds : Exception() val account = BankAccount() println(account.balance) // 0.0 account.deposit(100.0) println(account.balance) // 100.0 account.withdraw(50.0) println(account.balance) // 50.0 ``` ---- - "狀態持有":表示隨著時間變化的元素(方便使用),卻讓狀態變得"難以管理" - 在大型團隊的開發者來說,隨著變異點的增加導致的狀態一致性的問題以及專案複雜度不斷增加是很常見的 ---- ## some cases 下面例子用多線程來修改同一個屬性,但因為衝突導致一些運算遺失 ```kotlin= var num = 0 for (i in 1..1000){ thread { Thread.sleep(10) //如果不開執行序直接跑1..1000的loop花費大約11 ms,相對起來每一個loop大約 0.011 ms,所以大約每100次就有機會衝突 num +=1 } } Thread.sleep(5000) print(num) //不會是1000 ,因為在某些時間點 對num+=1 出現衝突導致失效 suspend fun main() { var num = 0 coroutineScope { for (i in 1..1000) { launch { delay(10) num += 1 } } } print(num) // Every time a different number } ``` ---- ## How to fix it? - 正確的“同步操作”,可是不容易實作 - 限制可變性 ```kotlin= val lock = Any() var num = 0 for (i in 1..1000) { thread { Thread.sleep(10) synchronized(lock) { num += 1 } } } Thread.sleep(1000) print(num) // 1000 ``` --- ## Limiting mutability in Kotlin - Read-only properties val - Separation between mutable and read-only collections - Copy in data classes ---- ## Read-only properties val - 在Kotlin中,我們使用*val*使屬性變成`唯讀`,而*var*是讓屬性`可讀可寫`(變數) ```kotlin= val a = 10 a = 20 // ERROR val list = mutableListOf(1,2,3) list.add(4) print(list) // [1, 2, 3, 4] ``` ---- - Kotlin中的`屬性(properties)`預設是封裝的,它可以有自定義的getters與setters - 核心概念是`val`不提供可變異點,它只有getter - 而*var*則是有getter與setter ---- ### some cases ```kotlin= fun calculate(): Int { print("Calculating... ") return 42 } val fizz = calculate() // Calculating... val buzz get() = calculate() fun main() { print(fizz) // 42 print(fizz) // 42 print(buzz) // Calculating... 42 print(buzz) // Calculating... 42 } ``` ---- ### smart-casting `Non-local`屬性只有在它們是不可變且沒有getter才會被smart casting ```kotlin= val name: String? = "Márton" val surname: String = "Braun" val fullName: String? get() = name?.let { "$it $surname" } val fullName2: String? = name?.let { "$it $surname" } fun main() { //因為fullName是透過getter取值,如同前面所提及:即便我們做了null check, //在print時他有可能被其他Thread修改,如果下面這行要正常運作要改寫成println(fullName?.length) if (fullName != null) { println(fullName.length) // ERROR } if (fullName2 != null) { //fullName2會被智能轉換 String? --> String println(fullName2.length) // Márton Braun } } ``` --- ## Separation between mutable and read-only collections - 如同Kotlin分離了屬性的`可讀寫`與`唯讀`一樣,也分離了`可讀寫`與`唯讀`的Collections(集合), ---- ![集合架構圖](https://i.imgur.com/wZTDwLO.png) ---- ### 唯讀集合不一定是不可變的 大部分的時候是可變的,但是`不能被修改`(因為它們只有唯讀的介面) ```kotlin= inline fun <T, R> Iterable<T>.map( transformation: (T) -> R ): List<R> { val list = ArrayList<R>() for (elem in this) { list.add(transformation(elem)) } return list } ``` ---- 讓集合介面只能唯讀,但`不是真正的不可變`的這個設計是非常重要的,這種設計方法的安全性接近於擁有**不可變集合的安全性** ---- ### 唯一風險 開發者試圖“破壞系統”並執行`down-casting` - *避免在Kotlin中將唯讀集合向下轉型為可變集合* ```kotlin= val list = listOf(1,2,3) // DON’T DO THIS! if (list is MutableList) { list.add(4) } // 錯誤 Exception in thread “main” java.lang.UnsupportedOperationException at java.util.AbstractList.add(AbstractList.java:148) at java.util.AbstractList.add(AbstractList.java:108) ``` ---- - 我們返回一個List(`唯讀`)時,那它應該只用來讀取,這是一種規約(contract) - 如果有轉換需求,應該使用*List.toMutableList*方法 ```kotlin= val list = listOf(1, 2, 3) val mutableList = list.toMutableList() mutableList.add(4) // ---------- fun main() { val x = testFu() // x : List<Int> print(x) } fun testFu():List<Int>{ val ls = mutableListOf<Int>() ls.add(1) ls.add(2) return ls //ls: MutableList<Int> } ``` --- ## Copy in data classes #### *不可變物件*的優勢 - 容易分析 - 更容易進行平行操作(parallelize) - 他們的參照(references)可以被cached - 不需要做防禦性copy - 建構其他物件的完美素材 - 可放進Set或者Map中當key使用 ---- - `可變性物件`使用上有些風險也難以預測 ```kotlin= //使用可變性物件 fun main() { val names: MutableSet<MyName> = mutableSetOf() val person = MyName(name1 = "A", name2 = "A") names.add(person) names.add(MyName("Jordan", "Hansen")) names.add(MyName("David", "Blanc")) println(names) // [AAA AAA, David Blanc, Jordan Hansen] println(person in names) //true person.name2 = "BBB" println(names) // [BBB AAA, David Blanc, Jordan Hansen] println(person in names) //false } data class MyName( var name1: String? = "AAA", //可變 var name2: String? = "AAA" //可變 ) ``` ---- ### `不可變性物件`最大的問題 某些情況下我們需要改變狀態: *讓不可變物件包含方法,在一些操作變化之後產生新的物件* ```kotlin= class User( val name: String, val surname: String ) { fun withSurname(surname: String) = User(name, surname) } var user = User("Maja", "Markiewicz") user = user.withSurname("Moskała") print(user) // User(name=Maja, surname=Moskała) ``` ---- - 但如果每個屬性都要編寫一個函數,那麼程式碼會變得很冗長 - 使用data modifier的copy() ```kotlin= data class User( val name: String, val surname: String ) var user = User("Maja", "Markiewicz") user = user.copy(surname = "Moskała") print(user) // User(name=Maja, surname=Moskała) ``` --- ## Different kinds of mutation points - 當我們需要表示一個可變列表時,有兩種方法可以達成 - `mutableListOf` - 可讀寫屬性`var` ---- ```kotlin= val list1: MutableList<Int> = mutableListOf() var list2: List<Int> = listOf() //修改的方法 list1.add(1) list2 = list2 + 1 list1 += 1 // Translates to list1.plusAssign(1) list2 += 1 // Translates to list2 = list2.plus(1) ``` ---- 在多執行緒沒有處理同步問題還是會遺失資料 ```kotlin=0 var list = listOf<Int>() for (i in 1..1000) { //沒有使用lock來鎖定 thread { list = list + i } } Thread.sleep(1000) print(list.size) // Very unlikely to be 1000, // every time a different number, like for instance 911 ``` ---- ### 另外可以使用可變屬性(var)而不是可變列表,可以讓我們在自定義setter或者使用委託時,追蹤該屬性的變化 ```kotlin! var names by Delegates.observable(listOf<String>()) { _, old, new -> println("Names changed from $old to $new") } names += "Fabio" // Names changed from [] to [Fabio] names += "Bill" // Names changed from [Fabio] to [Fabio, Bill]” ``` ---- - 針對這個可變屬性的唯讀集合來說,控制它的變化相對容易 ```kotlin var announcements = listOf<Announcement>() private set //設定為私有,只能在該類別內修改 ``` ---- ### 表示一個mutable list的選擇 - 使用mutableListOf - 是較快的選擇 - 使用可變屬性(var) - 對物件的變化方式有較多的控制 ==小孩子才做選擇,我全都要?== ```kotlin! //同時擁有可變屬性與可變集合 // Don’t do that var list3 = mutableListOf<Int>() ``` ---- 一般來說,我們應該避免創造不必要的方法來改變狀態,這都是一種成本,需要被理解與維護,所以我們更傾向於`限制可變性` --- ## Do not leak mutation points - 當我們暴露了一個可以構成狀態的可變物件時,是非常危險的事情, ```kotlin= data class User(val name: String) class UserRepository { private val storedUsers: MutableMap<Int, String> = mutableMapOf() fun loadAll(): MutableMap<Int, String> { return storedUsers } //... } ``` ---- 調用者可以使用loadAll来修改UserRepository的私有狀態 ```kotlin= val userRepository = UserRepository() val storedUsers = userRepository.loadAll() //因為loadAll返回了類別內私有物件storedUsers的ref給外部 storedUsers[4] = "Kirill" //... print(userRepository.loadAll()) // {4=Kirill} ``` ---- ### 應對方式 1. 防禦式複製 ```kotlin= class UserHolder { private val user: MutableUser() fun get(): MutableUser { return user.copy() } //... } ``` ---- 2. 限制可變性:使用向上轉型 MutableMap -> Map ```kotlin= data class User(val name: String) class UserRepository { private val storedUsers: MutableMap<Int, String> = mutableMapOf() fun loadAll(): Map<Int, String> { return storedUsers } //... } ``` --- ### Summary 關於限制可變性這邊提出幾個簡單規則: 1. 優先使用val而非var 2. 優先使用不可變的屬性而非可變的屬性 3. 優先使用不可變物件與類別而非可變的 4. 如果有改變狀態的需求,請使用data class並使用其中的copy()方法來改變狀態 5. 當我們持有狀態時,優先使用read-only而非可變的集合 6. 謹慎的設計可變點(要改變狀態的時機點),不要產生非必要的可變點 7. 不要暴露出可變物件 --- ## Item 2: Minimize the scope of variables - 我們定義狀態時,我們傾向於藉由下方所列方式來收緊變數的屬性的範圍: - 使用局部變數( local variables)取代屬性 - 在盡可能狹小的範圍內使用變數 ---- ### 元素的scope - 泛指在電腦程式中,該元素的可見區域(在Kotlin中幾乎都是由大括號所建立) ```kotlin= val a = 1 fun fizz() { // scope內 可以訪問外部的元素a val b = 2 print(a + b) } val buzz = { // scope內 可以訪問外部的元素a val c = 3 print(a + c) } // 這邊可以訪問到a ,但是無法訪問b, c ``` ---- ### example ```kotlin= // 不好的寫法 var user: User for (i in users.indices) { user = users[i] print("User at $i is $user") } // 較好的寫法 for (i in users.indices) { val user = users[i] print("User at $i is $user") } // 相同變數的scope,更好的寫法 for ((i, user) in users.withIndex()) { print("User at $i is $user") } ``` ---- ### 在定義變數時就對它初始化 ```kotlin= // 不好的寫法 val user: User if (hasValue) { user = getValue() } else { user = User() } // 較好的寫法 val user: User = if(hasValue) { getValue() } else { User() } ``` ---- ### 如果需要設定多個屬性 - 解構式聲明(destructuring declarations) ```kotlin= // 不好的寫法 fun updateWeather(degrees: Int) { val description: String val color: Int if (degrees < 5) { description = "cold" color = Color.BLUE } else if (degrees < 23) { description = "mild" color = Color.YELLOW } else { description = "hot" color = Color.RED } // ... } // 較好的寫法 fun updateWeather(degrees: Int) { val (description, color) = when { degrees < 5 -> "cold" to Color.BLUE degrees < 23 -> "mild" to Color.YELLOW else -> "hot" to Color.RED } // ... } ``` ---- ### Capturing - 無限質數數列 ```kotlin= val primes: Sequence<Int> = sequence { var numbers = generateSequence(2) { it + 1 } while (true) { val prime = numbers.first() yield(prime) numbers = numbers.drop(1) .filter { it % prime != 0 } } } print(primes.take(10).toList()) // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] ``` --- 團隊中有人試圖”優化“它,提取prime為while外部可變的變數 `keyword: lazy` ```kotlin= val primes: Sequence<Int> = sequence { var numbers = generateSequence(2) { it + 1 } var prime: Int while (true) { prime = numbers.first() yield(prime) numbers = numbers.drop(1) .filter { it % prime != 0 } } } print(primes.take(10).toList()) // [2, 3, 5, 6, 7, 8, 9, 10, 11, 12] ``` ---- ### Summary 1. 在盡可能小的scope內定義變數,優先使用`val`大於`var` 2. 對於變數是在lambda中被捕捉的這件事要有所警覺 --- ## Item 3: Eliminate platform types as soon as possible - Platform type:是一種來自其他語言且具有未知的nullability(可空性) - 在型別後面加上`!`來表示,例如: String!(但是不能用在程式碼中) ---- - 對於Java沒有明確指定nullability,可以視為platform type - 或使用 @Nullable和 @NotNull指定 ```kotlin= // Java public class UserRepo { public User getUser() { //... } } // Kotlin val repo = UserRepo() val user1 = repo.getUser() // Type of user1 is User! val user2: User = repo.getUser() // Type of user2 is User val user3: User? = repo.getUser() // Type of user3 is User? ``` ---- ### cases - 開發人員假設getValue不會返回null,但他錯了,這兩種情況都會導致NPE,差別在於發生的地方不同 ```kotlin= // Java public class JavaClass { public String getValue() { return null; } } // Kotlin fun platformType() { val value = JavaClass().value //... println(value.length) // NPE } fun statedType() { val value: String = JavaClass().value // NPE //... println(value.length) } ``` ---- - statedType: 錯誤會發生在從Java取值的地方發生 - platformType: 該value作為platform type變數可以同時視為null與not-null,這樣的變數可能會被安全的使用數次,然後被不安全的使用,最後拋出NPE ---- ### 如果platform type被進一步傳遞 - 在介面自動推斷出來的型別是platform type - 意味著型別的決定權在於實作該介面的人 ```kotlin= interface UserRepo { fun getUserName() = JavaClass().value } class RepoImpl: UserRepo { override fun getUserName(): String? { return null } } fun main() { val repo: UserRepo = RepoImpl() val text: String = repo.getUserName() // NPE in runtime print("User name length is ${text.length}") } ``` ---- ### Summary - 傳遞platform type是引發災難的作法,應該盡快地消除它們 --- ## Item 4: Do not expose inferred types - 型別推斷:賦值的推斷類型是被賦值對象右邊的確切類型,而不是超類別或者是介面類型 ```kotlin= open class Animal class Zebra: Animal() fun main() { var animal = Zebra() //這邊的類型推斷是 Zebra這個類型 animal = Animal() // Error: Type mismatch } //. fix var animal: Animal = Zebra() //指定為Animal類型 animal = Animal() ``` ---- ### example - 假設我們有個介面表示生產工廠 - 如果沒有其他參數,預設生產Fiat126P ```kotlin= interface CarFactory { fun produce(): Car } val DEFAULT_CAR: Car = Fiat126P() ``` ---- #### 狀況題 - 因為絕大多數的車廠都可以生產Fiat126P,所以我們把它設定為預設值,但卻沒有聲明返回值的類型,而我們開發者會認為DEFAULT_CAR不管怎樣都是Car的實例 ```kotlin= interface CarFactory { fun produce() = DEFAULT_CAR } ``` ---- 此時,若有其他開發者看到DEFAULT_CAR的宣告,並且認為它的類型可以被推斷出來 ```kotlin= // 把指定類型移除 val DEFAULT_CAR = Fiat126P() //這樣推斷出來的類型就會是Fiat126P而非Car ``` 接下來就會發現所有工廠只能生產Fiat126P了 ---- ### Summary - 如果在我們不確定返回類型時,我們應該顯式的聲明它,我們不應該把它隱藏起來(Item 14會提到) - 為了安全起見,在外部的API中,我們應該始終指定類型,可以確保它們不被隨意改變 --- ## Item 5: Specify your expectations on arguments and state - 當你對參數有所期待時,儘快的聲明它們,在Kotlin中大部分使用 1. require 2. check 3. assert 4. ?:(Elvis operator) ---- ### example ```kotlin= // Part of Stack<T> fun pop(num: Int = 1): List<T> { require(num <= size) { "Cannot remove more elements than current size" } check(isOpen) { "Cannot pop from closed stack" } val ret = collection.take(num) collection = collection.drop(num) assert(ret.size == num) return ret } ``` ---- - 聲明式檢查的優點 - 對於那些沒有閱讀文檔的開發人員也能看到那些期望 - 若該期望條件沒有被滿足,那麼該函數會拋出異常 - 程式碼在一定程度上是自檢( self-checking)的 - 適用於smart-casting ---- ### Arguments - 當我們對參數指定需求時,會使用require - require函數的敘述不被滿足時會拋出 IllegalArgumentException ```kotlin= fun factorial(n: Int): Long { require(n >= 0) return if (n <= 1) 1 else factorial(n - 1) * n } fun findClusters(points: List<Point>): List<Cluster> { require(points.isNotEmpty()) //... } fun sendEmail(user: User, message: String) { requireNotNull(user.email) require(isValidEmail(user.email)) //... } fun factorial(n: Int): Long { require(n >= 0) { "Cannot calculate factorial of $n " + "because it is smaller than 0" } return if (n <= 1) 1 else factorial(n - 1) * n } ``` ---- ### State - 當我們對當前的狀態有期望時,可以使用check() - check內所聲明的期望沒有被滿足時會拋出一個 IllegalStateException ```kotlin= fun speak(text: String) { check(isInitialized) //... } fun getUserInfo(): UserInfo { checkNotNull(token) //... } fun next(): T { check(isOpen) //... } ``` ---- ### Assertions - 當一個函數功能被正確的實現時,我們知道有些事是正確的,可以用assert來斷言 - 比較常出現在Unit test中 ```kotlin= class StackTest { @Test fun `Stack pops correct number of elements`() { val stack = Stack(20) { it } val ret = stack.pop(10) assertEquals(10, ret.size) } //... } ``` ---- ### Nullability and smart casting - 在require()或check()區塊內檢查過的所有內容,稍後將在同一個函數內被視為true,這適用於智能轉換(smart-casting) ```kotlin= fun changeDress(person: Person) { require(person.outfit is Dress) val dress: Dress = person.outfit //... } //------- class Person(val email: String?) fun sendEmail(person: Person, message: String) { require(person.email != null) val email: String = person.email //... } ``` ---- `requireNotNull`和`checkNotNull`,它們都具有智能轉換(smart-casting)的能力 ```kotlin= class Person(val email: String?) fun validateEmail(email: String) { /*...*/ } fun sendEmail(person: Person, text: String) { val email = requireNotNull(person.email) validateEmail(email) //... } fun sendEmail(person: Person, text: String) { requireNotNull(person.email) validateEmail(person.email) //... } ``` ---- - 對於nullability,使用Elvis運算子也是很受歡迎的選擇 ```kotlin= fun sendEmail(person: Person, text: String) { val email: String = person.email ?: return //... } // 需要執行複數個操作,搭配run使用 fun sendEmail(person: Person, text: String) { val email: String = person.email ?: run { log("Email not sent, no email address") return } //... } ``` ---- ### Summary 指定你的期望: - 使它們更明顯 - 保護應用程式的穩定性 - 保護程式碼的正確性 - 智能轉換(smart-casting)變數 --- ## Item 6: Prefer standard errors to custom ones - 盡可能地使用標準庫中提供的異常 - 除非沒有適合的可以用,才使用自定義的 --- ## Item 7: Prefer null or Failure result when the lack of result is possible - 當函數無法返回預期的結果: - 返回一個null或者一個表示失敗的密封類別(通常命名為failure) - 拋出一個 Exception ---- ### Exceptions 所有異常都是不正確的、特殊的,我們應該只在特殊情況下使用異常,因為: - 對開發者來說可讀性較差 - 在Kotlin中,異常都是未經檢查的 - 為特殊情況而設計的,(JVM)底層執行效率有差 - try-catch區塊內的程式碼,某些優化可能會被禁止 ---- ### null與failure - 當發生了預期内的錯誤時,我們應該更傾向於返回null或failure ```kotlin= inline fun <reified T> String.readObjectOrNull(): T? { //... if (incorrectSign) { return null } //... return result } inline fun <reified T> String.readObject(): Result<T> { //... if (incorrectSign) { return Failure(JsonParsingException()) } //... return Success(result) } sealed class Result<out T> class Success<out T>(val result: T) : Result<T>() class Failure(val throwable: Throwable) : Result<Nothing>() class JsonParsingException : Exception() ``` ---- ### null 與failure選哪個? - 如果發生例外錯誤時需要傳遞額外的訊息,我們應該選後者(failure),否則選擇null ---- 函數提供兩種變體:一種會拋出異常,另一種不會 像是List: - get:返回指定索引值的元素,如果索引值超出範圍則會拋出異常IndexOutOfBoundsException - getOrNull:當我們知道我們可能會訪問一個超出List範圍的元素時。我們應該使用它,這樣的話,如果超出索引範圍,它會返回null --- ## Item 8: Handle nulls properly ```kotlin= val printer: Printer? = getPrinter() printer.print() // Compilation Error printer?.print() // Safe call if (printer != null) printer.print() // Smart casting printer!!.print() // Not-null assertion ``` ---- - 一般來說,有三種方法可以處理null - 利用safe-call(?.),smart-casting,Elvis operator(?:) - 拋出一個錯誤 - 重構這個函數或屬性,使它不為null ---- ### example ```kotlin= printer?.print() // Safe call if (printer != null) printer.print() // Smart casting //---- val printerName1 = printer?.name ?: "Unnamed" val printerName2 = printer?.name ?: return val printerName3 = printer?.name ?: throw Error("Printer must be named") //---- println("What is your name?") val name = readLine() if (!name.isNullOrBlank()) { println("Hello ${name.toUpperCase()}") } val news: List<News>? = getNews() if (!news.isNullOrEmpty()) { news.forEach { notifyUser(it) } } ``` ---- ### Defensive and offensive programming - printer?.print() 可以稱之為一種防禦性編程 - require、check與assert 來達到攻擊性編程 ---- ### Throw an error - 使用safety call潛在問題 當printer為空時,我們不會被通知,而是不會調用print方法 ---- 當我們對未能達成的事情抱有強烈期望時,最好是拋出錯誤以告知開發人員這個非預期的的狀況,可以使用throw來完成(或者非空斷言(!!)、requireNotNull、checkNotNull) ```kotlin= fun process(user: User) { requireNotNull(user.name) val context = checkNotNull(context) val networkService = getNetworkService(context) ?: throw NoInternetConnection() networkService.getData { data, userData -> show(data!!, userData!!) } } ``` ---- ### The problems with the not-null assertion !! - nullability的訊息被忽視了 ```kotlin= fun largestOf(a: Int, b: Int, c: Int, d: Int): Int = listOf(a, b, c, d).maxOrNull()!! //重構 fun largestOf(vararg nums: Int): Int = nums.maxOrNull()!! largestOf() // NPE ``` ---- 通常我們應該避免使用非空斷言(!!) ---- ### Avoiding meaningless nullability - null是一種成本,因為它需要被正確的處理 ---- #### 方法 - 類別可以提供預期結果的函數變體 - 請使用lateinit或者notNull委託 - 處理集合時,返回空集合而不是null ---- ### lateinit property and notNull delegate - 當我們確定變數會在第一次使用之前被初始化,使用`lateinit` ```kotlin= class UserControllerTest { private lateinit var dao: UserDao private lateinit var controller: UserController @BeforeEach fun init() { dao = mockk() controller = UserController(dao) } @Test fun test() { controller.doSomething() } } ``` ---- ### lateinit與nullable的差別 - 不需要每次都將屬性unpack為not null - 若未來該屬性有null需求,方便修改 - 一旦初始化就無法回到未初始化狀態 ---- ### 不能使用lateinit的狀況 - 需要在JVM上初始化原生資料型別(Int,Long,Boolean...) ---- - 改用Delegates.notNull ```kotlin= class DoctorActivity: Activity() { private var doctorId: Int by Delegates.notNull() private var fromNotification: Boolean by Delegates.notNull() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) doctorId = intent.extras.getInt(DOCTOR_ID_ARG) fromNotification = intent.extras .getBoolean(FROM_NOTIFICATION_ARG) } } ``` --- ## Item 9: Close resources with use - Kotlin/JVM中使用的Java標準庫包含很多resources: - InputStream和OutputStream - java.sql.Connection - java.io.Reader(FileReader, BufferedReader, CSSParser) - java.new.Socket和java.util.Scanner ---- - 有些resources是無法自動關閉的,需要手動調用`close()` - 或者我們對該resource沒有參照,最終被GC回收 ---- - 習慣上會將resource包在`try-finally`中,並調用close ```kotlin= fun countCharactersInFile(path: String): Int { val reader = BufferedReader(FileReader(path)) try { return reader.lineSequence().sumBy { it.length } } finally { reader.close() } } ``` 這樣的結構是複雜又不正確的 ---- ### 使用Kotlin標準庫的`use()` - 適用於任何Closeable物件 ```kotlin= fun countCharactersInFile(path: String): Int { val reader = BufferedReader(FileReader(path)) reader.use { return reader.lineSequence().sumBy { it.length } } } //====== 逐行讀取 fun countCharactersInFile(path: String): Int { File(path).useLines { lines -> return lines.sumBy { it.length } } } ``` ---- ### Summary - 使用use對Closeable或AutoCloseable的物件進行操作是安全且簡單的選擇 - 當你需要操作一個文件時,考慮使用useLines --- ## Item 10: Write unit tests - 應用程式對工程師而言,首要任務就是確保它正常運作 - 而最好的方式就是透過測試來檢查它,尤其是單元測試 --- ###### tags: `讀書會`