Разработчики Android работают в основном с Kotlin. Но знание Swift полезно при понимании того, как реализован функционал в iOS, а также при изучении Kotlin Multiplatform.

Это же относится и к разработчикам iOS: просматривать код Android им проще, зная Kotlin.

Попробуем разобраться в типичных концепциях Swift, обнаруживаемых при просмотре кода iOS, сравним их реализацию в Kotlin.

Это не полное сравнение Swift и Kotlin: рассмотрим основы и типичные концепции.

1. Основы

Переменные

Переменные и константы определяются в Swift ключевыми словами var и let соответственно, аннотации типов снабжаются точкой с запятой, но они не обязательны:

// Swift
let animDurationMillis: Int = 500
var clickCount = 0

// Kotlin
val animDurationMillis: Int = 500
var clickCount = 0

Единственное здесь различие между языками  —  каким ключевым словом, let или val, определяются переменные только для чтения.

Опционалы/допустимость значения «null»

Для опционалов или типов, которыми допускаются значения «null», в обоих языках применяется один и тот же символ ?, единственное отличие  —  nil или null для состояния без значения:

// Swift                      
var foundItem: String? = nil

// Kotlin
var foundItem: String? = null

Состояние nil обрабатывается:

  • выражением if для проверки значения, об этом позже;
  • опциональной привязкой, о ней позже;
  • указанием резервного значения/значения по умолчанию;
  • принудительным снятием обертки.

Вот пример двух последних подходов. Swift и Kotlin немного различаются синтаксисом  —  сравните: ?? и ?:! и !!.

// Swift
// - резервное значение / значение по умолчанию
let actualFoundItem = foundItem ?? "empty"
// - принудительное снятие обертки
let actualFoundItem = foundItem!

// Kotlin
// - резервное значение / значение по умолчанию
val actualFoundItem = foundItem ?: "empty"
// - принудительное снятие обертки
val actualFoundItem2 = foundItem!!

Поток управления

В Swift и Kotlin оператор if идентичен, небольшое различие  —  в Swift скобки опускаются:

// Swift
if foundItem != nil {
// делаем что-нибудь
}

// Kotlin
if (foundItem != null) {
// делаем что-нибудь
}

В обоих языках  —  одинаковый синтаксис для ветвлений else if/else, используемый как выражение:

// Swift
let description = if delta <= 10 {
"low"
} else if delta >= 50 {
"high"
} else {
"medium"
}

// Kotlin
val description = if (delta <= 10) {
"low"
} else if (delta >= 50) {
"high"
} else {
"medium"
}

Функции

В Swift функции объявляются ключевым словом func, за которым следуют название функции, входные параметры, тип возвращаемого значения:

func addTwoNumbers(a: Int, b: Int) -> Int {
return a + b
}

В Kotlin применяется ключевое слово fun, а для определения типа возвращаемого значения  —  : вместо ->:

fun addTwoNumbers(a: Int, b: Int): Int {
return a + b
}

2. Структуры и классы

В Swift имеются структуры и классы, при моделировании данных это одно и то же, поскольку и структурами, и классами поддерживается определение свойств и функций. Ключевое различие: классы передаются по ссылке, структуры  —  по значению.

По умолчанию рекомендуется использовать структуры. Когда нужно наследование, совместимость с objective-C и другая дополнительная функциональность, задействуются классы:

struct VehicleStructure {
var maxSpeed = 0

func printInfo() {
print("Max speed \(maxSpeed)")
}
}

class VehicleClass {
var maxSpeed = 0

func printInfo() {
print("Max speed \(maxSpeed)")
}
}

Чтобы создать экземпляр, ссылаются на название структуры или класса, за которым следуют пустые скобки.

Структуры неизменяемы по умолчанию. Поэтому для изменения значения любого из их свойств оно объявляется как var, а не let. Автоматически генерируемыми в них инициализаторами задаются значения свойств элемента:

// создание экземпляра изменяемой структуры
var car = VehicleStructure()
car.maxSpeed = 250
car.printInfo()

// создание экземпляра неизменяемой структуры
let carSimple = VehicleStructure(maxSpeed: 200)
carSimple.printInfo()

// создание экземпляра класса
let bike = VehicleClass()
bike.maxSpeed = 50
bike.printInfo()

Kotlin

Основной строительный блок в Kotlin  —  это класс, его объявление и применение в Swift практически идентичны:

class Vehicle {
var maxSpeed = 0

fun printInto(){
println("Max speed is $maxSpeed")
}
}

// создание экземпляра класса
val vehicle = Vehicle()
vehicle.maxSpeed = 250
vehicle.printInfo()

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

class Vehicle(var maxSpeed: Int) {

fun printInfo(){
println("Max speed is $maxSpeed")
}
}

val vehicle = Vehicle(250)

Kotlin поддерживаются другие связанные структуры: абстрактные классы, классы данных, интерфейсы, закрытые классы, интерфейсы. Подробнее  —  здесь.

3. Опциональная привязка

If let

В кодовой базе Swift часто встречается такая закономерность:

let fetchedUserId: String? = "Optional id of the fetched user"
if let userId = fetchedUserId {
// «userId» используется как неопциональная константа
print(userId)
} else {
// «fetchedUserId» — это «nil/null»
throw Error("Missing user id")
}

// «fetchedUserId» и «userId» используются вне оператора «if»,
// но оба по-прежнему опциональны, им требуется снятие обертки.

Это называется опциональной привязкой, которой:

  • проверяется наличие в опциональной переменной fetchedUserId значения, отличного от nil;
  • если значение имеется, оно присваивается новой неопциональной константе userId;
  • на новую константу userId ссылаются внутри блока кода;
  • если fetchedUserId  —  nil, выполняется блок else.

С помощью названия имеющейся переменной упрощаем дальше:

let fetchedUserId: String? = "Optional id of the fetched user"
if let fetchedUserId {
// «fetchedUserId» используется как неопциональная константа
print(fetchedUserId)
}

В обоих случаях константы fetchedUserId и userId используются вне оператора if, но им требуется дополнительное снятие обертки, ведь обе по-прежнему считаются опциональными.

Kotlin

В Kotlin этому нет специального эквивалентного аналога. Один из вариантов  —  оператор if/else. Однако рабочий он только для переменных локальной области видимости, а не глобальной. Чтобы включить поддержку глобальных переменных, присвоим значение новой локальной переменной / константе:

// свойство глобального класса
var fetchedUserId: String? = "Optional id of the fetched user"

val userId = fetchedUserId
if (userId != null) {
// «userId» используется как неопциональная
} else {
throw Exception("Missing user id")
}

// «userId» используется как неопциональная везде

В этом примере на константу userId можно ссылаться как на неопциональную даже после оператора if, это не поддерживается в Swift с его опциональной привязкой.

Другое решение  —  использовать одну из функций области видимости, например .let {}. Код внутри функции выполняется, только если fetchedUserId не является null. Любым ссылкам на fetchedUserId после этого блока кода по-прежнему требуется защита от значений null, поскольку переменная считается опциональной:

fetchedUserId?.let { userId ->
// «userId» используется как неопциональная
} ?: throw Exception("Missing user id")

Guard

Другой функционал  —  похожий на if let оператор guard, применяемый обычно для раннего выхода из функции, еще одно отличие  —  требуется блок else:

func checkUsernameValid(username: String?): Bool {
guard let username else {
// «username» — это «nil», не вычисляется
return false
}
// «username» используется как неопциональная
return username.count > 3
}

В этом коде функцией получается неопциональная переменная username, которая затем проверяется оператором guard на наличие значения. Если оно отсутствует, переменная из функции возвращается. Если присутствует, используется username, как если бы оно было неопциональным в остальной части функции.

Kotlin

В Kotlin это пишется различными способами, вот два из них:

fun checkUsernameValid(username: String?): Boolean {
if (username.isNullOrEmpty()){
return false
}
return username.length > 3
}

// или

fun checkUsernameValid(username: String?): Boolean {
val actualUsername = username ?: return false
return actualUsername.length > 3
}

4. Перечисления

В Swift перечисления определяются ключевым словом enum, а значения  —  ключевым словом case, за которым следует название случая перечисления  —  в нижнем регистре и единственном числе. Каждому случаю, помещаемому в новой строке, требуется case. Случаи на одной строке разделяются запятыми:

enum Direction {
case left
case up
case right
case down
}

// или

enum Direction {
case left, up, right, down
}

Чтобы использовать случай перечисления, ссылаются на тип Direction и соответствующий случай. Далее тип пропускается, и case задействуется напрямую с помощью более короткого точечного синтаксиса:

var selectedDirection = Direction.up
selectedDirection = .right

Значение перечисления проверяется оператором switch, от которого требуется быть всеохватным относительно перечислений, поэтому в Xcode автоматически записываются все ветви:

switch(selectedDirection){
case .left:
goLeft()
case .up:
goForward()
case .right:
goRight()
case .down:
goBackward()
}

Кроме того, перечислениями Swift поддерживаются связанные значения, то есть у каждого случая перечисления может быть разное количество типов значений. Это мощный инструмент моделирования предметной области, аналогичный запечатанному классу в Kotlin.

Подробнее  —  здесь.

Kotlin

В Kotlin перечисления определяются ключевым словом enum class. Определяемые значения разделяются запятыми. Названия значений перечисления пишутся в верхнем регистре, но в зависимости от стиля проекта здесь возможны вариации:

enum class Direction {
LEFT, UP, RIGHT, DOWN
}

Чтобы использовать перечисление, ссылаются на название класса и соответствующее значение:

var selectedDirection = Direction.UP

Значение проверяется оператором when, которым опять же охватываются все возможные значения перечисления:

when(selectedDirection){
Direction.LEFT -> goLeft()
Direction.UP -> goForward()
Direction.RIGHT -> goRight()
Direction.DOWN -> goBackward()
}

Классами перечислений в Kotlin также поддерживается определение дополнительных свойств, для которых каждым значением перечисления должно предоставляться значение. Однако, в отличие от связанных значений Swift, свойства находятся на уровне класса, а не на уровне значения. Поэтому они должны быть одного типа для каждого значения.

Подробнее —  здесь.

5. Словарь/карта

Синтаксис словарей Swift и карт Kotlin сильно отличается, хотя базовая концепция у них похожая.

Swift

Словарь  —  это структура данных Swift, в которой неупорядоченно хранятся ассоциативные связи между ключами и значениями одного и того же типа. Каждый ключ  —  уникальное значение, на основе которого получается доступ к соответствующему этому ключу значению.

Чтобы объявить словарь, в квадратных скобках определяются пары «ключ-значение» в виде [ключ: значение], пары разделяются друг от друга запятыми. Определив хотя бы одну пару «ключ-значение», объявление типа можно опустить: типы определятся компилятором:

var httpErrorCodes: [Int: String] = [404: "Not found", 401: "Unauthorized"]

Значение словаря считывается ключом с помощью такого синтаксиса сабскрипта: dictionary[key]. Если ключа в словаре нет, возвращается nil, тогда оператором ?? указываем значение по умолчанию:

func getHttpErrorCodeMessage(code: Int) -> String {
let errorCodeMessage = httpErrorCodes
?? "Unknown"
return "Http error code \(errorCodeMessage)"
}

Чтобы записать в словарь новое значение, ключу присваивается значение. Если ключа нет, в коллекцию добавляется новая пара «ключ-значение», а если ключ уже имеется  —  его значение обновляется:

// добавляем новую пару «ключ-значение»
httpErrorCodes[500] = "Internal Server Error"

// обновляем значение для имеющегося ключа
httpErrorCodes[401] = "Requires authentication"

Какой словарь использовать: изменяемый или неизменяемый, то есть только для чтения  —  зависит от присваивания значения. С let определяется словарь, из которого считывают только после его объявления. Чтобы включить поддержку записи, он объявляется как var.

Подробнее  —  здесь.

Kotlin

Карта  —  это коллекция, в которой содержатся пары уникальных ключей и значений и поддерживается эффективное извлечение значения, соответствующего каждому ключу.

Неизменяемая, то есть только для чтения, карта объявляется в Kotlin с помощью типа Map<KeyType, ValueType>, инициализируемого функцией стандартной библиотеки mapOf(varargs pairs: Pair<KeyType, ValueType>). Определив хотя бы одну пару «ключ-значение», явное объявление типа переменной можно опустить: типы определятся компилятором.

Значения объявляются напрямую классом Pair(key, value) или инфиксной функцией to, в которой объект и создается:

val httpErrorCodes: Map<Int, String> = mapOf(
404 to "Not found",
Pair(401, "Unauthorized"),
)

Значение карты считывается ключом с помощью скобочной записи map[key]. Если ключа в карте нет, возвращается null, тогда оператором ?: указываем значение по умолчанию:

fun getHttpErrorCodeMessage(code: Int): String {
val errorCodeMessage = httpErrorCodes
?: "Unknown"
return "Http error code $errorCodeMessage"
}

Чтобы записать в карту новое значение, с помощью типа MutableMap<KeyType, ValueType> и фабричной функции mutableMapOf() объявляется изменяемая карта. Затем ключу присваивается значение. Если ключа нет, в коллекцию добавляется новая пара «ключ-значение», а если ключ уже имеется  —  его значение обновляется:

// добавляем новую пару «ключ-значение»
httpErrorCodes[500] = "Internal Server Error"

// обновляем значение для имеющегося ключа
httpErrorCodes[401] = "Requires authentication"

Подробнее  —  здесь.

6. Расширения

Расширения  —  это способ добавить новую функциональность имеющемуся классу или структуре, в том числе таким, к коду которых нет доступа.

В Swift расширения объявляются ключевым словом extension, за которым следует название расширяемого класса или структуры. Расширения объявляются на верхнем уровне, вне других классов или структур:

extension String {
func doubled() -> String {
return self + self
}
}

В приведенном выше примере в типе String определили новую функцию-расширение doubled(), которая теперь вызывается для любого экземпляра строки, как если бы она была частью исходного определения:

let originalStr = "Swift"
let doubledStr = originalStr.doubled()
print(doubledStr) // выводится «SwiftSwift»

Kotlin

В Kotlin применяются функции-расширения с подобным поведением, добавлением новой функциональности имеющимся классам и определяются как функции верхнего уровня с названием расширяемого класса, за которым следуют точка и название функции:

fun String.doubled(): String {
return this + this
}

Теперь функция вызывается для любого экземпляра строки, как если бы она была частью исходного определения:

val originalStr = "Kotlin"
val doubledStr = originalStr.doubled()
println(doubledStr) // выводится «KotlinKotlin»

7. Протоколы

Протокол в Swift  —  это набор свойств, методов и других требований, принимаемых классом, структурой или перечислением в фактической реализации этих требований.

Протокол определяется ключевым словом protocol, за которым следует название протокола, аналогично объявлению структуры или класса. Внутри протокола определяются свойства с доступом на чтение { get } или на чтение и запись { get set }:

protocol RequestError {
var errorCode: Int { get }
var isRecoverable: Bool { get set}
}

protocol PrintableError {
func buildErrorMessage() -> String
}

В этом примере определили протокол RequestError со свойством с доступом на чтение errorCode и другим свойством с доступом на запись isRecoverable. И определили протокол PrintableError, в который включается функция buildErrorMessage(), ее предстоит реализовать.

Чтобы применить протокол, определяем класс или структуру и добавляем : ProtocolName после его названия. Объявляемые протоколы разделяются запятыми. Затем для тела класса или структуры определяются требования из протокола:

class ServerHttpError: RequestError, PrintableError {
var errorCode: Int = 500
var isRecoverable: Bool = false

func buildErrorMessage() -> String {
return "Server side http error with error code \(errorCode)"
}
}

struct ConnectionError: RequestError, PrintableError {
var errorCode: Int
var isRecoverable: Bool

func buildErrorMessage() -> String {
return "Local connection error"
}
}

Здесь определили класс ServerHttpError, в котором применяются протоколы RequestError и PrintableError и определяются значения по умолчанию для двух свойств и реализация функции. А еще имеется структура ConnectionError, где объявляется два свойства и предоставляется реализация функции.

Теперь создаются экземпляры ServerHttpError и ConnectionError и передаются, как если бы при применении этого протокола они были типов RequestError или PrintableError. В функции onRequestError(), которой принимается тип RequestError, для создания сообщения об ошибке проверяется соответствие ошибки протоколу PrintableError:

func onRequestError(error: RequestError) {
if let printableError = error as? PrintableError {
print(printableError.buildErrorMessage())
}
print("Is recoverable: \(error.isRecoverable)")
}

let firstError = ServerHttpError()
firstError.errorCode = 503
firstError.isRecoverable = false

let secondError = ConnectionError(errorCode: 404, isRecoverable: true)

// «Ошибка http на стороне сервера с кодом ошибки «503». Подлежит восстановлению: false»
onRequestError(error: firstError)
// «Ошибка локального соединения. Подлежит восстановлению: true»
onRequestError(error: secondError)

Это простой пример применения протоколов. Протоколами Swift поддерживаются варианты использования посложнее: наследование, композиция, связанные типы, дженерики и другие. Подробнее  —  здесь.

Kotlin

Приведенный выше пример пишется на Kotlin несколькими способами: с интерфейсами, абстрактными и запечатанными классами. Ближайшим представлением протокола Swift в Kotlin, вероятно, является интерфейс. Им поддерживаются определение свойств и функций, наследование, композиция, дженерики.

Интерфейс определяется ключевым словом interface, за которым следует название. В теле определяются свойства и функции. Свойства с доступом на чтение определяются ключевым словом val, свойства с доступом на чтение и запись  —  ключевым словом var.

Чтобы реализовать этот интерфейс в классе, после названия класса указываются : и название интерфейса, реализуемые интерфейсы разделяются запятыми, затем ключевым словом override определяются все свойства и функции:

interface RequestError {
val errorCode: Int
var isRecoverable: Boolean
}

interface PrintableError {
fun buildErrorMessage(): String
}

class ServerHttpError(
override val errorCode: Int,
override var isRecoverable: Boolean
) : RequestError, PrintableError {
override fun buildErrorMessage(): String {
return "Server side http error with error code $errorCode"
}
}

class ConnectionError : RequestError, PrintableError {
override val errorCode: Int
get() = 404
override var isRecoverable: Boolean = true

override fun buildErrorMessage(): String {
return "Local connection error"
}
}

Теперь создаются экземпляры ServerHttpError и ConnectionError и передаются в функции, как если бы они были типа RequestError:

fun onRequestError(error: RequestError) {
if (error is PrintableError) {
println(error.buildErrorMessage())
}
println("$errorMessage. Is recoverable: ${error.isRecoverable}")
}

val firstError = ServerHttpError(errorCode = 503, isRecoverable = false)
val secondError = ConnectionError()
// «Ошибка http на стороне сервера с кодом ошибки «503». Подлежит восстановлению: false»
onRequestError(firstError)
// «Ошибка локального соединения. Подлежит восстановлению: true»
onRequestError(secondError)

Подробнее  —  здесь.

Заключение

Зная типичные концепции Swift и умея переносить их в Kotlin, проще понять, что делается в коде: как реализуется функционал на соседней платформе, анализируется код, просматриваются или пишутся технические спецификации/предложения или ведется работа с Kotlin Multiplatform.

Мы рассмотрели основы языка Swift и сравнили его с Kotlin. Кроме того, изучили концепции, обнаруживаемые в типовом проекте iOS: опциональные привязки, словари, расширения, структуры, протоколы.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Domen Lanišnik: Swift Cheatsheet for Android/Kotlin Developers

Предыдущая статьяREST API CRUD в Laravel 11 с лучшими практиками
Следующая статьяОбработка дублированных сообщений в Kafka