Kotlinの概要とC#との比較

著者から:



この記事では、膝に関する注意事項を紹介します。構文については、C#言語とのわずかな比較など、Kotlinの簡単な概要です。 これは私の意見であり、Javaプラットフォームの世界でこの比較的若い言語についての私の考えであり、私の意見では成功する可能性が高いです。





Kotlinは、Javaプラットフォーム(およびJavaScript)用にコンパイルされた静的に型指定されたオブジェクト指向プログラミング言語です。 2010年からJetBrainsによって開発されました。 同時リリースはそれほど前ではありませんでした。 著者は、Javaよりも簡潔でタイプセーフで、Scalaよりもシンプルな言語を作成することを目指しました。 また、Scalaと比較して簡素化されたため、IDEでのコンパイルが高速になり、言語サポートが向上しました。 とりわけ、会社がこの言語の開発を発表したとき、開発者がScalaのプラグインを思い起こさせた方が良いとの批判が殺到しました(私が理解すると、まだ通常のIDEはありません)。 しかし、企業にとってプログラミング言語は非常に重要なツールであり、Java開発者は急いで新しい機能を言語に導入することはありません。 そして、ポイントは彼らがそれを望んでいないということではなく、あまりにも多くのコードが書かれており、あまりにも多くのシステムがこのプラットフォームで動作しているからです。 そして今、あなたはバラストのようにあなた自身のために後方互換性を引き出す必要があります。 また、言語の最後の8バージョンで新しい機能(ラムダ式など)が追加されたとしても、エンタープライズの世界はJVMの更新を急ぐことはありませんでした。 経験が示すように、一部のカスタムメイドの企業や企業 、マシンをバージョン7にのみ更新しており、システム内の数百台のマシンをバージョン8に更新することは、顧客の会社にとってあまり便利で高価ではありません。 私の観点からは、開発中の言語のこのような遅延は、十分に開発された強力なツールであり、使用頻度を知ることができると特徴づけています。 ただし、他のJava言語と比較すると、冗長に見える場合がありますが、これは、C#で十分にプログラミングし、たとえばコードをよりコンパクトにする同じLINQ、ラムダ式、その他の構文シュガーパンを使用した人としての私の意見です。

そのため、JetBrainsの人々は、Javaとの完全な互換性を備え、プログラマの日々の作業を簡素化し、生産性を向上させる追加機能を提供する言語を作成することにしました。



知人...



私は偶然彼に出会いました。 Javaでのプログラミングでは、C#の利点を逃しました。どうにかして自分自身を喜ばせ、顧客の要件を満たしたいと思います。 Kotlinのドキュメントを確認した後、これが必要なものであることに気付きました。 150ページのドキュメントは読みやすく、言語は学習しやすく、簡潔です。 ただし、C#との共通点が十分にあり、言語での作業がさらに快適になるという事実について最も気に入っています。 それでも、.NETを忘れたくありません。



グッズ...



クラスを操作する



さて、今から最も興味深いものに移り、言語の機能のいくつかと私がそれについて好きなことを考えてみましょう。

Kotlinでクラスを宣言する方法:

class Man { var name: String //var -   , val -   var age: Int constructor(name: String, age: Int) { this.name = name this.age = age } }
      
      





コンストラクターがコンストラクターキーワードでタグ付けされていることを除いて、ほとんど異常なことはありません。 これは、実際にはKotlin(a)の観点から見るとセカンダリコンストラクターであり、プライマリコンストラクターまたはプライマリコンストラクターはクラスヘッダーの一部です。

 class Man constructor(var name: String, var age: Int) //      class Man (var name: String, var age: Int)
      
      





まったく同じ構文は、前述のコードと同等です。 変数名と年齢もクラスに存在し、それぞれvar(かなり興味深い機能)を使用してプライマリコンストラクターで作成されました。 一見普通ではありませんが、しばらくすると非常に便利だと気づきます。 ただし、メインコンストラクターにはコードを含めることができないため、オブジェクトが作成されるたびに呼び出される初期化ブロック(init)があります。

 class Man (var name: String, var age: Int){ init { //-  } }
      
      





私の意見に興味深い。 コンストラクターのチェーンを作成することもできます。

 class Man (var name: String){ var name: String? = null //,  null,        ,    ,   C# var age: Int = 0 //   ,    , getter  setter    constructor(name: String, age: Int) : this(name) { this.age = age } }
      
      





宣言のプロパティと完全な構文は、ここで興味深い実装がされています。

 var <propertyName>: <PropertyType> [= <property_initializer>] [<getter>] [<setter>]
      
      





最初の例に示すように、クラスを記述する場合、初期化子、ゲッター、およびセッターはオプションです。 変数がvalとして記述されている場合、セッターはそれに応じて禁止されています。 プロパティの説明方法:

 class Man { var name: String get() { return "Name man: $field" //field -   ,     .  getter    , field      } private set(value) { //       field = value } var age: Int constructor(name: String, age: Int) { this.name = name this.age = age } }
      
      





データクラス



興味深いのはデータクラスです。 これらのクラスは、データを保存するために使用され、他には何もしません。 コンパイラは、メインコンストラクターで宣言されたすべてのプロパティから自動的にメンバーを推測します。



これにより、このタイプのクラスを操作するときに便利になります。

 data class Man (var name: String, var age: Int) fun main(args: Array<String>) { var man = Man("Alex", 26) //     new println(man) // Man(name=Alex, age=26) //  val (name, age) = man //  : val name = man.component1(); val age = man.component2(); println(name) // Alex println(age) // 26 // copy() var man2 = man.copy() //  ,   var man2 = man.copy(age = 20) // ,     println(man2) //Man(name=Alex, age=20) }
      
      





このクラスの説明で、私は言語のその部分であるハイライトである部分を終了して次に進みたいと思います。



関数とラムダ



Kotlinの関数はfunキーワードを使用して宣言され、特定のクラスにバインドされることなくグローバルに定義できます。

 fun f1(x: Int): Int { return x * 2 } //  fun f1(x: Int): Int = x * 2 //   fun main(args: Array<String>) { println(f1(5)) // 10 }
      
      





関数は、次の場合に挿入記法を使用して呼び出すこともできます。



 //   Int infix fun Int.extent(x: Int): Int { return this + x } //  infix fun Int.extent(x: Int) = this + x fun main(args: Array<String>) { // -   infix  println(5 extent 10) // 15 //   println(5.extent(10)) }
      
      





関数には、名前付きパラメーターとデフォルトの引数値もあります。

可変数の引数を渡すことができます。

 fun <T> asList(vararg ts: T): List<T> { val result = ArrayList<T>() for (t in ts) // ts is an Array result.add(t) return result } fun main(args: Array<String>) { val list = asList(1, 2, 3) // ,     }
      
      





ローカル関数がサポートされています(C#7.0もこの関数を実装しました)

 fun f1(x: Man): String { fun isTeenager(age: Int): Boolean { return age in 13..19 } if (isTeenager(x.age)) return "Man teenager" return "Man is not a teenager" }
      
      





高階関数とラムダ式



特に興味深いのは、言語のこの部分です。 高階関数は通常、他の関数を引数として取る関数、または結果として別の関数を返す関数と呼ばれます。 さらに、主なアイデアは、関数が他のデータオブジェクトと同じステータスを持つことです。 高階関数を使用すると、計算の複雑さを考慮して、抽象的でコンパクトなプログラムが作成されます。

高階関数の例を考えてみましょう。

 //   ,    ,     fun<T> List<T>.filter(transform: (T) -> Boolean): List<T> { val result = arrayListOf<T>() for (item in this) { if (transform(item)) { result.add(item) } } return result } fun main(args: Array<String>) { val list = arrayListOf(1, 4, 6, 7, 9, 2, 5, 8) val listEven = list.filter { item -> item % 2 == 0 } listEven.forEach { item -> print(item.toString() + " ") } // : 4 6 2 8 }
      
      





このアプローチにより、LINQスタイルのコードを記述できます。

 strings.filter { it.length == 5 }.sortBy { it }.map { it.toUpperCase() }
      
      





ラムダ式の完全な構文は次のとおりです。

 val sum = { x: Int, y: Int -> x + y }
      
      





さらに、追加の注釈を残すと、次のようになります。

 val sum: (Int, Int) -> Int = { x, y -> x + y }
      
      





括弧内には、パラメーターが常に示されており、->を使用して本体に転送されます。

ラムダ式の構文に欠けているものの1つは、戻り値の型を指定する機能です。 ほとんどの場合、戻り値の型は自動的に推測されるため、これは不要です。 ただし、明示的に指定する必要がある場合は、別の構文、 匿名関数を使用できます。

 fun(x: Int, y: Int): Int = x + y //  val listEven = list.filter(fun(item) = item % 2 == 0)
      
      





関数のカリー化と部分的な適用



例として関数のカリー化と部分的な適用を検討し、KotlinとC#の実装を比較します。

一部の人々 は、関数の カリー化部分的な使用という用語を混同し(そして、少し前にやった)、それらを交換可能に使用します。 カリー化と部分適用の両方は、ある種の機能を別の種類に変換する方法です。



部分関数の使用



部分的なアプリケーションは、N個のパラメーターとこれらのパラメーターのいずれかの値を持つ関数を取り、N-1パラメーターを持つ関数を返します。そのため、呼び出されると、必要なすべての値(部分アプリケーション関数自体に渡される最初の引数と、残りのN-1戻り関数に渡される引数)。 したがって、これらの2つの呼び出しは、3つのパラメーターを持つメソッドと同等でなければなりません。 C#では、これにデリゲートが使用されます。 もちろん、これらは高階関数の完全な代替物ではありませんが、実証するには十分です。

  class Program { static Int32 SampleFunc(Int32 a, Int32 b, Int32 c) { return a + b + c; } //  ApplyPartial             static Func<T2, T3, TResult> ApplyPartial<T1, T2, T3, TResult> (Func<T1, T2, T3, TResult> function, T1 arg1) { return (b, c) => function(arg1, b, c); } static Func<T3, TResult> ApplyPartial<T2, T3, TResult> (Func<T2, T3, TResult> function, T2 arg2) { return (c) => function(arg2, c); } static Func<TResult> ApplyPartial<T3, TResult> (Func<T3, TResult> function, T3 arg3) { return () => function(arg3); } static void Main(string[] args) { Func<Int32, Int32, Int32, Int32> function = SampleFunc; Func<Int32, Int32, Int32> partial1 = ApplyPartial(function, 1); Func<Int32, Int32> partial2 = ApplyPartial(partial1, 2); Func<Int32> partial3 = ApplyPartial(partial2, 3); var resp = partial3(); //      Console.WriteLine(resp); Console.ReadKey(); } }
      
      





一般化により、ApplyPatrialメソッドは実際よりも見た目が難しくなります。 C#に高次型が存在しないということは、使用するデリゲートごとにメソッド実装が必要であることを意味します。 これには、アクションファミリが必要になる場合があります。

Kotlinサンプルコード:

 fun sampleFunc(a: Int, b: Int, c: Int): Int { return a + b + c } fun f3(a: Int, b: Int): Int { return sampleFunc(a, b, 3) } fun f2(a: Int): Int { return f1(a, 2) } fun f1(): Int { return f2(1) } //    - val sampleFunc = { a: Int, b: Int, c: Int -> a + b + c } val f3 = { a: Int, b: Int -> sampleFunc(a, b, 3) } val f2 = { a: Int -> f3(a, 2) } val f1 = { -> f2(1) } fun main(args: Array<String>) { println(f1()) // 6 }
      
      





Kotlinでは、C#と同様に、N-1個の引数を持つ関数を取得するために、別個の関数(オブジェクト)を作成する必要があります。 言語のアプローチは同じですが、Kotlinでのみ構文がコンパクトであるため、これを行う方が便利です。



キャリング



部分アプリケーションは、1つの引数を使用してN個のパラメーターを持つ関数をN-1個のパラメーターを持つ関数に変換しますが、カリー化は関数を1つの引数の関数に分解します。 変換された関数を除いて、Curryメソッドに追加の引数を渡しません。



C#での実装は次のようになります。

  class Program { static Int32 SampleFunc(Int32 a, Int32 b, Int32 c) { return a + b + c; } static Func<T1, Func<T2, Func<T3, TResult>>> Curry<T1, T2, T3, TResult> (Func<T1, T2, T3, TResult> function) { return a => b => c => function(a, b, c); } static void Main(string[] args) { Func<Int32, Int32, Int32, Int32> function = SampleFunc; //    Func<Int32, Func<Int32, Func<Int32, Int32>>> f1 = Curry(function); Func<Int32, Func<Int32, Int32>> f2 = f1(1); Func<Int32, Int32> f3 = f2(2); Int32 result = f3(3); //     ... var curried = Curry(function); result = curried(1)(2)(3); Console.WriteLine(result); // 6 Console.ReadKey(); } }
      
      





コトリンコード:

 fun curry(body: (a: Int, b: Int, c: Int) -> Int): (Int) -> (Int) -> (Int) -> Int { return fun(a: Int): (Int) -> (Int) -> Int { return fun(b: Int): (Int) -> Int { return fun(c: Int): Int = body(a, b, c) } } } //   fun curry(body: (a: Int, b: Int, c: Int) -> Int) = fun(a: Int) = fun(b: Int) = fun(c: Int) = body(a, b, c) fun main(args: Array<String>) { val f = curry { a: Int, b: Int, c: Int -> a + b + c } val response = f(1)(1)(1) println(response) }
      
      





インライン関数



より高い関数を使用すると、オーバーヘッドが発生します。 関数オブジェクトのメモリの割り当て、およびその後のクリーニング。 多くの場合、この種のオーバーヘッドは、ラムダ式を置き換えることで排除できます。 パラメーターとして関数を受け取り、ロックオブジェクトと関数を受け取り、ロックを受け取り、関数を実行し、ロックを解除する関数を考えます。

 fun <T> lock(lock: Lock, body: () -> T): T { lock.lock() try { return body() } finally { lock.unlock() } }
      
      





ただし、呼び出されると、オブジェクトが作成されます。 オブジェクトを作成する代わりに、コンパイラは次のコードを挿入できます。

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





コンパイラーにこれを強制するには、メソッド宣言にインライン修飾子を追加する必要があります。

 inline fun lock<T>(lock: Lock, body: () -> T): T { // ... }
      
      





ただし、大きな関数を埋め込まないでください。これはパフォーマンスに影響する場合があります。 すべての関数を埋め込む必要がない場合は、noinline修飾子を追加できます。

 inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { // ... }
      
      







結論...



コトリンは、学ぶのが楽しい面白い言語です。 コンパクトな構文とそれが提供する幅広い可能性が気に入っています。 別のメリットとして、1つのプロジェクトでJavaと一緒に使用できるという事実に言及する価値があります。これは非常に興味深いことであり、プロジェクトを作成する際に非常に柔軟性があります。 この言語を使用すると、プログラムをすばやく開発し、非常にうまく実行できます。 同じC#を使用した同様の構文により、学習がさらに容易になり、より優れたものになります。 したがって、誰かが突然.NETプラットフォームからJavaプラットフォームに切り替えたい場合、この言語は良い印象を残すかもしれません。



PS Javaプログラマーとしてのこの言語とC#についての興味深い意見。 プロジェクトでKotlinを使用しますか?



All Articles