Kotlin'de bir satır içi işlevi ne zaman kullanılır?


108

Satır içi bir işlevin performansı artıracağını ve oluşturulan kodun büyümesine neden olabileceğini biliyorum, ancak birini kullanmanın ne zaman doğru olduğundan emin değilim.

lock(l) { foo() }

Parametre için bir işlev nesnesi oluşturmak ve bir çağrı oluşturmak yerine, derleyici aşağıdaki kodu yayınlayabilir. ( Kaynak )

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

ancak satır içi olmayan bir işlev için kotlin tarafından oluşturulmuş bir işlev nesnesi olmadığını buldum. neden?

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

7
Bunun için iki ana kullanım durumu vardır, biri belirli tip yüksek dereceli fonksiyonlarla, diğeri ise reified tip parametreleridir. Satır içi işlevlerin dokümantasyonu şunları
zsmb13

2
@ zsmb13 teşekkürler efendim. ancak şunu anlamıyorum: "Parametre için bir işlev nesnesi oluşturmak ve bir çağrı oluşturmak yerine, derleyici aşağıdaki kodu yayınlayabilir"
holi-java

2
Ben de bu örneği alamıyorum.
filthy_wizard

Bir inlineişlevin performansı artırabileceğini ve kullanıcı tarafından farkedileceğini size söyleten nedir?
IgorGanapolsky

Yanıtlar:


285

Diyelim ki lambda türü () -> Unit(parametre yok, dönüş değeri yok) alan ve bunu şu şekilde çalıştıran daha yüksek dereceli bir işlev oluşturduğunuzu varsayalım :

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

Java dilinde, bu şu gibi bir şeye çevrilecektir (basitleştirilmiş!):

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

Ve onu Kotlin'den aradığınızda ...

nonInlined {
    println("do something here")
}

Kaputun altında, Functionburada kodu lambda'nın içine saran bir örnek oluşturulacak (yine bu basitleştirilmiştir):

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

Yani temel olarak, bu işlevi çağırmak ve ona bir lambda geçirmek her zaman bir Functionnesnenin örneğini oluşturacaktır .


Öte yandan, inlineanahtar kelimeyi kullanırsanız :

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

Böyle çağırdığınızda:

inlined {
    println("do something here")
}

Hiçbir Functionörnek oluşturulmayacak, bunun yerine, blocksatır içi işlevin içindeki çağrının etrafındaki kod çağrı sitesine kopyalanacak, böylece bayt kodunda şuna benzer bir şey elde edeceksiniz:

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

Bu durumda, hiçbir yeni örnek oluşturulmaz.


21
İlk etapta Function nesne sarmalayıcısına sahip olmanın avantajı nedir? yani - neden her şey satır içi değil?
Arturs Vancans

14
Bu şekilde, işlevleri isteğe bağlı olarak parametreler olarak aktarabilir, bunları değişkenler içinde saklayabilirsiniz, vb.
zsmb13

2
Yapabilirsiniz ve onlarla karmaşık şeyler yaparsanız, sonunda noinlineve crossinlineanahtar sözcükleri hakkında bilgi sahibi olmak isteyeceksiniz - belgelere bakın .
zsmb13

2
Dokümanlar, varsayılan olarak satır içi yapmak
CorayThan

2
Ama "Ne zaman kullanılmalı" sorusuna cevap vermedin. Nasıl kullanıldığını açıkladın. Bunun cevabı belgelerde yer alır ve eğer az miktarda kod içeren bir fonksiyonunuz varsa ve fonksiyon çok kullanılırsa, bir satır içi fonksiyon kullanmanın performansı artırabileceğini belirtir.
AndroidDev

45

Ekleyeyim: Ne zaman kullanılmazinline :

  1. Diğer işlevleri argüman olarak kabul etmeyen basit bir işleviniz varsa, bunları satır içine almak mantıklı değildir. IntelliJ sizi uyaracak:

    Satır içi '...' ifadesinin beklenen performans etkisi önemsizdir. Inlining, işlevsel türlerin parametrelerine sahip işlevler için en iyi şekilde çalışır

  2. "İşlevsel türlerin parametrelerine sahip" bir işleviniz olsa bile, derleyicinin satır içi yazmanın çalışmadığını söylemesi ile karşılaşabilirsiniz. Şu örneği düşünün:

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

    Bu kod derlenmez ve şu hatayı verir:

    '...' içinde satır içi parametrenin 'işlem' yasadışı kullanımı. Parametre bildirimine 'noinline' değiştiricisini ekleyin.

    Bunun nedeni, derleyicinin bu kodu, özellikle de operationparametreyi satır içi yapamamasıdır . Bir operationnesneye sarılmamışsa (bu uygulamanın sonucu olur inline), bir değişkene nasıl atanabilir? Bu durumda, derleyici argüman yapmayı önerir noinline. inlineTek noinlineişleve sahip bir işleve sahip olmak bir anlam ifade etmiyor, bunu yapma. Bununla birlikte, fonksiyonel tiplerin birden fazla parametresi varsa, gerekirse bazılarını satır içine almayı düşünün.

İşte önerilen bazı kurallar:

  • Sen edebilirsiniz tüm fonksiyonel türü parametreleri doğrudan denilen veya geçirilen zaman satır içi diğer satır içi işlevi
  • Sen olmalıdır ^ durum olduğunda satır içi.
  • Sen olamaz fonksiyon parametre işlevi içinde bir değişkene atanan edilirken satır içi
  • Sen olmalıdır senin fonksiyonel tip parametrelerin en az bir, kullanımı satır içine yerleştirilmiş eğer inlining düşünün noinlinebaşkaları için.
  • Sen olmamalıdır satır içi büyük fonksiyonlar, oluşturulan bayt kodu hakkında düşünüyorum. İşlevin çağrıldığı tüm yerlere kopyalanacaktır.
  • Başka bir kullanım durumu, kullanmanızı reifiedgerektiren tür parametreleridir inline. Burada okuyun .

5
teknik olarak hala lambda ifadeleri almayan satır içi işlevler olabilir mi? .. buradaki avantaj, bu durumda işlev çağrısı ek yükünden kaçınılmasıdır .. Scala gibi dil buna izin verir .. Kotlin'in neden bu tür satır içi ifadeleri yasakladığından emin değilim- ing
rogue-one

4
@ rogue-one Kotlin bu kez inlining yapmayı yasaklamıyor. Dil yazarları, performans avantajının muhtemelen önemsiz olduğunu iddia ediyorlar. Küçük yöntemler, JIT optimizasyonu sırasında, özellikle de sık sık çalıştırılırlarsa, muhtemelen JVM tarafından satır içine alınmışlardır. inlineZararlı olabilecek başka bir durum , işlevsel parametrenin, farklı koşullu dallarda olduğu gibi, satır içi işlevde birden çok kez çağrılmasıdır. Fonksiyonel argümanlar için tüm bayt kodunun bu nedenle çoğaltıldığı bir durumla karşılaştım.
Mike Hill

crossinlineParametreleri ne zaman kullanmalıyız ?
IgorGanapolsky

5

Satır içi değiştiriciyi kullandığımızdaki en önemli durum, kullanım benzeri işlevleri parametre işlevleriyle tanımladığımız zamandır. Toplama veya dizi işleme (gibi filter, mapveya joinToString) veya yalnızca bağımsız işlevler mükemmel bir örnektir.

Bu nedenle satır içi değiştirici, kitaplık geliştiricileri için çoğunlukla önemli bir optimizasyondur. Nasıl çalıştığını ve iyileştirmelerinin ve maliyetlerinin neler olduğunu bilmeleri gerekir. Kendi kullanım fonksiyonumuzu fonksiyon tipi parametreleri ile tanımlarken projelerimizde satır içi değiştiriciyi kullanmalıyız.

İşlev türü parametresine, reified type parametresine sahip değilsek ve yerel olmayan dönüşe ihtiyacımız yoksa, büyük olasılıkla satır içi değiştiriciyi kullanmamalıyız. Bu nedenle Android Studio veya IDEA IntelliJ'de bir uyarı alacağız.

Ayrıca bir kod boyutu sorunu var. Büyük bir işlevi satır içine almak, bayt kodunun boyutunu önemli ölçüde artırabilir çünkü her arama sitesine kopyalanır. Bu gibi durumlarda, işlevi yeniden düzenleyebilir ve kodu normal işlevlere çıkarabilirsiniz.


Ne yerel olmayan dönüş ?
IgorGanapolsky

5

Daha yüksek sıralı işlevler çok faydalıdır ve reusabilitykodu gerçekten geliştirebilirler . Ancak, bunları kullanmanın en büyük endişelerinden biri verimliliktir. Lambda ifadeleri sınıflara (genellikle anonim sınıflara) derlenir ve Java'da nesne oluşturma ağır bir işlemdir. İşlevleri satır içi yaparak tüm faydaları korurken, üst düzey işlevleri etkili bir şekilde kullanmaya devam edebiliriz.

burada satır içi işlevi resme geliyor

Bir işlev olarak işaretlendiğinde inline, kod derleme sırasında derleyici tüm işlev çağrılarını işlevin gerçek gövdesiyle değiştirir. Ayrıca, bağımsız değişkenler olarak sağlanan lambda ifadeleri gerçek gövdeleriyle değiştirilir. İşlevler olarak değil, gerçek kod olarak ele alınacaklar.

Kısaca: - Satır içi -> çağrılmak yerine, derleme zamanında işlevin vücut koduyla değiştirilirler ...

Kotlin'de, bir işlevi başka bir işlevin parametresi olarak kullanmak (üst düzey işlevler olarak adlandırılır), Java'dakinden daha doğaldır.

Yine de lambda kullanmanın bazı dezavantajları vardır. Anonim sınıflar (ve dolayısıyla nesneler) olduklarından, belleğe ihtiyaçları vardır (ve hatta uygulamanızın genel yöntem sayısını artırabilirler). Bundan kaçınmak için yöntemlerimizi satır içi yapabiliriz.

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

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

Yukarıdaki örnekten : - Bu iki işlev tam olarak aynı şeyi yapar - getString işlevinin sonucunu yazdırır. Biri satır içi ve biri değil.

Derlenmiş java kodunu kontrol ederseniz, yöntemlerin tamamen aynı olduğunu görürsünüz. Bunun nedeni, satır içi anahtar sözcüğün, derleyiciye kodu çağrı sitesine kopyalamak için bir talimat olmasıdır.

Bununla birlikte, herhangi bir işlev türünü aşağıdaki gibi başka bir işleve aktarıyorsak:

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

Bunu çözmek için fonksiyonumuzu aşağıdaki gibi yeniden yazabiliriz:

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

Aşağıdaki gibi daha yüksek dereceli bir fonksiyonumuz olduğunu varsayalım:

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

Burada, derleyici bize sadece bir lambda parametresi olduğunda ve onu başka bir işleve geçirdiğimizde inline anahtar kelimesini kullanmamamızı söyleyecektir. Dolayısıyla, yukarıdaki işlevi aşağıdaki gibi yeniden yazabiliriz:

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

Not : -Noinline anahtar sözcüğünü de kaldırmak zorundaydık çünkü yalnızca satır içi işlevler için kullanılabilir!

Bunun gibi bir işleve sahip olduğumuzu varsayalım ->

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

Bu iyi çalışıyor ancak işlevin mantığı ölçüm koduyla kirlenmiş ve iş arkadaşlarınızın neler olup bittiğini anlamasını zorlaştırıyor.:)

Bir satır içi işlevin bu koda nasıl yardımcı olabileceği aşağıda açıklanmıştır:

 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
 }

Şimdi, ölçüm kodu satırlarını atlamadan, intercept () işlevinin ana amacının ne olduğunu okumaya konsantre olabilirim. Ayrıca, bu kodu istediğimiz diğer yerlerde yeniden kullanma seçeneğinden de yararlanıyoruz.

satır içi, lambda benzeri ölçüyü (myLamda) iletmek yerine bir kapanış ({...}) içinde lambda argümanı olan bir işlevi çağırmanıza izin verir

Bu ne zaman faydalıdır?

Inline anahtar sözcüğü, diğer işlevleri veya lambdaları bağımsız değişken olarak kabul eden işlevler için kullanışlıdır.

Bir işlevde satır içi anahtar sözcüğü olmadan, bu işlevin lambda bağımsız değişkeni, derleme zamanında invoke () adlı tek bir yöntemle bir İşlev arabiriminin bir örneğine dönüştürülür ve lambda'daki kod, bu İşlev örneğinde invoke () çağrısı yapılarak yürütülür. işlev gövdesinin içinde.

Bir fonksiyondaki satır içi anahtar kelimeyle, bu derleme zaman dönüşümü asla gerçekleşmez. Bunun yerine, satır içi işlevin gövdesi çağrı sitesine eklenir ve kodu, bir işlev örneği oluşturma ek yükü olmadan yürütülür.

Hmmm? Android'deki örnek ->

Diyelim ki bir aktivite yönlendirici sınıfında bir aktivite başlatmak ve bazı ekstralar uygulamak için bir fonksiyonumuz var

fun startActivity(context: Context,
              activity: Class<*>,
              applyExtras: (intent: Intent) -> Unit) {
  val intent = Intent(context, activity)
  applyExtras(intent)
  context.startActivity(intent)
  }

Bu işlev bir amaç oluşturur, applyExtras işlevi bağımsız değişkenini çağırarak bazı ekstralar uygular ve etkinliği başlatır.

Derlenmiş bayt koduna bakarsak ve onu Java'ya dönüştürürsek, bu şuna benzer:

void startActivity(Context context,
               Class activity,
               Function1 applyExtras) {
  Intent intent = new Intent(context, activity);
  applyExtras.invoke(intent);
  context.startActivity(intent);
  }

Bunu bir etkinlikteki tıklama dinleyiciden çağırdığımızı varsayalım:

override fun onClick(v: View) {
router.startActivity(this, SomeActivity::class.java) { intent ->
intent.putExtra("key1", "value1")
intent.putExtra("key2", 5)
}
 }

Bu tıklama dinleyicisi için derlenmiş bayt kodu daha sonra şuna benzer:

@Override void onClick(View v) {
router.startActivity(this, SomeActivity.class, new Function1() {
@Override void invoke(Intent intent) {
  intent.putExtra("key1", "value1");
  intent.putExtra("key2", 5);
}
 }
}

Tıklama dinleyicisi her tetiklendiğinde yeni bir Function1 örneği oluşturulur. Bu iyi çalışıyor, ancak ideal değil!

Şimdi etkinlik yönlendirici yöntemimize satır içi ekleyelim:

inline fun startActivity(context: Context,
                     activity: Class<*>,
                     applyExtras: (intent: Intent) -> Unit) {
 val intent = Intent(context, activity)
 applyExtras(intent)
 context.startActivity(intent)
 }

Tıklama dinleyici kodumuzu hiç değiştirmeden, artık bu Function1 örneğinin oluşturulmasını önleyebiliyoruz. Tıklama dinleyici kodunun Java eşdeğeri artık şuna benzer:

@Override void onClick(View v) {
Intent intent = new Intent(context, SomeActivity.class);
intent.putExtra("key1", "value1");
intent.putExtra("key2", 5);
context.startActivity(intent);
}

Bu kadar.. :)

Bir işlevi "satır içi" yapmak, temelde bir işlevin gövdesini kopyalayıp işlevin çağrı sitesine yapıştırmak anlamına gelir. Bu, derleme sırasında olur.


Kullanmanın avantajını görmüyorum inline functions. Gerçek dünya kullanım senaryosu verebilir misiniz (yani üzerinde çalıştığınız gerçek bir projede)?
IgorGanapolsky

1
@IgorGanapolsky, android örneğiyle eklendi, bir göz atın
Wini

3

Birini isteyebileceğiniz basit bir durum, bir askıya alma bloğunu alan bir kullanım işlevi oluşturduğunuz zamandır. Bunu düşün.

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

fun logic() { }

suspend fun asyncLogic() { }

fun main() {
    timer { logic() }

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

Bu durumda, zamanlayıcımız askıya alma işlevlerini kabul etmeyecektir. Çözmek için, onu da askıya almak isteyebilirsiniz.

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

Ancak daha sonra yalnızca coroutine / suspend işlevlerinden kullanılabilir. Ardından, bu araçların zaman uyumsuz bir sürümünü ve eşzamansız olmayan bir sürümünü oluşturursunuz. Satır içi yaparsanız sorun ortadan kalkar.

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

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

    launch {
        timer { asyncLogic() }
    }
}

İşte hata durumuna sahip bir kotlin oyun alanı . Çözmek için zamanlayıcıyı sıralı yapın.


Kotlin oyun alanı pasajınız kullanmıyor inline fun. Lütfen açıklar mısın?
IgorGanapolsky

@IgorGanapolsky Yazımın son kısmına tekrar göz atın. Hangisinin satır içi olması gerektiğini söylüyorum ve yukarıdaki cevapta da örnek var.
Anthony Naddeo
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.