Здравствуйте, коллеги. Заголовок самоочевиден, приглашаем вас ознакомиться с переводом интересной статьи Кшиштофа Турека. Также отметим, что автор подмечает интересные доработки, отличающие 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)