【Kotlin】高階函數Lambda內聯函數

【Kotlin】簡介&變量&類&接口
【Kotlin】數字&字符串&數組&集合
【Kotlin】高階函數&Lambda&內聯函數
【Kotlin】表達式&關鍵字

文章目錄

    • 函數還是屬性
    • 高階函數
      • 抽象和高階函數
      • 實例: 函數作為參數的需求
        • 方法引用表達式更多使用場景
    • 匿名函數
    • Lambda表達式
      • lambda表達式類型
      • Lambda開銷
      • Function類型
    • 閉包
      • Java實現閉包
    • Anonymous inner class is not closure
    • Closure in Java
        • 為什么java中內部類訪問外部局部變量需要用final修飾
      • Kotlin中的閉包
    • 內聯函數
    • 優化Lambda開銷
        • 1. invokedynamic
        • 2. 內聯函數
        • noinline: 避免參數被內聯
        • 非局部返回
        • 使用標簽實現Lambda非局部返回
          • 為什么要設計noinline
        • crossinline
        • 具體化參數類型

函數還是屬性

在某些情況下,不帶參數的函數可與只讀屬性互換。 雖然語義相似,但是在某種程度上有一些風格上的約定。

底層算法優先使用屬性而不是函數:

  • 不會拋異常
  • 計算開銷小(或者在首次運行時緩存)
  • 如果對象狀態沒有改變,那么多次調用都會返回相同結果

高階函數

高階函數(Higher Order Function)是一種特殊的函數,它接收函數作為參數,或者返回一個函數。
函數的基本格式如下:

fun 高階函數名(參數函數名: 參數函數類型): 高階函數返回類型 {// 高階函數體
}

高階函數的一個很好的例子就是lock函數,它的參數是一個Lock類型對象和一個函數。

該函數執行時獲取鎖,運行函數參數,運行結束后再釋放鎖。

fun <T> lock(lock: Lock, body: () -> T): T {lock.lock()try {return body()} finally {lock.unlock()}
}

在這個例子中,body為函數的類型對象,該函數是一個無參函數,而且返回值類型是T。

調用lock函數時,可以傳入另一個函數作為參數。

和許多編程語言類似,如果函數的最后一個參數也是函數,則該函數參數還可以定義在括號外面,如:

val result = lock(lock, { sharedResource.operation()})
// 等同于
lock(lock) {sharedResource.operation()
}

高階函數類似C語言的函數指針,它的另一個使用場景是map函數。

如果函數只有一個參數,則可以忽略聲明的函數參數,用it來代替。

val doubled = mMap.map {it -> it * 2}

Kotlin天然支持了部分函數式特性。函數式語言一個典型的特征就在于函數是頭等公民——我們不僅可以像類一樣在底層直接定義一個函數,也可以在一個函數內部定義一個局部函數。

fun foo(x: Int) {fun double(y: Int): Int {return y * 2}println(double(x))
}

抽象和高階函數

我們會善于對熟悉或重復的事物進行抽象,比如2歲左右的小孩就會開始認識數字1、2、3…之后,我們總結除了一些公共的行為,如對數字做加減、求立方,這被稱為過程,它接收的數字是一種數據,然后也可能產生另一種數據。

過程也是一種抽象,幾乎我們所熟悉的所有高級語言都包含了定義過程的能力,也就是函數。

然而,在我們以往熟悉的編程中,過程限制為只能接收數據為參數,這個無疑限制了進一步抽象的能力。

由于我們經常會遇到一些同樣的程序設計模式能夠用于不同的過程,比如一個包含了正整數的列表,需要對它的元素進行各種轉換操作,例如對所有元素都乘以3,或者都除以2。我們就需要提供一種模式,同時接收這個列表及不同的元素操作過程,最終返回一個新的列表。

為了把這種類似的模式描述為相應的概念,我們就需要構造出一種更加高級的過程,表現為:接收一個或多個過程為參數,或者以一個過程作為返回結果。這個就是所謂的高階函數,你可以把它理解為“以其他函數作為參數或返回值的函數”。高階函數是一種更加高級的抽象機制,它極大地增強了語言的表達能力。

實例: 函數作為參數的需求

Shaw因為旅游喜歡上了地理,然后他建了一個所有國家的數據庫。作為一名程序員,他設計了一個CountryApp類對國家數據進行操作。Shaw偏好歐洲的國家,于是他設計了一個程序來獲取歐洲的所有國家。

data class Country {val name: String, val continient: String,val population: Int
}class CountryApp {fun filterCountries(countries: List<Country>): List<Country> {val res = mutableListOf<Country>()for (c in countries) {if (c.continent == "EU") { // EU代表歐洲res.add(c)}}return res}
}

后來Shaw對非洲也產生了興趣,于是他又改進了上述方法的實現,支持根據具體的州來篩選國家。

fun filterCountries(countries: List<Country>, continient: String): List<Country> {val res = mutableListOf<Country>()for (c in countries) {if (c.continient == continient) {res.add(c)}}return res
}

以上程序具備了一定的復用性。然而,Shaw的地理知識越來越豐富了,他想對國家的特點做進一步的研究,比如篩選具有一定人口規模的國家,于是代碼又變成下面這個樣子:

fun filterCountries(countries: List<Country>, continient: String, population: Int) : List<Country> {val res = mutableListOf<Country>()for (c in countries) {if (c.continient == continient && c.population > population) {res.add(c)}}return res
}

新增了一個population的參數來代表人口(單位:萬)。Shaw開始感覺到不對勁,如果按照現有的設計,更多的篩選條件會作為方法參數而不斷增加,而且業務邏輯也會高度耦合。

解決問題的核心在于對filterCountries方法進行解耦,我們能否把所有的篩選邏輯行為都抽象成一個參數呢?傳入一個類對象是一種解決方法,我們可以根據不同的篩選需求創建不同的子類,它們都各自實現了一個校檢方法。然而,Shaw了解到Kotlin是支持高階函數的,理論上我們同樣可以把篩選的邏輯變成一個方法來傳入,這樣思路更簡單。

他想要進一步了解高級的特性,所以很快寫了一個新的測試類:

class CountryTest {fun isBigEuropeanCountry(country: Country): Boolean {return country.continient == "EU" && country.population > 10000}
}

調用isBigEuropeanCountry方法就能夠判斷一個國家是否是一個人口超過1億的歐洲國家。然而,怎樣才能把這個方法變成filterCountries方法的一個參數呢?要實現這一點似乎要先解決以下兩個問題:

  • 方法作為參數傳入,必須像其他參數一樣具備具體的類型信息

    在kotlin中,函數類型的格式非常簡單:

    • 通過 -> 符號來組織參數類型和返回值類型,左邊是參數類型,右邊是返回值類型
    • 必須用一個括號來包裹參數類型,如果是一個沒有參數的函數類型,參數類型部分就用()表示
    • 返回值類型即使是Unit,也必須顯式聲明

    舉個例子:

    (Int) -> Unit
    () -> Unit
    (Int, String) -> Unit
    // 還支持為聲明參數指定名字
    (errCode: Int, errMsg: String) -> Unit
    // ?表示可選,可在某種情況下為空
    ((errCode: Int, errMsg: String?) -> Unit)?
    // 表示傳入一個類型為Int的參數,然后返回另一個類型為(Int) -> Unit的函數
    (Int) -> ((Int) -> Unit) 
    

    在學習了Kotlin函數類型知識之后,Shaw重新定義了filterCountries方法的參數聲明:

    // 增加了一個函數類型的參數test
    fun filterCountries(countries: List<Country>, test: (Country) -> Boolean): List<Country> {val res = mutableListOf<Country>()for (c in countries) {// 直接調用test函數來進行篩選if (test(c)) {res.add(c)}}return res
    }
    

    接下來就是如何把isBigEuropeanCountry方法傳遞給filterCountries呢? 直接把isBigEuropeanCountry當參數肯定不行,因為函數名并不是一個表達式不具有類型信息。所以我們需要的是一個單純的方法引用表達式。

  • 需要把isBigEuropeanCountry的方法引用當做參數傳遞給filterCountries

    Kotlin存在一種特殊的語法,通過兩個冒號來實現對于某個類的方法進行引用(方法引用表達式)。
    以上面的代碼為例,假如我們有一個CountryTest類的對象實例countryTest,如果要引用它的isBigEuropeanCountry方法,就可以這樣寫:

    countryTest::isBigEuropeanCountry
    

    于是,Shaw便使用了方法引用來傳遞參數:

    val countryApp = CountryApp()
    val countryTest = CountryTest()
    val countries = ...countryApp.filterContries(countries, countryTest::isBigEuropeanCountry)
    

經過重構后的程序顯然比之前要優雅許多,程序可以根據任意的需求篩選,調用同一個filterCountries方法來獲取國家數據。

方法引用表達式更多使用場景

此外,我們還可以直接通過這種語法,來定義一個類的構造方法引用變量。

class Book(val name: String) {fun main(args: Array<String>) {val getBook = ::Bookprintln(getBook("Dive into Kotlin").name)}
}

可以發現,getBook類型為(name: String) -> Book。類似的道理,如果我們要引用某個類的成員變量,如Book類中的name,就可以這樣引用:

Book::name

以上創建的Book::name的類型為(Book) -> String。當我們再對Book類對象的集合應用一些函數式API的時候,這會顯得格外有用,比如:

fun main(args: Array<String>) {val bookNames = listOf (Book("Thinking in java")Book("Dive into Kotlin")    ).map(Book::name)println(bookNames)
}

匿名函數

再來思考下上面代碼中的CountryTest類,這仍算不上是一種很好的方案。因為每增加一個需求,我們都需要在類中專門寫一個新增的篩選方法。然而Shaw的需求很多都是臨時性的,不需要被復用。Shaw覺得這樣還是比較麻煩,他打算用匿名函數對程序進一步的優化。

Kotlin支持在缺省函數名的情況下,直接定義一個函數。所以isBigEuropeanCountry方法我們可以直接定義為:

// 沒有函數名字
fun(country: Country): Boolean {return country.continient == "EU" && country.population > 10000
}

于是,Shaw直接調用filterCountries,如下:

countryApp.filterCountries(countries, fun(country: Country): Boolean) {return country.continient == "EU" && country.population > 10000    
})

這一次我們甚至不需要CountryTest這個類了,代碼的簡潔性又上了一層樓。Shaw開始意識到Kotlin這門語言的魅力,很快他發現還有一種語法可以讓代碼更簡單,這就是Lambda表達式。

我們繼續看上面的filterCountries方法的匿名函數,會發現:

  • fun(country: Country)顯得比較啰嗦,因為編譯器會推導類型,所以只需要一個代表變量的country就行了。
  • return關鍵字也可以省略,這里返回的是一個有值的表達式
  • 模仿函數類型的語法,我們可以用 -> 把函數和返回值連接在一起

因此,簡化后的表達就變成了這個樣子:

countryApp.filterCountries(countries, {country -> country.continient == "EU" && country.population > 10000
})

這就是Lambda表達式。

Lambda表達式本質上是一個匿名函數,它是一種函數字面值,即一個沒有聲明的函數,可以把該函數當做普通表達式進行參數傳遞,它可以訪問自己的閉包函數。

首先來看一下Lambda的定義,如果用最直白的語言來闡述的話,Lambda就是一小段可以作為參數傳遞的代碼。從定義上看,這個功能就很厲害了,因為正常情況下,我們向某個函數傳參時只能傳入變量,而借助Lambda卻允許傳入一小段代碼。這里兩次使用了“一小段代碼”這種描述,那么到底多少代碼才算一小段代碼呢?Kotlin對此并沒有進行限制,但是通常不建議在Lambda表達式中編寫太長的代碼,否則可能會影響代碼的可讀性。

Lambda的語法:

  • 一個Lambda表達式必須通過{}來包裹
  • 如果Lambda聲明了參數部分的類型,且返回值類型支持類型推導,那么Lambda變量就可以省略函數類型聲明
  • 如果Lambda變量聲明了函數類型,那么Lambda的參數部分的類型就可以省略

此外,如果Lambda表達式返回的不是Unit,那么默認最后一行表達式的值類型就是返回值類型,如:

val foo = { x: Int -> val y = x + 1y // 返回值是y           
}

Lambda表達式

{參數名1: 參數類型, 參數名2: 參數類型 -> 函數體}

“Lambda 表達式”(lambda expression)其實就是匿名函數,Lambda表達式基于數學中的λ演算得名,直接對應于其中的lambda抽象
(lambda abstraction),是一個匿名函數,即沒有函數名的函數。Lambda表達式可以表示閉包。

Java 8的一個大亮點是引入Lambda表達式,使用它設計的代碼會更加簡潔。

// 沒有使用Lambda的老方法:   
button.addActionListener(new ActionListener(){public void actionPerformed(ActionEvent ae){System.out.println("Actiondetected");}
});
// 使用Lambda:  
button.addActionListener(() -> { System.out.println("Actiondetected");
});// 不采用Lambda的老方法: 
Runnable runnable1 = new Runnable() {@Overridepublic void run() {System.out.println("RunningwithoutLambda");}
};// 使用Lambda:   
Runnable runnable2 = () -> {System.out.println("RunningfromLambda");
};

Lambda能讓代碼更簡潔,Kotlin的支持如下:

  • lambda表達式總是被大括號括著
  • 其參數(如果有的話)在->之前聲明(參數類型可以省略),
  • 函數體(如果存在的話)在->后面。

Lambda表達式是定義匿名函數的簡單方法。由于Lambda表達式避免在抽象類或接口中編寫明確的函數聲明,進而也避免了類的實現部分,
所以它是非常有用的。

Lambda表達式由箭頭左側函數的參數(在圓括號里的內容)定義的,將值返回到箭頭右側。
view.setOnClickListener({ view -> toast("Click")})
在定義函數時,必須在箭頭的左側用方括號,并指定參數值,而函數的執行代碼在箭頭右側。如果左側不使用參數,甚至可以省去左側部分:
view.setOnClickListener({ toast("Click") })
如果函數的最后一個參數是一個函數的話,可以將作為參數的函數移到圓括號外面:
view.setOnClickListener() { toast("Click") }

先看一個例子:

fun compare(a: String, b: String): Boolean {return a.length < b.length
}
max(strings, compare)

就是找出strings里面最長的那個。但是我個人覺得compare還是很礙眼的,因為我并不想在后面引用他,那我怎么辦呢,就是用“匿名函數”方式。

max(strings, (a,b) -> {a.length < b.length})

(a,b) -> {a.length < b.length}就是一個沒有名字的函數,直接作為參數賦給max方法的第二個參數。但這個方法有很多東西都沒有寫明,如:

  • 參數的類型
  • 返回值的類型

但這些真的必要嗎?a.length < b.length很明顯返回一個Boolean的值,再就是max的定義中肯定也定義了這個函數的參數類型和返回值類型。
這么明顯的事為什么不讓計算機自己去做而要讓人寫代碼去做呢?這就是匿名函數的好處了。到這里,我們已經和Lambda很接近了。

val sum: (Int, Int) -> Int = { x, y -> x + y }

Lambda表達式就是被大括號括著的那一部分,在->符號之前有參數聲明,函數體跟在一個->符號之后。
而且此Lambda表達式之前有一個匿名的函數聲明(在此例中兩個Int型的輸入,一個Int型的返回值),這個聲明是可以不使用的。
則此Lambda表達式變成val sum = { x: Int, y: Int -> x + y },此時Lambda表達式會根據主體中的最后一個(或可能是單個)表達式會視為
返回值。當然,在某些特定情況下,xy的類型了是可以推斷的,所以val sum = { x, y -> x + y }

通過調用lambda來執行它的代碼你可以使用invoke函數調用lambda,并傳入參數的值。例如,以下代碼定義了變量addInts,并將用于將兩個Int參數相加的lambda賦值給它。然后代碼調用了該lambda,傳入參數值6和7,并將結果賦值給變量result:

val addInts = { x: Int, y: Int -> x + y }
val result = addInts.invoke(6, 7)
// 還可以使用如下快捷方式調用lambda:
val result = addInts(6, 7)

lambda表達式類型

就像任何其他類型的對象一樣,lambda也具有類型。然而,lambda類型的不同點在于,它不會為lambda的實現指定類名,而是指定lambda的參數和返回值的類型。lambda類型的格式如下:

(parameters) -> return_type

因此,如果你的lambda具有單獨的Int參數并返回一個字符串,如下代碼所示:

val msg = { x: Int -> "xxx" }

其類型為:

(Int) -> String

如果將lambda賦值給一個變量,編譯器會根據該lambda來推測變量的類型,如上例所示。然而,就像任何其他類型的對象一樣,你可以顯式地定義該變量的類型。例如,以下代碼定義了一個變量add,該變量可以保存對具有兩個Int參數并返回Int類型的lambda的引用:

val add: (Int, Int) -> Intadd = { x: Int, y: Int -> x + y }

Lambda類型也被認為是函數類型。

當Lambda表達式的參數列表中只有一個參數時,那么可以不聲明唯一的參數名,而是可以使用it關鍵字來代替,因為Kotlin會隱含的聲明一個名為it的參數.這叫做單個參數的隱式名稱,代表了這個Lambda所接收的單個參數

val list = listOf("Apple", "Bnana", "Orange", "Pear")
val maxLengthFruit = list.maxBy {it.length}

Lambda開銷

fun foo(int: Int) = {print(int)
}
listOf(1, 2, 3).forEach { foo(it) } // 對一個整數列表的元素遍歷調用foo

這里的調用等價于:

listOf(1, 2, 3).forEach { item -> foo(item) }

假設使用以下代碼將lambda賦值給變量:

val addFive: (Int) -> Int = { x -> x + 5 }

由于lambda具有單獨的參數x,而且編譯器能夠推斷出x為Int類型,因此我們可以省略該x參數,并在lambda的主體中使用it替換它:

val addFive: (Int) -> Int = { it + 5 }

在上述代碼中,{it+5}等價于{x->x+5},但更加簡潔。

請注意,你只能在編譯器能夠推斷該參數類型的情況下使用it語法。

例如,以下代碼將無法編譯,因為編譯器不知道it應該是什么類型:

val addFive = { it + 5 } // 該代碼無法編譯,因為編譯器不能推斷其類型

我們看一下foo函數用IDE轉換后的Java代碼:

@JvmStatic
@NotNull
public static final Function0 foo(final int var0) {return (Function0)(new Function0() {// $FF: synthetic method// $FF: bridge methodpublic Ojbect invoke() {this.invoke();return Unit.INSTANCE;}public final void invoke() {int var1 = var0;System.out.printlln(var1);}});
}

以上是字節碼反編譯的Java代碼,從中我們可以發現Kotlin實現Lambda表達式的機理。

Function類型

Kotlin在JVM層設計了Function類型(Function0、Function1 … Function22、FunctionN)來兼容Java的Lambda表達式,其中的后綴數字代表了Lambda參數的數量,如以上的foo函數構建的其實是一個無參Lambda,所以對應的接口是Function0,如果有一個參數那么對應的就是Function1.它在源碼是如下定義的:

package kotlin.jvm.functions
public interface Function1<in P1, out R> : Function<R> {/** Invokes the function with the specified argument. */public operator fun invoke(p1: P1): R
}

可見每個Function類型都有一個invoke方法。設計Function類型的主要目的之一就是要兼容Java,實現在Kotlin中也能調用Java的Lambda。在Java中,實際上并不支持把函數作為參數,而是通過函數式接口來實現這一特性。

foo函數的返回類型是Function()。這也意味著,如果我們調用了foo(n),那么實質上僅僅是構造了一個Function()對象。這個對象并不等價于我們要調用的過程本身。通過源碼可以發現,需要調用Function()的invoke方法才能執行println方法。所以上面的例子必須如下修改,才能最終打印出我們想要的結果:

fun foo(int: Int) = {print(int)
}
listOf(1, 2, 3).forEach { foo(it).invoke() } // 增加了invoke調用

但是invoke這種語法顯得丑陋,不符合Kotlin簡潔表達的設計理念,所以我們還可以用熟悉的括號調用來替代invoke,如下所示:

listOf(1, 2, 3).forEach{ foo(it)() }

閉包

閉包就是能夠讀取其他函數內部變量的函數。

它是函數內部和函數外部信息交換的橋梁。

在Kotlin中,Lambda表達式或匿名函數(局部函數、對象表達式等)都可以訪問它的閉包。

在Kotlin中,你會發現匿名函數體、Lambda在語法上都存在“{}",由這對花括號包裹的代碼如果訪問了外部環境變量則被稱為一個閉包。
一個閉包可以被當做參數傳遞或直接使用,它可以簡單的看成”訪問外部環境變量的函數“。Lambda是Kotlin中最常見的閉包形式。

與Java不一樣的地方在于,Kotlin中的閉包不僅可以訪問外部變量,還能夠對其進行修改(我有點疑惑,Java為啥不能修改?下面說),如下:

var sum = 0
listOf(1, 2, 3).filter { it > 0 }.forEach {// 外部的變量,且可以修改sum += it
}
println(sum)  // 6

閉包就是能夠讀取其他函數內部變量的函數。例如在javascript中,只有函數內部的子函數才能讀取局部變量,所以閉包可以理解成“定義在一個函數內部的函數“。在本質上,閉包是將函數內部和函數外部連接起來的橋梁。–百度百科
第一句總結的很簡潔了:閉包就是能夠讀取其他函數內部變量的函數。

Java實現閉包

在Java8之前,是不支持閉包的,但是可以通過“接口+匿名內部類”來實現一個偽閉包的功能,為什么說是偽閉包?

Anonymous inner class is not closure

Anonymous classes in java are close to being called as a closure. They don’t 100% support the definition but come close to it and thats why we see lot of literature calling anonymous inner classes as closure. Why do I say its not 100%? An anonymous inner class can access “only” the final local variable of the enclosing method. It is because of this restriction, anonymous inner class in java is not a closure.

If you remember the memory management in java, you can recall that the local variables are stored in a stack. These java stacks are created when the method starts and destroyed when it returns. Unlike local variables, final fields are stored in method area which lives longer even after the return of the method. If we want to make anonymous inner class as a closure, then we should allow it to access all the fields surrounding it. But, as per the current memory management, they will be destroyed and will not be accessible after the method has returned.

Closure in Java

In that case will we get closure in java in future? We have a specification written by Peter Ahe, James Gosling, Neal Gafter and Gilad Bracha on closures for java. It gives detailed description of how a closure can be implemented in java and example code on how to use them. We have JSR 335 for closures in java named as Lambda Expressions for the Java Programming Language.

class ClosureTest {public interface MutableAdder {int add(int x, boolean change);}public MutableAdder makeAdderB(int n) {// Variable 'intHolder' is accessed from within inner class, needs to be declared finalfinal int[] intHolder = new int[]{n};return new MutableAdder() {public int add(int x, boolean change) {if (change) {intHolder[0] = x;return x;} else {return intHolder[0] + x;}}};}
}

OK,實現看完了,那這么做有什么用呢? 反正我平時沒用到…

現在繼續來說上面提到的與Java不一樣的地方在于,Kotlin中的閉包不僅可以訪問外部變量,還能夠對其進行修改。

為什么java中內部類訪問外部局部變量需要用final修飾

我學java的時候我就記著這種情況一定要加final,但是我不知道為啥。 今天就仔細看看

Variable ‘a’ is accessed from within inner class, needs to be final or effectively final(java 8)。

Java doesn’t support closures, i.e. local variable can’t be accessed outside the method, but fields of class can be accessed from outside the class.

What are local variables in java?

All variables of the method are called local variables in java.

Where do local variables live in java?

Methods are pushed on stack so local variables live on the stack.

Local variables of the method are kept on the stack and are lost as soon as the method ends in java.

Where do object of local inner class live in java?

As object of local inner class live on the heap, objects may be alive even after method ends in which local inner class have been defined.

As, local variables of the method are kept on the stack and are lost as soon as the method ends, but even after the method ends, the local inner class object may still be alive on the heap.

What java docs says about “Local Inner class cannot access the non-final local variables but can access final local variables.”

A local class can only access local variables that are declared final in java. When a local class accesses a local variable or parameter of the enclosing block, it captures that variable or parameter in java.

JVM create a synthetic field inside the inner class in java -

As final variable will not change after initialization, when a inner class access final local variables compiler create a synthetic field inside the inner class and also copy that variable into the heap. So, these synthetic fields can be accessed inside local inner class even when execution of method is over in java.

You must be wondering, What are synthetic fields in java?

Synthetic fields are created by compiler and they actually doesn’t exist in source code in java.

The reason is that after the enclosing method returns, the local variable no longer exists. Therefore a copy of the variable is created when the anonymous class is instanciated. If Java allowed the local variable to be changed afterwards, the anonymous class would only know the old value.

簡單的說就是: JVM在內部類初始化的時候幫我們拷貝了一個局部變量的備份到內部類中,并且把它的值復制到了堆內存中(變量有兩份,同樣的名字,一個在局部變量中用,一個在內部類中)。所以要是不用final修飾,那你后面把外部類中的變量的值修改了,而內部類中拷貝的值還是原來的,那這樣豈不是兩邊的值不一樣了? 所以不能讓你改,必須加final。The solution was to required that captured variables are final (before JDK 8) or effectively final (since JDK 8), which means they cannot be assigned to.

Kotlin中的閉包

想要理解kotlin中閉包的實現,首先要懂kotlin中的一個概念:在Kotlin中,函數是“一等公民”。

對比一下java和kotlin更好理解:

java代碼:

public class TestJava{private void test(){private void test(){//錯誤,因為Java中不支持函數包含函數}}
}

在java中是不支持這種寫法的,因為函數是“二等公民”。

下面再看下kotlin代碼:

fun test(): () -> Unit {var a = 0return fun() {a++println(a)}
}fun main() {val t = test()t()
}

是不是發現了新世界的大門,內部函數很輕松地調用了外部變量a。

這只是一個最簡單的閉包實現。按照這種思想,其他的實現例如:函數、條件語句、Lambda表達式等等都可以理解為閉包,這里不再贅述。

不過萬變不離其宗,只要記得一句話:閉包就是能夠讀取其他函數內部變量的函數。就是一個函數A可以訪問另一個函數B的局部變量,即便另一個函數B執行完成了也沒關系。目前把滿足這樣條件的函數A叫做閉包。

內聯函數

剛被閉包搞蒙,這里又沒搞明白內聯函數到底是干什么? 有什么作用?Kotlin中的內聯函數其實顯得有點尷尬,因為它之所以被設計出來,主要是為了優化Kotlin支持Lambda表達式之后所帶來的開銷。

然而,在Java中我們似乎并不需要特別關注這個問題,因為在Java 7之后,JVM引入了一種叫做invokedynamic的技術,它會自動幫助我們做Lambda優化。但是為什么Kotlin要引入內聯函數這種手動的語法呢? 這主要還是因為Kotlin要兼容Java 6。

優化Lambda開銷

在Kotlin中每聲明一個Lambda表達式,就會在字節碼中產生一個匿名類(也就是說我們一直使用的Lambda表達式在底層被轉換成了匿名類的實現方式)。

該匿名類包含了一個invoke方法,作為Lambda的調用方法,每次調用的時候,還會創建一個新的匿名類對象。

可想而知,Lambda語法雖然簡潔,但是額外增加的開銷也不少。并且,如果Lambda捕捉了某個變量,那么每次調用的時候都會創建一個新的對象,這樣導致效率較低。

尤其對Kotlin這門語言來說,它當今優先要實現的目標,就是在Android這個平臺上提供良好的語言特性支持。Kotlin要在Android中引入Lambda語法,必須采用某種方法來優化Lambda帶來的額外開銷,也就是內聯函數。

1. invokedynamic

在講述內聯函數具體的語法之前,我們先來看看Java中是如何解決這個問題的。與Kotlin這種在編譯期通過硬編碼生成Lambda轉換類的機制不同,Java在SE 7之后通過invokedynamic技術實現了在運行期才產生相應的翻譯代碼。在invokedynamic被首次調用的時候,就會觸發產生一個匿名類來替換中間碼invokedynamic,后續的調用會直接采用這個匿名類的代碼。這種做法的好處主要體現在:

  • 由于具體的轉換實現是在運行時產生的,在字節碼中能看到的只有一個固定的invokedynamic,所以需要靜態生成的類的個數及字節碼大小都顯著減少。
  • 與編譯時寫死在字節碼中的策略不同,利用invokedynamic可以把實際的翻譯策略隱藏在JDK庫的實現, 這極大提高了靈活性,在確保向后兼容性的同時,后期可以繼續對編譯策略不斷優化升級
  • JVM天然支持了針對該方式的Lambda表達式的翻譯和優化,這也意味著開發者在書寫Lambda表達式的同時,可以完全不用關心這個問題,這極大地提升了開發的體驗。
2. 內聯函數

invokedynamic固然不錯,但Kotlin不支持它的理由似乎也很充分,我們有足夠的理由相信,其最大的原因是Kotlin在一開始就需要兼容Android最主流的Java版本SE 6,這導致它無法通過invovkedynamic來解決Android平臺的Lambda開銷問題。

因此,作為另一種主流的解決方案,Kotlin擁抱了內聯函數,在C++、C#等語言中也支持這種特性。

簡單的來說,我們可以用inline關鍵字來修飾函數,這些函數就稱為了內聯函數。他們的函數體在編譯期被嵌入每一個被調用的地方,以減少額外生成的匿名類數,以及函數執行的時間開銷。
所以內聯函數的工作原理并不復雜,就是Kotlin編譯器會將內斂函數中的代碼在編譯的時候自動替換到調用它的地方,這樣也就不存在運行時的開銷了。

所以如果你想在用Kotlin開發時獲得盡可能良好的性能支持,以及控制匿名類的生成數量,就有必要來學習下內聯函數的相關語法。

這里通過一個實際的例子,看看Kotlin的內聯函數是具體如何操作的:

fun main(args: Array<String>) {foo {println("dive into Kotlin...")}
}fun foo(block: () -> Unit) {println("before block")block()println("end block")
}

首先,我們聲明了一個高階函數foo,可以接受一個類型為() -> Unit的Lambda,然后在main函數中調用它。以下是通過字節碼反編譯的相關Java代碼:

public static final void main(@NotNull String[] args) {Intrinsics.checkParameterIsNotNull(args, "args");foo((Function0)null.INSTANCE);	        
}public static final void foo(@NotNull Function0 block) {Intrinsics.checkParameterIsNotNull(block, "block");String var1 = "before block";System.out.println(var1);block.invoke();var1 = "end block";System.out.println(var1);
}

據我們所知,調用foo就會產生一個Function()類型的block類,然后通過invovke方法來執行,這會增加額外的生成類和調用開銷。現在,我們給foo函數加上inline修飾符,如下:

inline fun foo(block: () -> Unit) {println("before block")block()println("end block")
}

再來看看相應的Java代碼:

public static final void main(@NotNull String[] args) {Intrinsics.checkParameterIsNotNull(args, "args");String va1 = "before block";System.out.println(var1);// block函數體在這里開始粘貼String var2 = "dive into Kotlin...";System.out.println(var2);// block函數體在這里結束粘貼var1 = "end block";System.out.println(var1);
}public static final void foo(@NotNull Function0 block) {Intrinsics.checkParameterIsNotNull(block, "block");String var2 = "before block";System.out.println(var2);block.invoke();var2 = "end block";System.out.println(var2);
}

果然,foo函數體代碼及被調用的Lambda代碼都粘貼到了相應調用的位置。試想下,如果這是一個工程中公共的方法,或者被嵌套在一個循環調用的邏輯體中,這個方法勢必會被調用很多次。通過inline的語法,我們可以徹底消除這種額外調用,從而節省了開銷。

內聯函數典型的一個應用場景就是Kotlin的集合類。如果你看過Kotlin的集合類API文檔或者源碼實現就會發現,集合函數式API,如map、filter都被定義成內聯函數,如:

inline fun <T, R> Array<out T>.map {transform: (T) -> R
}: List<R>inline fun <T> Array<out T>.filter {predicate: (T) -> Boolean
}: List<T>

這個很容易理解,由于這些方法都接收Lambda作為參數,同時都需要對集合元素進行遍歷操作,所以把相應的實現進行內聯無疑是非常適合的。

但是內聯函數不是萬能的,以下情況我們應避免使用內聯函數:

  • 由于JVM對普通的函數已經能夠根據實際情況智能地判斷是否進行內聯優化,所以我們并不需要對其使用Kotlin的inline語法,那只會讓字節碼變得更加復雜。
  • 盡量避免對具有大量函數體的函數進行內聯,這樣會導致過多的字節碼數量。
  • 一旦一個函數被定義為內聯函數,便不能獲取閉包類的私有成員,除非你把他們聲明為internal。
noinline: 避免參數被內聯

通過上面的例子我們已經知道,如果在一個函數的開頭加上inline修飾符,那么它的函數體及Lambda參數都會被內聯。然而現實中的情況比較復雜,有一種可能是函數需要接受多個參數,但我們只想對其中部分Lambda參數內聯,其他的則不內聯,這個又該如何處理?

解決這個問題也很簡單,Kotlin在引入inline的同時,也新增了noinline關鍵字,我們可以把它加在不想要被內聯的參數開頭,該參數便不會具有內聯的效果:

fun main(args: Array<String>) {foo ( {println("I am inlined...")	     	}, {println("I am not inlined...")})
}inline fun foo(block1: () -> Unit, noinline block2: () -> Unit) {println("before block")block1()block2()println("end block")
}

同樣的方法,再來看看反編譯的Java版本:

public static final void main(@NotNull String[] args) {Intrinsics.checkParameterIsNotNull(args, "args");Function0 block2$iv = (Function0)null.INSTANCE;String var2 = "before block";System.out.println(var2);// block1 被內聯了String var3 = "I am inlined...";System.out.println(var3);// block2 還是原樣block2$iv.invoke();System.out.println(var2);
}
public static final void foo(@NotNull Function0 block1, @NotNull Function0 block2) {Intrinsics.checkParameterIsNotNull(block1, "block1");Intrinsics.checkParameterIsNotNull(block2, "block2");String var3 = "before block";System.out.println(var3);block1.invoke();block2.invoke();var3 = "end block";System.out.println(var3);
}

可以看出,foo函數的block2參數在帶上noinline之后,反編譯后的Java代碼中并沒有將其函數體代碼在調用處進行替換。

非局部返回

Kotlin中的內聯函數除了優化Lambda開銷之外,還帶來了其他方面的特效,典型的就是非局部返回和具體化參數類型。我們先來看下Kotlin如何支持非局部返回。

以下是我們常見的局部返回的例子:

fun main(args: Array<String>) {foo()
}
fun localReturn() {return
}
fun foo() {println("before local return")localReturn()println("after local return")return
}
// 運行結果
before local return
after local return

正如我們所熟知的,localReturn執行后,其函數體中的return只會在該函數的局部生效,所以localReturn()之后的println函數依舊生效。我們再把這個函數換成Lambda表達式的版本:

fun main(args: Array<String>) {foo { return }
}
fun foo(returning: () -> Unit) {println("before local return")returning()println("after local return")return 
}
// 運行結果
Error:(2, 11)Kotlin: 'return' is not allowed here

這時,編譯器報錯了,就是說在Kotlin中,正常情況下Lambda表達式不允許存在return關鍵字。這時候,內聯函數又可以排上用場了。我們把foo進行內聯后再試試看:

fun main(args: Array<String>) {foo { return }
}
inline fun foo(returning: () -> Unit) {println("before local return")returning()println("after local return")return
}
// 運行結果
before local return 

編譯順利通過了,但結果與我們的局部返回效果不同,Lambda的return執行后直接讓foo函數退出了執行。如果你仔細考慮一下,可能很快就想出了原因。因為內聯函數foo的函數體及參數Lambda會直接替代具體的調用。所以實際產生的代碼中,retrurn相當于是直接暴露在main函數中,所以returning()之后的代碼自然不會執行,這個就是所謂的非局部返回。

使用標簽實現Lambda非局部返回

另外一種等效的方式,是通過標簽利用@符號來實現Lambda非局部返回。同樣以上的例子,我們可以在不聲明inline修飾符的情況下,這么做來實現相同的效果:

fun main(args: Array<String>) {foo { return@foo }
}
fun foo(returning: () -> Unit) {println("before local return")returning()println("after local return")return
}
// 運行結果
before local return

非局部返回尤其在循環控制中顯得特別有用,比如Kotlin的forEach接口,它接收的就是一個Lambda參數,由于它也是一個內聯函數,所以我們可以直接在它調用的Lambda中執行return退出上一層的程序。

fun hasZeros(list: List<Int>): Boolean {list.forEach {if (it == 0) return true // 直接返回foo函數結果}return false
}
為什么要設計noinline

這里我已經蒙了,前面已經說了內聯函數的好處,那為什么Kotlin還要提供一個noinline關鍵字來排除內聯功能呢?
這是因為內聯的函數類型參數在編譯的時候會被進行代碼替換,因此它沒有真正的參數屬性。
非內聯的函數類型參數可以自由地傳遞給其他任何函數,因為它就是一個真實的參數,而內聯的函數類型參數只允許傳遞給另外一個內聯函數,這也是它最大的局限性。
另外,內聯函數和非內聯函數還有一個重要的區別,那就是內聯函數所引用的Lambda表達式中是可以使用return關鍵字來進行函數返回的,而非內聯函數只能進行局部返回。為了說明這個問題,我們來看下面的例子:

fun printString(str: String, block: (String) -> Unit) {println("printString begin")block(str)println("printString end")
}fun main() {println("main start")val str = ""printString(str) { s ->println("lambda start")if (s.isEmpty()) return@printStringprintln(s)println("lambda end")}println("main end")
}

這里定義了一個叫作printString()的高階函數,用于在Lambda表達式中打印傳入的字符串參數。但是如果字符串參數為空,那么就不進行打印。

注意,Lambda表達式中是不允許直接使用return關鍵字的,這里使用了return@printString的寫法,表示進行局部返回,并且不再執行Lambda表達式的剩余部分代碼。現在我們就剛好傳入一個空的字符串參數,運行程序,打印結果如下:

main start
printString begin
lambda start
printString end
main end

可以看到,除了Lambda表達式中return@printString語句之后的代碼沒有打印,其他的日志是正常打印的,說明return@printString確實只能進行局部返回。但是如果我們將printString()函數聲明成一個內聯函數,那么情況就不一樣了,如下所示:

inline fun printString(str: String, block: (String) -> Unit) {println("printString begin")block(str)println("printString end")
}fun main() {println("main start")val str = ""printString(str) { s ->println("lambda start")if (s.isEmpty()) returnprintln(s)println("lambda end")}println("main end")
}

現在printString()函數變成了內聯函數,我們就可以在Lambda表達式中使用return關鍵字了。此時的return代表的是返回外層的調用函數,也就是main()函數,如果想不通為什么的話,可以回顧一下在上一小節中學習的內聯函數的代碼替換過程。現在重新運行一下程序,打印結果如下:

main start
printString begin 
lambda start

可以看到,不管是main()函數還是printString()函數,確實都在return關鍵字之后停止執行了,和我們所預期的結果一致。
將高階函數聲明成內聯函數是一種良好的編程習慣,事實上,絕大多數高階函數是可以直接聲明成內聯函數的,但是也有少部分例外的情況。觀察下面的代碼示例:

inline fun runRunnable(block: () -> Unit) {val runnable = Runnable {block()}runnable.run()
}

這段代碼在沒有加上inline關鍵字聲明的時候絕對是可以正常工作的,但是在加上inline關鍵字之后就會提示如下:

Image

這個錯誤出現的原因解釋起來可能會稍微有點復雜。首先,在runRunnable()函數中,我們創建了一個Runnable對象,并在Runnable的Lambda表達式中調用了傳入的函數類型參數。

而Lambda表達式在編譯的時候會被轉換成匿名類的實現方式,也就是說,上述代碼實際上是在匿名類中調用了傳入的函數類型參數。

而內聯函數所引用的Lambda表達式允許使用return關鍵字進行函數返回,但是由于我們是在匿名類中調用的函數類型參數,此時是不可能進行外層調用函數返回的,最多只能對匿名類中的函數調用進行返回,因此這里就提示了上述錯誤。
也就是說,如果我們在高階函數中創建了另外的Lambda或者匿名類的實現,并且在這些實現中調用函數類型參數,此時再將高階函數聲明成內聯函數,就一定會提示錯誤。

那么是不是在這種情況下就真的無法使用內聯函數了呢?也不是,比如借助crossinline關鍵字就可以很好地解決這個問題:

inline fun runRunnable(crossinline block: () -> Unit) {val runnable = Runnable {block()}runnable.run()
}

可以看到,這里在函數類型參數的前面加上了crossinline的聲明,代碼就可以正常編譯通過了。
那么這個crossinline關鍵字又是什么呢?前面我們已經分析過,之所以會提示上面所示的錯誤,就是因為內聯函數的Lambda表達式中允許使用return關鍵字,和高階函數的匿名類實現中不允許使用return關鍵字之間造成了沖突。而crossinline關鍵字就像一個契約,它用于保證在內聯函數的Lambda表達式中一定不會使用return關鍵字,這樣沖突就不存在了,問題也就巧妙地解決了。

聲明了crossinline之后,我們就無法在調用runRunnable函數時的Lambda表達式中使用return關鍵字進行函數返回了,但是仍然可以使用return@runRunnable的寫法進行局部返回。總體來說,除了在return關鍵字的使用上有所區別之外,crossinline保留了內聯函數的其他所有特性。

crossinline

值得注意的是,非局部返回雖然在某些場合下非常有用,但可能也存在危險。因為有時候,我們內聯的函數所接收的Lambda參數常常來自于上下文其他地方。為了避免帶有return的Lambda參數產生破壞,我們還可以使用crossinline關鍵字來修飾該參數,從而杜絕此類問題的發生。就像這樣子:

fun main(args: Array<String>) {foo { return }
}
inline fun foo(crossinline returning: () -> Unit) {println("before local return")returning()println("after local return")return
}
// 運行結果
Error: (2, 11) Kotlin: 'return' is not allowed here
具體化參數類型

除了非局部返回之外,內聯函數還可以幫助Kotlin實現具體化參數類型。

Kotlin與Java一樣,由于運行時的類型擦除,我們并不能直接獲取一個參數的類型。

然而,由于內聯函數會直接在字節碼中生成相應的函數體實現,這種情況下我們反而可以獲得參數的具體類型。我們可以用reified修飾符來實現這一效果。

fun main(args: Array<String>) {getType<Int>()
}
inline fun <reified T> getType() {print(T::class)
}
// 運行結果
class kotlin.Int

這個特性在Android開發中也格外有用。比如在Java中,當我們要調用startActivity時,通常需要把具體的目標視圖類作為一個參數。然而,在Kotlin中,我們可以用reified來進行簡化:

inline fun <refied T : Activity> Activity.startActivity() {startActivity(Intent(this, T::class.java))
}

這樣,我們進行視圖導航就非常容易了,如:

startActivity<DetailActivity>()

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/82459.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/82459.shtml
英文地址,請注明出處:http://en.pswp.cn/web/82459.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

飛算JavaAI 炫技賽重磅回歸!用智能編碼攻克老項目重構難題

深夜還在排查十年前Hibernate框架埋下的N1查詢隱患&#xff1f;跨語言遷移時發現SpringMVC控制器里的業務邏輯像一團亂麻&#xff1f;當企業數字化進入深水區&#xff0c;百萬行代碼的老系統就像一座隨時可能崩塌的"技術債冰山"。近日&#xff0c;飛算科技發布JavaAI…

Linux I2C 子系統全解:結構、機制與工程實戰

Linux I2C 子系統全解&#xff1a;結構、機制與工程實戰 前言 I2C&#xff08;Inter-Integrated Circuit&#xff09;作為嵌入式系統和各種電子產品中最常用的串行通信總線之一&#xff0c;在 Linux 內核中的地位極其重要。然而&#xff0c;Linux I2C 子系統的分層結構、對象模…

多線程編程技術解析及示例:pthread_cond_timedwait、pthread_mutex_lock 和 pthread_mutex_trylock

多線程編程技術解析及示例&#xff1a;pthread_cond_timedwait、pthread_mutex_lock 和 pthread_mutex_trylock 摘要 本文深入解析了多線程編程中 pthread_cond_timedwait、pthread_mutex_lock 和 pthread_mutex_trylock 三個函數的功能、使用場景及注意事項&#xff0c;并通…

元胞自動機(Cellular Automata, CA)

一、什么是元胞自動機&#xff08;Cellular Automata, CA&#xff09; 元胞自動機&#xff08;CA&#xff09; 是一種基于離散時間、離散空間與規則驅動演化的動力系統&#xff0c;由 馮諾依曼&#xff08;John von Neumann&#xff09; 于1940年代首次提出&#xff0c;用于模…

Flutter面試題

Flutter架構解析 1. Flutter 是什么?它與其他移動開發框架有什么不同? Flutter 是 Google 開發的開源移動應用開發框架,可用于快速構建高性能、高保真的移動應用(iOS 和 Android),也支持 Web、桌面和嵌入式設備。。它與其他移動開發框架(如 React Native、Xamarin、原…

MySQL 如何判斷某個表中是否存在某個字段

在MySQL中&#xff0c;判斷某個表中是否存在某個字段&#xff0c;可以通過查詢系統數據庫 INFORMATION_SCHEMA.COLUMNS 實現。以下是詳細步驟和示例&#xff1a; 方法&#xff1a;使用 INFORMATION_SCHEMA.COLUMNS 通過查詢系統元數據表 COLUMNS&#xff0c;檢查目標字段是否存…

golang 實現基于redis的并行流量控制(計數鎖)

在業務開發中&#xff0c;有時需要對某個操作在整個集群中限制并發度&#xff0c;例如限制大模型對話的并行數。基于redis zset實現計數鎖&#xff0c;做個筆記。 關鍵詞&#xff1a;并行流量控制、計數鎖 package redisutilimport ("context""fmt""…

從線性方程組角度理解公式 s=n?r(3E?A)

從線性方程組角度理解公式 sn?r(3E?A) 這個公式本質上是 ?齊次線性方程組解空間維度 的直接體現。下面通過三個關鍵步驟解釋其在線性方程組中的含義&#xff1a; 1. ?公式對應的線性方程組 考慮矩陣方程&#xff1a; (3E?A)x0 其中&#xff1a; x 是 n 維未知向量3E?…

Docker 在 AI 開發中的實踐:GPU 支持與深度學習環境的容器化

人工智能(AI)和機器學習(ML),特別是深度學習,正以前所未有的速度發展。然而,AI 模型的開發和部署并非易事。開發者常常面臨復雜的依賴管理(如 Python 版本、TensorFlow/PyTorch 版本、CUDA、cuDNN)、異構硬件(CPU 和 GPU)支持以及環境復現困難等痛點。這些挑戰嚴重阻…

解決CSDN等網站訪問不了的問題

原文網址&#xff1a;解決CSDN等網站訪問不了的問題-CSDN博客 簡介 本文介紹解決CSDN等網站訪問不了的方法。 問題描述 CSDN訪問不了了&#xff0c;頁面是空的。 問題解決 方案1&#xff1a;修改DNS 可能是dns的問題&#xff0c;需要重新配置。 國內常用的dns是&#x…

使用tortoisegit連接遠程倉庫進行克隆、拉取、獲取、提交、推送、新建/切換分支、重命名、刪除的一套流程(附帶巨全面的git命令)

1.整備好tortoisegit工具。 2.新建一個文件夾&#xff0c;并進入這個文件夾后鼠標右擊&#xff08;選擇克隆&#xff09;&#xff1a; 3.先去項目中拿到https地址&#xff0c;再填入&#xff1a; 4.新建分支&#xff0c;右擊克隆到本地的項目文件&#xff1a; 5.推送到遠程&am…

ArcGIS Pro 3.4 二次開發 - 地圖創作 1

環境:ArcGIS Pro SDK 3.4 + .NET 8 文章目錄 ArcGIS Pro 3.4 二次開發 - 地圖創作 11 樣式管理1.1 如何通過名稱獲取項目中的樣式1.2 如何創建新樣式1.3 如何向項目添加樣式1.4 如何從項目中移除樣式1.5 如何向樣式添加樣式項1.6 如何從樣式中移除樣式項1.7 如何判斷樣式是否可…

Express 集成Sequelize+Sqlite3 默認開啟WAL 進程間通信 Conf 打包成可執行 exe 文件

代碼&#xff1a;express-exe: 將Express開發的js打包成exe服務丟給客戶端使用 實現目標 Express 集成 Sequelize 操作 Sqlite3 數據庫&#xff1b; 啟動 Sqlite3 時默認開啟 WAL 模式&#xff0c;避免讀寫互鎖&#xff0c;支持并發讀&#xff1b; 利用 Conf 實現主進程與 Ex…

.Net Framework 4/C# 初識 C#

一、C# 專欄 由于博主原先是做的Linux C/C 嵌入式領域&#xff0c;因此對 C# 也較為懵懂&#xff0c;C# 是典型的 OOP 編程&#xff0c;這一點與 C 類似&#xff0c;但是在語法上&#xff0c;C# 移除了對指針的運用以及內存管理&#xff0c;所以既不用考慮指針的復雜運用也不用…

Python趣學篇:Pygame實現粒子煙花綻放效果

名人說:路漫漫其修遠兮,吾將上下而求索。—— 屈原《離騷》 創作者:Code_流蘇(CSDN)(一個喜歡古詩詞和編程的Coder??) 專欄介紹:《Python星球日記》?? 目錄 一、項目亮點與效果預覽1. 核心特色功能2. 技術學習價值二、技術原理深度解析1. 向量運算:煙花運動的數學基…

NiceGUI 是一個基于 Python 的現代 Web 應用框架

NiceGUI 是一個基于 Python 的現代 Web 應用框架&#xff0c;它允許開發者直接使用 Python 構建交互式 Web 界面&#xff0c;而無需編寫前端代碼。以下是 NiceGUI 的主要功能和特點&#xff1a; 核心功能 1.簡單易用的 UI 組件 提供按鈕、文本框、下拉菜單、滑塊、圖表等常見…

Linux中的mysql邏輯備份與恢復

一、安裝mysql社區服務 二、數據庫的介紹 三、備份類型和備份工具 一、安裝mysql社區服務 這是小編自己寫的&#xff0c;沒有安裝的去看看 Linux換源以及yum安裝nginx和mysql-CSDN博客 二、數據庫的介紹 2.1 數據庫的組成 數據庫是一堆物理文件的集合&#xff0c;主要包括…

鴻蒙UI開發——組件的自適應拉伸

1、概 述 針對常見的開發場景&#xff0c;ArkUI開發框架提供了非常多的自適應布局能力&#xff0c;這些布局可以獨立使用&#xff0c;也可多種布局疊加使用。本文針對ArkUI提供的拉伸能力做簡單討論。 拉伸能力是指容器組件尺寸發生變化時&#xff0c;增加或減小的空間全部分…

K 值選對,準確率翻倍:KNN 算法調參的黃金法則

目錄 一、背景介紹 二、KNN 算法原理 2.1 核心思想 2.2 距離度量方法 2.3 算法流程 2.4算法結構&#xff1a; 三、KNN 算法代碼實現 3.1 基于 Scikit-learn 的簡單實現 3.2 手動實現 KNN&#xff08;自定義代碼&#xff09; 四、K 值選擇與可視化分析 4.1 K 值對分類…

Azure DevOps Server 2022.2 補丁(Patch 5)

微軟Azure DevOps Server的產品組在4月8日發布了2022.2 的第5個補丁。下載路徑為&#xff1a;https://aka.ms/devops2022.2patch5 這個補丁的主要功能是修改了代理(Agent)二進制安裝文件的下載路徑&#xff1b;之前&#xff0c;微軟使用這個CND(域名為vstsagentpackage.azuree…