khi nào sử dụng hàm nội tuyến trong Kotlin?


105

Tôi biết rằng một hàm nội tuyến có thể sẽ cải thiện hiệu suất và khiến mã được tạo phát triển, nhưng tôi không chắc khi nào sử dụng một hàm là chính xác.

lock(l) { foo() }

Thay vì tạo một đối tượng hàm cho tham số và tạo một cuộc gọi, trình biên dịch có thể phát ra đoạn mã sau. ( Nguồn )

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

nhưng tôi thấy rằng không có đối tượng hàm nào được tạo bởi kotlin cho một hàm không nội tuyến. tại sao?

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

7
Có hai trường hợp sử dụng chính cho việc này, một là với một số loại hàm bậc cao hơn, và trường hợp kia là các tham số kiểu được sửa đổi. Tài liệu về các hàm nội tuyến bao gồm những điều này: kotlinlang.org/docs/reference/inline-functions.html
zsmb13

2
@ zsmb13 xin cảm ơn. nhưng tôi không hiểu rằng: "Thay vì tạo ra một đối tượng hàm cho tham số và tạo ra một cuộc gọi, trình biên dịch có thể phát ra các mã sau"
holi-java

2
tôi cũng không nhận được ví dụ đó tbh.
filthy_wizard

Câu trả lời:


279

Giả sử bạn tạo một hàm thứ tự cao hơn có kiểu lambda () -> Unit(không có tham số, không có giá trị trả về) và thực thi nó như sau:

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

Theo cách nói của Java, điều này sẽ dịch thành một thứ như thế này (đơn giản hóa!):

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

Và khi bạn gọi nó từ Kotlin ...

nonInlined {
    println("do something here")
}

Dưới mui xe, một phiên bản của Functionsẽ được tạo ở đây, bao bọc mã bên trong lambda (một lần nữa, điều này được đơn giản hóa):

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

Vì vậy, về cơ bản, việc gọi hàm này và chuyển một lambda cho nó sẽ luôn tạo ra một thể hiện của một Functionđối tượng.


Mặt khác, nếu bạn sử dụng inlinetừ khóa:

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

Khi bạn gọi nó như thế này:

inlined {
    println("do something here")
}

Sẽ không có Functiontrường hợp nào được tạo, thay vào đó, mã xung quanh lệnh gọi blockbên trong hàm nội tuyến sẽ được sao chép vào trang web cuộc gọi, vì vậy bạn sẽ nhận được một cái gì đó như thế này trong mã bytecode:

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

Trong trường hợp này, không có phiên bản mới nào được tạo.


19
Lợi thế của việc có trình bao bọc đối tượng Hàm ngay từ đầu là gì? tức là - tại sao mọi thứ không phải là nội tuyến?
Arturs Vancans

14
Bằng cách này bạn cũng có thể tùy tiện vượt qua chức năng xung quanh như thông số, lưu trữ chúng trong các biến, vv
zsmb13

6
Lời giải thích tuyệt vời bởi @ zsmb13
Yajairo87

2
Bạn có thể, và nếu bạn làm những việc phức tạp với chúng, cuối cùng bạn sẽ muốn biết về noinlinecrossinlinetừ khóa - hãy xem tài liệu .
zsmb 13/12/1217

2
Tài liệu đưa ra lý do mà bạn không muốn nội tuyến theo mặc định: Nội tuyến có thể khiến mã được tạo phát triển; tuy nhiên, nếu chúng ta làm điều đó một cách hợp lý (nghĩa là tránh nội tuyến các hàm lớn), nó sẽ mang lại hiệu quả cao về hiệu suất, đặc biệt là tại các trang web gọi "megamorphic" bên trong các vòng lặp.
CorayThan

43

Để tôi thêm: "Khi nào không sử dụng inline" :

1) Nếu bạn có một hàm đơn giản không chấp nhận các hàm khác làm đối số, thì việc nội dòng chúng sẽ không hợp lý. IntelliJ sẽ cảnh báo bạn:

Tác động hiệu suất dự kiến ​​của nội tuyến '...' là không đáng kể. Nội tuyến hoạt động tốt nhất cho các chức năng có tham số của các loại chức năng

2) Ngay cả khi bạn có một hàm "với các tham số của kiểu hàm", bạn có thể gặp phải trình biên dịch nói với bạn rằng nội tuyến không hoạt động. Hãy xem xét ví dụ này:

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

Mã này sẽ không biên dịch với lỗi:

Sử dụng bất hợp pháp tham số nội tuyến 'hoạt động' trong '...'. Thêm công cụ sửa đổi 'noinline' vào khai báo tham số.

Lý do là trình biên dịch không thể nội dòng mã này. Nếu operationkhông được bao bọc trong một đối tượng (được ngụ ý bởi inlinevì bạn muốn tránh điều này), làm thế nào nó có thể được gán cho một biến? Trong trường hợp này, trình biên dịch đề nghị đưa ra đối số noinline. Có một inlinechức năng với một noinlinechức năng duy nhất không có ý nghĩa gì cả, đừng làm vậy. Tuy nhiên, nếu có nhiều tham số của các kiểu chức năng, hãy xem xét nội tuyến một số tham số trong số đó nếu cần.

Vì vậy, đây là một số quy tắc được đề xuất:

  • Bạn có thể nội dòng khi tất cả các tham số kiểu hàm được gọi trực tiếp hoặc được truyền cho hàm nội tuyến khác
  • Bạn nên nội dòng khi ^ là trường hợp.
  • Bạn không thể nội dòng khi tham số hàm đang được gán cho một biến bên trong hàm
  • Bạn nên xem xét nội tuyến nếu ít nhất một trong các tham số kiểu chức năng của bạn có thể được nội tuyến, sử dụng noinlinecho các tham số khác.
  • Bạn không nên nội tuyến các hàm lớn, hãy nghĩ về mã byte được tạo. Nó sẽ được sao chép đến tất cả những nơi mà hàm được gọi từ đó.
  • Một trường hợp sử dụng khác là reifiedtham số kiểu, yêu cầu bạn sử dụng inline. Đọc ở đây .

4
về mặt kỹ thuật, bạn vẫn có thể nội tuyến các hàm không sử dụng biểu thức lambda phải không? .. ở đây ưu điểm là chi phí gọi hàm được tránh trong trường hợp đó .. ngôn ngữ như Scala cho phép điều này .. không chắc tại sao Kotlin lại cấm loại nội tuyến đó- ing
rogue-one,

3
@ rogue-one Kotlin không cấm thời gian nội tuyến này. Các tác giả ngôn ngữ chỉ đơn giản tuyên bố rằng lợi ích hiệu suất có thể là không đáng kể. Các phương thức nhỏ có khả năng đã được JVM đưa vào trong quá trình tối ưu hóa JIT, đặc biệt nếu chúng được thực thi thường xuyên. Một trường hợp khác inlinecó thể gây hại là khi tham số hàm được gọi nhiều lần trong hàm nội tuyến, chẳng hạn như trong các nhánh có điều kiện khác nhau. Tôi vừa gặp phải một trường hợp mà tất cả các mã bytecode cho các đối số chức năng đã bị sao chép vì điều này.
Mike Hill

5

Trường hợp quan trọng nhất khi chúng ta sử dụng công cụ sửa đổi nội tuyến là khi chúng ta xác định các hàm tương tự với các hàm tham số. Bộ sưu tập hoặc xử lý chuỗi (như filter, maphoặc joinToString) hoặc chỉ các hàm độc lập là một ví dụ hoàn hảo.

Đây là lý do tại sao công cụ sửa đổi nội tuyến chủ yếu là một tối ưu hóa quan trọng cho các nhà phát triển thư viện. Họ nên biết nó hoạt động như thế nào và những cải tiến cũng như chi phí của nó là gì. Chúng ta nên sử dụng công cụ sửa đổi nội tuyến trong các dự án của mình khi chúng ta xác định các hàm sử dụng của riêng mình với các tham số kiểu hàm.

Nếu chúng ta không có tham số kiểu hàm, tham số kiểu đã sửa đổi và chúng ta không cần trả về không phải cục bộ, thì rất có thể chúng ta không nên sử dụng công cụ sửa đổi nội tuyến. Đây là lý do tại sao chúng tôi sẽ có cảnh báo trên Android Studio hoặc IDEA IntelliJ.

Ngoài ra, có một vấn đề về kích thước mã. Nội tuyến một hàm lớn có thể làm tăng đáng kể kích thước của mã bytecode vì nó được sao chép vào mọi trang web cuộc gọi. Trong những trường hợp như vậy, bạn có thể cấu trúc lại hàm và trích xuất mã thành các hàm thông thường.


4

Các hàm bậc cao rất hữu ích và chúng thực sự có thể cải thiện reusabilitymã. Tuy nhiên, một trong những mối quan tâm lớn nhất về việc sử dụng chúng là hiệu quả. Các biểu thức Lambda được biên dịch thành các lớp (thường là các lớp ẩn danh) và việc tạo đối tượng trong Java là một thao tác nặng. Chúng ta vẫn có thể sử dụng các hàm bậc cao một cách hiệu quả, trong khi vẫn giữ được tất cả các lợi ích, bằng cách làm cho các hàm nội dòng.

ở đây có chức năng nội tuyến thành hình ảnh

Khi một hàm được đánh dấu là inline, trong quá trình biên dịch mã, trình biên dịch sẽ thay thế tất cả các lệnh gọi hàm bằng phần thân thực của hàm. Ngoài ra, các biểu thức lambda được cung cấp dưới dạng đối số được thay thế bằng phần thân thực của chúng. Chúng sẽ không được coi là hàm mà là mã thực tế.

Tóm lại: - Inline -> thay vì được gọi, chúng được thay thế bằng mã nội dung của hàm tại thời điểm biên dịch ...

Trong Kotlin, việc sử dụng một hàm làm tham số của một hàm khác (được gọi là các hàm bậc cao) cảm thấy tự nhiên hơn trong Java.

Tuy nhiên, sử dụng lambdas có một số nhược điểm. Vì chúng là các lớp ẩn danh (và do đó, các đối tượng), chúng cần bộ nhớ (và thậm chí có thể thêm vào tổng số phương thức của ứng dụng của bạn). Để tránh điều này, chúng ta có thể nội dòng các phương pháp của mình.

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

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

Từ ví dụ trên : - Hai hàm này làm hoàn toàn giống nhau - in ra kết quả của hàm getString. Một là nội tuyến và một không.

Nếu bạn kiểm tra mã java đã dịch ngược, bạn sẽ thấy rằng các phương pháp hoàn toàn giống nhau. Đó là bởi vì từ khóa nội tuyến là một chỉ dẫn cho trình biên dịch để sao chép mã vào trang web gọi.

Tuy nhiên, nếu chúng ta đang chuyển bất kỳ loại hàm nào sang một hàm khác như bên dưới:

//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)
    /*...*/
 }

Để giải quyết vấn đề đó, chúng ta có thể viết lại hàm của mình như sau:

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)
    /*...*/}

Giả sử chúng ta có một hàm bậc cao hơn như sau:

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

Ở đây, trình biên dịch sẽ yêu cầu chúng ta không sử dụng từ khóa nội tuyến khi chỉ có một tham số lambda và chúng ta đang chuyển nó cho một hàm khác. Vì vậy, chúng ta có thể viết lại hàm trên như sau:

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

Lưu ý : -chúng tôi cũng phải xóa từ khóa noinline vì nó chỉ có thể được sử dụng cho các hàm nội tuyến!

Giả sử chúng ta có chức năng như thế này ->

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

Điều này hoạt động tốt nhưng phần logic của hàm bị ô nhiễm với mã đo lường khiến đồng nghiệp của bạn khó làm việc hơn với những gì đang xảy ra. :)

Đây là cách một hàm nội tuyến có thể giúp mã này:

      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
    }

Bây giờ tôi có thể tập trung vào việc đọc mục đích chính của hàm intercept () là gì mà không bỏ qua các dòng mã đo lường. Chúng tôi cũng được hưởng lợi từ tùy chọn sử dụng lại mã đó ở những nơi khác mà chúng tôi muốn

inline cho phép bạn gọi một hàm có đối số lambda trong một bao đóng ({...}) thay vì truyền tham số lambda like (myLamda)


2

Một trường hợp đơn giản mà bạn có thể muốn là khi bạn tạo một hàm sử dụng có chức năng tạm ngưng. Xem xét điều này.

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

fun logic() { }

suspend fun asyncLogic() { }

fun main() {
    timer { logic() }

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

Trong trường hợp này, bộ hẹn giờ của chúng tôi sẽ không chấp nhận các chức năng tạm ngưng. Để giải quyết vấn đề đó, bạn cũng có thể muốn làm cho nó tạm ngừng

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

Nhưng sau đó nó chỉ có thể được sử dụng từ chính các hàm coroutines / pause. Sau đó, bạn sẽ kết thúc việc tạo phiên bản không đồng bộ và phiên bản không đồng bộ của những utils này. Vấn đề sẽ biến mất nếu bạn làm cho nó nội dòng.

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

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

    launch {
        timer { asyncLogic() }
    }
}

Đây là một sân chơi kotlin với trạng thái lỗi. Làm cho bộ đếm thời gian nội dòng để giải quyết nó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.