Короткий огляд Kotlin і порівняння з C#

Від автора:
Ця стаття являє собою замітки на коліні і є скоріше коротким оглядом Kotlin, включаючи невелике порівняння з мовою С# з точки зору синтаксису. Це моя думка і мої роздуми з приводу цього порівняно молодого мови в світі Java-платформи, який на мій погляд має добрі шанси домогтися успіху.


Kotlin являє собою статично типізований об'єктно-орієнтована мова програмування, компільований для платформ Java (ще і JavaScript). Розробляється з 2010 року компанія JetBrains. Реліз при цьому відбувся не так давно. Автори ставили за мету створити мову більш лаконічний і типобезопасный, ніж Java, і більш простий, ніж Scala. Наслідком спрощення порівняно з Scala стали також більш швидка компіляція і краща підтримка мови в IDE. Крім усього іншого, коли компанія оголосила про розробку даного мови, на неї обрушився шквал критики з приводу того, що краще б розробники довели до розуму плагін для Scala (у якій як я розумію, досі немає нормальної IDE). Однак, для компанії мову програмування є досить важливим інструментом, а розробники Java не зовсім поспішають впроваджувати в мову нову функціональність. І справа навіть не в тому, що цього не хочуть, а з-за того, що занадто багато коду написано і занадто багато систем працює на цій платформі. І ось доводиться тягнути зворотну сумісність за собою як баласт. І навіть якщо у останньої, 8 версії мови і додали нові фічі (як лямбда-вирази, наприклад), то світ Enterprise не кинувся оновлювати 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
}
}

Майже нічого незвичайного, за винятком того, що конструктор позначений ключовим словом constructor. Насправді — це вторинний конструктор з точки зору Kotlin(а), а первинний або основний конструктор є частиною заголовка класу:
class Man constructor(var name: String, var age: Int) 
//або ще можна без ключового слова
class Man (var name: String, var age: Int)

Точної такий же синтаксис еквівалентний кодом, що був описаний раніше. Змінні name і age також присутні в класі і були створені відповідно в первинному конструкторі за допомогою 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>]

Инициализатор, getter і setter необов'язкові, якщо описувати клас, як було показано в першому прикладі. Якщо ж змінну описувати як val, то setter відповідно заборонений. Як описувати властивості:
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 Classes
Представляють інтерес Data Classes. Дані класи використовуються для зберігання даних і більше нічого не роблять. Компілятор автоматично виводить члени з усіх властивостей, заявлених в основному конструкторі:
  • equals()/hashCode()
  • метод toString() форми Man(«Alex», 26)
  • опції для відповідних властивостей в порядку їх оголошення (деструктурированные оголошення)
  • функція copy()
Це надає зручність при роботі з класами подібного типу:
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)
}

На цьому опис класів я б хотів закінчити і перейти до тієї частини мови, яка є його родзинкою.

Functions and Lambdas
Функції 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
}

Функції також можуть бути викликані за допомогою инфиксной нотації, коли:
  • Вони є функціями-членами або функціями розширення
  • Вони мають один параметр
  • Вони відзначені ключовим словом infix
//Визначаємо розширення для 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 }

В круглих дужках завжди вказуються параметри, які потім передаються в тіло за допомогою ->.
Одна річ, яка відсутня в синтаксисі лямбда-вирази, це можливість вказати тип значення, що повертається. У більшості випадків це зайве, тому що повертається тип може бути виведений автоматично. Однак, якщо його треба явно вказати, можна використовувати альтернативний синтаксис:анонімна функція.
fun(x: Int, y: Int): Int = x + y

//альтернативний варіант
val listEven = list.filter(fun(item) = item % 2 == 0)

Карринг і часткове застосування функції
Розглянемо в якості прикладу карринг і часткове застосування функції і порівняємо реалізацію на Kotlin і C#.
Деякі люди іноді плутають (та і я деякий час назад) терміни карринг часткове застосування функції і використовують їх як взаємозамінні. І карринг і часткове застосування це способи перетворення одного виду функції в інший.

Часткове застосування функції

Часткове застосування бере функцію з параметрами N і значення для одного з цих параметрів і повертає функцію з N-1 параметрами, таку, що, будучи викликаною, вона збере всі необхідні значення (перший аргумент, переданий самої функції часткового застосування, і решта N-1 аргументи передані повертається функції). Таким чином, ці два виклики повинні бути еквівалентні методом з трьома параметрами. На 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 © => 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# означає, що необхідна реалізація методу для кожного делегата, який ми хочемо використовувати. Для цього, можливо, буде потрібно сімейство Action.
Приклад коду на 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 це робити зручніше за рахунок більш компактного синтаксису.

Карринг

У той час як часткове застосування перетворює функцію з N параметрів у функцію з N-1 параметрами, застосовуючи один аргумент, карринг декомпозирует функцію на функції від одного аргументу. Ми не передаємо ніяких додаткових аргументів на метод Curry, крім перетворюваної функції:
  • Curry(f) повертає функцію f1, таку що…
  • f1(a) повертає функцію f2, таку що…
  • f2(b) повертає функцію f3, таку що…
  • f3© викликає f(a, b, c)
Реалізація на 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();
}
}

Код на Kotlin:
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)
}

Inline function
Використання вищих функцій призводить до накладних витрат. Виділення пам'яті, на об'єкти функцій, а також подальша очищення. У багатьох випадках такого роду витрати можуть бути усунені шляхом підстановки лямбда-виразів. Розглянемо функцію, яка приймає в якості параметрів функцію, приймає об'єкт блокування і функції, отримує блокування, виконує функції і знімає блокування:
fun <T> lock(lock: Lock, body: () -> T): T {
lock.lock()
try {
return body()
}
finally {
lock.unlock()
}
}

Однак при виклику відбувається створення об'єкта. Замість створення об'єкта, компілятор може вставити наступний код:
l.lock()
try {
foo()
}
finally {
l.unlock()
}

Щоб змусити компілятор це зробити, необхідно додати в оголошенні методу модифікатор inline:
inline fun lock<T>(lock: Lock, body: () -> T): T {
// ...
}

Однак не варто вбудовувати великі функції, це може позначитися на продуктивності. Якщо є необхідність у тому, щоб відбувалося вбудовування не всіх функцій, можна додати модифікатор noinline:
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
// ...
}


Висновок...
Kotlin досить цікавий мову, який вивчати одне задоволення. Мені подобається його компактний синтаксис і ті широкі можливості, які він надає. Окремою заслугою варто згадати той факт, що його можна використовувати разом з Java в одному проекті, що теж досить цікаво і дає більшу гнучкість при створенні проекту. Ця мова дозволяє швидко розробити програму і причому зробити це досить красиво. Схожий синтаксис з тим же З# робить його в освоєнні ще простіше, ну і приємніше. Тому якщо комусь раптом захочеться перейти на платформу Java з платформи .NET, ця мова, можливо, залишить приємні враження.

P. S. цікаво думку з приводу цієї мови як Java-програмістів, так і C#. Стали б Ви використовувати Kotlin у своїх проектах?

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.