СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

| сохранено

H Swift и Kotlin — о самых важных различиях между этими языками в черновиках Перевод



Здравствуйте, коллеги. Заголовок самоочевиден, приглашаем вас ознакомиться с переводом интересной статьи Кшиштофа Турека. Также отметим, что автор подмечает интересные доработки, отличающие Kotlin от Java при разработке под Android. Слово автору:
Вероятно, многие из вас видели следующее сравнение Swift и Kotlin: nilhcem.com/swift-is-like-kotlin. Получилось довольно интересно, правда? Соглашусь, между этими языками много параллелей, но в этой статье хотелось бы обсудить некоторые важные отличия между ними.

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

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

Спустя примерно семь месяцев, я вернулся к Android. Но стал писать не на Java, а на Kotlin. На конференции Google IO 2017 представитель Google заявил, что Kotlin будет одним из приоритетных языков для Android-разработки. Поэтому я решил его выучить. Достаточно быстро я заметил, что Kotlin и Swift очень похожи. Однако, никогда не соглашусь, что они почти идентичны. Далее я расскажу о некоторых отличиях между ними. Не обо всех, а о тех, что наиболее меня заинтересовали. Рассмотрим несколько примеров.

Структура vs. класс данных или значимые типы в сравнении со ссылочными

Структура и класс данных – это упрощенные версии класса. Они используются и выглядят схоже:

Kotlin:

data class Foo(var data: Int)

Swift:

struct Foo {
 var data: Int
}

Однако, как понятно из названия, класс данных – это все равно класс. Ссылочный тип. Структура, в свою очередь – значимый тип. «Ну и что»? – могли бы спросить вы. Позвольте объясню на примере.

Создадим класс данных в 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 для класса данных в Kotlin и 2 для структуры в Swift.



Результаты отличаются, поскольку при копировании структуры (var foo2 = foo1) создается независимый экземпляр с собственной уникальной копией данных (как здесь), а при копировании ссылки создаются разделяемые экземпляры (как здесь).

Вероятно, при работе с Java вам приходилось иметь дело с паттерном «Резервная копия». Если нет – самое время это исправить. Здесь эта тема рассмотрена подробнее.

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

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

Я объявил страницы как MutableList, так как хотел изменять их внутри объекта (добавлять, удалять, т.д.). Страницы у меня не приватные, потому что я хочу предусмотреть, чтобы и вызывающая сторона могла считывать их текущее состояние. Пока – все нормально.

val book = Book()
print(“$book”) // Book(pages=[Page(title=Chapter 1), Page(title=Chapter 2)])
Теперь я могу считать актуальное состояние книги вот так:
val bookPages = book.pages

Добавляю новую страницу (Page) в список 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 – это на самом деле структуры; таковы, например, enum, Int, CGPoint, Array, т.д.

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

Это моя любимая тема!

Для начала сравним интерфейс и протокол. В целом это почти одно и то же.

  • Оба могут требовать, чтобы класс/структура реализовывали специфичные методы (в Swift речь идет о соответствии (conforming), в Kotlin – о реализации (implementation));
  • Оба могут требовать предоставления специфичного свойства. Свойство может быть устанавливаемым и получаемым, либо только получаемым;
  • Оба* позволяют добавить реализацию метода, используемую по умолчанию.

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

Kotlin:

interface MyInterface {
 var myVariable: Int
 val myReadOnlyProperty: Int
 fun myMethod()
 fun myMethodWithBody() {
 // здесь находится реализация
 }
}

Swift:

protocol MyProtocol {
 init(parameter: Int)
 var myVariable: Int { get set }
 var myReadOnlyProperty: Int { get }
 
 func myMethod()
 func myMethodWithBody()
}
extension MyProtocol {
 
 func myMethodWithBody() {
   // здесь находится реализация
   }
}

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

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

Для 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 – это обобщенное перечисление с заданным типом, именуемым Wrapped. В данном случае Wrapped – это строка, поэтому расширение должно выглядеть так:

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

и использоваться так:

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

Гораздо сложнее, чем вариант Kotlin, верно? Но, к сожалению, здесь есть и другой недостаток. Как мы уже знаем, Optional в Swift – это обобщенное перечисление, поэтому ваше расширение будет применяться ко всем типам. Прямо скажем, не обнадеживает:



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

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

Протоколы и расширения созданы для совместного использования. Вы можете создать собственный протокол и написать расширение для класса, чтобы адаптировать его к этому протоколу. Это уже может показаться безумным – но это еще не все! Также существует феномен, именуемый «условное соответствие протоколу». Это означает, что класс или структура может соответствовать протоколу, если выполняются определенные условия.

Допустим, у нас в приложении много точек, где требуется открывать всплывающие уведомления. Мы чтим принцип 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 просто создает уведомление, но ничего не представляет. Здесь нам понадобится ссылка на контроллер представления. Следует ли передавать ее в метод в аргументе? По-моему, не очень хорошая идея. Давайте лучше воспользуемся условием 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. Таким образом нам удается отобразить уведомление.

Идем дальше:

extension UIViewController : AlertPresentable {}

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





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

Рассмотрим такую ситуацию и допустим, что у нас есть протокол Swift и расширение на Kotlin. Если сигнатура метода нам известна, то можно создать подходящий протокол с такой же сигнатурой метода, затем написать расширение, где финальный класс или одиночка непосредственно реализуют этот протокол – вуаля! Вместо того, чтобы напрямую обращаться к классу, можно легко протестировать его при помощи вашего протокола. Код стоит тысячи слов, так что попробуйте сами.

final class FrameworkMap {
 private FrameworkMap() { … }
   public final void drawSomething() { … }
}
class MyClass {
   …
   fun drawSomethingOnMap(map: FrameworkMap) {
        map.drawSomething()
   }
}

При тестировании вы, возможно, соберетесь проверить, вызывает ли ваш метод drawSomethingOnMap метод drawSomething в словаре фреймворка. Это бывает затруднительно даже при помощи Mockito (широко известная библиотека тестов для Android). Однако, с протоколом и расширением код приобретет следующий вид:

protocol Map {
   fun drawSomething()
}
extension FrameworkMap : Map {}

Теперь, смотрите, ваш метод drawSomethingOnMap использует протокол, а не класс

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

Запечатанные классы —  это прокачанные перечисления

Напоследок хотелось бы поговорить о перечислениях.

Перечисление в Kotlin ничем не отличается от перечисления в классической Java, поэтому здесь мне нечего добавить. Однако, теперь у нас есть кое-что новое, можно сказать «суперперечисление» — речь о запечатанном классе. Откуда взялся термин «суперперечисление»? Смотрим документацию Kotlin:
“ …В некотором смысле, это расширение классов-перечислений: множество значений для типа enum также ограничивается, но каждая константа enum существует лишь в единственном экземпляре, тогда как подкласс запечатанного класса может иметь много экземпляров, каждый из которых может содержать состояние.”
Круто, могут содержать состояние. Но как же их использовать?

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

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

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

С перечислением так не получится. Как только вариант перечисления создан, изменить его уже нельзя.

А вы заметили еще какие-нибудь отличия? Здесь я использовал еще одну возможность запечатанного класса. Речь об ассоциированных значениях иного типа. Классическое перечисление требует предоставлять ассоциированное значение для всех случаев, и все значения должны относиться к одному и тому же типу.

Естественно, в Swift есть эквивалент запечатанного класса, и называется он… перечисление. Перечисление в Kotlin – просто реликт из Java, в 90% случаев в этом языке используются запечатанные классы. Сложно провести грань между запечатанным классом Kotlin и перечислением Swift. Они просто называются по-разному, и, конечно же, запечатанный класс – это ссылочный тип, а перечисление – значимый. Пожалуйста, поправьте меня, если я в чем-то неправ.

комментарии (0)