Share on facebook
Facebook
Share on twitter
Twitter
Share on linkedin
LinkedIn
Share on pinterest
Pinterest
Share on vk
VK
Share on odnoklassniki
OK
Share on telegram
Telegram
Share on whatsapp
WhatsApp

Собеседование Swift — вопросы и ответы

Автор оригинала: Antonio Bello, Bill Morefield

Языку программирования Swift всего четыре года, но он уже становится основным языком разработки для iOS. Развиваясь до версии 5.0, Swift превратился в сложный и мощный язык, отвечающий как объектно-ориентированной, так и функциональной парадигме. И с каждым новым релизом в нем добавляется еще больше возможностей.

Но насколько хорошо вы на самом деле знаете 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: <)

Вопрос 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(_ 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(_ array: Array) -> 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

Ответ:

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.

Вопрос 4:

Swift разрешает создавать рекурсивные перечисления. Вот пример такого перечисления, которое содержит вариант Node с двумя ассоциативными типами, T and List:

enum List {
  case node(T, List)
}

Здесь будет ошибка компиляции. Что мы пропустили?

Ответ:

Мы забыли ключевое слово indirect, которое позволяет подобные рекурсивные варианты перечисления:

enum List {
  indirect case node(T, List)
}

Ответы на эти и другие вопросы вы можете найти на наших курсах по Swift и на занятиях в нашей Swift онлайн школе

👉Хочешь больше новостей из мира Swift и iOS разработки?
❤️ Лайк и подписка на @swiftlab приветствуются.

Пролистать наверх