Irbisa · Шпаргалка

Swift: ООП и протоколы

Классы, struct, enum, протоколы, расширения, protocol-oriented programming

Junior-разработчик · 24 вопроса

  1. 01

    Чем структура отличается от класса в Swift?

    Помимо копирования и разделения, у структуры нет наследования, deinit и идентичности, а у класса нет поэлементного (memberwise) инициализатора, mutating и автоматического Sendable.

  2. 02

    Назовите основные принципы ООП и покажите, как каждый из них выражается в Swift.

    Инкапсуляция, наследование, полиморфизм и абстракция; Swift поддерживает все четыре, но там, где классические ООП-языки опираются на иерархии классов, Swift опирается на протоколы и значимые типы.

  3. 03

    Чем перегрузка (overloading) отличается от переопределения (overriding), и что может переопределить подкласс?

    Перегрузка означает несколько функций с одним именем и разными сигнатурами, нужную выбирает компилятор в точке вызова; переопределение означает замену унаследованной реализации в подклассе, и выбирается она во время выполнения по реальному классу объекта.

    class Shape {  var name: String { "shape" }  func area() -> Double { 0 }  final func describe() -> String { "\(name): \(area())" }}…
  4. 04

    Что такое протокол в Swift и чем он отличается от интерфейса или абстрактного класса?

    Протокол представляет собой контракт из методов, свойств, инициализаторов и ассоциированных типов, который могут принять классы, структуры и enum, в том числе чужие типы; в отличие от абстрактного класса он не хранит состояние.

    protocol Named {  var name: String { get }               // a let or a var satisfies it} protocol Greeter: Named {                // protocol inheritance  var greeting: String { get set }       // needs something writable…
  5. 05

    Чем raw values отличаются от associated values в enum Swift?

    Raw value задаёт для каждого case одну фиксированную константу одного и того же типа, известную на этапе компиляции; associated value представляет собой данные, которые прикрепляются к case при создании значения, и у разных case они могут быть разных типов.

    enum HTTPStatus: Int {  case ok = 200, notFound = 404}let status = HTTPStatus(rawValue: 404)   // Optional(.notFound)print(HTTPStatus.ok.rawValue)            // 200…
  6. 06

    Что можно добавить к типу через extension в Swift, а что нельзя?

    Extension добавляет вычисляемые свойства, методы, инициализаторы, сабскрипты, вложенные типы и соответствие протоколам к любому типу, даже чужому; добавить хранимые свойства или переопределить существующие члены он не может.

    extension String {  var isBlank: Bool { trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }} extension Array where Element: Numeric {      // constrained extension  func sum() -> Element { reduce(0, +) }…
  7. 07

    Какие уровни доступа есть в Swift и чем public отличается от open?

    В Swift шесть уровней: open, public, package, internal (по умолчанию), fileprivate и private; public и open видны из других модулей, но только open позволяет этим модулям наследоваться от класса и переопределять его члены.

    // Inside a framework moduleopen class BaseViewController {               // apps may subclass and override  open func configure() {}} public class Logger {                         // apps may use, not subclass…
  8. 08

    Что такое lazy-свойство, чем оно отличается от вычисляемого и наблюдаемого свойства и может ли оно вычислиться больше одного раза?

    lazy var выполняет свой инициализатор при первом чтении и запоминает результат, а willSet/didSet выполняют код вокруг каждого присваивания; lazy вычисляется один раз, если только два потока не прочитают его впервые одновременно.

    final class ProfileHeader {  var name: String {    willSet { print("about to show \(newValue)") }    didSet {      guard name != oldValue else { return }   // observers fire even for equal values      setNeedsLayout()…
  9. 09

    Что делает mutating и почему методам структуры он нужен, а методам класса нет?

    mutating помечает метод структуры или enum, который меняет self: self передаётся как inout, и изменение записывается обратно в переменную вызывающего кода.

    struct Counter {  private(set) var value = 0  mutating func bump() { value += 1 }} enum Step {…
  10. 10

    Что требуют Equatable и Hashable и что произойдёт, если у двух разных значений одинаковый хэш?

    Equatable требует ==, а Hashable добавляет hash(into:) с одним правилом: равные значения должны давать равные хэши.

    struct Tag: Hashable {                  // == and hash(into:) synthesized  let name: String  let colorHex: String} struct User: Hashable, Identifiable {…
  11. 11

    Это объявление делегата не компилируется. Какая ошибка и как её исправить?

    protocol DownloaderDelegate {  func downloader(_ downloader: Downloader, didFinish text: String)} final class Downloader {  weak var delegate: DownloaderDelegate?   func finish(with text: String) {    delegate?.downloader(self, didFinish: text)  }}

    weak работает только с экземплярами классов, а этому протоколу может соответствовать и структура, поэтому компилятор выдаёт 'weak' must not be applied to non-class-bound 'any DownloaderDelegate'.

  12. 12

    Что такое deinit, когда он вызывается и в каком потоке выполняется?

    deinit служит методом очистки класса, который вызывается автоматически прямо перед тем, как ARC освободит экземпляр, то есть когда исчезает последняя сильная ссылка.

    final class ImageLoader {  private var task: Task<Void, Never>?   func start() { task = Task { /* download */ } }   deinit {…
  13. 13

    Чем отличаются static- и class-члены и что меняет final?

    Оба объявляют члены типа, а не экземпляра, но class-член подкласс может переопределить, а static нет: в классе static означает class final.

    class Formatter {  static func make() -> Formatter { Formatter() }   // cannot be overridden  class var kind: String { "generic" }             // subclasses may override  // class var cache = 0      // error: class stored properties not supported}…
  14. 14

    Как устроена инициализация классов в Swift: designated и convenience-инициализаторы, две фазы и failable-инициализаторы?

    Designated-инициализатор присваивает значения всем хранимым свойствам своего класса и затем вызывает designated-инициализатор суперкласса; convenience-инициализатор обязан вызвать другой инициализатор того же класса.

    class Vehicle {  let wheels: Int  var name: String   init(wheels: Int, name: String) {      // designated: own properties, then up    self.wheels = wheels…
  15. 15

    Что такое протокол-ориентированное программирование и чем оно отличается от наследования классов?

    Протокол-ориентированное программирование (POP) собирает поведение из маленьких протоколов с реализациями по умолчанию в extension, а не из дерева базовых классов.

    protocol Loggable { var label: String { get } } extension Loggable {  func log() { print(label) }}…
  16. 16

    Что выведет этот код и почему два вызова ведут себя по-разному?

    protocol Greeter {  func greet() -> String} extension Greeter {  func greet() -> String { "default greet" }  func shout() -> String { "default shout" }}struct Robot: Greeter {  func greet() -> String { "beep" }  func shout() -> String { "BEEP" }} let robot: any Greeter = Robot()print(robot.greet())print(robot.shout())

    Выведет beep, а затем default shout.

  17. 17

    Могут ли у протокола в Swift быть необязательные методы? Сравните @objc optional и реализацию по умолчанию.

    Помечать требования как optional можно только в @objc-протоколе, и тогда соответствовать ему могут только классы; в чистом Swift тот же эффект даёт реализация по умолчанию в расширении протокола.

    @objc protocol ChartDataSource {  func numberOfPoints() -> Int  @objc optional func title() -> String     // may be missing} final class SalesChart: NSObject, ChartDataSource {…
  18. 18

    Почему эта структура не компилируется и как вместо неё смоделировать связный список или дерево?

    struct Node {  let value: Int  var next: Node?} var head = Node(value: 1, next: Node(value: 2, next: nil))

    Структура хранится inline, поэтому Node, содержащий опциональный Node, имел бы бесконечный размер; компилятор сообщает «value type 'Node' cannot have a stored property that recursively contains it».

  19. 19

    Когда варианты лучше моделировать через enum с associated values, а когда через протокол и отдельный тип на каждый вариант?

    Enum выбирают, когда весь набор вариантов принадлежит вам и вы хотите, чтобы компилятор нашёл каждое место, которое надо поменять; протокол выбирают, когда новые варианты должны добавляться без правки существующего кода, например из другого модуля.

  20. 20

    Найдите баг: после обновления 2.3 заказы, закэшированные предыдущей версией, показывают неверный статус.

    enum OrderStatus: Int, Codable {  case draft      // added in 2.3  case paid  case shipped  case delivered} struct Order: Codable {  let id: Int  var status: OrderStatus} // Orders are cached on disk between launchesfunc loadCachedOrder(from data: Data) throws -> Order {  try JSONDecoder().decode(Order.self, from: data)}

    Неявные Int raw values задаются позициями: вставка draft в начало сдвигает все кейсы на единицу, поэтому сохранённая 1, означавшая shipped в 2.2, теперь декодируется как paid, а старые заказы delivered показываются как shipped.

  21. 21

    Почему не-final класс должен помечать инициализатор, выполняющий требование init из протокола, как required?

    Каждый подкласс наследует соответствие протоколу, значит, у каждого подкласса тоже должен быть этот инициализатор, и через required компилятор это гарантирует.

    protocol Buildable {  init(id: String)} class Node: Buildable {  let id: String…
  22. 22

    Как устроена диспетчеризация методов в Swift (static, vtable, witness table и message dispatch) и сколько стоит каждая?

    Swift использует самую дешёвую диспетчеризацию, которую допускает объявление: прямые вызовы для значимых типов и final-членов, vtable для переопределяемых методов классов, witness table для требований протоколов и сообщения Objective-C только для @objc dynamic.

  23. 23

    Найдите баг: pull-to-refresh вызывает refreshAll, но экран ленты так и не перезагружается.

    @MainActor protocol Refreshable: AnyObject {  func refresh()}extension Refreshable {  func refresh() {}  // default: nothing to reload} class BaseViewController: UIViewController, Refreshable {} final class FeedViewController: BaseViewController {  func refresh() { reloadFeed() }} @MainActor func refreshAll(_ screens: [any Refreshable]) {  screens.forEach { $0.refresh() }}

    BaseViewController соответствует протоколу через пустую реализацию по умолчанию из extension, поэтому witness table протокола указывает на неё.

  24. 24

    Тип соответствует двум протоколам, и каждый даёт свою реализацию refresh() по умолчанию. Что сделает Swift и как разрешить конфликт?

    Если ни одна реализация по умолчанию не специфичнее другой, Swift не угадывает: либо соответствие протоколу не компилируется, либо вызов неоднозначен, а исправить это можно, реализовав refresh() в самом типе.

    protocol Reloadable { func refresh() }protocol Pollable { func refresh() }extension Reloadable { func refresh() { print("from cache") } }extension Pollable { func refresh() { print("from network") } } struct Feed: Reloadable, Pollable {…