你瞭解Kotlin的let,with,run,apply,also作用域函數的區別嗎?

前言: 成和敗要努力嘗試,人若有志應該不怕遲。

一、概述

  Kotlin提供了不少比Java高級的語法,在Kotlin標準庫中(Standard.kt)提供了一些Kotlin拓展的內置函數,可以優化編碼,比如let,with,run,apply,also等。Standard.kt是Kotlin庫的一部分,它定義了一些基本函數,是一種使代碼更簡潔的方法,功能非常強大。下面會詳細講解,同時也涉及到takeIftakeUnless函數。

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函數其實就是 letwith兩個函數的結合體,準確來說它彌補了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),返回值是對象本身。對於沒有返回值並且主要對接收方對象的成員進行操作的代碼塊使用applyapply函數的常見情況是對象配置,這樣的調用可以理解爲“對對象的應用以下賦值”。

(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

  除了作用域函數外,標準庫還提供了takeIftakeUnless函數,這些函數允許你在調用鏈中嵌入對對象狀態的檢查。

在提供某條件的對象上調用,如果與某條件匹配,則takeIf返回該對象,否則它返回nulltakeIf是針對單個對象的過濾函數。反過來,如果不匹配某條件,則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函數與作用域函數

takeIftakeUnless函數與作用域函數一起使用特別有用,當在takeIftakeUnless之後鏈接其他函數,必須執行空檢查或者安全調用(?.),因爲它們的返回值可能爲空(null)。

比如將它們和let函數鏈接起來,以便在匹配給定某條件的對象上運行代碼塊,所以在對象上調用takeIf或者takeUnless,需要使用安全調用(?.)調用let,對於不匹配某條件的對象,返回nulllet函數不被調用。

	//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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章