Шпаргалка Swift для разработчиков Android/Kotlin
Разработчики 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[code] ?? "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[code] ?: "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: опциональные привязки, словари, расширения, структуры, протоколы.
Читайте также:
Перевод статьи Domen Lanišnik: Swift Cheatsheet for Android/Kotlin Developers