コトリンを詳しく見てみましょう

画像







https://trends.google.com/trends/explore?q=%2Fm%2F0_lcrx4







上記は「kotlin」という単語を検索したときのGoogleトレンドのスクリーンショットです。 突然の急増は、 GoogleがKotlinがAndroidの主要言語になっていることを発表したときです 。 これは数週間前のGoogle I / Oカンファレンスで起こりました。 これまで、あなたはあなたの周りの誰もが突然それについて話し始めたので、あなたは以前にこの言語を使用したか、またはそれに興味を持ちました。







Kotlinの主な機能の1つは、Javaとの相互運用性です。JavaからKotlinコードを呼び出し、KotlinからJavaコードを呼び出すことができます。 これはおそらく、言語が広く配布されているため、最も重要な機能です。 すべてをすぐに移行する必要はありません。既存のコードベースの一部を取得して、Kotlinコードの追加を開始するだけで機能します。 Kotlinを試してみて、気に入らない場合は、いつでも拒否できます(ただし、試してみることをお勧めします)。







Javaで5年間働いた後、初めてKotlinを使用したとき、いくつかのことが魔法のように思えました。







「ちょっと待って、何? 定型コードを回避するためにdata class



を記述することはできますか?」

「やめて、 apply



を書いたら、それに関してメソッドを呼び出すたびにオブジェクトを定義する必要はありませんか?」







時代遅れで面倒ではない言語がようやく登場したという事実からの最初の安ighのため、私は不快感を感じ始めました。 Javaとの相互互換性が必要な場合、これらすべての優れた機能はKotlinでどの程度正確に実装されますか? キャッチは何ですか?







この記事は専用です。 Kotlinコンパイラーが特定のコンストラクトをどのように変換してJavaとの互換性を持たせるかを学ぶことに非常に興味がありました。 私の研究では、Kotlin標準ライブラリから最も一般的な4つの方法を選択しました。







  1. apply



  2. with



  3. let



  4. run





この記事を読むと、恐れる必要がなくなります。 今、私はすべてがどのように機能するかを理解し言語とコンパイラを信頼できることを知っているので、はるかに自信を感じています。







適用する



 /** *    [block]   `this`        `this`. */ @kotlin.internal.InlineOnly public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
      
      





apply



は単純です。これは、拡張型インスタンス(「レシーバー」と呼ばれる)に関してブロックパラメーターを実行し、レシーバー自体を返す拡張関数です。







この機能を適用するには多くの方法があります。 オブジェクトの作成を初期構成にバインドできます。







val layout = LayoutStyle().apply { orientation = VERTICAL }









ご覧のように、作成時に新しいLayoutStyle



構成を提供します。これにより、 コードクリーンになり、 実装のエラーがはるかに少なくなります。 同じ名前であるため、間違ったインスタンスでメソッドを呼び出しましたか? さらに悪いことに、リファクタリングに完全に欠陥があったときは? 上記のアプローチでは、このような問題に直面するのがはるかに困難になります。 また、 this



パラメーターを定義する必要がないことに注意してください。 クラス自体と同じスコープ内にいます 。 クラス自体を拡張しているかのようであるため、 this



は暗黙的に指定されます。







しかし、それはどのように機能しますか? 短い例を見てみましょう。







 enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = HORIZONTAL } fun main(vararg args: Array<String>) { val layout = LayoutStyle().apply { orientation = VERTICAL } }
      
      





IntelliJ IDEA Show Kotlinバイトコードツール([ Tools > Kotlin > Show Kotlin Bytecode



Kotlinバイトコードの表示])のおかげで、コンパイラがコードをJVMバイトコードに変換する方法を確認できます。







 NEW kotlindeepdive/LayoutStyle DUP INVOKESPECIAL kotlindeepdive/LayoutStyle.<init> ()V ASTORE 2 ALOAD 2 ASTORE 3 ALOAD 3 GETSTATIC kotlindeepdive/Orientation.VERTICAL : Lkotlindeepdive/Orientation; INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (Lkotlindeepdive/Orientation;)V ALOAD 2 ASTORE 1
      
      





バイトコードの指向があまりよくない場合は、 これらの 素晴らしい 記事を読むことをお勧めします。その後、よりよく理解することができます(各メソッドはスタックで呼び出されるため、コンパイラは毎回オブジェクトをロードする必要があることに注意することが重要です)。







ポイントを分析しましょう:







  1. LayoutStyle



    新しいインスタンスLayoutStyle



    スタックに複製LayoutStyle



    ます。
  2. パラメーターがゼロのコンストラクターが呼び出されます。
  3. ストア/ロード操作が実行されます(以下を参照)。
  4. Orientation.VERTICAL



    スタックに渡されます。
  5. setOrientation



    setOrientation



    れ、スタックからオブジェクトと値が発生します。


ここで注意すべき点がいくつかあります。 まず、魔法は一切関係なく、すべてが期待どおりに行わsetOrientation



ます。作成したLayoutStyle



インスタンスに関連して、 LayoutStyle



メソッドがsetOrientation



ます。 さらに、コンパイラーがインライン化するため、 apply



関数はどこにも表示されません。







さらに、バイトコードは、Javaのみを使用した場合に生成されるものとほぼ同じです! 自分で判断する:







 // Java enum Orientation { VERTICAL, HORIZONTAL; } public class LayoutStyle { private Orientation orientation = HORIZONTAL; public Orientation getOrientation() { return orientation; } public void setOrientation(Orientation orientation) { this.orientation = orientation; } public static void main(String[] args) { LayoutStyle layout = new LayoutStyle(); layout.setOrientation(VERTICAL); } } // Bytecode NEW kotlindeepdive/LayoutStyle DUP ASTORE 1 ALOAD 1 GETSTATIC kotlindeepdive/Orientation.VERTICAL : kotlindeepdive/Orientation; INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (kotlindeepdive/Orientation;)V
      
      





ヒント:多数のASTORE/ALOAD



気づいたかもしれません。
これらはKotlinコンパイラーによって挿入されるため、デバッガーはラムダでも機能します! これについては、記事の最後のセクションで説明します。









 /** *    [block]   [receiver]       . */ @kotlin.internal.InlineOnly public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
      
      





apply



に似ていapply



、いくつかの重要な違いがあります。 まず、 with



型の拡張関数ではありません。レシーバーはパラメーターとして明示的に渡される必要があります。 さらに、 with



ブロック関数の結果を返し、 apply



はレシーバー自体の結果を返します。







何でも返すことができるので、この例は非常に信じられそうです。







 val layout = with(contextWrapper) { // `this` is the contextWrapper LayoutStyle(context, attrs).apply { orientation = VERTICAL } }
      
      





ここでは、 contextWrapper



プレフィックスを省略できます。 contextWrapper



with



関数のレシーバーであるため、 context



およびattrs



場合。 しかし、この場合でも、適用方法はapply



と比較するとそれほど明白ではありません。この関数は特定の条件下で役立つ場合があります。







これが与えられたので、例に戻り、以下で使用with



場合に何が起こるかを見てみましょう:







 enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = HORIZONTAL } object SharedState { val previousOrientation = VERTICAL } fun main() { val layout = with(SharedState) { LayoutStyle().apply { orientation = previousOrientation } } }
      
      





with



レシーバーはSharedState



シングルトンであり、レイアウトに設定する方向パラメーターが含まれています。 ブロック関数内で、 LayoutStyle



インスタンスを作成します。 apply



おかげで、 SharedState



から読み取ることで簡単に方向を設定できapply









生成されたバイトコードをもう一度見てみましょう。







 GETSTATIC kotlindeepdive/SharedState.INSTANCE : Lkotlindeepdive/SharedState; ASTORE 1 ALOAD 1 ASTORE 2 NEW kotlindeepdive/LayoutStyle DUP INVOKESPECIAL kotlindeepdive/LayoutStyle.<init> ()V ASTORE 3 ALOAD 3 ASTORE 4 ALOAD 4 ALOAD 2 INVOKEVIRTUAL kotlindeepdive/SharedState.getPreviousOrientation ()Lkotlindeepdive/Orientation; INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (Lkotlindeepdive/Orientation;)V ALOAD 3 ASTORE 0 RETURN
      
      





特別なことは何もありません。 SharedState



クラスの静的フィールドとして実装された取得されたシングルトン。 以前と同様にLayoutStyle



のインスタンスが作成され、コンストラクターが呼び出されますSharedState



内でpreviousOrientation



値を取得する別の呼び出しSharedState



およびLayoutStyle



インスタンスに値を割り当てる最後の呼び出し。







ヒント:「Show Kotlin Bytecode」を使用する場合、「Decompile」をクリックすると、Kotlinコンパイラー用に作成されたバイトコードのJava表現を確認できます。 ネタバレ:それはまさにあなたが期待するものです!







させる



 /** *    [block]   `this`      . */ @kotlin.internal.InlineOnly public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
      
      





let



、nullになる可能性があるオブジェクトを操作するときに非常に便利です。 if-else式の無限のチェーンを作成する代わりに、単純にステートメントを結合できます?



(「安全な呼び出しステートメント」と呼ばれる) let



を使用すると、結果として、引数が元のオブジェクトのnull不可バージョンであるラムダを取得します。







 val layout = LayoutStyle() SharedState.previousOrientation?.let { layout.orientation = it }
      
      





例全体を考えてみましょう。







 enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = HORIZONTAL } object SharedState { val previousOrientation: Orientation? = VERTICAL } fun main() { val layout = LayoutStyle() // layout.orientation = SharedState.previousOrientation -- this would NOT work! SharedState.previousOrientation?.let { layout.orientation = it } }
      
      





これで、 previousOrientation



はnullになる場合があります。 レイアウトに直接割り当てようとすると、null許容型をnon-null許容型に割り当てることができないため、コンパイラが怒ります。 もちろん、if式を記述できますが、これによりSharedState.previousOrientation



式への二重参照がSharedState.previousOrientation



ます。 let



を使用すると、レイアウトに安全に割り当てることができる同じパラメーターへのnull不可の参照を取得します。

バイトコードの観点から見ると、すべてが非常に単純です。







 NEW kotlindeepdive/let/LayoutStyle DUP INVOKESPECIAL kotlindeepdive/let/LayoutStyle.<init> ()V GETSTATIC kotlindeepdive/let/SharedState.INSTANCE : Lkotlindeepdive/let/SharedState; INVOKEVIRTUAL kotlindeepdive/let/SharedState.getPreviousOrientation ()Lkotlindeepdive/let/Orientation; DUP IFNULL L2 ASTORE 1 ALOAD 1 ASTORE 2 ALOAD 0 ALOAD 2 INVOKEVIRTUAL kotlindeepdive/let/LayoutStyle.setOrientation (Lkotlindeepdive/let/Orientation;)V GOTO L9 L2 POP L9 RETURN
      
      





単純なIFNULL



条件付き遷移を使用します。これは、本質的には手動で行う必要があります。ただし、コンパイラが効率的に実行する場合を除き、言語はこのようなコードを作成する優れた方法を提供します。 これは素晴らしいと思います!







走る



runには2つのバージョンがあります。1つ目は単純な関数、2つ目はジェネリック型の拡張関数です。 最初の関数はパラメーターとして渡されるブロック関数のみを呼び出すため、2番目の関数を分析します。







 /** *    [block]   `this`      . */ @kotlin.internal.InlineOnly public inline fun <T, R> T.run(block: T.() -> R): R = block()
      
      





おそらく、 run



は、考えられる最も単純な関数です。 これは、インスタンスがレシーバーとして渡されるタイプの拡張関数として定義され、 block



関数の実行結果を返します。 run



let



apply



ハイブリッドのように思えるかもしれませんが、実際はそうです。 唯一の違いは戻り値です。 apply



の場合は受信者自身を返し、 run



の場合はblock



関数の結果を返します( let



の場合と同様)。







この例では、 run



block



関数の結果を返すという事実を強調してrun



ます。この場合、それは割り当て( Unit



)です。







 enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = HORIZONTAL } object SharedState { val previousOrientation = VERTICAL } fun main() { val layout = LayoutStyle() layout.run { orientation = SharedState.previousOrientation } // returns Unit }
      
      





同等のバイトコード:







 NEW kotlindeepdive/LayoutStyle DUP INVOKESPECIAL kotlindeepdive/LayoutStyle.<init> ()V ASTORE 0 ALOAD 0 ASTORE 1 ALOAD 1 ASTORE 2 ALOAD 2 GETSTATIC kotlindeepdive/SharedState.INSTANCE : Lkotlindeepdive/SharedState; INVOKEVIRTUAL kotlindeepdive/SharedState.getPreviousOrientation ()Lkotlindeepdive/Orientation; INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (Lkotlindeepdive/Orientation;)V RETURN
      
      





run



は他の関数と同様にインラインであり、単純なメソッド呼び出しになりました。 ここでも奇妙なことはありません!










標準ライブラリの機能には多くの類似点があることに注意しました。これは、できるだけ多くのアプリケーションをカバーするために意図的に行われました。 一方、特定のタスクにどの関数が最適であるかを理解することは、それらの違いがわずかであることを考えると、それほど簡単ではありません。







標準ライブラリを扱うのを助けるために、考慮された主な機能間のすべての違いを要約した表を描きました(例外もあります ):







画像







アプリケーション:追加のstore/load



操作



「Javaバイトコード」と「Kotlinバイトコード」を比較すると、まだ完全に理解できませんでした。 私が言ったように、Kotlinでは、Javaとは異なり、追加の操作astore/aload



。 これはラムダと関係があることは知っていましたが、なぜラムダが必要なのか理解できました。







デバッガーがラムダをスタックフレームとして処理するには、これらの追加の操作が必要であるように思わます 。これにより、作業に介入(ステップイン)できます。 ローカル変数が何であるか、誰がラムダを呼び出しているか、誰がラムダから呼び出されるかなどを確認できます。







しかし、APKを実稼働環境に渡すとき、デバッガの機能は気にしませんか? そのため、これらの関数は、サイズが小さく重要でないにもかかわらず、冗長であり、削除される可能性があると考えることができます。







このためには、すべての人に知られ、誰もが愛するツールであるProGuardが適しています。 バイトコードレベルで動作し、難読化とトリミングに加えて、最適化パスを実行してバイトコードをよりコンパクトにします。 JavaとKotlinで同じコードを作成し、1つのルールセットを使用してProGuardの両方のバージョンに適用し、結果を比較しました。 これが何が起こったのかです。







ProGuardの構成



 -dontobfuscate -dontshrink -verbose -keep,allowoptimization class kotlindeepdive.apply.LayoutStyle -optimizationpasses 2 -keep,allowoptimization class kotlindeepdive.LayoutStyleJ
      
      





ソースコード



Java:







 package kotlindeepdive enum OrientationJ { VERTICAL, HORIZONTAL; } class LayoutStyleJ { private OrientationJ orientation = HORIZONTAL; public OrientationJ getOrientation() { return orientation; } public LayoutStyleJ() { if (System.currentTimeMillis() < 1) { main(); } } public void setOrientation(OrientationJ orientation) { this.orientation = orientation; } public OrientationJ main() { LayoutStyleJ layout = new LayoutStyleJ(); layout.setOrientation(VERTICAL); return layout.orientation; } }
      
      





コトリン:







 package kotlindeepdive.apply enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = Orientation.HORIZONTAL init { if (System.currentTimeMillis() < 1) { main() } } fun main() { val layout = LayoutStyle().apply { orientation = Orientation.VERTICAL } layout.orientation } }
      
      





バイトコード



Java:







  sgotti@Sebastianos-MBP ~/Desktop/proguard5.3.3/lib/PD/kotlindeepdive > javap -c LayoutStyleJ.class Compiled from "SimpleJ.java" final class kotlindeepdive.LayoutStyleJ { public kotlindeepdive.LayoutStyleJ(); Code: 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: aload_0 5: getstatic #6 // Field kotlindeepdive/OrientationJ.HORIZONTAL$5c1d747f:I 8: putfield #5 // Field orientation$5c1d747f:I 11: invokestatic #9 // Method java/lang/System.currentTimeMillis:()J 14: lconst_1 15: lcmp 16: ifge 34 19: new #3 // class kotlindeepdive/LayoutStyleJ 22: dup 23: invokespecial #10 // Method "<init>":()V 26: getstatic #7 // Field kotlindeepdive/OrientationJ.VERTICAL$5c1d747f:I 29: pop 30: iconst_1 31: putfield #5 // Field orientation$5c1d747f:I 34: return }
      
      





コトリン:







  sgotti@Sebastianos-MBP ~/Desktop/proguard5.3.3/lib/PD/kotlindeepdive > javap -c apply/LayoutStyle.class Compiled from "Apply.kt" public final class kotlindeepdive.apply.LayoutStyle { public kotlindeepdive.apply.LayoutStyle(); Code: 0: aload_0 1: invokespecial #13 // Method java/lang/Object."<init>":()V 4: aload_0 5: getstatic #11 // Field kotlindeepdive/apply/Orientation.HORIZONTAL:Lkotlindeepdive/apply/Orientation; 8: putfield #10 // Field orientation:Lkotlindeepdive/apply/Orientation; 11: invokestatic #14 // Method java/lang/System.currentTimeMillis:()J 14: lconst_1 15: lcmp 16: ifge 32 19: new #8 // class kotlindeepdive/apply/LayoutStyle 22: dup 23: invokespecial #16 // Method "<init>":()V 26: getstatic #12 // Field kotlindeepdive/apply/Orientation.VERTICAL:Lkotlindeepdive/apply/Orientation; 29: putfield #10 // Field orientation:Lkotlindeepdive/apply/Orientation; 32: return }
      
      





2つのバイトコードリストを比較した後の結論:







  1. 「Kotlinバイトコード」の追加のastore/aload



    は消えましたastore/aload



    はそれらを冗長であると判断し、すぐに削除したためです(このため、1つが削除されなかった後、2つの最適化パスを作成する必要がありました)。
  2. JavaバイトコードとKotlinバイトコードはほぼ同じです。 最初のものは列挙値を操作するときに面白い/奇妙な瞬間がありますが、Kotlinにはそのようなものはありません。


おわりに



開発者に非常に多くの機能を提供する新しい言語を取得することは素晴らしいことです。 しかし、使用するツールに頼ることができることを理解し、それらを使用するときに自信を持っていることも重要です。 コンパイラが余分なことや危険なことを何もしないことを知っているという意味で、「私はKotlinを信頼しています」と言うことができてうれしいです。 Javaで手動で行う必要があることだけを行い、時間とリソースを節約します(そして、JVMのコーディングの長年の喜びを返します)。 これは、エンドユーザーにとってもある程度のメリットがあります。より厳密な型の安全性により、アプリケーションのバグが少なくなるからです。







さらに、Kotlinコンパイラーは絶えず改善されているため、生成されたコードはより効率的になっています。 そのため、コンパイラを使用してKotlinコードを最適化する必要はありません。より効率的で慣用的なコードの作成に専念し、残りはコンパイラに任せることをお勧めします。








All Articles