Ответы на вопросы для собеседования Swift-разработчика
Какие структуры данных есть в Swift?
В Swift существуют следующие структуры данных:
- enum
- struct
- class
- actor
- protocol
Что такое перечисления (enumerations) в Swift?
Перечисление (enumeration) - это группа связанных значений.
Перечисления позволяют писать безопасный для типов код.
enum Direction {
case North
case East
case South
case West
}
Теперь в своем коде вы можете вызвать, например, Direction.North
, вместо того чтобы использовать мистическую строку "North"
(которая легко может быть неправильно написана и вызвать раздражающие ошибки).
В чем отличие ссылочных типов от типов-значений?
Экземпляры типов-значений (value type) хранятся в стеке, в то время, как экземпляры ссылочных типов (reference type) хранятся в куче.
Также, при присваивании одного экземпляра value type другому экземпляру того же типа происходит копирование из одного экземпляра в другой, в то время как при присваивании одного экземпляра reference type другому экземпляру того же типа происходит копирование ссылки на объект, а не самого объекта.
К value type относятся enum
и struct
.
К reference type относятся class
, actor
, closure
, func
.
Можно ли переопределять структуры и перечисления в Swift?
Нельзя создать подкласс структуры или перечисления, как и переопределить их. Это связано с тем, что структура является типом-значением (value type), и компилятор должен знать ее точный размер во время компиляции, что переопределение делает невозможным.
Что такое Copy On Write в Swift?
Механизм CoW - это стратегия оптимизации памяти, которая используется для избежания неэффективных копирований данных. Что это такое? Когда мы создаем копию какого-либо объекта, структуры или массива, копирование может быть дорогим в плане производительности и затрат памяти. Вместо того, чтобы создавать новую копию объекта, механизм CoW использует "умные" указатели, которые указывают на одну и ту же область памяти до тех пор, пока не происходит изменение данных. Когда данные изменяются, механизм CoW создает новую копию данных, которая может изменяться независимо от оригинала. Как это работает? Механизм CoW используется для работы со структурами и классами в Swift. Когда вы создаете экземпляр структуры или класса, Swift создает копию этого объекта и сохраняет ее в памяти. Внутри структуры или класса могут быть свойства, которые могут изменяться, и когда вы изменяете эти свойства, Swift создает новую копию объекта, которая содержит измененные данные. Процесс работы механизма CoW в Swift можно разделить на три основных этапа: 1. Создание копии объекта. Когда вы создаете копию объекта в Swift, он использует механизм CoW для создания "умного" указателя, который указывает на одну и ту же область памяти, где хранятся данные. 2. Изменение данных. Когда вы изменяете данные в объекте, механизм CoW проверяет, есть ли другие "умные" указатели на ту же область памяти. Если такие указатели есть, механизм создает новую копию объекта, содержащую измененные данные, и перенаправляет указатели на новую область памяти. 3. Удаление объектов. Когда все "умные" указатели на объект становятся недействительными (то есть объект больше не используется в программе), объект должен быть удален из памяти.
Какие типы коллекций есть в Swift?
В Swift есть три типа для работы с коллекциями - Array
, Set
и Dictionary
.
Array
- массив данных. Элементы имеют порядок, доступ к элементам происходит по индексу.
Set
- неупорядоченное множество. В множестве не может быть двух одинаковых элементов. Равенство элементов достигается проверкой hash-значения каждого элемента. В связи с этим элементы множества обязаны реализовать протокол Hashable
. Доступ по индексу невозможен.
Dictionary
- словарь, элементы которого представляют из себя пару (ключ, значение). Ключи словаря должны быть уникальны. Это достигается ровно так же, как и в случае с множеством, через протокол Hashable
. Значения словаря могут быть любым типом, на них не накладываются ограничения. Так же значения не должны быть уникальными. Доступ осуществляется по ключу и возвращает Optional
значение, так как элемента по заданному ключу может не быть. Реализован через hash table.
Какие методы протокола Sequence вы знаете?
1. forEach
- выполняет блок кода для каждого элемента коллекции.
let arr = [1, 2, 3, 4]
arr.forEach { print($0) }
// prints
// 1
// 2
// 3
// 4
2. map
- выполняет блок кода для каждого элемента коллекции, в результате чего образовывается новая коллекция такого же размера, но тип значений коллекции может быть изменен, в зависимости от того, как реализован блок кода, который передается в map
.
let numbers = [1, 2, 3, 4]
let mapped = numbers.map {
Array(repeating: $0, count: $0)
} // mapped = [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]
3. flatMap
- выполняет блок кода для каждого элемента коллекции, в результате чего образовывается новая коллекция такого же размера. Отличается от map тем, что, если результирующий элемент коллекции так же является коллекцией, то он будет встроен в результирующую коллекцию. Тип элементов результирующей коллекции так же может измениться, как и при выполнении map
.
let numbers = [1, 2, 3, 4]
let flatMapped = numbers.flatMap {
Array(repeating: $0, count: $0)
}
// flatMapped = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
4. compactMap
- выполняет блок кода для каждого элемента коллекции, в результате чего образовывается новая коллекция. Отличается от map
тем, что размер результирующей коллекции не обязательно равен размеру изначальной. Элементы изначальной коллекции можут быть Optional
и хранить в себе nil
. Такой элемент не попадает в результирующую коллекцию. Тип элементов результирующей коллекции так же может измениться, как и при выполнении map
.
let numbers: [Int?] = [1, nil, 3, 4, nil]
let compactMapped = numbers.compactMap {
if let element = $0 {
Array(repeating: element, count: element)
} else {
return nil
}
}
// compactMapped = [[1], [3, 3, 3], [4, 4, 4, 4]]
5. filter
- выполняет блок кода для каждого элемента коллекции, результатом которого есть значение Bool
. При возврате из блока false
, элемент исключается из результирующей коллекции.
let map = [
"weather": "Good",
"temperature": 27,
"humidity": 34.3
]
let result = map.filter { (key, _) in
key == "temperature"
} // result = ["temperature": 27]
6. reduce
- выполняет блок кода для каждого элемента коллекции, результатом которого становится накопление нового результата. Используется, как правило, для получения единого результата из коллекции.
let arr = [1, 2, 3, 4]
let resut = arr.reduce(0) { partialResult, value in
if partialResult == 0 {
return value
}
return partialResult * 10 + value
} // result = 1234
Что такое протокол в Swift?
protocol
- особый тип данных, экземпляры которого не могут быть созданы. Протоколы описывают будущий функционал, но не реализовывают его. Имплементация протокола кладется на плечи конкретного типа, который собирается его реализовать.
Протокол может быть принят классом, структурой или перечислением для обеспечения фактической реализации этих требований. Любой тип, который удовлетворяет требованиям протокола, реализует данный протокол.
Что такое associated type?
associated type
- механизм параметризации протокола. Используется для обеспечения большей гибкости работы с протоколом в силу ухода от конкретного типа к параметру.
По своей сути generics
для протокола.
Так как протоколы с assotiated type
не являются достаточно конкретными, объявить переменную данного типа или передать напрямую подобный тип как параметр функции невозможно.
Здесь на помощь нам приходят ключевые слова any
и some
. Данные два ключевых слова решают одну и ту же проблему, но немного по-разному.
any
создает коробку вокруг конкретного типа, который удовлетворяет данному протоколу.
some
указывает, что пусть это неизвестно заранее, но на момент компиляции программы конкретный тип переменной будет известен точно.
Что такое Any и AnyObject?
Any
- специальный тип, который является всем сразу и ничем одновременно.
Каждый тип в Swift может быть привиден к типу Any
. Any
используется тогда, когда мы не можем использовать generics, но при этом хотим иметь гибкость передачи аргументов разного типа. Недостаток у такого подхода всего один: так как в итоге тип данных будет Any
, мы должны наверняка знать, какой тип был до этого, чтобы получить возможность восстановить данные после получения.
AnyObject
- специальный тип, такой же, как и Any
, но с ограничением только на reference type. Каждый AnyObject
может быть Any
, но не каждый Any
может быть AnyObject
.
Ключевое слово AnyObject
также используется для ограничения протокола на реализацию только reference type типами.
Что такое замыкания?
Замыкания - это экземпляр функции, которая имеет доступ к определенному контексту и/или захватывает определенные значения и может быть вызвана позже. Замыкания могут захватывать и хранить ссылки на любые константы и переменные из контекста, в котором они объявлены. Эта процедура известна как заключение этих констант и переменных, отсюда и название "замыкание". Swift выполняет всю работу с управлением памятью при захвате за вас. Замыкания принимают одну из трех форм: 1. Глобальные функции являются замыканиями, у которых есть имя и которые не захватывают никакие значения. 2. Вложенные функции являются замыканиями, у которых есть имя и которые могут захватывать значения из включающей их функции. 3. Замыкающие выражения являются безымянными замыканиями, написанные в облегченном синтаксисе, которые могут захватывать значения из их окружающего контекста.
Что такое escaping и nonescaping?
Согласно документации, вы обязаны пометить замыкание, передаваемое в функцию в качестве параметра, ключевым словом @escaping
, если оно будет вызвано после возвращения из функции.
Иными словами, если замыкание (closure) будет вызвано асинхронно внутри (callback) или за пределами (delegate) функции, то вы должны его пометить как @escaping
.
По сути @escaping
позволяет вам отложить выполнение передаваемого в качестве параметра замыкания до нужного вам момента (например, по срабатыванию таймера или по завершению асинхронной операции).
Если вы попробуете присвоить non-escaping
замыкание в свойство класса, структуры или перечисления вы получите compile-time ошибку:
class MainDispatcher {
var work: (() -> Void)? = nil
func async(_ block: () -> Void) {
self.work = block
}
}
Если вы попробуете вызвать non-escaping
замыкание асинхронно (то есть там где ожидается escaping
замыкание), вы также получите compile-time ошибку:
class MainDispatcher {
func async(_ block: () -> Void) {
DispatchQueue.main.async {
block()
}
}
}
По умолчанию замыкание, передаваемое в качестве параметра, является non-escaping
, поэтому там, где вам необходимо вы должны использовать ключевое слово @escaping
.
class MainDispatcher {
var work: (() -> Void)? = nil
func async(_ block: @escaping () -> Void) {
self.work = block
DispatchQueue.main.async {
self.work?()
}
}
func sync(_ block: () -> Void) {
DispatchQueue.main.sync {
block()
}
}
}