Язык 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

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Golang разработчик (middle)
от 230000 RUB до 300000 RUB
PHP Developer
от 200000 RUB до 270000 RUB

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