Да, язык Swift прост, но не настолько, чтобы некоторые вопросы с собеседований не загоняли в тупик. Давайте разберемся, как на них отвечать.
Хотя Свифту всего три года, за это время он успел стать одним из самых популярных языков. Его синтаксис до безобразия простой – настолько, что с выходом Swift разработчики одного из самых простых языков JavaScript почувствовали себя немного ущемленными.
В действительности Swift – сложный язык. Он охватывает как объектно-ориентированные (ООП), так и функциональные подходы, и продолжает совершенствоваться с каждым новым выпуском.
В Swift есть много полезных инструментов, но как вы сможете проверить, насколько хорошо вы его изучили? В этой статье мы собрали список самых разных вопросов и ответов по этому языку программирования, которые только могут попасться на собеседовании.
Используйте эти вопросы, чтобы проверить знания потенциальных кандидатов на рабочее место или свои собственные! Если вы не уверены в своем ответе, не волнуйтесь: к каждому вопросу в этой статье прилагается скрытое «под катом» решение, чтобы вы могли учить и запоминать новую информацию.
Вопросы разделены на две основные категории:
- Письменные. Хорошее дополнение практического тестирования по электронной почте, так как зачастую это базовый этап с последующим допуском к собеседованию.
- Устные. Они могут озвучиваться телефону или в личном интервью.
Кроме того, каждая из этих категорий разделена на три уровня:
- Начальный: подходит для начинающего Swift-программиста, который прочел одну или две книги, после чего начал писать несложные приложения.
- Средний: для тех, кто сильно заинтересован в языковых концепциях, читает литературу, блоги, смотрит видео и экспериментирует с полученной информацией в реализации своих приложений.
- Продвинутый: для лучших из лучших – людей, которые успели хорошо освоить язык, постоянно себя проверяют и используют самые современные методы.
Если вы хотите по-настоящему себя испытать, постарайтесь не раскрывать спойлеры с ответами сразу. Все решения были протестированы с помощью Xcode 7.0 beta 6.
Письменные вопросы
Начальный уровень
Вопрос №1 - язык Swift 1.0 и далее
Каков наиболее оптимальный способ написать этот цикл for с диапазонами?
for var i = 0; i < 5; i++ { print("Hello!") }
[spoiler title='Решение внутри' style='default' collapse_link='true']
for _ in 0...4 { print("Hello!") }
Swift реализует два оператора диапазона: замкнутый оператор и полуоткрытый оператор. Первый включает все значения в диапазоне. Например, следующее включает в себя все целые числа от 0 до 4:
0...4
Полуоткрытый оператор не включает последний элемент. Ниже приводятся те же результаты от 0 до 4:
0..<5
[/spoiler]
Вопрос №2 - язык Swift 1.0 и далее
Рассмотрим следующий пример:
struct Tutorial { var difficulty: Int = 1 } var tutorial1 = Tutorial() var tutorial2 = tutorial1 tutorial2.difficulty = 2
Что из себя представляют tutorial1.difficulty и tutorial2.difficulty? Было бы все иначе, если бы Tutorial был классом? Почему?
[spoiler title='Решение внутри' style='default' collapse_link='true']
tutorial1.difficulty = 1, тогда как tutorial2.difficulty = 2.
Структуры в Swift – это типы значений, и значения копируются, а не передаются по ссылке. Следующая строка создает копию tutorial1 и присваивает ее tutorial2:
var tutorial2 = tutorial1
Исходя из этой строки, можем сделать вывод, что любые изменения в tutorial2 не отражены в tutorial1.
Если бы Tutorial был классом, то и tutorial1.difficulty, и tutorial2.difficulty были бы равны 2. Классы в языке программирования Swift являются ссылочными типами. Любое изменение свойства tutorial1 будет отражено в tutorial2 и наоборот.
[/spoiler]
Вопрос №3 - язык Swift 1.0 и далее
view1 объявляется с var, а view2 объявляется с let. В чем разница, и будет ли последняя строка компилироваться?
import UIKit var view1 = UIView() view1.alpha = 0.5 let view2 = UIView() view2.alpha = 0.5 // Будет ли эта строка компилироваться?
[spoiler title='Решение внутри' style='default' collapse_link='true']
view1 является переменной и может быть переназначена в новый экземпляр UIView. С let вы можете назначить только один раз, поэтому следующий код не будет компилироваться:
view2 = view1 // Ошибка: view2 неизменен
Однако UIView – это класс со ссылочной семантикой, поэтому свойства view2 поддаются модификации (значит, последняя строка будет скомпилирована):
let view2 = UIView() view2.alpha = 0.5 // Да! Строка будет компилироваться.
[/spoiler]
Вопрос №4 - язык Swift 1.0 и далее
Этот код сортирует массив имен по алфавиту и выглядит сложным. Упростите его:
let animals = ["fish", "cat", "chicken", "dog"] let sortedAnimals = animals.sort { (one: String, two: String) -> Bool in return one < two }
[spoiler title='Решение внутри' style='default' collapse_link='true']
Первое упрощение связано с параметрами:
let sortedAnimals = animals.sort { (one, two) -> Bool in return one < two }
Тип возвращаемого значения также может быть выведен, поэтому оставьте его:
let sortedAnimals = animals.sort { (one, two) in return one < two }
Обозначение $i может заменить имена параметров:
let sortedAnimals = animals.sort { return $0 < $1 }
При закрытии одного оператора ключевое слово return может быть опущено. Возвращаемое значение последнего оператора становится возвращаемым значением замыкания:
let sortedAnimals = animals.sort { $0 < $1 }
Это уже проще, но идем дальше. Для строк существует функция сравнения, определяемая следующим образом:
func <(lhs: String, rhs: String) -> Bool
Эта аккуратная небольшая функция делает ваш код таким же простым, как:
let sortedAnimals = animals.sort(<)
Обратите внимание, что каждый шаг компилирует и выводит один и тот же результат!
[/spoiler]
Вопрос №5 - язык Swift 1.0 и далее
Этот код создает два класса – Address и 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.address.fullAddress = "148 Tutorial Street"
Что произошло? Что не так?
[spoiler title='Решение внутри' style='default' collapse_link='true']
Рэй тоже переехал в новое здание! Address – это класс, который характеризуется ссылочной семантикой, поэтому штаб-квартира (headquarters) является одним и тем же экземпляром, независимо от того, обращаетесь ли вы к нему через ray или brian. Изменение адреса штаб-квартиры изменит его для обоих. Можете ли вы представить, что произойдет, если Брайан получит почту Рэя или наоборот?
Решение состоит в том, чтобы создать новый Address для назначения Брайану. А еще можно объявить Address как структуру вместо класса.
[/spoiler]
Средний уровень
С повышением уровня будет больше каверзных вопросов, ведь язык Swift не так прост. Вы готовы?
Вопрос №1 – язык Swift 2.0 и далее
Рассмотрим следующее:
var optional1: String? = nil var optional2: String? = .None
В чем разница между nil и .None? Чем отличаются переменные optional1 и optional2?
[spoiler title='Решение внутри' style='default' collapse_link='true']
Нет никакой разницы. Optional.None (.None как сокращение) – это правильный способ инициализации необязательной переменной, не имеющей значения, тогда как nil – это просто «синтаксический сахар» для .None.
Фактически, это утверждение выводит true:
nil == .None // В Swift 1.x это не компилируется. Вам потребуется Optional.None
Помните, что перечисление является необязательным:
enum Optional { case None case Some(T) }
[/spoiler]
Вопрос №2 – язык Swift 1.0 и далее
Вот модель термометра как класса и структуры:
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)
Этот код не компилируется. Почему? Где ошибка?
[spoiler title='Решение внутри' style='default' collapse_link='true']
Компилятор будет жаловаться на последнюю строку. ThermometerStruct правильно объявлен с помощью модифицирующей функции, чтобы изменить внутреннюю переменную temperature, но компилятор жалуется, потому что registerTemperature вызывается в экземпляре, созданном через let, поэтому он является неизменным.
Методы, которые изменяют внутреннее состояние, в структурах должны быть отмечены как mutating, но вызов их из неизменяемых переменных недопустим.
[/spoiler]
Вопрос №3 – язык Swift 1.0 и далее
Что выводит этот код и почему?
var thing = "cars" let closure = { [thing] in print("I love \(thing)") } thing = "airplanes" closure()
[spoiler title='Решение внутри' style='default' collapse_link='true']
Этот код выводит I love cars. Список захвата создает копию thing, когда объявляется закрытие, поэтому зафиксированное значение не изменяется, даже если вы назначаете новое значение для thing.
Если вы опустите список захвата в закрытии, тогда компилятор использует ссылку вместо копии. В этом случае любое изменение переменной отражается при вызове замыкания, как в следующем примере:
var thing = "cars" let closure = { print("I love \(thing)") } thing = "airplanes" closure() // Выведет "I love airplanes"
[/spoiler]
Вопрос №4 – язык Swift 2.0 и далее
Вот глобальная функция, которая подсчитывает количество уникальных значений в массиве:
func countUniques(array: Array) -> Int {
let sorted = array.sort(<)
let initial: (T?, Int) = (.None, 0)
let reduced = sorted.reduce(initial) { ($1, $0.0 == $1 ? $0.1 : $0.1 + 1) }
return reduced.1
}
Здесь используются операторы < и ==, что ограничивает T. Вызывается это следующим образом:
countUniques([1, 2, 3, 3]) // // результат = 3
Перепишите этот код следующим образом:
[1, 2, 3, 3].countUniques() // должно вывести 3
[spoiler title='Решение внутри' style='default' collapse_link='true']
В Swift 2.0 общие типы могут быть расширены с помощью условий путем принудительного ограничения. Если общий тип не удовлетворяет условиям ограничения, расширение не является ни видимым, ни доступным. Таким образом, глобальная функция countUniques может быть переписана как расширение массива:
extension Array where Element: Comparable { func countUniques() -> Int { let sorted = sort(<) let initial: (Element?, Int) = (.None, 0) let reduced = sorted.reduce(initial) { ($1, $0.0 == $1 ? $0.1 : $0.1 + 1) } return reduced.1 } }
Обратите внимание, что новый метод доступен только тогда, когда общий тип Element реализует протокол Comparable. Например, компилятор будет жаловаться, если вы вызываете его в массиве UIViews следующим образом:
import UIKit let a = [UIView(), UIView()] a.countUniques() // Ошибка компилятора здесь, потому что UIView не реализует Comparable
[/spoiler]
Вопрос №5 – язык Swift 2.0 и далее
Язык Swift не так прост. Вот функция для вычисления делений, заданных для двух (необязательных) даблов. Перед выполнением необходимо проверить три условия:
- Делимое должно содержать значение, отличное от nil.
- Делитель должен содержать значение, отличное от nil.
- Делитель не должен быть равен нулю.
func divide(dividend: Double?, by divisor: Double?) -> Double? { if dividend == .None { return .None } if divisor == .None { return .None } if divisor == 0 { return .None } return dividend! / divisor! }
Этот код работает так, как и было задумано, но есть две проблемы:
- Предварительные условия могут воспользоваться преимуществами оператора guard.
- Используется принудительное разворачивание.
Улучшите эту функцию, используя guard и избегая принудительного разворачивания.
[spoiler title='Решение внутри' style='default' collapse_link='true']
Новый оператор guard, введенный в Swift 2.0, предоставляет выход, если не выполняется указанное условие. Он позволяет сократить код и реализовать его без типичной «пирамиды погибели». Рассмотрим пример:
guard dividend != .None else { return .None }
Он также может быть использован для необязательного связывания:
guard let dividend = dividend else { return .None }
Функция разделения переписывается таким образом:
func divide(dividend: Double?, by divisor: Double?) -> Double? { guard let dividend = dividend else { return .None } guard let divisor = divisor else { return .None } guard divisor != 0 else { return .None } return dividend / divisor }
Еще кое-что. Вы можете группировать операторы guard, чтобы упростить эту функцию:
func divide(dividend: Double?, by divisor: Double?) -> Double? { guard let dividend = dividend, divisor = divisor where divisor != 0 else { return .None } return dividend / divisor }
[/spoiler]
Итак, второй этап письменных вопросов пройден. Двигаемся дальше?
Высокий уровень
Вопрос №1 – язык Swift 1.0 и далее
Рассмотрим следующую структуру, которая моделирует термометр:
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
Сможете реализовать? Каким образом? Подсказка: это связано с кабриолетами, но не с такими как Camaro и Mustang :)
[spoiler title='Решение внутри' style='default' collapse_link='true']
Язык Swift определяет следующие протоколы, которые позволяют инициализировать тип с литеральными значениями с помощью оператора присваивания:
- NilLiteralConvertible
- BooleanLiteralConvertible
- IntegerLiteralConvertible
- FloatLiteralConvertible
- UnicodeScalarLiteralConvertible
- ExtendedGraphemeClusterLiteralConvertible
- StringLiteralConvertible
- ArrayLiteralConvertible
- DictionaryLiteralConvertible
Принятие соответствующего протокола и предоставление публичного инициализатора допускает литеральную инициализацию определенного типа. В случае с типом Thermometer вы реализуете протокол FloatLiteralConvertible следующим образом:
extension Thermometer : FloatLiteralConvertible { public init(floatLiteral value: FloatLiteralType) { self.init(temperature: value) } }
И теперь вы можете создать экземпляр, используя простой float.
var thermometer: Thermometer = 56.8
[/spoiler]
Вопрос №2 – язык Swift 1.0 и далее
Язык Swift обладает набором предопределенных операторов, которые ориентированы на различные типы операций (например, арифметика или логика). Он также позволяет создавать пользовательские операторы: как унарные, так и бинарные. Определите и реализуйте собственный оператор ^^ со следующими характеристиками:
- Принимает два входа в качестве параметров.
- Возвращает первый параметр, поднятый до степени второго.
- Игнорирует возможные ошибки переполнения.
[spoiler title='Решение внутри' style='default' collapse_link='true']
Создайте новый пользовательский оператор в два этапа: объявление и реализация.
Объявление использует ключевое слово operator, чтобы указать тип (унарный или двоичный), последовательность символов, составляющих оператор, его ассоциативность и приоритет. В нашем случае оператор ^^, а тип - infix (двоичный). Ассоциативность right, а приоритет равен 155, учитывая, что умножение и деление имеют значение 150. Получаем:
infix operator ^^ { associativity right precedence 155 }
Реализация осуществляется следующим образом:
func ^^(lhs: Int, rhs: Int) -> Int { let l = Double(lhs) let r = Double(rhs) let p = pow(l, r) return Int(p) }
Обратите внимание, что он не учитывает переполнения. Если операция дает результат, который Int не может представлять (например, больше Int.max), тогда возникает ошибка времени выполнения.
[/spoiler]
Вопрос №3 – язык Swift 1.0 и далее
Можете ли вы определить перечисление с такими значениями? Почему?
enum Edges : (Double, Double) { case TopLeft = (0.0, 0.0) case TopRight = (1.0, 0.0) case BottomLeft = (0.0, 1.0) case BottomRight = (1.0, 1.0) }
[spoiler title='Решение внутри' style='default' collapse_link='true']
Нет, не можем. Тип значения должен:
- Соответствовать протоколу Equatable.
- Литерально преобразовываться из типов: Int, String, Character.
В приведенном выше коде это не соблюдено.
[/spoiler]
Вопрос №4 – язык Swift 2.0 и далее
Рассмотрим следующий код, который определяет 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"]) } }
Теперь вы определяете ресторан Lombardi следующим образом:
struct Lombardis: Pizzeria { func makePizza(ingredients: [String]) -> Pizza { return Pizza(ingredients: ingredients) } func makeMargherita() -> Pizza { return makePizza(["tomato", "basil", "mozzarella"]) } }
Следующий код создает два экземпляра Lombardi. Кто из двух сделает Маргариту с базиликом?
let lombardis1: Pizzeria = Lombardis() let lombardis2: Lombardis = Lombardis() lombardis1.makeMargherita() lombardis2.makeMargherita()
[spoiler title='Решение внутри' style='default' collapse_link='true']
Оба сделают. Протокол Pizzeria объявляет метод makeMargherita() и обеспечивает реализацию по умолчанию. Метод переопределяется в реализации Lombardis. Так как метод объявлен в протоколе в обоих случаях, правильная реализация вызывается во время выполнения.
Что делать, если протокол не объявляет метод makeMargherita(), но расширение по-прежнему обеспечивает реализацию по умолчанию?
protocol Pizzeria { func makePizza(ingredients: [String]) -> Pizza } extension Pizzeria { func makeMargherita() -> Pizza { return makePizza(["tomato", "mozzarella"]) } }
В этом случае только lombardis2 сделает пиццу с базиликом, тогда как lombardis1 приготовит пиццу без него, потому что будет использовать метод, определенный в расширении.
[/spoiler]
Вопрос №5 – язык Swift 2.0 и далее
Следующий код имеет ошибку. Можете ли вы определить, где и почему она происходит?
struct Kitten { } func showKitten(kitten: Kitten?) { guard let k = kitten else { print("There is no kitten") } print(k) }
Подсказка: Есть три способа это пофиксить.
[spoiler title='Решение внутри' style='default' collapse_link='true']
Тело else любого guard требует выхода, используя return, путем исключения (exception) или вызова @noreturn. Самое простое решение – добавить оператор return:
func showKitten(kitten: Kitten?) { guard let k = kitten else { print("There is no kitten") return } print(k) }
Вот версия с exception:
enum KittenError: ErrorType { 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(nil)
Наконец, реализация, вызывающая fatalError() с функцией @noreturn:
struct Kitten { } func showKitten(kitten: Kitten?) { guard let k = kitten else { print("There is no kitten") fatalError() } print(k) }
[/spoiler]
Поздравляем! Мы рассмотрели самые распространенные варианты задач, которые используются для предварительного письменного тестирования. Теперь переходим к устным вопросам, которые по сложности также разделены на три уровня: начальный, средний и продвинутый. Их разберем во второй части нашего материала.
Следующая статья: Собеседование iOS-разработчика: устные вопросы по языку Swift
Комментарии