Языку программирования Swift всего четыре года, но он уже становится основным языком разработки для iOS. Развиваясь до версии 5.0, Swift превратился в сложный и мощный язык, отвечающий как объектно-ориентированной, так и функциональной парадигме. И с каждым новым релизом в нем добавляется еще больше возможностей.
Но насколько хорошо вы на самом деле знаете Swift? В этой статье вы найдете примеры вопросов для собеседования по Swift.
Вы можете использовать эти вопросы для интервьюирования кандидатов, чтобы проверить их знания или вы можете проверить свои собственные. Если вы не знаете ответа, не переживайте: к каждому вопросу есть ответ.
Вопросы разделены на три группы:
Для каждого уровня есть два типа вопросов:
Читая статью, держите открытым playground, чтобы иметь возможность проверить код из вопроса. Все ответы были протестированы на Xcode 10.2 и Swift 5.
Но насколько хорошо вы на самом деле знаете Swift? В этой статье вы найдете примеры вопросов для собеседования по Swift.
Вы можете использовать эти вопросы для интервьюирования кандидатов, чтобы проверить их знания или вы можете проверить свои собственные. Если вы не знаете ответа, не переживайте: к каждому вопросу есть ответ.
Вопросы разделены на три группы:
- Beginner: для начинающих. Вы прочитали пару книг и применяли Swift в своих собственных приложениях.
- Intermediate: подходят для тех, кто действительно заинтересовался языком. Вы уже достаточно много прочитали о нём и часто экспериментируете.
- Advanced: подходят для самых продвинутых разработчиков — тех, кому нравится влезать в дебри синтаксиса и использовать передовые приёмы.
Для каждого уровня есть два типа вопросов:
- письменные: подойдут для тестирования по email, так как предполагают написание кода.
- устные: можно использовать при беседе по телефону или при личном контакте, так как тут достаточно ответа словами.
Читая статью, держите открытым playground, чтобы иметь возможность проверить код из вопроса. Все ответы были протестированы на Xcode 10.2 и Swift 5.
Beginner
письменные вопросыВопрос 1Рассмотрим следующий код:
struct Tutorial { var difficulty: Int = 1 } var tutorial1 = Tutorial() var tutorial2 = tutorial1 tutorial2.difficulty = 2
Чему равны значения tutorial1.difficulty и tutorial2.difficulty? Была бы какая-то разница, если бы Tutorial был классом? Почему?
ответtutorial1.difficulty равен 1, а tutorial2.difficulty равен 2.
В Swift структуры — типы-значения (value type). Они копируются, а не ссылаются. Следующая строка копирует tutorial1 и присваивает её tutorial2:
var tutorial2 = tutorial1
Изменения в tutorial2 не отражаются на tutorial1.
Если бы Tutorial был бы классом, tutorial1.difficulty и tutorial2.difficulty равнялись бы 2. Классы в Swift — ссылочные типы (reference type). Когда вы меняете свойство tutorial1, вы увидите такое же изменение у tutorial2 — и наоборот.
Вопрос 2Вы объявили view1 при помощи var, а view2 — при помощи let. В чём разница и скомпилируется ли последняя строка?
import UIKit var view1 = UIView() view1.alpha = 0.5 let view2 = UIView() view2.alpha = 0.5 // Эта строка скомпилируется?
ответДа, последняя строка скомпилируется. view1 — это переменная, и вы можете назначить её значение новым экземпляром UIView. Используя let, вы можете присвоить значение лишь однажды, так что следующий код не скомпилируется:
view2 = view1 // Ошибка: view2 is immutable
Однако, UIView — это класс со ссылочной семантикой, так что вы можете изменять свойства view2 — что означает, что код скомпилируется.
Вопрос 3Этот код сортирует массив по алфавиту. Максимально упростите замыкание.
var animals = ["fish", "cat", "chicken", "dog"] animals.sort { (one: String, two: String) -> Bool in return one < two } print(animals)
ответSwift автоматически определяет тип параметров замыкания и возвращаемый тип, так что вы можете убрать их:
animals.sort { (one, two) in return one < two }
Вы можете заменить имена параметров использованием нотации $i:
animals.sort { return $0 < $1 }
Замыкания, состоящие из одного оператора, могут не содержать ключевое слово return. Значение последнего выполненного оператора становится возвращаемым результатом замыкания:
animals.sort { $0 < $1 }
Наконец, так как Swift знает, что элементы массива соответствуют протоколу Equatable, вы можете просто написать:
animals.sort(by: <)
Upd: hummingbirddj упростил ещё больше:
В данном случае можно еще короче:
– сортирует по возрастанию, работает для типов, реализующих Comparable.animals.sort()
Вопрос 4Этот код создаёт два класса: Address и Person. Также создаются два экземпляра класса Person (Ray и Brian).
class Address { var fullAddress: String var city: String init(fullAddress: String, city: String) { self.fullAddress = fullAddress self.city = city } } class Person { var name: String var address: Address init(name: String, address: Address) { self.name = name self.address = address } } var headquarters = Address(fullAddress: "123 Tutorial Street", city: "Appletown") var ray = Person(name: "Ray", address: headquarters) var brian = Person(name: "Brian", address: headquarters)
Предположим, что Brian переехал по новому адресу и вы хотите обновить его запись следующим образом:
brian.address.fullAddress = "148 Tutorial Street"
Это компилируется и выполняется без ошибок. Но, если вы проверите теперь адрес Ray, то вы увидите, что он тоже «переехал».
Что здесь произошло и как мы можем исправить это?
ответAddress — это класс и обладает ссылочной семантикой. Таким образом, headquarters — это один и тот же экземпляр класса, который разделяют ray и brian. Изменение headquarters изменит адрес обоих.
Чтобы исправить это, можно создать новый экземпляр класса Address и присвоить его Brian, или объявить Address как struct вместо class.
устные вопросыВопрос 1Что такое optional и какие проблемы они решают?
ответoptional позволяет переменной любого типа представить ситуацию "отсутствие значения". В Objective-C «отсутствие значения» было доступно только в ссылочных типах с использованием специального значения nil. У типов-значений (value types), вроде int или float, такой возможности не было.
Swift расширил концепцию «отсутствия значения» на типы-значения. Переменная optional может содержать либо значение, либо nil, сигнализирующее об отсутствии значения.
Вопрос 2Коротко перечислите основные отличия между structure и class.
ответКлассы поддерживают наследование, а структуры — нет.
Классы — ссылочный тип, структуры — тип-значение.
Вопрос 3Что такое generics и для чего они нужны?
ответВ Swift вы можете использовать generics в классах, структурах и перечислениях.
Generics устраняют проблему дублирования кода. Если у вас есть метод, который принимает параметры одного типа, иногда приходится дублировать код, чтобы работать с параметрами другого типа.
Например, в этом коде вторая функция — это «клон» первой, за исключением того, что у неё параметры string, а не integer.
func areIntEqual(_ x: Int, _ y: Int) -> Bool { return x == y } func areStringsEqual(_ x: String, _ y: String) -> Bool { return x == y } areStringsEqual("ray", "ray") // true areIntEqual(1, 1) // true
Применяя generics, вы совмещаете две функции в одной и одновременно обеспечиваете безопасность типов:
func areTheyEqual<T: Equatable>(_ x: T, _ y: T) -> Bool { return x == y } areTheyEqual("ray", "ray") areTheyEqual(1, 1)
Так как вы тестируете равенство, вы ограничиваете типы теми, которые отвечают протоколу Equatable. Этот код обеспечивает требуемый результат и препятствует передаче параметров неподходящего типа.
Вопрос 4В некоторых случаях не получится избежать неявного разворачивания (implicitly unwrapped) optionals. Когда и почему?
ответНаиболее частые причины для использования implicitly unwrapped optionals:
- когда вы не можете инициализировать свойство, которое не nil в момент создания. Типичный пример — outlet у Interface Builder, который всегда инициализируется после его владельца. В этом особенном случае, если в Interface Builder всё правильно сконфигурировано — вам гарантировано, что outlet не-nil перед его использованием.
- чтобы разрешить проблему цикла сильных ссылок, когда два экземпляра классов ссылаются друг на друга и требуется не-nil ссылка на другой экземпляр. В этом случае вы помечаете ссылку на одной стороне как unowned, а на другой стороне используете неявное разворачивание optional.
Вопрос 5Какими способами можно развернуть optional? Оцените их в смысле безопасности.
var x : String? = "Test"
Подсказка: всего 7 способов.
ответПринудительное развёртывание (forced unwrapping) — небезопасно.
let a: String = x!
Неявное развертывание при объявлении переменной — небезопасно.
var a = x!
Optional binding — безопасно.
if let a = x { print("x was successfully unwrapped and is = \(a)") }
Optional chaining — безопасно.
let a = x?.count
Nil coalescing operator — безопасно.
let a = x ?? ""
Оператор Guard — безопасно.
guard let a = x else { return }
Optional pattern — безопасно.
if case let a? = x { print(a) }
Intermediate
письменные вопросыВопрос 1В чём разница между nil и .none?
ответНет никакой разницы, Optional.none (кратко .none) и nil эквивалентны.
Фактически, следующий оператор вернёт true:
nil == .none
Использование nil более общепринято и рекомендовано.
Вопрос 2Здесь модель термометра в виде класса и структуры. Компилятор жалуется на последнюю строчку. Что там не так?
public class ThermometerClass { private(set) var temperature: Double = 0.0 public func registerTemperature(_ temperature: Double) { self.temperature = temperature } } let thermometerClass = ThermometerClass() thermometerClass.registerTemperature(56.0) public struct ThermometerStruct { private(set) var temperature: Double = 0.0 public mutating func registerTemperature(_ temperature: Double) { self.temperature = temperature } } let thermometerStruct = ThermometerStruct() thermometerStruct.registerTemperature(56.0)
ответThermometerStruct корректно объявлен с mutating функцией для изменения внутренней переменной. Компилятор жалуется на то, что вы вызываете метод registerTemperature экземпляра, который был создан при помощи let, таким образом, этот экземпляр неизменяемый (immutable). Изменение let на var исправит ошибку компиляции.
В структурах вы должны помечать методы, которые изменяют внутренние переменные, как mutating, но вы не можете вызывать эти методы, используя immutable экземпляр.
Вопрос 3Что выведет этот код и почему?
var thing = "cars" let closure = { [thing] in print("I love \(thing)") } thing = "airplanes" closure()
ответБудет напечатано: I love cars. Список захвата создаст копию переменной в момент объявления замыкания. Это означает, что захваченная переменная не изменит своего значения, даже после присвоения нового значения.
Если вы опустите список захвата в замыкании, то компилятор будет использовать ссылку, а не копию. Вызов замыкания отразит изменение в переменной:
var thing = "cars" let closure = { print("I love \(thing)") } thing = "airplanes" closure() // Prints: "I love airplanes"
Вопрос 4Это функция, которая считает количество уникальных значений в массиве:
func countUniques<T: Comparable>(_ array: Array<T>) -> Int { let sorted = array.sorted() let initial: (T?, Int) = (.none, 0) let reduced = sorted.reduce(initial) { ($1, $0.0 == $1 ? $0.1 : $0.1 + 1) } return reduced.1 }
Она использует sorted, так что она использует только типы, соответствующие протоколу Comparable.
Вы можете вызвать ее так:
countUniques([1, 2, 3, 3]) // результат 3
Перепишите эту функцию как расширение Array, чтобы можно было использовать так:
[1, 2, 3, 3].countUniques() // должна вывести 3
примечание переводчикаЧто-то уж больно монструозная функция. Почему бы не так:
func countUniques<T: Hashable>(_ array: Array<T>) -> Int { return Set(array).count }
ответextension Array where Element: Comparable { func countUniques() -> Int { let sortedValues = sorted() let initial: (Element?, Int) = (.none, 0) let reduced = sortedValues.reduce(initial) { ($1, $0.0 == $1 ? $0.1 : $0.1 + 1) } return reduced.1 } }
Вопрос 5Вот функция, которая делит два optional doubles. Есть три условия, которые должны соблюдаться:
- делимое не должно быть nil
- делитель не должен быть nil
- делитель не должен быть равен 0
func divide(_ dividend: Double?, by divisor: Double?) -> Double? { if dividend == nil { return nil } if divisor == nil { return nil } if divisor == 0 { return nil } return dividend! / divisor! }
Перепишите эту функцию, используя оператор guard и не используя принудительное развёртывание (forced unwrapping).
ответОператор guard, появившийся в Swift 2.0, обеспечивает выход в случае не соблюдения условия. Вот пример:
guard dividend != nil else { return nil }
Вы также можете использовать оператор guard для optional binding, после чего развернутая переменная будет доступна и за пределами оператора guard:
guard let dividend = dividend else { return .none }
Таким образом, вы можете переписать функцию так:
func divide(_ dividend: Double?, by divisor: Double?) -> Double? { guard let dividend = dividend else { return nil } guard let divisor = divisor else { return nil } guard divisor != 0 else { return nil } return dividend / divisor }
Обратите внимание на отсутствие принудительной распаковки, так как мы уже распаковали делимое и делитель и поместили их в non-optional immutable переменные.
Обратите также внимание на то, что результат распакованных optionals в операторе guard доступен и за пределами оператора guard.
Вы можете еще больше упростить функцию путем группировки операторов guard:
func divide(_ dividend: Double?, by divisor: Double?) -> Double? { guard let dividend = dividend, let divisor = divisor, divisor != 0 else { return nil } return dividend / divisor }
Вопрос 6Перепишите метод из вопроса 5 с использованием оператора if let.
ответОператор if let позволяет вам распаковывать optionals и использовать это значение внутри этого блока кода. Вне его эти значения будут недоступны.
func divide(_ dividend: Double?, by divisor: Double?) -> Double? { if let dividend = dividend, let divisor = divisor, divisor != 0 { return dividend / divisor } else { return nil } }
устные вопросыВопрос 1В Objective-C вы объявляете константу таким образом:
const int number = 0;
А так в Swift:
let number = 0
В чём тут разница?
ответВ Objective-C константа инициализируется во время компиляции значением, которое должно быть известно на этом этапе.
Неизменяемое значение, созданное при помощи let — это константа, определяемая на этапе выполнения. Вы можете инициализировать ее статическим или динамическим выражением. Поэтому мы можем делать так:
let higherNumber = number + 5
Обратите внимание: такое присвоение возможно сделать лишь однажды.
Вопрос 2Чтобы объявить статическое свойство или функцию для типов-значений, используется модификатор static. Вот пример для структуры:
struct Sun { static func illuminate() {} }
А для классов возможно использовать модификаторы static или class. Результат один и тот же, но есть отличие. Опишите его.
ответstatic делает свойство или функцию статической и неперекрываемой. Использование class позволит перекрыть свойство или функцию.
Здесь компилятор будет ругаться на попытку перекрыть illuminate():
class Star { class func spin() {} static func illuminate() {} } class Sun : Star { override class func spin() { super.spin() } // error: class method overrides a 'final' class method override static func illuminate() { super.illuminate() } }
Вопрос 3Можно ли добавить stored property к типу, используя extension? Каким образом или почему нет?
ответНет, это невозможно. Мы можем использовать extension, чтобы добавить новое поведение существующему типу, но не можем изменить сам тип или его интерфейс. Для хранения нового stored property нам потребуется дополнительная память, а extension не может это сделать.
Вопрос 4Что такое протокол в Swift?
ответПротокол — это тип, который определяет набросок методов, свойств и т.д. Класс, структура или перечисление могут принимать протокол, чтобы реализовать все это. Протокол сам по себе не реализует функционал, но определяет его.
Advanced
письменные вопросыВопрос 1Допустим, у нас есть структура, определяющая модель термометра:
public struct Thermometer { public var temperature: Double public init(temperature: Double) { self.temperature = temperature } }
Чтобы создать экземпляр, мы пишем:
var t: Thermometer = Thermometer(temperature:56.8)
Но было бы гораздо удобнее что-то вроде этого:
var thermometer: Thermometer = 56.8
Возможно ли это? Как?
ответSwift определяет протоколы, которые позволяют инициализировать тип с использованием литералов путем присваивания. Применение соответствующего протокола и обеспечение публичного инициалайзера позволит инициализацию при помощи литералов. В случае Thermometer мы реализуем ExpressibleByFloatLiteral:
extension Thermometer: ExpressibleByFloatLiteral { public init(floatLiteral value: FloatLiteralType) { self.init(temperature: value) } }
Теперь мы можем создать экземпляр вот так:
var thermometer: Thermometer = 56.8
Вопрос 2У Swift есть набор предопределенных операторов для арифметических и логических действий. Он также позволяет создавать свои собственные операторы, как унарные, так и бинарные.
Определите и реализуйте собственный оператор возведения в степень (^^) по следующим требованиям:
- принимает в качестве параметров два Int
- возвращает результат возведения первого параметра в степень второго
- корректно обрабатывает порядок алгебраических операций
- игнорирует возможные ошибки переполнения
ответСоздание нового оператора происходит в два этапа: объявление и реализация.
Объявление использует ключевое слово operator для задания типа (унарный или бинарный), для задания последовательности символов нового оператора, его ассоциативности и старшинства выполнения.
Здесь оператор — это ^^ и его тип infix (бинарный). Ассоциативность правая.
В Swift нет предопределенного старшинства для возведения в степень. В алгебре возведение в степень должно вычисляться перед умножением/делением. Таким образом, мы создаем пользовательский порядок выполнения, помещая возведение в степень выше умножения.
Это объявление:
precedencegroup ExponentPrecedence { higherThan: MultiplicationPrecedence associativity: right } infix operator ^^: ExponentPrecedence
Это реализация:
func ^^(base: Int, exponent: Int) -> Int { let l = Double(base) let r = Double(exponent) let p = pow(l, r) return Int(p) }
Вопрос 3Следующий код определяет структуру Pizza и протокол Pizzeria с расширением для реализации по умолчанию метода makeMargherita():
struct Pizza { let ingredients: [String] } protocol Pizzeria { func makePizza(_ ingredients: [String]) -> Pizza func makeMargherita() -> Pizza } extension Pizzeria { func makeMargherita() -> Pizza { return makePizza(["tomato", "mozzarella"]) } }
Теперь мы определяем ресторан Lombardis:
struct Lombardis: Pizzeria { func makePizza(_ ingredients: [String]) -> Pizza { return Pizza(ingredients: ingredients) } func makeMargherita() -> Pizza { return makePizza(["tomato", "basil", "mozzarella"]) } }
Следующий код создает два экземпляра Lombardis. В котором из них делают маргариту с базиликом?
let lombardis1: Pizzeria = Lombardis() let lombardis2: Lombardis = Lombardis() lombardis1.makeMargherita() lombardis2.makeMargherita()
ответВ обоих. Протокол Pizzeria объявляет метод makeMargherita() и обеспечивает реализацию по умолчанию. Реализация Lombardis перекрывает метод по умолчанию. Так как мы объявили метод в протоколе в двух местах, будет вызвана правильная реализация.
А что если бы протокол не объявлял метод makeMargherita(), а extension по-прежнему обеспечивал реализацию по умолчанию, вот так:
protocol Pizzeria { func makePizza(_ ingredients: [String]) -> Pizza } extension Pizzeria { func makeMargherita() -> Pizza { return makePizza(["tomato", "mozzarella"]) } }
В этом случае только у lombardis2 была бы пицца с базиликом, тогда как у lombardis1 была бы без, потому что он использовал бы метод, определенный в extension.
Вопрос 4Следующий код не компилируется. Можете объяснить, что с ним не так? Предложите варианты решения проблемы.
struct Kitten { } func showKitten(kitten: Kitten?) { guard let k = kitten else { print("There is no kitten") } print(k) }
Подсказка: есть три способа исправить ошибку.
ответБлок else оператора guard требует варианта выхода, или с использованием return, бросая исключение или вызывая @noreturn. Простейшее решение — добавить оператор return.
func showKitten(kitten: Kitten?) { guard let k = kitten else { print("There is no kitten") return } print(k) }
Это решение бросит исключение:
enum KittenError: Error { case NoKitten } struct Kitten { } func showKitten(kitten: Kitten?) throws { guard let k = kitten else { print("There is no kitten") throw KittenError.NoKitten } print(k) } try showKitten(kitten: nil)
Наконец, здесь вызов fatalError(), @noreturn-функция.
struct Kitten { } func showKitten(kitten: Kitten?) { guard let k = kitten else { print("There is no kitten") fatalError() } print(k) }
устные вопросыВопрос 1Замыкания — это ссылочный тип или тип-значение?
ответЗамыкания — это ссылочный тип. Если вы присваиваете переменной замыкание, а затем копируете в другую переменную, вы копируете ссылку на то же самое замыкание и его список захвата.
Вопрос 2Вы используете тип UInt для хранения беззнакового целого. Он реализует инициалайзер для конвертации из целого со знаком:
init(_ value: Int)
Однако, следующий код не скомпилируется, если вы зададите отрицательное значение:
let myNegative = UInt(-1)
Целые со знаком по определению не могут быть отрицательными. Однако, возможно использовать представление отрицательного числа в памяти для перевода его в беззнаковое.
Как можно сконвертировать отрицательное целое в UInt с сохранением его представления в памяти?
ответДля этого есть инициалайзер:
UInt(bitPattern: Int)
И использование:
let myNegative = UInt(bitPattern: -1)
Вопрос 3Опишите циклические ссылки в Swift? Как их можно исправить?
ответЦиклические ссылки происходят, когда два экземпляра содержат сильную ссылку друг на друга, что приводит к утечке памяти из-за того, что ни один из этих экземпляров не может быть освобождён. Экземпляр не может быть освобождён, пока есть еще сильные ссылки на него, но один экземпляр держит другой.
Это можно разрешить, заменив на одной из сторон ссылку, указав ключевое слово weak или unowned.
Вопрос 4Swift разрешает создавать рекурсивные перечисления. Вот пример такого перечисления, которое содержит вариант Node с двумя ассоциативными типами, T and List:
enum List<T> { case node(T, List<T>) }
Здесь будет ошибка компиляции. Что мы пропустили?
ответМы забыли ключевое слово indirect, которое позволяет подобные рекурсивные варианты перечисления:
enum List<T> { indirect case node(T, List<T>) }