ตัวอย่างเวลาที่เราควรใช้ run, let, apply และกับบน Kotlin


คำตอบ:


122

ฟังก์ชันทั้งหมดนี้ใช้สำหรับการสลับขอบเขตของฟังก์ชันปัจจุบัน / ตัวแปร ใช้เพื่อเก็บสิ่งของที่อยู่ร่วมกันในที่เดียว (ส่วนใหญ่เป็นการเริ่มต้น)

นี่คือตัวอย่างบางส่วน:

run - ส่งคืนทุกสิ่งที่คุณต้องการและกำหนดขอบเขตตัวแปรที่ใช้ในการใหม่ this

val password: Password = PasswordGenerator().run {
       seed = "someString"
       hash = {s -> someHash(s)}
       hashRepetitions = 1000

       generate()
   }

เครื่องกำเนิดไฟฟ้ารหัสผ่าน rescoped ในขณะนี้thisและเราสามารถตั้งค่าดังนั้นseed, hashและhashRepetitionsโดยไม่ต้องใช้ตัวแปร จะกลับมาตัวอย่างของgenerate()Password

applyคล้ายกัน แต่จะกลับมาthis:

val generator = PasswordGenerator().apply {
       seed = "someString"
       hash = {s -> someHash(s)}
       hashRepetitions = 1000
   }
val pasword = generator.generate()

สิ่งนี้มีประโยชน์อย่างยิ่งเมื่อใช้แทนรูปแบบ Builder และหากคุณต้องการใช้การกำหนดค่าบางอย่างซ้ำ

let- ส่วนใหญ่ใช้เพื่อหลีกเลี่ยงการตรวจสอบโมฆะ แต่ยังสามารถใช้แทนrunไฟล์. ความแตกต่างคือthisจะยังคงเหมือนเดิมและคุณเข้าถึงตัวแปร re-scoped โดยใช้it:

val fruitBasket = ...

apple?.let {
  println("adding a ${it.color} apple!")
  fruitBasket.add(it)
}

โค้ดด้านบนจะเพิ่มแอปเปิ้ลลงในตะกร้าก็ต่อเมื่อไม่เป็นโมฆะ โปรดสังเกตด้วยว่าitตอนนี้ไม่ใช่ทางเลือกอีกต่อไปดังนั้นคุณจะไม่พบ NullPointerException ที่นี่ (aka. คุณไม่จำเป็นต้องใช้?.เพื่อเข้าถึงแอตทริบิวต์)

also- ใช้เมื่อต้องการใช้applyแต่ไม่ต้องการเงาthis

class FruitBasket {
    private var weight = 0

    fun addFrom(appleTree: AppleTree) {
        val apple = appleTree.pick().also { apple ->
            this.weight += apple.weight
            add(apple)
        }
        ...
    }
    ...
    fun add(fruit: Fruit) = ...
}

การใช้applyที่นี่จะทำให้เกิดเงาthisดังนั้นจึงthis.weightหมายถึงแอปเปิ้ลไม่ใช่ตะกร้าผลไม้


หมายเหตุ: ฉันหยิบตัวอย่างจากบล็อกของฉันอย่างไร้ยางอาย


3
สำหรับใครก็ตามเช่นฉันที่ตกใจกับรหัสแรกบรรทัดสุดท้ายของแลมบ์ดาถือเป็นค่าตอบแทนใน Kotlin
Jay Lee

62

มีบทความอื่น ๆ เช่นที่นี่และที่นี่มีค่าที่จะดู

ฉันคิดว่ามันขึ้นอยู่กับเวลาที่คุณต้องการสั้นกระชับมากขึ้นภายในสองสามบรรทัดและเพื่อหลีกเลี่ยงการตรวจสอบคำสั่งที่แตกแขนงหรือเงื่อนไข (เช่นถ้าไม่ใช่โมฆะให้ทำสิ่งนี้)

ฉันชอบแผนภูมิที่เรียบง่ายนี้ดังนั้นฉันจึงเชื่อมโยงที่นี่ คุณสามารถดูได้จากสิ่งนี้ซึ่งเขียนโดย Sebastiano Gottardo

ป้อนคำอธิบายภาพที่นี่

โปรดดูแผนภูมิที่มาพร้อมกับคำอธิบายด้านล่าง

แนวคิด

ฉันคิดว่ามันเป็นวิธีการเล่นบทบาทภายในบล็อกโค้ดของคุณเมื่อคุณเรียกใช้ฟังก์ชันเหล่านั้น + ไม่ว่าคุณจะต้องการตัวเองกลับมา (ไปยังฟังก์ชันการโทรแบบลูกโซ่หรือตั้งค่าเป็นตัวแปรผลลัพธ์ ฯลฯ )

ข้างบนคือสิ่งที่ฉันคิด

ตัวอย่างแนวคิด

ลองดูตัวอย่างทั้งหมดได้ที่นี่

1. ) myComputer.apply { }หมายถึงคุณต้องการแสดงเป็นนักแสดงหลัก (คุณคิดว่าคุณเป็นคอมพิวเตอร์) และคุณต้องการตัวเองกลับมา (คอมพิวเตอร์) เพื่อที่คุณจะได้ทำ

var crashedComputer = myComputer.apply { 
    // you're the computer, you yourself install the apps
    // note: installFancyApps is one of methods of computer
    installFancyApps() 
}.crash()

ใช่คุณเองก็แค่ติดตั้งแอพพังตัวเองและบันทึกตัวเองไว้เป็นข้อมูลอ้างอิงเพื่อให้คนอื่นเห็นและทำอะไรกับมัน

2. ) myComputer.also {}หมายความว่าคุณแน่ใจอย่างสมบูรณ์ว่าคุณไม่ใช่คอมพิวเตอร์คุณเป็นคนนอกที่ต้องการทำอะไรบางอย่างกับมันและต้องการให้คอมพิวเตอร์เป็นผลลัพธ์

var crashedComputer = myComputer.also { 
    // now your grandpa does something with it
    myGrandpa.installVirusOn(it) 
}.crash()

3. ) with(myComputer) { }หมายถึงคุณเป็นนักแสดงหลัก (คอมพิวเตอร์) และคุณไม่ต้องการให้ตัวเองกลับมา

with(myComputer) {
    // you're the computer, you yourself install the apps
    installFancyApps()
}

4. ) myComputer.run { }หมายถึงคุณเป็นนักแสดงหลัก (คอมพิวเตอร์) และคุณไม่ต้องการให้ตัวเองกลับมา

myComputer.run {
    // you're the computer, you yourself install the apps
    installFancyApps()
}

แต่มันแตกต่างจากwith { }ในแง่ที่ลึกซึ้งมากที่คุณสามารถโทรแบบลูกโซ่ได้run { }ดังต่อไปนี้

myComputer.run {
    installFancyApps()
}.run {
    // computer object isn't passed through here. So you cannot call installFancyApps() here again.
    println("woop!")
}

เนื่องจากrun {}เป็นฟังก์ชันส่วนขยาย แต่with { }ไม่ใช่ ดังนั้นคุณจึงเรียกrun { }และthisภายในบล็อกรหัสจะสะท้อนไปยังวัตถุประเภทผู้โทร คุณสามารถดูนี้สำหรับคำอธิบายที่ดีเยี่ยมสำหรับความแตกต่างระหว่างและrun {}with {}

5. ) myComputer.let { }หมายถึงคุณเป็นคนนอกที่ดูคอมพิวเตอร์และต้องการทำบางอย่างกับมันโดยไม่สนใจว่าอินสแตนซ์คอมพิวเตอร์จะถูกส่งกลับมาหาคุณอีกครั้ง

myComputer.let {
    myGrandpa.installVirusOn(it)
}

วิธีที่จะมองมัน

ฉันมักจะมองalsoและletเป็นสิ่งที่เป็นภายนอกภายนอก เมื่อใดก็ตามที่คุณพูดสองคำนี้ก็เหมือนกับว่าคุณพยายามทำอะไรบางอย่าง letติดตั้งไวรัสบนคอมพิวเตอร์เครื่องนี้และalsoเกิดข้อผิดพลาด ดังนั้นนี่เป็นการตอกย้ำว่าคุณเป็นนักแสดงหรือไม่

ในส่วนของผลลัพธ์ก็มีอยู่อย่างชัดเจน alsoเป็นการแสดงออกว่ามันเป็นอีกสิ่งหนึ่งด้วยดังนั้นคุณยังคงรักษาความพร้อมของวัตถุไว้ ดังนั้นจึงส่งคืนเป็นผลลัพธ์

thisทุกสิ่งทุกอย่างร่วมงานด้วย นอกจากนี้run/withเห็นได้ชัดว่าไม่สนใจในการส่งคืนวัตถุกลับด้วยตนเอง ตอนนี้คุณสามารถแยกความแตกต่างทั้งหมดได้

ฉันคิดว่าบางครั้งเมื่อเราก้าวออกจากตัวอย่างการเขียนโปรแกรม / ตรรกะแบบ 100% เราก็อยู่ในตำแหน่งที่ดีกว่าในการกำหนดคอนเซ็ปต์ต่างๆ แต่ขึ้นอยู่กับว่า :)


1
แผนภาพบอกทุกอย่าง ที่ดีที่สุดจนถึงตอนนี้
Shukant Pal

นี่ควรเป็นคำตอบที่ได้รับการยอมรับและได้รับการโหวตมากที่สุด
Segun Wahaab

8

ให้ใช้, takeIf, takeUnlessเป็นฟังก์ชันส่วนขยายใน Kotlin

เพื่อให้เข้าใจฟังก์ชันเหล่านี้คุณต้องเข้าใจฟังก์ชันส่วนขยายและฟังก์ชัน Lambdaใน Kotlin

ฟังก์ชันส่วนขยาย:

ด้วยการใช้ฟังก์ชันส่วนขยายเราสามารถสร้างฟังก์ชันสำหรับคลาสได้โดยไม่ต้องสืบทอดคลาส

Kotlin คล้ายกับ C # และ Gosu ให้ความสามารถในการขยายคลาสด้วยฟังก์ชันใหม่โดยไม่ต้องสืบทอดจากคลาสหรือใช้รูปแบบการออกแบบประเภทใด ๆ เช่น Decorator สิ่งนี้ทำได้ผ่านการประกาศพิเศษที่เรียกว่าส่วนขยาย Kotlin รองรับฟังก์ชันส่วนขยายและคุณสมบัติส่วนขยาย

ดังนั้นหากต้องการค้นหาว่ามีเพียงตัวเลขเท่านั้นStringคุณสามารถสร้างวิธีการดังต่อไปนี้โดยไม่ต้องสืบทอดStringคลาส

fun String.isNumber(): Boolean = this.matches("[0-9]+".toRegex())

คุณสามารถใช้ฟังก์ชันส่วนขยายด้านบนเช่นนี้

val phoneNumber = "8899665544"
println(phoneNumber.isNumber)

trueซึ่งเป็นพิมพ์

ฟังก์ชั่น Lambda:

ฟังก์ชัน Lambda เหมือนกับอินเทอร์เฟซใน Java แต่ใน Kotlin ฟังก์ชันแลมบ์ดาสามารถส่งผ่านเป็นพารามิเตอร์ในฟังก์ชันได้

ตัวอย่าง:

fun String.isNumber(block: () -> Unit): Boolean {
    return if (this.matches("[0-9]+".toRegex())) {
        block()
        true
    } else false
}

คุณจะเห็นว่าบล็อกเป็นฟังก์ชันแลมด้าและถูกส่งผ่านเป็นพารามิเตอร์ คุณสามารถใช้ฟังก์ชันด้านบนเช่นนี้

val phoneNumber = "8899665544"
    println(phoneNumber.isNumber {
        println("Block executed")
    })

ฟังก์ชั่นด้านบนจะพิมพ์แบบนี้

Block executed
true

ฉันหวังว่าตอนนี้คุณมีความคิดเกี่ยวกับฟังก์ชันส่วนขยายและฟังก์ชัน Lambda แล้ว ตอนนี้เราสามารถไปที่ฟังก์ชันส่วนขยายได้ทีละรายการ

ปล่อย

public inline fun <T, R> T.let(block: (T) -> R): R = block(this)

T และ R สองประเภทที่ใช้ในฟังก์ชันข้างต้น

T.let

Tอาจเป็นวัตถุเช่นคลาส String เพื่อให้คุณสามารถเรียกใช้ฟังก์ชันนี้กับวัตถุใด ๆ

block: (T) -> R

ในพารามิเตอร์ let คุณจะเห็นฟังก์ชันแลมด้าด้านบน นอกจากนี้วัตถุที่เรียกใช้จะถูกส่งผ่านเป็นพารามิเตอร์ของฟังก์ชัน ดังนั้นคุณสามารถใช้อ็อบเจ็กต์คลาสที่เรียกใช้ภายในฟังก์ชัน จากนั้นจะส่งคืนR(วัตถุอื่น)

ตัวอย่าง:

val phoneNumber = "8899665544"
val numberAndCount: Pair<Int, Int> = phoneNumber.let { it.toInt() to it.count() }

ในตัวอย่างข้างต้นให้ใช้Stringเป็นพารามิเตอร์ของฟังก์ชันแลมบ์ดาและส่งคืนคู่ในทางกลับกัน

ในทำนองเดียวกันฟังก์ชันส่วนขยายอื่น ๆ ก็ใช้งานได้

ด้วย

public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }

ฟังก์ชันส่วนขยายalsoใช้คลาสการเรียกใช้เป็นพารามิเตอร์ฟังก์ชันแลมบ์ดาและส่งกลับค่าอะไรเลย

ตัวอย่าง:

val phoneNumber = "8899665544"
phoneNumber.also { number ->
    println(number.contains("8"))
    println(number.length)
 }

สมัคร

public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

เช่นเดียวกับ แต่วัตถุที่เรียกใช้เดียวกันส่งผ่านเป็นฟังก์ชันดังนั้นคุณสามารถใช้ฟังก์ชันและคุณสมบัติอื่น ๆ ได้โดยไม่ต้องเรียกมันหรือชื่อพารามิเตอร์

ตัวอย่าง:

val phoneNumber = "8899665544"
phoneNumber.apply { 
    println(contains("8"))
    println(length)
 }

คุณสามารถดูในตัวอย่างข้างต้นฟังก์ชันของคลาส String ที่เรียกใช้โดยตรงภายในแลมบ์ดา funtion

takeIf

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? = if (predicate(this)) this else null

ตัวอย่าง:

val phoneNumber = "8899665544"
val number = phoneNumber.takeIf { it.matches("[0-9]+".toRegex()) }

ในตัวอย่างข้างต้นnumberจะมีสตริงที่phoneNumberตรงกับregex. nullมิฉะนั้นก็จะเป็น

ใช้

public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? = if (!predicate(this)) this else null

เป็นการกลับกันของ takeIf

ตัวอย่าง:

val phoneNumber = "8899665544"
val number = phoneNumber.takeUnless { it.matches("[0-9]+".toRegex()) }

numberจะมีสตริงphoneNumberก็ต่อเมื่อไม่ตรงกับregex. nullมิฉะนั้นก็จะเป็น

คุณสามารถดูคำตอบที่คล้ายกันซึ่งมีประโยชน์ที่นี่ความแตกต่างระหว่าง kotlin ยังใช้ปล่อยให้ใช้ takeIf และ takeUnless ใน Kotlin


คุณได้พิมพ์ผิดในตัวอย่างล่าสุดของคุณคุณอาจหมายแทนphoneNumber. takeUnless{} phoneNumber. takeIf{}
Ryan Amaral

1
แก้ไขแล้ว. ขอบคุณ @Ryan Amaral
Bhuvanesh BS

5

มี 6 ฟังก์ชั่นการกำหนดขอบเขตที่แตกต่างกัน:

  1. ต. รัน
  2. T.let
  3. T. สมัคร
  4. ด้วย
  5. วิ่ง

ฉันเตรียมบันทึกภาพดังต่อไปนี้เพื่อแสดงความแตกต่าง:

data class Citizen(var name: String, var age: Int, var residence: String)

ป้อนคำอธิบายภาพที่นี่

การตัดสินใจขึ้นอยู่กับความต้องการของคุณ กรณีการใช้งานของฟังก์ชันต่างๆทับซ้อนกันเพื่อให้คุณสามารถเลือกฟังก์ชันตามข้อตกลงเฉพาะที่ใช้ในโครงการหรือทีมของคุณ

แม้ว่าฟังก์ชันขอบเขตจะเป็นวิธีที่ทำให้โค้ดมีความกระชับมากขึ้น แต่ก็ควรหลีกเลี่ยงการใช้งานมากเกินไปเพราะสามารถลดความสามารถในการอ่านโค้ดของคุณและทำให้เกิดข้อผิดพลาดได้ หลีกเลี่ยงฟังก์ชั่นขอบเขตการซ้อนและระมัดระวังในการผูกมัดมัน: ง่ายต่อการสับสนเกี่ยวกับอ็อบเจ็กต์บริบทปัจจุบันและค่าของสิ่งนี้หรือมัน

นี่คือแผนภาพอื่นสำหรับการตัดสินใจว่าจะใช้อันไหนจากhttps://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84 ป้อนคำอธิบายภาพที่นี่

อนุสัญญาบางประการมีดังต่อไปนี้:

ใช้ยังสำหรับการดำเนินการเพิ่มเติมที่ไม่เปลี่ยนแปลงวัตถุเช่นการเข้าสู่ระบบหรือพิมพ์ข้อมูลการแก้ปัญหา

val numbers = mutableListOf("one", "two", "three")
 numbers
 .also { println("The list elements before adding new one: $it") }
 .add("four")

กรณีทั่วไปสำหรับการใช้คือการกำหนดค่าวัตถุ

val adam = Person("Adam").apply {
age = 32
city = "London"        
}
println(adam)

หากคุณต้องการเงาให้ใช้run

fun test() {
    var mood = "I am sad"

    run {
        val mood = "I am happy"
        println(mood) // I am happy
    }
    println(mood)  // I am sad
}

หากคุณต้องการส่งคืนวัตถุตัวรับเองให้ใช้ใช้หรือยัง


3

จากประสบการณ์ของฉันเนื่องจากฟังก์ชั่นดังกล่าวเป็นน้ำตาลซินแทติกแบบอินไลน์โดยไม่มีความแตกต่างด้านประสิทธิภาพคุณควรเลือกอันที่ต้องเขียนโค้ดในจำนวนน้อยที่สุด

ในการทำสิ่งนี้ก่อนอื่นให้กำหนดว่าคุณต้องการให้แลมด้าส่งคืนผลลัพธ์ (เลือกrun/ let) หรือตัววัตถุเอง (เลือกapply/ also); จากนั้นในกรณีส่วนใหญ่เมื่อแลมบ์ดาเป็นนิพจน์เดียวให้เลือกรายการที่มีประเภทฟังก์ชันบล็อกเดียวกันกับนิพจน์นั้นเนื่องจากเมื่อเป็นนิพจน์ตัวรับthisสามารถละเว้นได้เมื่อเป็นนิพจน์พารามิเตอร์itจะสั้นกว่าthis:

val a: Type = ...

fun Type.receiverFunction(...): ReturnType { ... }
a.run/*apply*/ { receiverFunction(...) } // shorter because "this" can be omitted
a.let/*also*/ { it.receiverFunction(...) } // longer

fun parameterFunction(parameter: Type, ...): ReturnType { ... }
a.run/*apply*/ { parameterFunction(this, ...) } // longer
a.let/*also*/ { parameterFunction(it, ...) } // shorter because "it" is shorter than "this"

อย่างไรก็ตามเมื่อแลมบ์ดาประกอบด้วยส่วนผสมเหล่านี้ขึ้นอยู่กับคุณว่าจะเลือกอันที่เหมาะกับบริบทหรือคุณรู้สึกสบายใจมากกว่า

นอกจากนี้ให้ใช้สิ่งที่มีฟังก์ชันบล็อกพารามิเตอร์เมื่อจำเป็นต้องมีการถอดรหัส:

val pair: Pair<TypeA, TypeB> = ...

pair.run/*apply*/ {
    val (first, second) = this
    ...
} // longer
pair.let/*also*/ { (first, second) -> ... } // shorter

นี่คือการเปรียบเทียบโดยย่อระหว่างฟังก์ชันเหล่านี้ทั้งหมดจากหลักสูตร Kotlin อย่างเป็นทางการของ JetBrains ใน Coursera Kotlin สำหรับนักพัฒนา Java : ตารางความแตกต่าง การใช้งานที่ง่ายขึ้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.