函數式編程(FP)是基於一個簡單又意義深遠的前提的:只用純函數來構建程序。這句話的深層意思是,咱們應該用無反作用的函數來構建程序。什麼是反作用呢?帶有反作用的函數在調用的過程當中不只僅是隻有簡單的輸入和輸出行爲,它還幹了一些其它的事情。而且這些反作用會把影響擴散到函數外,好比:java
舉個簡單的例子:git
public class BookStore {
//書店的叢書,省略初始化過程
public Map<String, Book> collection;
public Book buyABook(CreateCard lc, String bookCode){
if(!collection.containsKey(bookCode)){
return null;
}
Book book = collection.get(bookCode);
lc.charge(book.getPrice());
return book;
}
}
複製代碼
buyBook方法的的做用是根據圖書編號從書店中買一本書,這個方法是一個反作用函數。由於買書的過程當中會涉及一些外部操做的,例如須要和庫存管理系統進行交互、須要經過web service聯繫支付公司進行支付等操做。而咱們的函數只不過是返回了一本書,獲取書的過程當中發生了一些額外的行爲,這些行爲咱們就稱爲「反作用」。程序員
爲何會把這些行爲稱爲「反作用」呢?由於這些行爲的做用域不僅僅是屬於書店系統的範疇的了,它會把影響擴散到其它的系統中。反作用會致使這段代碼很難測試,由於咱們測試這段代碼的時候,會影響到其它系統,牽一髮而動全身。而且,客戶端(調用這段代碼的地方)沒辦法爲所欲爲的調用這段代碼,在使用的時候,還要考慮反作用帶來的具體影響,避免把系統帶入異常狀態。反作用讓咱們的代碼使用、維護、測試、修改都更麻煩。github
函數式編程的最重要也是最基礎的知識點是:經過純函數構建程序。web
在文章的最後,會給出一些思路解決上面例子中存在的反作用問題編程
咱們在使用函數式編程時,最新接觸的概念通常是閉包、Lambda表達式等,這兩個概念表達的是一樣的意思。Lambda表達式是一種語法糖,它能幫助咱們寫出更簡潔,更容易理解的程序。因此在開始函數式編程以前,咱們要先掌握Lambda表達式究竟是什麼,它的原理是什麼。設計模式
下面先看一段你們很是熟悉的代碼:緩存
//匿名函數
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread 1");
}
}).start();
//Lambda表達式
new Thread(() -> System.out.println("thread 2")).start();
複製代碼
線程調度器運行線程時,線程的run方法會被執行,線程的調度原理這裏不作介紹,有興趣的讀者能夠自行了解。安全
上面兩段代碼的做用是同樣的,它們的做用都是:當線程被調度時,就執行run方法中的代碼。能夠看到,使用Lambda表達式會讓咱們的代碼更簡單,更容易理解,這一點在Kotlin上會更明顯。bash
Kotlin版本:
Thread{
print("kotlin thread")
}.start()
複製代碼
Kotlin版本語法結構更簡單
Lambda和匿名函數它們有什麼不同呢?答案是,沒有任何不一樣的地方。在JVM的角度來講,它們是如出一轍的。不管是Java8仍是Kotlin甚至是全部運行在JVM平臺上的語言,它們的原理都是同樣的,Lambda表達式只是匿名函數的一種高糖寫法。那麼什麼樣的匿名函數能用Lambda呢?答案是:只有一個方法的接口,也能夠說是隻有一個方法的匿名函數。
咱們能夠看看Runnable這個接口的定義:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
複製代碼
這裏咱們要注意一下@FunctionalInterface
這個註解,這個註解的意思是,這是一個函數接口。函數接口的做用是,代表這個接口能使用Lambda表達式的風格來實現。這個註解是Java8後引入的,只起到提示做用。
咱們再來看看Kotlin的一些Lambda的基礎類是怎麼樣的:
public interface Function0<out R> : Function<R> {
/** Invokes the function. */
public operator fun invoke(): R
}
複製代碼
咱們觀察下Java的一些函數式接口,和Kotlin的一些能使用Lambda的接口就能發現,在JVM上,全部的Lambda都是經過只有一個方法的接口來實現的。例如Android中常用的View#setOnClickListnner
方法。
咱們可使用Kotlin寫一個簡單的demo體驗下:
fun main(args: Array<String>) {
delay(2000) { println("delay: $it ms hello word") }
}
fun delay(ms: Long, action: (Long) -> Unit){
Thread.sleep(ms)
action(ms)
}
複製代碼
當咱們運行main函數時,會打印出:delay: 2000 ms hello word。咱們重點關注下調用代碼:
delay(2000) { println("delay: $it ms hello word") }
複製代碼
不是特別瞭解FP範式的同窗能夠這樣理解:延遲2000毫秒後,咱們就打印"delay: $it ms hello word"
。
如今再回顧下線程調用的例子:當線程被調度時,就執行run方法中的代碼。
Android中的click事件的監聽:當view被點擊時,就執行onClick方法中代碼。
因此當咱們大量使用Lambda時,其實會大大加強咱們代碼的可讀性的,由於它們均可以這樣去理解:當x發生時,咱們就執行y行爲,這會比匿名函數容易理解不少。
如今,再來看看咱們再Java中以匿名函數的方式調用delay方法時,代碼是怎麼樣的:
public class Main {
public static void main(String[] args) {
MainKt.delay(2000, new Function1<Long, Unit>() {
@Override
public Unit invoke(Long aLong) {
System.out.print("delay: " + aLong + " ms hello word")
return Unit.INSTANCE;
}
});
}
}
複製代碼
Kotlin中定義delay方法中的action(注: fun delay(ms: Long, action: (Long) -> Unit) )變量在Java中會被編譯成了Function1接口,這和上述討論的函數式接口的結論一致。
這段代碼,一眼看下去很難理解它究竟是作什麼的。繁瑣、難以理解、不夠優雅。因此當咱們使用FP範式編程時,建議大量使用Lambda表達式。固然,若是有人特別喜歡匿名函數的話,也是能夠以FP的方式來使用匿名函數的,在Java7的環境中使用RxJava就會有相似的體驗。(筆者不太推薦大家虐待本身,若是匿名函數裏面的代碼有幾十行的話,畫面太美我不敢看)
這裏總結一下,若是大家的項目比較保守,須要追求穩定的話,儘可能把大家的語言升級到Java8,若是激進點的話,能夠直接升級到Kotlin。好的語法糖,能大大增強咱們代碼的可讀性的。
使用高階函數是會帶來一些性能損失的,由於每一個Lambda表達式都是一個對象。從上文的分析可知,在JVM中,Lambda表達式實際上是經過函數接口實現的。因此咱們在使用lambda的時候,就至關於new了一個新對象出來。並且因爲Lambda在訪問外部變量時,會捕獲變量的緣由,捕獲變量也會帶來必定的內存開銷。若是咱們大量使用Lambda的時候不想帶來這些影響的話,咱們可使用內聯函數來解決這些問題。
內聯函數時如何解決這些問題的呢?咱們能夠嘗試下把上面的delay
函數改形成內聯函數的形式。
fun main(args: Array<String>) {
delay(2000) {
println("delay: $it ms hello word")
}
}
inline fun delay(ms: Long, action: (Long) -> Unit){
Thread.sleep(ms)
action(ms)
}
複製代碼
咱們如今知道了,若是delay
不是內聯函數的話,編譯器會把上面的代碼編譯成new函數接口對象的形式。而內聯函數的調用代碼會編譯成下面的形式:
fun main(args: Array<String>) {
Thread.sleep(2000)
println("delay: $2000 ms hello word")
}
複製代碼
上面的代碼不就是咱們一開始編寫的非函數式代碼嗎?沒錯。經過內聯函數,咱們能夠通知編譯器,讓編譯器幫咱們作這種脫糖處理。這樣作的好處是,避免了大量使用lambda表達式致使對象大量建立和lambda捕獲致使的性能開銷。若是咱們能合理使用內聯函數,咱們的應用會在性能上有所提高。
內聯函數會致使編譯生成的代碼量變多,因此咱們要合理使用避免內聯過大的函數。舉個簡單的例子,若是咱們的delay函數裏面有100句代碼的話,那麼代碼就會變成下面這個樣子。
fun main(args: Array<String>) {
Thread.sleep(2000)
//假設還有一百句代碼
println("delay: $2000 ms hello word")
}
inline fun delay(ms: Long, action: (Long) -> Unit){
Thread.sleep(ms)
//假設還有一百行代碼
action(ms)
}
複製代碼
這樣看起來問題好像不大,可是若是delay函數在項目中被大量調用的話,這將是一場災難(想象下若是有100處調用,內聯致使的代碼增量是 100 * 100 -100)。合理使用內聯函數才能帶來性能的提高。
熟悉JVM編譯器的小夥伴會對內聯這個詞很是熟悉。這裏的內聯函數的原理和JVM中內聯的原理是同樣的,有興趣的讀者能夠了解JVM的內聯優化下。
篇幅有限,這裏只對內聯函數的做用與原理做個簡單的介紹。讀者有興趣的話,能夠自行查閱JVM內聯優化的相關資料。
函數式編程中,咱們用得最多的就是集合操做。集合操做的鏈式調用比起普通的for循環會更直觀、寫法更簡單。下面咱們以一個簡單的例子來學習下函數式的集合操做。
如今假設有一個書店在線銷售商場,他的初始化代碼以下:
fun initBookList() = listOf(
Book("Kotlin", "小明", 55, Group.Technology),
Book("中國民俗", "小黃", 25, Group.Humanities),
Book("娛樂雜誌", "小紅", 19, Group.Magazine),
Book("灌籃", "小張", 20, Group.Magazine),
Book("資本論", "馬克思", 50, Group.Political),
Book("Java", "小張", 30, Group.Technology),
Book("Scala", "小明", 75, Group.Technology),
Book("月亮與六便士", "毛姆", 25, Group.Fiction),
Book("追風箏的人", "卡勒德", 30, Group.Fiction),
Book("文明的衝突與世界秩序的重建", "塞繆爾·亨廷頓", 24, Group.Political),
Book("人類簡史", "尤瓦爾•赫拉利", 40, Group.Humanities)
)
data class Book(
val name: String,
val author: String,
//單位元,假設只能標價整數
val price: Int,
//group爲可空變量,假設可能會存在沒有(不肯定)分類的圖書
val group: Group?)
enum class Group{
//科技
Technology,
//人文
Humanities,
//雜誌
Magazine,
//政治
Political,
//小說
Fiction
}
複製代碼
咱們先嚐試用命令式的風格獲取Technology類型的書名列表。
fun getTechnologyBookList(books: List<Book>) : List<String>{
val result = mutableListOf<String>()
for (book in books){
if (book.group == Group.Technology){
result.add(book.name)
}
}
return result
}
複製代碼
若是咱們要使用函數式的風格來實現這個功能的話,能夠經過filter與map函數來實現。
fun getTechnologyBookListFp(books: List<Book>) =
books.filter { it.group == Group.Technology }.map { it.name }
複製代碼
這兩段代碼輸出的結果都是同樣的,咱們能夠把返回的列表打印出來看看。
[Kotlin, Java, Scala]
複製代碼
能夠看出來,若是用函數式的風格,代碼會比使用for循環更容易理解,而且更簡潔。上面的函數式代碼實現的功能一目瞭然:先過濾出group 等於 Technology的書本,而後把書本轉換成書本的名字。
這個例子簡單的介紹了filter和map的做用
那麼咱們面對複雜一點的功能的時候又如何呢?
如今有一個簡單的需求: 把書按照分組分類放好。若是咱們用命令式編程風格的話,咱們會寫出相似下面的代碼:
fun groupBooks(books: List<Book>){
val groupBooks = mutableMapOf<Group?, MutableList<Book>>()
for (book in books){
if (groupBooks.containsKey(book.group)){
val subBooks = groupBooks[book.group] ?: mutableListOf()
subBooks.add(book)
}else{
val subBooks = mutableListOf<Book>()
subBooks.add(book)
groupBooks[book.group] = subBooks
}
}
for (entry in groupBooks){
println(entry.key)
println(entry.value.joinToString(separator = "") { "$it\n" })
println("——————————————————————————————————————————————————————————")
}
}
複製代碼
那咱們再看看,若是要用函數式的方式來實現一下這段函數要怎樣寫呢?咱們可使用操做符groupBy實現這個功能
fun groupBooksFp(books: List<Book>){
books.groupBy { it.group }.forEach { (key, value) ->
println(key)
println(value.joinToString(separator = "") { "$it\n" })
println("——————————————————————————————————————————————————————————")
}
}
複製代碼
咱們運行這兩個方法看看這兩段函數的輸出結果:
由於是輸出沒有區別,因此只貼一段結果
Technology
Book(name=Kotlin, author=小明, price=55, group=Technology)
Book(name=Java, author=小張, price=30, group=Technology)
Book(name=Scala, author=小明, price=75, group=Technology)
——————————————————————————————————————————————————————————
Humanities
Book(name=中國民俗, author=小黃, price=25, group=Humanities)
Book(name=人類簡史, author=尤瓦爾•赫拉利, price=40, group=Humanities)
——————————————————————————————————————————————————————————
Magazine
Book(name=娛樂雜誌, author=小紅, price=19, group=Magazine)
Book(name=灌籃, author=小張, price=20, group=Magazine)
——————————————————————————————————————————————————————————
Political
Book(name=資本論, author=馬克思, price=50, group=Political)
Book(name=文明的衝突與世界秩序的重建, author=塞繆爾·亨廷頓, price=24, group=Political)
——————————————————————————————————————————————————————————
Fiction
Book(name=月亮與六便士, author=毛姆, price=25, group=Fiction)
Book(name=追風箏的人, author=卡勒德, price=30, group=Fiction)
——————————————————————————————————————————————————————————
複製代碼
能夠看到,函數式的實現更簡單,並且也更直觀,當你習慣了這種方式以後,你的代碼會更簡潔。而且使用函數式的風格,能讓你更容易避開煩人的反作用。
關於集合的操做就介紹到這裏了,集合還有不少其它的函數(flatMap、find等限於篇幅,這裏不做深刻介紹)能讓你的代碼更簡潔。
高階函數和咱們在學習代數的時候的高級代數很是像,咱們能夠用一句話來解析清楚什麼是高階函數:**入參是函數或者出參是函數的函數就是高階函數。**這句話很是繞口,直接看代碼會更加容易理解:
//入參是函數的高階函數
fun fooIn(func: () -> Unit){
println("foo")
func()
}
//出參是函數的高階函數
fun fooOut() : () -> Unit{
println("hello")
return { println(" word!")}
}
複製代碼
上面這兩種就是最簡單的兩種形式的高階函數,那麼高階函數有什麼做用呢?咱們先來介紹一下第一種高階函數。咱們先回顧下咱們上面集合操做的函數,在這裏以filter
爲例,filter
函數是怎麼樣實現的呢?直接看源碼:
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
複製代碼
這是filter的實現,咱們再來看看filterTo是怎麼樣的:
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
//具體實現
for (element in this) if (predicate(element)) destination.add(element)
return destination
}
複製代碼
從代碼咱們能夠分析到,在咱們調用filter函數的時候,會經歷以下步驟:
這裏能夠看出,filter函數返回的是一個新集合,不會影響調用集合自己。
對於函數式編程的初學者可能會以爲,不管是代碼仍是筆者的描述都很是難以理解(若是理解了,跳過這段)。若是理解不了的,咱們能夠把函數在Java裏面脫糖後再理解。在這裏咱們會以上面books.filter { it.group == Group.Technology }
這段調用代碼爲例,進行脫糖處理。
由於filter函數涉及到Function1這個函數,因此在看實際代碼前先看下Function1的定義:
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}
複製代碼
Function1其實就是一個普通的函數接口,沒有什麼特別。下面看看脫糖的代碼。
books.filter(object : Function1<Book, Boolean>{
override fun invoke(p1: Book): Boolean {
return p1.group == Group.Technology
}
})
//爲了更方便讀者理解,這裏把泛型也去掉了,並在語法上也作了一些處理
inline fun Iterable<Book>.filter(predicate: Function1<Book, Boolean>): List<Book> {
return filterTo(ArrayList<Book>(), predicate)
}
inline fun Collection<Book>.filterTo(destination: MutableCollection<Book>, predicate:Function1<Book, Boolean>): MutableCollection<Book> {
for (element in this) {
val isAdd = predicate.invoke(element)
if (isAdd) destination.add(element)
}
return destination
}
複製代碼
能夠看到,脫糖處理後的filter函數和咱們經常使用的一種設計模式是很是相似的,這個filter函數咱們能夠看做是策略模式的一種實現,而傳入的predicate實例就是咱們的一種策略。在上面這種場景中,咱們也能夠用命令式編程的策略模式實現(如Java集合操做中的sorted函數)。
函數式編程可讀性更強更易於維護的緣由之一就是:在函數式編程的過程當中,咱們會被動使用大量的設計模式。就算咱們不刻意去定義/使用,咱們也會大量使用相似設計模式中的策略模式和觀察者模式去實現咱們的代碼。
語法不重要,重要的是思想
柯里化函數,這個名詞聽起來逼格很是高,給個人感受就和第一次聽到依賴注入這個詞同樣(笑哭)。可是當你稍微瞭解下以後就能發現,柯里化函數和依賴注入同樣,都是一些很是簡單很是基本的東西。柯里化函數實際上是高階函數的一種,它的定義是:**返回值是一個函數的函數。**就是這麼簡單。可是這句話理解起來會有點抽象,咱們直接看代碼吧:
fun sum(x: Int) : (Int) -> Int{
return { y: Int ->
x + y
}
}
複製代碼
這就是一個簡單的求和柯里化函數,咱們能夠這樣用它:
fun main(args: Array<String>) {
val s = sum(5)
println(s(10))
}
複製代碼
輸出結果是:15
這種函數看起來合普通的求和函數好像也沒有什麼區別。咱們能夠拓展下上面的調用代碼再看看:
fun main(args: Array<String>) {
val s = sum(5)
println(s(10))
println(s(20))
println(s(30))
}
複製代碼
這下輸出的結果是:
15 25 35
是否是以爲有點意思了。柯里化函數的特色是,第一次調用會獲得一個特定功能的函數,上面的例子就是,獲得一個和5求和的函數。而後第二次調用的做用是,求傳入的值和5的和。這樣看起來貌似也沒有什麼做用,只是語法上好像更炫了一點而已。
咱們能夠把上面的例子改爲普通函數的方式再對比下。
fun sum1(x: Int, y: Int) : Int{
return x + y
}
fun main(args: Array<String>) {
println(sum(5, 10))
println(sum(5, 20))
println(sum(5, 30))
}
複製代碼
不用柯里化函數的話,會很是依賴調用方的自覺性,由於咱們要獲得5與某個數字的和的話,咱們必需要要求調用方須要在使用函數的時候,第一個值須要傳入5。
咱們換個角度想象下,若是咱們如今有一個很是複雜的兩段式運算,咱們可能會須要複用第一段運算的結果。那麼這種場景下使用柯里化函數是很是方便的,咱們能夠直接把第一段運算的結果直接緩存起來。而且因爲柯里化函數第一次調用返回的是一個函數,因此,柯里化函數是無反作用的。柯里化函數會起到延遲做用的效果,第一次調用返回一個函數,第二次調用纔會獲得一個值,即在真正被消費的時候,纔會生成值。
柯里化函數很是適合框架的開發者使用,咱們能夠經過柯里化函數實現一些閱讀簡單而且很是有效的API。
scala源碼中有大量的柯里化函數
主要是scala和kotlin的對比,沒興趣能夠跳過
Kotlin的柯里化函數其實也是有本身的侷限性的,從語法角度來講,Kotlin的柯里化函數更難以理解,遠沒有Scala的簡單。例如咱們上面的sum函數,從定義上來講就沒這麼好理解。而Scala的柯里化函數會更加簡單明晰。下面咱們對比下兩種語言的柯里化函數的特色。
Scala
object Main{
def main(args: Array[String]): Unit = {
val s = sum(5)(_)
println(s(10))
println(s(20))
println(s(30))
//最終打印出15,25,35
}
def sum(x: Int)(y: Int) : Int = x + y
}
複製代碼
Kotlin
fun sum(x: Int) : (Int) -> Int{
return { y: Int ->
x + y
}
}
複製代碼
能夠看到,Scala的柯里化函數從定義上來講是更簡單的,和普通的函數定義差很少。而Kotlin的柯里化函數會更加難以理解一點。但願Kotlin有一天也能支持這種風格的柯里化函數。
這裏只是簡單介紹下kotlin和scala柯里化函數的語法區別,限於篇幅,這裏不對柯里化函數的應用做過多介紹
當咱們掌握了上面的知識點後,咱們就掌握了函數式編程的基礎知識了。是的,掌握了上面的知識後,只是處於FP編程入門的狀態。上文提到過,咱們在使用函數式編程的時候,會被動地使用一些命令式編程中的設計模式,設計模式能夠說是命令式編程的一種高階應用。那麼咱們要進一步理解、提升函數式編程技能,咱們須要瞭解一些函數式設計的通用結構。這種結構和咱們常說的設計模式有點相似,可是又不太同樣。在初學階段能夠把它當成是函數式編程中設計模式來理解。
咱們主要介紹三種比較經常使用的通用結構。
在咱們剛接觸Kotlin的時候,大部分人會先了解Kotlin的一個特性,就是:空安全。空安全是經過可空變量/常量實現的。在咱們使用可空變量/常量的時候,編譯器會強制咱們要作空檢查才能使用。通常咱們會使用?
這個語法糖實現,固然也能夠在使用前先判空,判空後Kotlin會自動幫咱們進行智能轉換,會把可空變量轉換成非空變量。通常狀況下,咱們可使用相似下面的代碼處理可空變量。
如今假設咱們須要定義一個函數,入參是可能爲空的書本列表,當列表長度大於5時,返回下表爲5的元素,小於5時,返回下標爲1的元素,爲0時返回空。咱們能夠用下面的三種方法來實現這個函數。
fun foo(books: List<Book>?) : Book?{
val size = books?.size ?: 0
return if (size > 5){
books?.get(5)
}else {
books?.firstOrNull()
}
}
fun foo1(books: List<Book>?) : Book?{
return if (books != null){
if (books.size > 5){
books[5]
}else{
books.firstOrNull()
}
}else{
null
}
}
fun foo2(books: List<Book>?) : Book?{
books ?: return null
return if (books.size > 5){
books[5]
}else{
books.firstOrNull()
}
}
複製代碼
相對來講foo和foo2這兩種風格可讀性都比較強,而foo1就有點囉嗦了。在面對這種比較簡單的場景的時候,Kotlin的空安全寫起來十分簡潔,維護也挺方便的。那麼假如咱們須要面對一些更爲複雜的需求的時候呢?這種時候可能咱們會須要寫一堆**?**號來解決這種問題。
例如:如今咱們用Kotlin實現一次文章開頭的那個BookStore的程序。
CreateCard
class CreateCard{
fun charge(price: Int){
println("pay $price yuan")
}
}
複製代碼
class BookStoreOption {
private val bookCollection = initBookCollection()
fun buyABook1(lc: CreateCard?, bookCode: Int) : Book?{
val result = bookCollection[bookCode]
//判空,result爲空時不能產生交易行爲
//lc?.charge(result?.price ?: 0) 這種寫法會致使金額爲0的交易行爲
if (lc != null && result != null){
lc.charge(result.price)
}
return result
}
}
複製代碼
上面是在不使用函數式通用結構時的比較合理的一種寫法。
這樣寫的時候,客戶端的調用代碼多是這樣的。
val buyBook1 = BookStoreOption().buyABook1(CreateCard(), 1)
if(buyBook1 != null ) {
println("book name = ${buyBook1.name}, author = ${buyBook1.author}")
}
複製代碼
輸出結果:
pay 55 yuan book name = Kotlin, author = 小明
客戶端的調用代碼,咱們經過Kotlin的語法糖稍微簡化了下代碼。如今加多個條件,當buyBook1不爲空時,打印出購買的書名、做者名。而且當buyBook1的group不爲空時,再打印書的分組。
這種狀況下咱們很容易就能寫出下面這種代碼:
val buyBook1 = BookStoreOption().buyABook1(CreateCard(), 1)
if(buyBook1 != null ) {
println("book name = ${buyBook1.name}, author = ${buyBook1.author}")
if (buyBook1.group != null){
println("book group = ${it.group}")
}
}
複製代碼
上面這種寫法雖然功能上沒什麼問題,可是結構不太合理。假如咱們再加多幾個判斷條件的話,這種代碼幾乎沒有可讀性。難以閱讀,也難以維護。那咱們試試用Option結構優化下這段代碼。在優化以前,咱們先簡單介紹下Option
這裏咱們直接採用arrow開源庫的Option演示,Option的結構很是簡單,不想依賴庫的話,能夠本身定義一個。
Kotlin裏面的Option其實和Java8的Optional是一個意思,就是用來處理可空變量的。咱們先簡單看下Option是怎麼用的:
fun main(args: Array<String>) {
fooOption("hello word!!!")
println("——————————————————————")
fooOption(null)
}
fun fooOption(str: String?){
val optionStr = Option.fromNullable(str)
val printStr = optionStr.getOrElse { "str is null" }
println(printStr)
optionStr.exists {
println("str is not null! str = $it")
true
}
}
複製代碼
Option.fromNullable(str)能夠用語法糖str.toOption()代替
咱們看看打印結果:
hello word!!! str is not null! str = hello word!!! —————————————————————— str is null
結合例子,咱們能夠知道:
Option.getOrElse函數的做用是:若是對象不爲空,返回對象自己,爲空則返回一個默認值。
Option.exists函數的做用是:若是對象不爲空,則執行Lambda(函數、閉包)裏面的代碼。
對於Option,咱們先了解這麼多就好了。
咱們直接看看,若是要用Option來優化buyABook1咱們要如何優化:
class BookStoreOption {
private val bookCollection = initBookCollection()
fun buyABook2(lc: CreateCard?, bookCode: Int) : Option<Book>{
val lcOption = lc.toOption()
val bookOption = bookCollection[bookCode].toOption()
lcOption.map2(bookOption){
it.a.charge(it.b.price)
}
return bookOption
}
}
//客戶端調用函數
fun main(args: Array<String>) {
println("\n——————————— createCard is null, bookCode 1 ———————————————")
val bookStoreOption2 = BookStoreOption()
buyBook2ForStore(bookStoreOption2, null, 1)
println("\n—————————————————————————— bookCode 1 ————————————————————————————————")
buyBook2ForStore(bookStoreOption2, CreateCard(), 1)
println("\n—————————————————————————— bookCode 20————————————————————————————————")
buyBook2ForStore(bookStoreOption2, CreateCard(), 20)
}
fun buyBook2ForStore(store: BookStoreOption, createCard: CreateCard?, bookCode: Int){
val buyBook2 = store.buyABook2(createCard, bookCode)
buyBook2.map{
println("book name = ${it.name}, author = ${it.author}")
it.group
}.exists {
println("book group = $it")
true
}
}
複製代碼
咱們能夠看到,buyABook2和buyABook1的主要區別是,buyABook2返回的是一個非空的Option<Book>
對象。單純看這個函數咱們看不出太大的優點,那咱們們再看看buyBook2ForStore這個函數。咱們回顧下前面的客戶端調用函數:
val buyBook1 = BookStoreOption().buyABook1(CreateCard(), 1)
if(buyBook1 != null ) {
println("book name = ${buyBook1.name}, author = ${buyBook1.author}")
if (buyBook1.group != null){
println("book group = ${it.group}")
}
複製代碼
這樣一看好像用Option後,代碼反而更多了。可是不難看出,buyBook2ForStore的結構更加清晰,可讀性更強。咱們能夠這樣理解這個函數:
使用Option以後,代碼結構和咱們人類的思考方式是很是相似的,可讀性會強不少。可是這樣看起來,使用Option其實也沒有帶來太大的提高。可是若是當咱們的代碼規模和複雜度更高了以後呢?例如,Book中的Group實際上是更加複雜的對象呢?Group還包含:名字,id,等等信息呢?而且他們都是可空變量(在Kotlin和Java混合編程裏面很是常見,由於Java對變量是否可空的限制比較弱)呢?假如咱們如今須要在打印group後再,讀取group裏面的name的值,可能要這樣寫:
val buyBook1 = BookStoreOption().buyABook1(CreateCard(), 1)
if(buyBook1 != null ) {
println("book name = ${buyBook1.name}, author = ${buyBook1.author}")
if (buyBook1.group != null ){
println("book group = ${it.group}")
if(buyBook1.group.name != null){
println("book group name = ${it.group.name}")
}
}
複製代碼
隨着迭代,這段代碼變得愈來愈難以理解了,如今它嵌套了三層了。在不使用Option的時候,咱們只能一層層使用if語句去判斷。**代碼嵌套會使複雜度暴增。**我相信沒什麼人會想維護一段多重嵌套的代碼,特別是這段代碼仍是別人寫的。若是使用Option的話,上面這個需求,能夠這樣實現:
val buyBook2 = store.buyABook2(createCard, bookCode)
buyBook2.map{
println("book name = ${it.name}, author = ${it.author}")
it.group
}
.map {
println("book group = $it")
it.name
}
.exists {
println("book name = $it")
true
}
複製代碼
使用Option以後,會比咱們使用if嵌套代碼結構清晰不少。Option在處理大量可空值的時候,能以線性的方式去處理,而簡單使用if的話,咱們的代碼須要以相似多重嵌套的方式去實現,代碼複雜度會暴增。
固然,在使用命令式編程的時候,咱們能夠經過設計模式去優化代碼。而在函數式編程中,咱們使用函數式的通用結構的話,自然就是在使用相似設計模式的方式去編寫代碼。函數式的通用結構和設計模式的做用是相似的,可是函數式結構能提供更高程度的抽象(例如,咱們不須要爲具體的業務場景定義一個具體的Option,全部場景使用Option的方式是同樣的,而設計模式作不到這麼完全的抽象)。使用這些結構,能讓咱們以相似代數演算的方式去實現咱們的代碼,經過不一樣的組合能讓咱們構建出更加複雜的功能。
雖然函數式通用模式在不少場景下工做得很好,可是並不能徹底替代設計模式。在實際開發中要根據業務場景來選擇,同時使用兩種方式進行cc設計也是能夠的。
Option是一個比較簡單的函數通用結構,可是它的功能場景比較侷限。讀者能夠把它做爲一個入門的函數式通用結構來學習。接下來咱們會介紹其它兩個更增強大也更有用的通用結構。
前面咱們介紹瞭如何使用Option更優雅的處理可空變量的問題,可是Option實際上是沒有解決咱們文章開頭提到的一個最核心的問題的。**如何實現無反作用的函數。**咱們這裏回顧下,由於咱們在調用buyABook函數的時候,會包含一個支付行爲。支付行爲會和外界系統進行交互,因此這個buyABook行爲不僅僅影響了咱們的圖書銷售系統,還會把影響擴散到其它系統,這就是咱們所說的反作用。
要消除這個反作用,咱們要作的是支付行爲從圖書銷售系統中分離開,實現圖書銷售系統 —— 支付系統解耦。如今咱們從新整理下咱們的需求。咱們的需求是要實用向書店購買書本並支付,現實場景中,咱們可能會購買多本書本。若是使用咱們上面的buyABook的話,咱們能夠循環遍歷這個方法,直到所有購買成功爲止。不過這裏會有個問題,每購買一本書,就付款一次,顯然是很是不合理的,而且可能會存在餘額不足的問題。咱們要重構BookStore這個對象,以實現咱們的需求。整理一下,咱們的重構的BookStore要支持下面的功能:
爲了實現這個需求,咱們會增長一個Charge類,這個類的做用是記錄費用。
下面直接上代碼,第一次重構:
/** * 費用 * [id] 惟一標識符,太懶了,用隨機數表示 */
data class Charge(
val createCard: CreateCard,
val price: Int,
val id: Int = Random.nextInt()
)
複製代碼
/** * 第一次重構BookStore */
class RefactoringBookStore{
private val bookCollection = initBookCollection()
/** * 購買多個 */
fun buyBooks(cc: CreateCard, bookIds: List<Int>) : Pair<List<Book>, Charge>{
val purchases = bookIds
.map { buyABook(cc, it).orNull() }
.filterNotNull()
val (books, charges) = purchases.unzip()
//傳入的createCard是同一個,因此reduce操做的時候不用判斷createCard是否一致
val totalCharge = charges.reduce {
acc, charge -> Charge(acc.createCard, acc.price + charge.price) }
return books to totalCharge
}
@Suppress("MemberVisibilityCanBePrivate")
fun buyABook(cc: CreateCard, bookId: Int) : Option<Pair<Book, Charge>> {
val book = bookCollection[bookId]
return book.toOption().map { Pair(it, Charge(cc, it.price)) }
}
}
複製代碼
重構後的代碼已經把支付這個「反作用」分離出去了,購買書的函數中不會產生支付行爲。支付行爲客戶端須要經過讀取咱們封裝好的Charge對象,而後再調用支付模塊實現支付(限於篇幅,省略支付模塊)。
如今咱們來看看客戶端的調用代碼:
fun main() {
val (books1, charge1) =
RefactoringBookStore().buyBooks(CreateCard(123), 1, 3, 5, 10, 20, 30)
printBuyBooks(books1, charge1)
println("——————————————————————————————\n")
val (books2, charge2) =
RefactoringBookStore().buyBooks(CreateCard(111, 120), 7, 2, 8)
printBuyBooks(books2, charge2)
}
fun printBuyBooks(books: List<Book>, charge: Charge){
val buyBookName = books.map { it.name }
println("但願購買書本名字: $buyBookName")
if (charge.createCard.amount >= charge.price){
val cc = charge.createCard.charge(charge.price)
println("支付成功,支付金額 = ${charge.price}; 剩餘額度 = ${cc.amount}")
}else{
println("額度不足,須要支付金額 = ${charge.price}; 可用額度 = ${charge.createCard.amount}")
}
}
複製代碼
咱們看看調用結果
但願購買書本名字: [Kotlin, 娛樂雜誌, 資本論, 文明的衝突與世界秩序的重建] pay 148 yuan 支付成功,支付金額 = 148; 剩餘額度 = 352 ——————————————————————————————
但願購買書本名字: [Scala, 中國民俗, 月亮與六便士] 額度不足,須要支付金額 = 125; 可用額度 = 120
上面的這段代碼已經成功把反作用分離出去了(省略支付模塊)。通常狀況下,這段代碼已經能工做得很好了(經驗豐富的同窗可能會以爲,這種代碼算是比較好維護的代碼了😂)。可是其實這段代碼仍是有問題的,它的問題就是,咱們須要經過List來維護Charge這個對象,在組合Charge的時候須要經過相似下面的方式來實現:
fun foo(){
Charge(acc.createCard, acc.price + charge.price)
charge.copy(charge.createCard, charge.price + charge1.price)
}
複製代碼
這樣看起來好像也沒什麼問題。可是假若有這樣的一個場景:書店裏面有位客人,買了一批書後,發現還有書忘記買了,再買一批後,又發現了一本本身很想買的書。在這種場景下咱們客戶端的代碼可能會變成相似這種:
val cc = CreateCard(12423)
val (b1, c1) = RefactoringBookStore().buyBooks(cc, 1, 2)
val (b2, c2) = RefactoringBookStore().buyBooks(cc, 4, 5)
val (b3, c3) = RefactoringBookStore().buyBooks(cc, 6, 7)
val books = mutableListOf<Book>().apply {
addAll(b1)
addAll(b2)
addAll(b3)
}
printBuyBooks(books, Charge(c1.createCard, c1.price + c2.price + c3.price))
複製代碼
這種代碼主要的問題就是在於Charge的合併上面,當咱們在組合更多更復雜的Charge的時候,會寫不少相似這種繁瑣又不利於維護的代碼。固然,你能夠把多個Charge放到List裏面再作處理,可是這兩種方式都不是那麼的方便。那麼有沒有更合理的方法解決這種問題呢?有的,咱們可使用Monoid來解決這個問題。
Monoid和Option同樣,也是函數設計的通用結構的其中一種。Monoid是一種純代數結構,它的中文名字叫幺半羣,它是一個代數定義。Monoid在函數式編程中常常出現,操做列表、鏈接字符、循環中進行累加操做均可以背解析成Monoid。Monoid的主要做用是:將問題拆分紅小部分而後並行計算和將簡單的部分組裝成複雜的計算。Monoid是一種數學上的概念。
在抽象代數此一數學分支中,幺半羣(英語:monoid,又稱爲單羣、亞羣、具幺半羣或四分之三羣)是指一個帶有可結合二元運算和單位元的代數結構。—— 維基百科
看不懂上面這段話也不要緊,大部分第一次接觸這個定義的時候,都會以爲很難理解。咱們下面經過簡單的例子來學習Monoid到底能作些什麼。
舉個整數計算的例子,假設有 x 、y、z三個整數,那麼咱們很容易就能得出下面的一些公式:x + y + z 等於( x + y ) + z 等於x + ( y + z )。並且有一個單位元(Monoid的定義)元素0,當0和其它整數相加時,結果不會發生改變。乘法運算也有一個單位元元素1。
咱們先看看Kotlin中的Monoid的定義是怎麼樣的:
Monoid
/** * ank_macro_hierarchy(arrow.typeclasses.Monoid) */
interface Monoid<A> : Semigroup<A>, MonoidOf<A> {
/** * A zero value for this A */
fun empty(): A
/** * Combine an [Collection] of [A] values. */
fun Collection<A>.combineAll(): A =
if (isEmpty()) empty() else reduce { a, b -> a.combine(b) }
/** * Combine an array of [A] values. */
fun combineAll(elems: List<A>): A = elems.combineAll()
companion object
}
複製代碼
Semigroup
interface Semigroup<A> {
/** * Combine two [A] values. */
fun A.combine(b: A): A
operator fun A.plus(b: A): A =
this.combine(b)
fun A.maybeCombine(b: A?): A = Option.fromNullable(b).fold({ this }, { combine(it) })
}
複製代碼
這是Monoid在arrow開源庫中的定義,若是在使用時不想依賴arrow庫的話,本身實現一個Monoid也是很是簡單的,30多行代碼就足以。
咱們簡單使用下Monoid:
fun main() {
val monoid = Int.monoid()
val sum = listOf(1, 2, 3, 4, 5).foldMap(monoid, ::identity)
println("sum = $sum")
}
複製代碼
結果:
sum = 15
能夠看到IntMonoid在這裏的做用就是定義了元素之間的結合規則。
Int.monoid()
給咱們返回了一個IntMonoid。咱們再來看看IntMonoid的定義:
interface IntMonoid : Monoid<Int>, IntSemigroup {
override fun empty(): Int = 0
}
interface IntSemiring : Semiring<Int> {
override fun zero(): Int = 0
override fun one(): Int = 1
override fun Int.combine(b: Int): Int = this + b
override fun Int.combineMultiplicate(b: Int): Int = this * b
}
複製代碼
IntMonoid的定義也很是簡單,主要是定義了0值,1值和它們兩兩結合的操做。
這樣看來Monoid好像給人一點多此一舉的感受,咱們不用monoid的話,摺疊集合的時候能夠這樣寫啊:
listOf(1, 2, 3, 4, 5).fold(0){acc, i ->
acc + i
}
複製代碼
那麼咱們爲何要使用Monoid呢,上一小節咱們經過Charge把反作用分離了,可是Charge的後續處理仍是存在不完善的地方。那麼咱們來嘗試使用Monoid來優化咱們的代碼。
咱們先定義個Monoid<Charge>
/** * 須要傳入[CreateCard] 由於同一張信用卡的費用才能作合併處理 */
class ChargeMonoid(private val cc: CreateCard) : Monoid<Charge>{
/** * 固定的單位元值,id也須要是固定的 * 由於同一個ChargeMonoid(equals 爲 true)的單位元([empty])是相等(equals 爲 true)的 */
override fun empty(): Charge = Charge(cc, 0, 0)
/** * 只會合併createCard等於[cc]的Charge * 不一樣信用卡的費用沒法合併 */
override fun Charge.combine(b: Charge): Charge =
if (cc == b.createCard){
Charge(cc, b.price + price)
}else{
this
}
}
複製代碼
咱們回顧下上文說的書店的例子:
咱們先來看看新的書店的代碼:
class MonoidBookStore {
private val bookCollection = initBookCollection()
/** * bookId能夠傳入多個 */
fun buyBooks(cc: CreateCard, vararg bookIds: Int) : Pair<List<Book>, Charge>{
val purchases = bookIds
.map { buyABook(cc, it).orNull() }
.filterNotNull()
val (books, charges) = purchases.unzip()
//使用ChargeMonoid摺疊列表
val totalCharge = charges.foldMap(Charge.monoid(cc), ::identity)
return books to totalCharge
}
@Suppress("MemberVisibilityCanBePrivate")
fun buyABook(cc: CreateCard, bookId: Int) : Option<Pair<Book, Charge>> {
val book = bookCollection[bookId]
return book.toOption().map { Pair(it, Charge(cc, it.price)) }
}
}
複製代碼
和第一次重構後的RefactoringBookStore
很是像,他們只有一句代碼是有區別的:
RefactoringBookStore
//傳入的createCard是同一個,因此reduce操做的時候不用判斷createCard是否一致
val totalCharge = charges.reduce {
acc, charge -> Charge(acc.createCard, acc.price + charge.price) }
複製代碼
MonoidBookStore
//使用ChargeMonoid摺疊列表
val totalCharge = charges.foldMap(Charge.monoid(cc), ::identity)
複製代碼
::identity是Kotlin的一個語法糖,在這裏的做用是,返回摺疊後的Charge對象。::identity的做用是返回自己,有興趣的能夠看看它的具體實現。
咱們能夠看到它們的差異很小,那麼咱們這樣寫有什麼好處呢?咱們再來回顧一下上面的一個場景:店裏面有位客人,買了一批書後,發現還有書忘記買了,再買一批後,又發現了一本本身很想買的書。
那麼咱們用ChargeMonoid要怎麼實現這個功能呢?直接看代碼。
fun main() {
val cc = CreateCard(12423)
val (b1, c1) = MonoidBookStore().buyBooks(cc, 1, 2)
val (b2, c2) = MonoidBookStore().buyBooks(cc, 4, 5)
val (b3, c3) = MonoidBookStore().buyBooks(cc, 6, 7)
val books = listOf(b1, b2, b3).flatten()
val charge = listOf(c1, c2, c3).foldMap(Charge.monoid(cc),::identity)
printBuyBooks(books, charge )
}
複製代碼
如今來看看輸出:
但願購買書本名字: [Kotlin, 中國民俗, 灌籃, 資本論, Java, Scala] pay 255 yuan 支付成功,支付金額 = 255; 剩餘額度 = 245
單單看代碼的話,上面這段代碼和前面的對比起來貌似只有一個好處:不須要人工維護Charge對象的合併。是的,它就真的只有這一個好處。Monoid它的主要做用就是這個,它定義了相同類型(羣)的對象的合併規律。在這裏咱們Charge的合併規律就是:CreateCard相同的Charge對象以價格累計的方式合併在一塊兒。
咱們爲了這個簡單的功能引入一個這樣複雜的概念(對初學者來講,這的確是有點難以理解)值得嗎?
如今咱們再來改一下這個需求( 敏捷開發😂 ):店裏面有位客人,買了一批書後,發現還有書忘記買了,再買一批後,又發現了一本本身很想買的書。而後他又想再買一批書,可是如今想用另一張卡付款,而後再買一批,再用另一張卡付款。這個過程當中,一共用了三張卡付款,你們能夠嘗試用命令式編程實現這個需求,這裏就不演示了,直接上Monoid的例子:
fun main() {
//數據初始化
val cc2 = CreateCard(124234)
val (b4, c4) = MonoidBookStore().buyBooks(cc2, 1, 2)
val (b5, c5) = MonoidBookStore().buyBooks(cc2, 4, 5)
val (b6, c6) = MonoidBookStore().buyBooks(cc2, 6, 7)
val cc3 = CreateCard(124234512)
val (b7, c7) = MonoidBookStore().buyBooks(cc3, 3)
val cc4 = CreateCard(151212)
val (b8, c8) = MonoidBookStore().buyBooks(cc4, 8)
//數據處理
val monoid3 = monoidTuple3(
//a monoid
Charge.monoid(cc2),
//b monoid
Charge.monoid(cc3),
//c monoid
Charge.monoid(cc4))
val result = listOf(c6, c7 ,c8).foldMap(monoid3){
Tuple3(it, it, it)
}
println("\n—————————————————第一張信用卡———————————————————")
//result.a -> monoid a收集的數據
printBuyBooks(listOf(b4, b5, b6).flatten(), result.a)
println("\n—————————————————第二張信用卡———————————————————")
//result.b -> monoid b收集的數據
printBuyBooks(b7 , result.b)
println("\n—————————————————第二張信用卡———————————————————")
//result.b -> monoid b收集的數據
printBuyBooks(b8, result.c)
}
/** * 不用理解下面的代碼(Tuple3是另一種函數設計通用結構) * 只須要明白個函數的做用值組合3個Monoid就好了 */
fun <A, B, C> monoidTuple(MA: Monoid<A>, MB: Monoid<B>, MC: Monoid<C>): Monoid<Tuple3<A, B, C>> =
object: Monoid<Tuple3<A, B, C>> {
override fun Tuple3<A, B, C>.combine(y: Tuple3<A, B, C>): Tuple3<A, B, C> {
val (xa, xb, xc) = this
val (ya, yb, yc) = y
return Tuple3(MA.run { xa.combine(ya) }, MB.run { xb.combine(yb) }, MC.run { xc.combine(yc) })
}
override fun empty(): Tuple3<A, B, C> = Tuple3(MA.empty(), MB.empty(), MC.empty())
}
複製代碼
如今咱們看看輸出的內容
—————————————————第一張信用卡——————————————————— 但願購買書本名字: [Kotlin, 中國民俗, 灌籃, 資本論, Java, Scala] pay 105 yuan 支付成功,支付金額 = 105; 剩餘額度 = 395
—————————————————第二張信用卡——————————————————— 但願購買書本名字: [娛樂雜誌] pay 19 yuan 支付成功,支付金額 = 19; 剩餘額度 = 481
—————————————————第二張信用卡——————————————————— 但願購買書本名字: [月亮與六便士] pay 25 yuan 支付成功,支付金額 = 25; 剩餘額度 = 475
咱們回顧下代碼的數據處理部分
val monoid3 = monoidTuple3(
//a monoid
Charge.monoid(cc2),
//b monoid
Charge.monoid(cc3),
//c monoid
Charge.monoid(cc4))
val result = listOf(c6, c7 ,c8).foldMap(monoid3){
Tuple3(it, it, it)
}
複製代碼
是的,數據處理就這麼多代碼,在這裏咱們經過了Monoid來處理了數據。ChargeMonoid會把數據按照CreateCard的類型來收集,因此咱們不用關心數據是如何收集的。若是採用命令式編程,你們能想象到代碼的糟糕程度。
而且在上面的場景中,就算咱們再用多幾張信用卡用來支付也不會增長太多代碼量,這就是Monoid的優點。
monoidTuple3在這裏的做用是組合Monoid,它能組合任何三個Monoid,是一段複用性很是強的代碼。固然讀者也能夠本身寫一個monoidTuple2用於組合兩個Monoid。
從文章開頭的思惟導圖能夠看出來,這裏少寫了一個函數式通用結構:Monad。少寫的緣由是:文章的文字量已經接近1.2W字了,做爲一篇博客,內容量已是過於多了。因此這裏就暫時就不繼續Monad結構了。對函數式通用結構有興趣的同窗能夠繼續關注個人博客/公衆號:代碼以外的程序員(懶鬼,一年沒更新了還好意思叫我關注。好吧,我今年會努力保持跟新的)。筆者會慢慢繼續更新函數式通用結構中其它的實用結構的。
函數式編程,你們能夠不侷限於Kotlin,由於FP更可能是一種思想,相似一條條代數公式,和語言是無關的。當你們掌握了FP以後,就算是切換到別的FP語言中,大部分
這裏給你們一個學習建議,我知道你們看到一篇感興趣的文章的時候,都喜歡當一個馬來人(mark)。你們回想下,收藏夾裏面有多少篇文章是你們只看了題目的?感興趣的東西,要立刻過一遍,由於這個時候你的熱情是最高的,過了一天你可能就沒興趣了。學習就是一個這樣的過程:興趣 -> 理解 -> 實戰 -> 深刻
因此這裏建議有興趣的同窗(看到這裏的同窗很贊),下載Demo體驗一下,本身再嘗試寫感興趣的部分。最後祝你們儘快在本身的項目中愉快地使用FP風格進行編程。
Demo是開源庫中的KotlinFp項目,請使用IntelliJ打開。若是以爲本文對你有幫助的話,能夠點一下star以資鼓勵😊。
若是下面的項目你有興趣的話,也能夠點進去看看。若是以爲還能夠的話,能夠點一下star
關於MVVM的兩個項目的文檔還在整理中,筆者會盡快整理出來。有興趣的同窗能夠關注下個人github動態,最後,我寫的這麼辛苦你們記得點個star哦。