Язык Swift: вопросы и ответы на собеседовании

Да, язык Swift прост, но не настолько, чтобы некоторые вопросы с собеседований не загоняли в тупик. Давайте разберемся, как на них отвечать.

Хотя Свифту всего три года, за это время он успел стать одним из самых популярных языков. Его синтаксис до безобразия простой – настолько, что с выходом Swift разработчики одного из самых простых языков JavaScript почувствовали себя немного ущемленными.

Язык Swift

В действительности Swift – сложный язык. Он охватывает как объектно-ориентированные (ООП), так и функциональные подходы, и продолжает совершенствоваться с каждым новым выпуском.

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

Используйте эти вопросы, чтобы проверить знания потенциальных кандидатов на рабочее место или свои собственные! Если вы не уверены в своем ответе, не волнуйтесь: к каждому вопросу в этой статье прилагается скрытое «под катом» решение, чтобы вы могли учить и запоминать новую информацию.

Вопросы разделены на две основные категории:

  1. Письменные. Хорошее дополнение практического тестирования по электронной почте, так как зачастую это базовый этап с последующим допуском к собеседованию.
  2. Устные. Они могут озвучиваться телефону или в личном интервью.

Кроме того, каждая из этих категорий разделена на три уровня:

  1. Начальный: подходит для начинающего Swift-программиста, который прочел одну или две книги, после чего начал писать несложные приложения.
  2. Средний: для тех, кто сильно заинтересован в языковых концепциях, читает литературу, блоги, смотрит видео и экспериментирует с полученной информацией в реализации своих приложений.
  3. Продвинутый: для лучших из лучших – людей, которые успели хорошо освоить язык, постоянно себя проверяют и используют самые современные методы.

Если вы хотите по-настоящему себя испытать, постарайтесь не раскрывать спойлеры с ответами сразу. Все решения были протестированы с помощью 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"

Что произошло? Что не так?

Язык Swift

[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!
}

Этот код работает так, как и было задумано, но есть две проблемы:

  1. Предварительные условия могут воспользоваться преимуществами оператора guard.
  2. Используется принудительное разворачивание.

Улучшите эту функцию, используя 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]

Итак, второй этап письменных вопросов пройден. Двигаемся дальше?

Язык Swift

Высокий уровень

Вопрос №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 обладает набором предопределенных операторов, которые ориентированы на различные типы операций (например, арифметика или логика). Он также позволяет создавать пользовательские операторы: как унарные, так и бинарные. Определите и реализуйте собственный оператор ^^ со следующими характеристиками:

  1. Принимает два входа в качестве параметров.
  2. Возвращает первый параметр, поднятый до степени второго.
  3. Игнорирует возможные ошибки переполнения.

[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

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Аналитик данных
Екатеринбург, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ