前言: 成和敗要努力嘗試,人若有志應該不怕遲。
一、概述
Kotlin提供了不少比Java高級的語法,在Kotlin標準庫中(Standard.kt)提供了一些Kotlin拓展的內置函數,可以優化編碼,比如let,with,run,apply,also等。Standard.kt是Kotlin庫的一部分,它定義了一些基本函數,是一種使代碼更簡潔的方法,功能非常強大。下面會詳細講解,同時也涉及到takeIf
和takeUnless
函數。
1.1 Kotlin回調函數的優化
Kotlin中對Java一些接口的回調做了優化,可以使用lambda函數來替代,可以簡化代碼和一些不必要的嵌套回調方法。但是注意:在lambda表達式,只支持單抽象方法模型,也就是說設計的接口裏面只有一個抽象方法,才符合lambda表達式的規則,多個回調方法不支持。
- 1、用Java代碼實現一個接口回調
mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//TODO
}
});
- 2、在Kotlin中實現一個接口的回調,不使用lambda表達式(這種方法非常適用於Kotlin中一個接口有多個回調方法)
mView.setOnClickListener(object : View.OnClickListener {
override fun onClick(view: View?) {
//TODO
}
})
- 3、如果在Kotlin中接口的回調方法只有一個,那麼就符合使用lambda函數,我們可以把以上代碼簡化:
mView.setOnClickListener({
view: View? ->
//TODO
})
//再進一步簡化,可以把View?直接直接省略
mView.setOnClickListener({
view ->
//TODO
})
- 4、如果上面的參數view沒有使用到的話,可以直接把view去掉:
mView.setOnClickListener({
//TODO
})
- 5、上面的代碼還可以做進一步的調整,如果
setOnClickListener()
函數的最後一個參數是一個函數的話,可以把函數{}的實現提到圓括號()外面:
mView.setOnClickListener() {
//TODO
}
- 6、如果
setOnClickListener()
函數只有一個參數的話,則可以直接省略圓括號():
mView.setOnClickListener {
//TODO
}
經過層層簡化最終可以寫成 mView.setOnClickListener { //TODO }
這樣的簡潔模式。但是注意了, 這種簡化模式支持接口裏面只有一個回調方法,多個回調方法不支持。
二、作用域函數let,with,run,apply,also詳解
2.1 let函數
let
函數實際是一個作用域函數,當你需要定義一個變量在一個特定的作用域範圍內,let
函數是一個不錯的選擇,它的另一個作用是避免寫一些判斷null
的操作。
(1)let函數的底層
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
表示以this
值作爲參數調用指定的函數[block]
並返回結果。let
函數的底層是inline拓展函數+lambda結構模式,從結構來看它只有一個lambda函數塊[block]
作爲參數的函數,調用T類型對象的let函數,則該對象爲該函數的參數。在代碼塊內可以通過it
替代該對象。返回值爲函數塊的最後一行。
(2)let函數的一般使用語法
//1.在函數體內使用it替代object對象去訪問其公有的屬性和方法
object.let{
it.todo()//it即表示object,通過it可以操作對象的相關方法
...
}
//2.判斷object爲null的操作
object?.let{//表示object不爲null的條件下,纔會去執行let函數體
it.todo()
···
}
(3)let函數的Kotlin和Java同等含義轉化
//Kotlin
private fun letForKotlin() {
val result = "HelloWord".let {
Log.e(TAG, "let == length:" + it.length)
1818
}
Log.e(TAG, "let == length:" + result)
}
//Java
private void letForJava() {
String str = "HelloWord";
Log.e(TAG, "let == length:" + str.length());
int result = 1818;
Log.e(TAG, "let == result:" + result);
}
上面的Kotlin和Java兩種寫法所表示的意義和結果是一樣的,打印log如下:
(4)let函數的使用場景
- 場景A:處理一個可爲
null
的對象,統一做空判斷處理; - 場景B:需要去明確一個變量所處的特定作用域範圍內可使用。
(5)Kotlin中使用let函數的前後對比
我們經常使用某個對象,每次都使用該對象做空判斷處理然後操作相關方法,這樣看起來不夠優雅,如下:
//let函數優化前
mTextView?.setLines(2)
mTextView?.setText("HelloWord")
mTextView?.setOnClickListener(this)
mTextView?.setTextColor(ContextCompat.getColor(this, R.color.colorAccent))
使用let
函數優化後:
//let函數優化後
mTextView?.let {
it.setLines(2)
it.setText("HelloWord")
it.setOnClickListener(this)
it.setTextColor(ContextCompat.getColor(this, R.color.colorAccent))
}
這樣使用let
優化後代碼就相對美觀很多了,上面提到let
函數裏面的it
表示mTextView
對象。
2.2 with函數
一個非拓展函數,上下文對象作爲參數傳遞,但是在lambda內部,它作爲[receiver] (this)
可用,返回值是lambda結果。當你需要的一個對象在一個特定的作用域範圍內多次使用到其方法時,可以省去對象名,直接訪問對象的公有屬性和方法。
(1)with函數的底層
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
表示使用給定的[receiver]
作爲接收方調用指定的函數[block]
並返回結果。with
函數與前面的函數略有不同,因爲它不是以拓展的形式存在的。with
接收了兩個參數,分別爲T類型的對象receiver和一個lambda函數塊。在函數塊內可以通過this
指定該對象,返回值爲函數的最後一行。
(2)with函數的一般使用語法
with(object){
//函數塊內的this表示傳入的object對象,同時可以直接調用該對象的方法
//todo
}
(3)with()函數的Kotlin和Java同等含義轉化
//Kotlin
private fun withForKotlin() {
var user = User("張三", 27, "男")
val result = with(user, {
"姓名:" + name + ", 年齡:" + age + ", 性別:" + sex
})
Log.e(TAG, "with == " + result)
}
//Java
private void withForJava() {
User user = new User("張三", 27, "男");
String result = "姓名:" + user.getName() + ", 年齡:" + user.getAge() + ", 性別:" + user.getSex();
Log.e(TAG, "with == " + result);
}
上面的Kotlin和Java兩種寫法所表示的意義和結果是一樣的,打印數據如下:
(4)with函數的使用場景
適用於同一個對象的多個方法時,可以省去類名重複,直接調用類的方法即可。
- 場景A:建議在不提供lambda結果的情況下調用上下文對象上的函數,在代碼中,
with
可以理解爲“使用這個對象,執行以下的操作”。
val list = mutableListOf("one", "two", "three")
with(list) {
Log.e(TAG, "with == argument:" + this + ", 列表長度爲:" + size)
}
打印數據如下:
- 場景B:
with()
函數的另一個用例是引入一個helper對象,它的屬性或者函數將用於計算值。
val list = mutableListOf("one", "two", "three")
val result = with(list) {
"list第一個參數:" + first() + ", 最後一個參數是:" + last()
}
Log.e(TAG, "with == " + result)
打印數據如下:
(5)Kotlin中使用with函數的前後對比
在RecyclerView中adapter的onBindItemHolder()
方法中,數據model映射到UI上面,比較適合適用with()
函數。
override fun onBindItemHolder(holder: SuperViewHolder?, position: Int) {
val user = mDataList[position] ?: return
val name = user.getName()
val age = user.getAge()
val sex = user.getSex()
holder?.tv_name.text = name
holder?.tv_age.text = age
holder?.tv_sex.text = sex
}
使用with
函數優化後:
override fun onBindItemHolder(holder: SuperViewHolder?, position: Int) {
val user: User = mDataList[position] ?: return
with(user) {
holder?.tv_name.text = name
holder?.tv_age.text = age
holder?.tv_sex.text = sex
}
}
2.3 run函數
run
函數上下文對象可以用作接收方[receiver] (this)
,返回值是lambda結果。與with
執行相同的操作,但是作爲上下文對象的拓展函數調用let
函數。當lambda包含對象初始化和返回值的計算時,run
非常有用。除了調用在[receiver]
對象上運行之外,還可以用作非拓展函數。非拓展運行在需要表達的地方執行多個語句塊。
(1)run函數的底層
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
表示以this
值作爲接收方[receiver]
調用指定的函數[block]
並返回其結果。底層是inline拓展函數+lambda結構模式,從結構來看它只有一個lambda函數塊[block]
作爲參數的函數,調用T類型對象的run
函數。它只接收一個lambda函數爲參數,以閉包的形式返回,返回值是最後一行。
(2)run函數的一般使用語法
object.run{
//函數塊內的this表示object對象,同時可以直接調用該對象的公有屬性和方法
//todo
}
(3)run函數的Kotlin和Java同等含義轉化
//Kotlin
private fun runForKotlin() {
var user = User("李思思", 18, "女")
val result = user?.run {
val userStr = "姓名:" + name + ", 年齡:" + age + ", 性別:" + sex
Log.e(TAG, "run == user:" + userStr)
1919
}
Log.e(TAG, "run == result:" + result)
}
//Java
private void runForJava() {
User user = new User("李思思", 18, "女");
String userStr = "姓名:" + user.getName() + ", 年齡:" + user.getAge() + ", 性別:" + user.getSex();
Log.e(TAG, "run == user:" + userStr);
int result = 1919;
Log.e(TAG, "run == result:" + result);
}
上面的Kotlin和Java兩種寫法所表示的意義和結果是一樣的,打印log如下:
(4)run函數的使用場景
適用於let
函數和with
函數的任何場景。run
函數其實就是 let
和with
兩個函數的結合體,準確來說它彌補了let
函數在函數內必須適用it
參數替代對象;另一方面它彌補了with
函數傳入對象判空問題。所以run
函數可以像with
函數那樣省略對象參數直接訪問對象的公有屬性和方法,同時像let
函數那樣對對象做空判斷處理。
(5)Kotlin中使用run函數的前後對比
在RecyclerView中adapter的onBindItemHolder()
方法中,獲取數據先空判斷item數據,然後再獲取具體數據:
override fun onBindItemHolder(holder: SuperViewHolder?, position: Int) {
val user: User = mDataList[position] ?: return
with(user) {
holder?.tv_name.text = name
holder?.tv_age.text = age
holder?.tv_sex.text = sex
}
}
使用run
函數優化後:
override fun onBindItemHolder(holder: SuperViewHolder?, position: Int) {
mDataList[position]?.run {
holder?.tv_name.text = name
holder?.tv_age.text = age
holder?.tv_sex.text = sex
}
}
這樣就可以先做item數據對象空判斷處理,再直接引用對象的公有屬性和方法。
2.4 apply函數
上下文對象可用作接收者(this),返回值是對象本身。對於沒有返回值並且主要對接收方對象的成員進行操作的代碼塊使用apply
,apply
函數的常見情況是對象配置,這樣的調用可以理解爲“對對象的應用以下賦值”。
(1)apply函數的底層
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
以this
值作爲接收方[receiver]
調用指定函數[block]
並且返回this
值。底層是inline拓展函數+lambda結構模式,從結構來看apply
函數和run
函數很像,只有一個lambda函數塊[block]
作爲參數的函數,調用T類型對象的apply
函數,它只接收一個lambda函數爲參數,返回值是對象本身。
(2)apply函數的一般使用語法
object.apply{
//類似run函數,不同的是返回值爲對象本身,即object
//todo
}
(3)apply函數的Kotlin和Java同等含義轉化
//Kotlin
private fun applyForKotlin() {
val list = mutableListOf("one", "two", "three")
val result = list.apply {
val value = "apply == list第一個參數::" + first() + ", 列表長度爲:" + size
Log.e(TAG, value )
2020
}
Log.e(TAG, "apply == list:" + result)
}
//Java
private void applyForJava() {
List<String> list = new ArrayList<>();
list.add("one");
list.add("two");
list.add("three");
val i = 2020
String value = "apply == list第一個參數:" + list.get(0) + ", 列表長度爲:" + list.size();
Log.e(TAG, value);
String result = "apply == list:" + list;
Log.e(TAG, result);
}
上面的Kotlin和Java兩種寫法所表示的意義和結果是一樣的,打印log如下:
(4)apply函數的使用場景
apply
函數整體上和run
函數相似,唯一不同就是它的返回值是對象本身。apply
函數一般用於對象實例初始化的時候,需要對對象中的屬性進行賦值;或者動態inflate一個View的時候需要給View綁定數據。
(5)Kotlin中使用apply函數的前後對比
mHeadView = View.inflate(activity, R.layout.head_task_view, null)
mHeadView?.tv_name?.text = "姓名XXX"
mHeadView?.tv_age?.text = "20"
mHeadView?.tv_sex?.text = "女"
mHeadView?.tv_name?.setOnClickListener(this@KExampleActivity)
mAdpter.addHeadView(mHeadView)
使用apply
函數優化後:
mHeadView = View.inflate(activity, R.layout.head_task_view, null).apply {
tv_name?.text = "姓名XXX"
tv_age?.text = "20"
tv_sex?.text = "女"
tv_name?.setOnClickListener(this@KExampleActivity)
}
mAdpter.addHeadView(mHeadView)
apply
函數通過將接收方[receiver] this作爲返回值,可以輕鬆地將apply包含到調用鏈中,以便進行更復雜的處理。
2.5 also函數
上下文對象可以作爲參數it
使用,返回值是對象本身。也適用於執行一些將上下文作爲參數對象的操作,也可用於需要引用對象而不是引用對象的屬性和函數的操作,或者當你不想從外部作用域隱藏該引用時。你可以理解爲“並對該對象執行以下操作”。
(1)also函數的底層
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
表示以this
值作爲接受方[receiver]
調用指定的函數[block]
並返回結果。底層是inline拓展函數+lambda結構模式,從結構來看它只有一個lambda函數塊[block]
作爲參數的函數,調用T類型對象的also
函數。返回值是傳入對象本身this
。
(2)also函數的一般使用語法
object.also{
//與let函數類似,返回值爲object對象本身
//todo
}
(3)also函數的Kotlin和Java同等含義轉化
//Kotlin
private fun alsoForKotlin() {
val result = "HelloWord".also {
Log.e(TAG, "also == length:" + it.length)
2121
}
Log.e(TAG, "also == result:" + result)
}
//Java
private void alsoForJava() {
String result = "HelloWord";
Log.e(TAG, "also == length:" + result.length());
int i = 2121;
Log.e(TAG, "also == result:" + result);
}
上面的Kotlin和Java兩種寫法所表示的意義和結果是一樣的,打印log如下:
(4)also函數的使用場景
適用於let
函數的任何場景,與let
函數不同的是let
函數以閉包的形式返回函數塊最後一行的值,如果最後一行值爲空則返回一個Unit類型的默認值,而also
函數返回的是傳入對象本身。同時可以對傳入的對象進行操作,一般用於多個拓展函數的鏈式調用。
(5)Kotlin中使用also函數的前後對比
在遍歷List元素後再添加一個子元素
val list = mutableListOf("one", "two", "three")
for (i in list.indices) {
Log.e(TAG, "apply == element:" + list[i])
}
list.add("four")
also
函數優化後:
val list = mutableListOf("one", "two", "three")
list.also {
for (i in it.indices) {
Log.e(TAG, "apply == element:" + it[i])
}
}.add("four")
三、總結及其他用法
3.1 作用域函數總結
爲了更好理解和選擇函數的正確範圍,我們用表格總結一下:
函數 | 函數塊對象引用 | 返回值 | 是否拓展函數 | 使用場景 |
---|---|---|---|---|
let | it | Lambda表達式結果 | 是 | 1.適用於處理不爲null的操作場景; 2.明確一個變量所處的特定作用域範圍內可使用。 |
with | this | Lambda表達式結果 | 否(上下文對象作爲參數) | 適用於同一個對象的公有屬性和函數調用。 |
run | this | Lambda表達式結果 | 是 | 適用於let函數和with函數的任何場景。對對象中的屬性進行賦值和計算結果; 或者在需要表達式的地方運行語句。 |
無 | 否(調用時沒有上下文對象) | |||
apply | this | 返回this (對象本身) |
是 | 1.一般用於對象實例初始化的時候,需要對對象中的屬性進行賦值; 2.動態inflate一個View的時候需要給View綁定數據。 |
also | it | 返回this (對象本身) |
是 | 適用於let函數的任何場景,對傳入的對象進行操作, 一般用於多個拓展函數的鏈式調用。 |
總的來說,不同函數的功能相互重疊,可以根據實際情況來使用作用域函數。儘管作用域函數是一種使代碼更簡潔的方法,但是要避免過度使用,它會降低代碼的可讀性並導致錯誤。避免嵌套作用域函數,在鏈式調用時要注意,當前上下文對象和this
或者it
的值。
3.2 takeIf和takeUnless
除了作用域函數外,標準庫還提供了takeIf
和takeUnless
函數,這些函數允許你在調用鏈中嵌入對對象狀態的檢查。
在提供某條件的對象上調用,如果與某條件匹配,則takeIf
返回該對象,否則它返回null
,takeIf
是針對單個對象的過濾函數。反過來,如果不匹配某條件,則takeUnless
返回對象,如果匹配則返回null,對象可以作爲lambda參數(it)使用。
(1)takeIf和takeUnless函數的底層
@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
}
這是takeIf
函數源碼,表示如果滿足給定條件,則返回this
值(對象本身),如果不滿足則返回null
。
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
contract {
callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
}
return if (!predicate(this)) this else null
}
這是takeUnless
函數源碼,表示如果不滿足給定條件,則返回this
值(對象本身),如果滿足則返回null
。
takeIf
函數和takeUnless
函數的底層都是inline拓展函數+lambda結構模式,從結構來看它只有一個lambda函數塊[predicate]
作爲參數的函數,函數塊內返回值類型必須爲Boolean類型。調用T類型對象的takeIf
或者takeUnless
函數,該對象爲該函數的參數。
(2)takeIf和takeUnless函數的一般使用語法
object.takeIf{
//函數體是Boolean類型,條件成立返回number,不成立返回null
//todo
}
object.takeUnless{
//函數體是Boolean類型,條件成立返回null,不成立返回number
//todo
}
(3)takeIf和takeUnless函數的Kotlin和Java同等含義轉化
- takeIf
//Kotlin
private fun takeIfForKotlin(number: Int) {
val result = number.takeIf {//條件成立返回number,不成立返回null
it > 0
}
Log.e(TAG, "takeIf == result:" + result)
}
takeIfForKotlin(2222)//調用
//Java
private void takeIfForJava(int number) {
Integer result;
if (number > 0) {
result = number;
} else {
result = null;
}
Log.e(TAG, "takeIf == result:" + result);
}
takeIfForJava(2222);//調用
上面takeIf
函數的Kotlin和Java兩種寫法所表示的意義和結果是一樣的,打印log如下:
- takeUnless
//Kotlin
private fun takeUnlessForKotlin(number: Int) {
val result = number.takeUnless {
//條件成立返回null,不成立返回number
it > 0
}
Log.e(TAG, "takeIf == result:" + result)
}
takeUnlessForKotlin(2323)//調用
//Java
private void takeUnlessForJava(int number) {
Integer result = number > 0 ? null : number;
Log.e(TAG, "takeUnless == result:" + result);
}
takeUnlessForJava(2323);//調用
上面takeUnless
函數的Kotlin和Java兩種寫法所表示的意義和結果是一樣的,打印log如下:
(4)takeIf和takeUnless函數與作用域函數
takeIf
和takeUnless
函數與作用域函數一起使用特別有用,當在takeIf
和takeUnless
之後鏈接其他函數,必須執行空檢查或者安全調用(?.),因爲它們的返回值可能爲空(null)。
比如將它們和let
函數鏈接起來,以便在匹配給定某條件的對象上運行代碼塊,所以在對象上調用takeIf
或者takeUnless
,需要使用安全調用(?.)調用let
,對於不匹配某條件的對象,返回null
,let
函數不被調用。
//Kotlin
private fun synForKotlin(str: String) {
str.takeIf { !it.isNullOrEmpty() }?.let {
Log.e(TAG, "syn == result:" + it.toUpperCase())
}
}
//調用
synForKotlin("HelloWord")
synForKotlin("")
//Java
private void synForJava(String str) {
if (!TextUtils.isEmpty(str)) {
Log.e(TAG, "syn == result:" + str.toUpperCase());
}
}
//調用
synForJava("HelloWord");
synForJava("");
上面Kotlin和Java兩種寫法所表示的意義和結果是一樣的,打印log如下:
總之一句話:takeIf
表示如果滿足給定條件,則返回this值(對象本身),如果不滿足則返回null。takeUnless
表示如果不滿足給定條件,則返回this值(對象本身),如果滿足則返回null。兩個正好相反。
至此!本文結束。
源碼地址:https://github.com/FollowExcellence/KotlinDemo-master