Pull to refresh

Swift vs. Kotlin. Отличия важны

Reading time 8 min
Views 38K

Этот пост является вольным переводом статьи Swift vs. Kotlin — the differences that matter by Krzysztof Turek


Вы наверняка видели это сравнение Swift и Kotlin: http://nilhcem.com/swift-is-like-kotlin/. Довольно интересное, правда? Я согласен, что в этих языках много схожего, но в этой статье я обращу внимание на некоторые аспекты, которые их все-таки разнят.


Я занимаюсь Android-разработкой с 2013 и большую часть времени разрабатывал приложения на Java. Недавно же у меня появилась возможность попробовать iOS и Swift. Я был впечатлен тем, что на Swift получается писать очень клевый код. Если вы приложите усилия — ваш код будет похож на поэму.


Через семь месяцев я вернулся к Android. Но вместо Java начал кодить на Kotlin. Google объявил на Google IO 2017, что Kotlin теперь официальный язык для Android. И я решил учить его. Мне не понадобилось много времени, чтобы заметить сходство между Kotlin и Swift. Но я бы не сказал, что они очень похожи. Ниже я покажу отличия между ними. Я не буду описывать все, а только те, которые мне интересны. Рассмотрим примеры.


Структуры vs. Data-классы. Значения и ссылки


Структуры и Data-классы — упрощенные версии классов. Они похожи в использовании, выглядит это так


Kotlin:


data class Foo(var data: Int)

Swift:


struct Foo {
    var data: Int
}

Но класс по прежнему остается классом. Этот тип передается по ссылке. А вот структура — по значению. "И что?" спросите вы. Я объясню на примере.


Давайте создадим наш data-класс в Kotlin и структуру в Swift, а затем сравним результаты.


Kotlin:


var foo1 = Foo(2)
var foo2 = foo1
foo1.data = 4

Swift:


var foo1 = Foo(data: 2)
var foo2 = foo1
foo1.data = 4

Чему равно data для foo2 в обоих случаях? Ответ 4 для data-класса Kotlin и 2 для структуры на Swift.



Результаты отличаются, потому что var foo2 = foo1 в Swift создает копию экземпляра структуры (детальнее тут), а в Kotlin — еще одну ссылку на тот же объект (детальнее тут)


Если вы работаете с Java, вы вероятно знакомы с паттерном Defensive Copy. Если нет — наверстаем упущенное. Здесь вы найдете больше информации по теме.


В общем: существует возможность изменения состояния объекта изнутри или извне. Первый вариант — предпочтительнее и более распространен, а вот второй — нет. Особенно когда вы работаете со ссылочным типом и не ожидаете изменений его состояния. Это может осложнить поиск багов. Для предотвращения этой проблемы, вам следует создавать защищенную копию мутабельного объекта перед тем как передавать его куда-либо. Kotlin гораздо полезнее в таких ситуациях, чем Java, но по неосторожности все еще могут возникать проблемы. Рассмотрим простой пример:


data class Page(val title: String)
class Book {
    val pages: MutableList<Page> = mutableListOf(Page(“Chapter 1”), Page(“Chapter 2”))
}

Я объявил pages как MutableList, потому что хочу их менять внутри этого объекта (добавлять, удалять и т.п.). Pages не private, потому что мне нужен доступ к их состоянию извне. Пока все идет нормально.


val book = Book()
print(“$book”) // Book(pages=[Page(title=Chapter 1), Page(title=Chapter 2)])

Теперь у меня есть доступ к текущему состоянию книги:


val bookPages = book.pages

Я добавляю новую страницу в bookPages:


bookPages.add(Page(“Chapter 3”))

К сожалению, я также изменил состояние исходной книги. А это совсем не то, чего я хотел.


print(“$book”) // Book(pages=[Page(title=Chapter 1), Page(title=Chapter 2), Page(title=Chapter 3)])

Мы можем воспользоваться защищенной копией, чтобы избежать этого. Это очень легко в Kotlin.


book.pages.toMutableList()

Теперь у нас все хорошо. :)


А что же Swift? Тут все работает из коробки. Да, массивы — это структуры. Структуры передаются по значению, как мы уже упоминали выше, поэтому когда вы пишете:


var bookPages = book.pages

вы работаете с копией списка страниц.


Таким образом мы имеем дело с передачей данных по значению. Это очень важно для понимания отличий, если вы не хотите испытывать головную боль во время отладки. :) Многие "объекты" являются структурами в Swift, например Int, CGPoint, Array и т.п.


Интерфейсы и Протоколы и Расширения


Это моя любимая тема. :D


Начнем со сравнения интерфейса и протокола. В принципе, они идентичны.


  • Оба могут требовать реализации определенных методов в классе/структуре;
  • Оба могут требовать объявления определенного свойства. Свойство может быть доступным на чтение/запись или только на чтение.
  • Оба* позволяют добавить реализацию метода по-умолчанию.

Кроме того, для протоколов может потребоваться определенный инициализатор (конструктор в Kotlin).


Kotlin:


interface MyInterface {
    var myVariable: Int
    val myReadOnlyProperty: Int

    fun myMethod()
    fun myMethodWithBody() {
        // implementation goes here
    }
}

Swift:


protocol MyProtocol {
    init(parameter: Int)

    var myVariable: Int { get set }
    var myReadOnlyProperty: Int { get }

    func myMethod()
    func myMethodWithBody()
}

extension MyProtocol {
    func myMethodWithBody() {
        // implementation goes here
    }
}

*Обратите внимание, что вы не можете добавить реализацию метода по-умолчанию прямо внутри протокола. Вот почему я добавил звездочку к последнему пункту списка. Вам нужно добавить расширение для этого. И это хороший способ перейти к более интересной части — расширениям!


Расширения позволяют добавлять функционал к существующим классам (или структурам ;)) не наследуя их. Это так просто. Согласитесь, это крутая возможность.


Это что-то новое для Android-разработчиков, поэтому нам нравится пользоваться этим постоянно! Создавать расширения в Kotlin — не запускать ракеты в космос.


Вы можете создавать расширения для свойств:


val Calendar.yearAhead: Calendar
get() {
    this.add(Calendar.YEAR, 1)
    return this
}

или для функций:


fun Context.getDrawableCompat(@DrawableRes drawableRes: Int): Drawable {
    return ContextCompat.getDrawable(this, drawableRes) ?: throw NullPointerException("Can not find drawable with id = $drawableRes")
}

Как видите, мы не использовали здесь никаких ключевых слов.


В Kotlin есть некоторые предопределенные расширения, которые довольно круты, например "orEmpty()" для опциональных строк:


var maybeNullString: String = null
titleView.setText(maybeNullString.orEmpty())

Это полезное расширение выглядит так:


public inline fun String?.orEmpty(): String = this ?: ""

'?:' пытается получить значение из 'this' (что является текущим значением нашей строки). Если же там будет null, взамен будет возвращена пустая строка.


Так-с, теперь посмотрим на расширения в Swift.


Определение у них то же, поэтому не буду повторяться как заезженная пластинка.


Если вы будете искать расширение подобное "orEmpty()" — у меня для вас плохие новости. Но можно его добавить, не так ли? Давайте попробуем!


extension String? {
    func orEmpty() -> String {
        return self ?? ""
    }
}

но вот что вы увидите:




Опционал в Swift — это generic-перечисление, с заданным типом Wrapped. В нашем случае Wrapped — это строка, поэтому расширение будет выглядеть так:


extension Optional where Wrapped == String {
    func orEmpty() -> String {
        switch self {
        case .some(let value):
            return value
        default:
            return ""
        }
    }
}

и в деле:


let page = Page(text: maybeNilString.orEmpty())

Выглядит сложнее, чем Kotlin-аналог, не так ли? И, к сожалению, есть еще и недостаток. Как вы знаете опционал в Swift — generic-перечисление, поэтому ваше расширение будет доступно для всех опциональных типов. Выглядит не очень хорошо:



Однако компилятор защитит вас и не скомпилирует этот код. Но если вы добавите больше таких расширений — ваша автоподсказка будет забита мусором.


Значит Kotlin-расширения удобнее чем в Swift? Я бы сказал, что расширения в Swift предназначены для других целей ;). Android-разработчики, держитесь!


Протоколы и расширения созданы, чтоб работать вместе. Вы можете создать свой протокол и расширение для класса, чтоб соответствовать этому протоколу. Это звучит безумно, но это еще не все! Есть такая вещь, как условное соответствие протоколу. Это означает, что класс/структура может соответствовать протоколу при выполнении определенных условий.


Допустим у нас есть много мест, где необходимо показать всплывающий alert. Нам нравится принцип DRY и мы не хотим копипастить код. Мы можем решить эту проблему используя протокол и расширение.


Сначала создадим протокол:


protocol AlertPresentable {
    func presentAlert(message: String)
}

Затем, расширение с реализацией по-умолчанию:


extension AlertPresentable {
    func presentAlert(message: String) {
        let alert = UIAlertController(title: “Alert”, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: “OK”, style: .default, handler: nil))
    }
}

Так-с, метод presentAlert только создает alert, но ничего не показывает. Нам нужна ссылка на вью-контроллер для этого. Можем ли мы передать его как параметр в этот метод? Не очень хорошая идея. Давайте воспользуемся условием Where!


extension AlertPresentable where Self: UIViewController {
    func presentAlert(message: String) {
        let alert = UIAlertController(title: “Alert”, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: “OK”, style: .default, handler: nil))
        self.present(alert, animated: true, completion: nil)
    }
}

Что у нас тут? Мы добавили специфическое требование к расширению нашего протокола. Оно предназначено только для UIViewController. Благодаря этому мы можем пользоваться методами UIViewController в методе presentAlert. Это позволяет нам вывести alert на экран.


Идем дальше:


extension UIViewController: AlertPresentable {}

Теперь у всех UIViewController появилась новая возможность:



Также, комбинация протоколов и расширений очень полезна для тестирования. Ребята, сколько раз вы пытались тестировать Android final-класс в своем приложении? Это не проблема для Swift.


Приглядимся к этой ситуации и предположим, что у нас есть final-класс в Swift. Если мы знаем сигнатуру метода, то можем создать протокол с таким же методом, а затем добавить расширение, реализующее этот протокол, к нашему final-классу, и вуаля! Вместо непосредственного использования этого класса — мы можем использовать протокол и легко тестировать. Пример кода вместо тысячи слов.


final class FrameworkMap {
    private init() { … }
    func drawSomething() { … }
}

class MyClass {
    …
    func drawSomethingOnMap(map: FrameworkMap) {
        map.drawSomething()
    }
}

В тесте нам нужно проверить вызывается ли метод drawSomething у объекта map при отработке метода drawSomethingOnMap. Это может быть сложно даже с Mockito (хорошо известной тест-библиотекой для Android). Но с протоколом и расширением — это будет выглядеть так:


protocol Map {
    func drawSomething()
}

extension FrameworkMap: Map {}

И теперь ваш метод drawSomethingOnMap использует протокол вместо класса.


class MyClass {
    …
    func drawSomethingOnMap(map: Map) {
        map.drawSomething()
    }
}

Sealed-классы — перечисления на стероидах


Наконец, я хотел бы упомянуть перечисления.


Нет отличий между Java-перечислениями и Kotlin-перечислениями, поэтому тут мне добавить нечего. Но у нас есть кое-что новое взамен, и это "супер-перечисления" — sealed-классы. Откуда взялось понятие "супер-перечисление"? Обратимся к документации Kotlin:


"… Они, в некотором смысле — расширения для enum-классов: набор возможных значений для перечислений также ограничен, но каждая enum-константа существует только в единственном экземпляре, в то время как наследник sealed-класса может иметь множество экземпляров, которые могут хранить состояние."

Окей, круто, они могут хранить состояние, но как мы можем этим пользоваться?


sealed class OrderStatus {
    object AwaitPayment : OrderStatus()
    object InProgress : OrderStatus()
    object Completed : OrderStatus()
    data class Canceled(val reason: String) : OrderStatus()
}

Это sealed-класс, который является моделью статуса заказа. Очень похоже на то, как мы работаем с перечислениями, но с одной оговоркой. Значение Canceled содержит причину отмены. Причины отмены могут быть разными.


val orderStatus = OrderStatus.Canceled(reason = "No longer in stock")
…
val orderStatus = OrderStatus.Canceled(reason = "Not paid")

Мы не можем делать так с обычными перечислениями. Если значение перечисления создано — его уже не изменить.


Вы обратили внимание, на другие отличия? Я воспользовался еще одной фишкой sealed-класса. Это — связанные данные разных типов. Классическое перечисления предполагает передачу связанных данных для всех вариантов значений перечисления, и все значения должны быть одного и того же типа.


В Swift есть эквивалент sealed-класса и он называется… перечисление. Перечисление в Kotlin — это просто пережиток Java, и 90% времени вы будете пользоваться sealed-классами. Трудно отличить sealed-класс от перечисления Swift. Они отличаются только названием и, конечно же, sealed-класс передается по ссылке, а перечисление в Swift — по значению. Пожалуйста, поправьте меня, если я не прав.


Мы не прощаемся


Существует еще нюансы влияния управления памятью на способ написания кода. Я знаю, что не охватил все аспекты, и это потому, что я еще учусь. Если вы, ребята, заметили какие-либо другие отличия между этими двумя языками — дайте мне знать. Я всегда открыт к новому!

Tags:
Hubs:
+13
Comments 22
Comments Comments 22

Articles