ควรใช้ฟังก์ชันอินไลน์ใน Kotlin เมื่อใด


105

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

lock(l) { foo() }

แทนที่จะสร้างอ็อบเจ็กต์ฟังก์ชันสำหรับพารามิเตอร์และสร้างการเรียกคอมไพเลอร์สามารถส่งโค้ดต่อไปนี้ ( ที่มา )

l.lock()
try {
  foo()
}
finally {
  l.unlock()
}

แต่ฉันพบว่าไม่มีฟังก์ชันอ็อบเจ็กต์ที่สร้างโดย kotlin สำหรับฟังก์ชันที่ไม่ใช่อินไลน์ ทำไม?

/**non-inline function**/
fun lock(lock: Lock, block: () -> Unit) {
    lock.lock();
    try {
        block();
    } finally {
        lock.unlock();
    }
}

7
มีสองกรณีการใช้งานหลักสำหรับสิ่งนี้กรณีหนึ่งมีฟังก์ชันลำดับที่สูงกว่าบางประเภทและอีกกรณีคือพารามิเตอร์ประเภท reified เอกสารของฟังก์ชันอินไลน์ครอบคลุมสิ่งเหล่านี้: kotlinlang.org/docs/reference/inline-functions.html
zsmb13

2
@ zsmb13 ขอบคุณครับ แต่ฉันไม่เข้าใจว่า: "แทนที่จะสร้างวัตถุฟังก์ชันสำหรับพารามิเตอร์และสร้างการเรียกใช้คอมไพเลอร์สามารถส่งรหัสต่อไปนี้"
holi-java

2
ฉันไม่ได้รับตัวอย่างนั้นทั้ง tbh
filthy_wizard

คำตอบ:


280

สมมติว่าคุณสร้างฟังก์ชันลำดับที่สูงขึ้นซึ่งรับประเภทแลมด้า() -> Unit(ไม่มีพารามิเตอร์ไม่มีค่าส่งคืน) และดำเนินการดังนี้:

fun nonInlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

ในภาษาจาวาสิ่งนี้จะแปลเป็นดังนี้ (ง่ายขึ้น!):

public void nonInlined(Function block) {
    System.out.println("before");
    block.invoke();
    System.out.println("after");
}

และเมื่อคุณโทรมาจาก Kotlin ...

nonInlined {
    println("do something here")
}

ภายใต้ประทุนFunctionจะมีการสร้างอินสแตนซ์ที่นี่ซึ่งจะรวมโค้ดไว้ในแลมบ์ดา (อีกครั้งซึ่งทำให้ง่ายขึ้น):

nonInlined(new Function() {
    @Override
    public void invoke() {
        System.out.println("do something here");
    }
});

โดยพื้นฐานแล้วการเรียกใช้ฟังก์ชันนี้และส่งแลมด้าไปยังฟังก์ชันนี้จะสร้างอินสแตนซ์ของFunctionวัตถุเสมอ


ในทางกลับกันหากคุณใช้inlineคำหลัก:

inline fun inlined(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

เมื่อคุณเรียกสิ่งนี้:

inlined {
    println("do something here")
}

จะไม่มีการFunctionสร้างอินสแตนซ์ แต่โค้ดรอบ ๆ การเรียกใช้blockภายในฟังก์ชันอินไลน์จะถูกคัดลอกไปยังไซต์การโทรดังนั้นคุณจะได้รับสิ่งนี้ใน bytecode:

System.out.println("before");
System.out.println("do something here");
System.out.println("after");

ในกรณีนี้จะไม่มีการสร้างอินสแตนซ์ใหม่


19
ข้อดีของการมี Function object wrapper ตั้งแต่แรกคืออะไร? คือ - ทำไมทุกอย่างไม่อินไลน์?
Arturs Vancans

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

6
คำอธิบายที่ดีโดย @ zsmb13
Yajairo87

2
คุณสามารถและถ้าคุณไม่ซับซ้อนสิ่งกับพวกเขาในที่สุดคุณจะอยากรู้เกี่ยวกับnoinlineและcrossinlineคำหลัก - ดูเอกสาร
zsmb13

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

43

ให้ฉันเพิ่ม: "เมื่อใดที่ไม่ควรใช้inline" :

1) หากคุณมีฟังก์ชันง่ายๆที่ไม่ยอมรับฟังก์ชันอื่น ๆ เป็นอาร์กิวเมนต์ก็ไม่สมเหตุสมผลที่จะอินไลน์ IntelliJ จะเตือนคุณ:

ผลกระทบด้านประสิทธิภาพที่คาดไว้ของการซับใน '... ' นั้นไม่มีนัยสำคัญ Inlining ทำงานได้ดีที่สุดสำหรับฟังก์ชันที่มีพารามิเตอร์ประเภทฟังก์ชันการทำงาน

2) แม้ว่าคุณจะมีฟังก์ชัน "พร้อมพารามิเตอร์ประเภทฟังก์ชัน" แต่คุณอาจพบว่าคอมไพเลอร์แจ้งว่าอินไลน์ไม่ทำงาน ลองพิจารณาตัวอย่างนี้:

inline fun calculateNoInline(param: Int, operation: IntMapper): Int {
    val o = operation //compiler does not like this
    return o(param)
}

รหัสนี้จะไม่รวบรวมข้อผิดพลาด:

การใช้อินไลน์พารามิเตอร์ 'operation' ใน '... ' อย่างผิดกฎหมาย เพิ่มตัวปรับแต่ง 'noinline' ในการประกาศพารามิเตอร์

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

ดังนั้นนี่คือกฎที่แนะนำ:

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

4
ในทางเทคนิคคุณยังคงสามารถใช้ฟังก์ชันอินไลน์ที่ไม่ใช้นิพจน์แลมบ์ดาได้ใช่ไหม .. ข้อดีก็คือค่าใช้จ่ายในการเรียกฟังก์ชันจะถูกหลีกเลี่ยงในกรณีนั้น .. ภาษาเช่นสกาล่าอนุญาตให้ใช้สิ่งนี้ .. ไม่แน่ใจว่าทำไม Kotlin จึงห้ามอินไลน์ประเภทนั้น - ing
rogue-one

3
@ rogue-one Kotlin ไม่ได้ห้ามเวลาในการซับในนี้ ผู้เขียนภาษาอ้างเพียงว่าประโยชน์ด้านประสิทธิภาพน่าจะไม่สำคัญ วิธีการขนาดเล็กมีแนวโน้มที่จะถูกแทรกโดย JVM ในระหว่างการเพิ่มประสิทธิภาพ JIT โดยเฉพาะอย่างยิ่งหากมีการดำเนินการบ่อยๆ อีกกรณีหนึ่งที่inlineอาจเป็นอันตรายได้คือเมื่อพารามิเตอร์การทำงานถูกเรียกหลายครั้งในฟังก์ชันอินไลน์เช่นในสาขาเงื่อนไขต่างๆ ฉันเพิ่งเจอกรณีที่มีการทำซ้ำ bytecode สำหรับอาร์กิวเมนต์เชิงฟังก์ชันทั้งหมดเนื่องจากสิ่งนี้
Mike Hill

5

กรณีที่สำคัญที่สุดเมื่อเราใช้อินไลน์โมดิฟายเออร์คือเมื่อเรากำหนดฟังก์ชันที่เหมือนยูทิลด้วยฟังก์ชันพารามิเตอร์ การเก็บหรือการประมวลผลสตริง (เช่นfilter, mapหรือjoinToString) หรือฟังก์ชั่นแบบสแตนด์อโลนเพียงตัวอย่างที่สมบูรณ์แบบ

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

หากเราไม่มีพารามิเตอร์ประเภทฟังก์ชันพารามิเตอร์ประเภท reified และเราไม่ต้องการการส่งคืนที่ไม่ใช่โลคัลเรามักไม่ควรใช้ตัวปรับแต่งแบบอินไลน์ นี่คือเหตุผลที่เราจะมีคำเตือนบน Android Studio หรือ IDEA IntelliJ

นอกจากนี้ยังมีปัญหาขนาดรหัส การแทรกฟังก์ชันขนาดใหญ่สามารถเพิ่มขนาดของ bytecode ได้อย่างมากเนื่องจากมีการคัดลอกไปยังไซต์การโทรทุกแห่ง ในกรณีเช่นนี้คุณสามารถ refactor ฟังก์ชันและแยกโค้ดเป็นฟังก์ชันปกติได้


4

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

นี่คือฟังก์ชันอินไลน์ในรูปภาพ

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

ในระยะสั้น: -อินไลน์ -> แทนที่จะถูกเรียกพวกเขาจะถูกแทนที่ด้วยรหัสเนื้อหาของฟังก์ชันในเวลาคอมไพล์ ...

ใน Kotlin การใช้ฟังก์ชันเป็นพารามิเตอร์ของฟังก์ชันอื่น (เรียกว่าฟังก์ชันลำดับที่สูงกว่า) ให้ความรู้สึกเป็นธรรมชาติมากกว่าใน Java

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

fun notInlined(getString: () -> String?) = println(getString())

inline fun inlined(getString: () -> String?) = println(getString())

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

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

อย่างไรก็ตามหากเรากำลังส่งประเภทฟังก์ชันใด ๆ ไปยังฟังก์ชันอื่นดังต่อไปนี้:

//Compile time error… Illegal usage of inline function type ftOne...
 inline fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/
 }

เพื่อแก้ปัญหานั้นเราสามารถเขียนฟังก์ชันของเราใหม่ได้ดังนี้:

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int, ftTwo: (Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/}

สมมติว่าเรามีฟังก์ชันลำดับที่สูงกว่าดังต่อไปนี้:

inline fun Int.doSomething(y: Int, noinline ftOne: Int.(Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/}

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

fun Int.doSomething(y: Int, ftOne: Int.(Int) -> Int) {
    //passing a function type to another function
    val funOne = someFunction(ftOne)
    /*...*/
}

หมายเหตุ : - เราต้องลบคีย์เวิร์ด noinline ด้วยเพราะสามารถใช้ได้กับฟังก์ชันอินไลน์เท่านั้น!

สมมติว่าเรามีฟังก์ชันดังนี้ ->

fun intercept() {
    // ...
    val start = SystemClock.elapsedRealtime()
    val result = doSomethingWeWantToMeasure()
    val duration = SystemClock.elapsedRealtime() - start
    log(duration)
    // ...}

วิธีนี้ใช้งานได้ดี แต่เนื้อของตรรกะของฟังก์ชันนั้นปนเปื้อนด้วยรหัสการวัดทำให้เพื่อนร่วมงานของคุณทำงานได้ยากขึ้น :)

นี่คือวิธีที่ฟังก์ชันอินไลน์สามารถช่วยโค้ดนี้ได้:

      fun intercept() {
    // ...
    val result = measure { doSomethingWeWantToMeasure() }
    // ...
    }

     inline fun <T> measure(action: () -> T) {
    val start = SystemClock.elapsedRealtime()
    val result = action()
    val duration = SystemClock.elapsedRealtime() - start
    log(duration)
    return result
    }

ตอนนี้ฉันสามารถมีสมาธิในการอ่านสิ่งที่เจตนาหลักของฟังก์ชัน intercept () คือโดยไม่ต้องข้ามบรรทัดของโค้ดการวัด นอกจากนี้เรายังได้รับประโยชน์จากตัวเลือกในการนำรหัสนั้นไปใช้ซ้ำในที่อื่น ๆ ที่เราต้องการ

อินไลน์ช่วยให้คุณสามารถเรียกใช้ฟังก์ชันที่มีอาร์กิวเมนต์แลมบ์ดาภายในการปิด ({... }) แทนที่จะส่งแลมด้าเหมือนการวัด (myLamda)


2

กรณีง่ายๆอย่างหนึ่งที่คุณอาจต้องการคือเมื่อคุณสร้างฟังก์ชัน util ที่ใช้ในบล็อกระงับ พิจารณาสิ่งนี้.

fun timer(block: () -> Unit) {
    // stuff
    block()
    //stuff
}

fun logic() { }

suspend fun asyncLogic() { }

fun main() {
    timer { logic() }

    // This is an error
    timer { asyncLogic() }
}

ในกรณีนี้ตัวจับเวลาของเราจะไม่ยอมรับฟังก์ชัน Suspend ในการแก้ปัญหาคุณอาจถูกล่อลวงให้ระงับเช่นกัน

suspend fun timer(block: suspend () -> Unit) {
    // stuff
    block()
    // stuff
}

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

inline fun timer(block: () -> Unit) {
    // stuff
    block()
    // stuff
}

fun main() {
    // timer can be used from anywhere now
    timer { logic() }

    launch {
        timer { asyncLogic() }
    }
}

นี่คือสนามเด็กเล่น kotlin ที่มีสถานะข้อผิดพลาด ทำให้ตัวจับเวลาแบบอินไลน์เพื่อแก้ปัญหา

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