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


    Этот пост является вольным переводом статьи 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 — по значению. Пожалуйста, поправьте меня, если я не прав.


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


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

    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 21
    • +1
      ваше расширение будет доступно для всех опциональных типов. Выглядит не очень хорошо: (...) Однако компилятор защитит вас и не скомпилирует этот код.

      То есть это не особенность языка, и ничего там не будет доступно, а просто просчёт в системе автодополнения, которая не была готова к таким фокусам(?)

      • +1

        Мне тоже не понятен этот момент. Там же чётко написано ограничение


        extension Optional where Wrapped == String { ...

        Я так же могу в котлин написать:


        class Wrapper<T>(val t: T)
        
        fun Wrapper<String>.printMe() {
            println(t)
        }

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

        В котлине автоподсказка мусором не забивается. Может, виновата ide, а не язык?

      • +1
        кстати, для
        extension Optional where Wrapped == String {
            func orEmpty() -> String {
                switch self {
                case .some(let value):
                    return value
                default:
                    return ""
                }
            }
        }

        можно было оставить и изначальную реализацию orEmpty()
        extension Optional where Wrapped == String {
            func orEmpty() -> String {
                return self ?? ""
            }
        }
        • 0

          Точно. Наверное автор оригинального поста так написал, чтоб было более понятно что там в опционале "под капотом" происходит. :)

        • +1
          Это мое больное воображение, или значки в начале правда увеличиваются?
          • 0

            Не обратил внимания, но да. Таки оптическая иллюзия имеет место быть. :)

          • –9

            Бла-бла-бла, сейчас популярен javascript, давайте деградируем до его уровня...

            • 0
              А это совсем не то, чего я хотел.

              Именно — передача по ссылке/значению очень субъективно. Лично на мой взгляд, передача по ссылке по-умолчанию на порядок удобнее и логичнее. Особенно если мои дата-классы иммутабельные. А если надо скопировать, то есть метод `copy` из коробки. Конечно бывают кейзы, когда хотелось бы иметь value-типы и в жвм возможно их скоро завезут, но таких случаев значительно меньше, чем те, что покрываются обычными дата-классами.

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

              Да это приятная фича языка поддерживаемая платформой (в отличии от жвм), только примеры про неё ничего не говорят.
              Это может быть сложно даже с Mockito

              Серьезно? Если у вас есть интерфейс, нет никакой проблемы замокировать его. С файналами не так всё просто конечно, но пример совершенно это не показывает.
              Они отличаются только названием и, конечно же, sealed-класс передается по ссылке, а перечисление в Swift — по значению.

              Сначала опять про ссылки и значения. Для перечислений нет никакой разницы, передавать адрес константы или её номер. А для sealed-классов, та же дилемма, что и в первом пункте. И да, различий нет уж и много, но они есть.
              • 0

                Да, про ссылки/значения — холивар может быть вечным. Вот, например, есть у вас объекты типа «монитор», и у них есть свойство «разрешение». Это свойство состоит из двух полей: ширина, высота. Вопрос: какого типа должно быть свойство «разрешение»? Класс или структура? Должно ли оно передаваться по ссылке или по значению. И если у меня есть два монитора с одинаковым разрешением, то меняя разрешение на одном из них ожидаю ли я, что оно изменится и на втором? :)


                Про Мокито вы невнимательно прочитали. Речь о невозможности замокать именно final-класс, а не интерфейс. И в этом и преимущество Свифта: можно даже final-класс подписать на свой протокол (интерфейс) и мокать уже протокол (интерфейс). Главное, чтоб в протоколе (интерфейсе) сигнатуры методов совпадали с тем, что объявлено в final-классе.

                • +1
                  да, прошу прощения, неправильно понял, всё верно про возможность мокировать по тайпклассу. Очень это приятная фича, которой нет в жвм платформе. Есть реализация в форке котлина, но у этого всего плохая совместимость с джавой и сложность в корректной поддержке во всех аспектах, так что не факт, что тайпклассы появятся даже как экспериментальная фича в котлине
                  • 0

                    Ну всё-таки в Mockito 2 final классы мокаются без проблем.

                    • 0
                      Лучше использовать эту фичу только для старого-старого кода, когда нет возможности сделать нормальную архитектуру.
                    • 0

                      Передача по ссылке или значению — это архаизмы реализаций компиляторов и языков прошлых лет, когда это имело смысл. На сегодняшний день важно лишь, что описываемая сущность является мутабельной или иммутабельной. В случае иммутабельной сущности нам не важно передается ли она по значению или по ссылке и где физически она находится в памяти — это уже внутренние проблемы компилятора и рантайма. При операциях с иммутабельными сущностями каждый раз создается новый экземпляр. Таким образом иммутабельные классы полностью идентичны value type (подразумевается наличие equals()/hashCode()).


                      В случае же мутабельной сущности, мы имеем каждый раз дело с экземпляром, занимающим определенное место в памяти, и поэтому осознанно работаем с ним по ссылке (некоему хэндлу, если угодно). Для передачи информации о внутреннем состоянии мутабельного объекта вовне есть два способа: хороший — обернуть его иммутабельным врапером, либо плохой: просто сделать defensive copy и отдать на растерзание. Структуры в Свифте — это просто реализация defencive copying по умолчанию, тогда как в Котлине это нужно делать въявную. Мне кажется не стоило делать отдельный класс объектов языка только лишь для реализации одного паттерна. Тем более, что изначально структуры (записи) в языках программирования означали немножко другое и появились задолго до классов.

                  • НЛО прилетело и опубликовало эту надпись здесь
                    • –3

                      Необходимость мокать final классы, говорит о просветах в дизайне.

                      • –1
                        Не очень понятно, за что минусы. За стиль? Или кто-то полагает, что моки из final классов — это хорошо?
                        • +2
                          Не минусовал, но конкретно я бы предпочел мокать final класс, чем создавать бесполезный интерфейс с одной реализацией, только для того, чтобы мокать его в тестах.

                          То есть я бы сказал, что просветом в дизайне являются скорее такие интерфейсы, если на их существование нет никакой другой весомой причины (например, скрыть платформо-зависимые компоненты).

                          Ну а открывать класс только для того, чтобы его можно было мокнуть без Mockito 2 — тоже не сильно хорошо.
                          • 0
                            Ну вот навскидку:
                            • Отделение контракта чтобы не тащить за собой зависимости реализации.
                            • Раздельное управление API и реализацией.
                            • Добавление явных декораторов вместо неявных аспектов.
                            • Улучшение читаемости API.
                            • Сокрытие платформозависимого компонента, конечно же.

                            Можно, наверное, еще подумать, зачем может быть нужен интерфейс с единственной
                            реализацией, но и приведенного должно быть достаточно.

                            Конечно, легко себе вообразить проект, где перечисленные выше причины вообще никогда не возникнут, но в моем текущем проекте и во всех предыдущих за последние лет 6 наличие интерфейса практически у любого сервиса подразумевалось по умолчанию.
                        • +1

                          Не могу плюсануть комментарий (видимо есть ограничение по времени), но вы не один)

                        • –1
                          А давайте сравним картошку с огурцом
                          • –2
                            А давайте еще минусанем, за попытку донести мысль об абсурдности поста

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое