Когда ты устраиваешься на новую работу - собеседование является неотъемлемой частью твоего пути. Ты наверняка прочитал уже много разных ресурсов на эту тему и вопросом value vs reference type тебя не удивить. Если говорим про UI - тут я не буду затрагивать UIKit, эта тема хорошо уже разобрана сотни и тысячи раз, тут мы поговорим про SwiftUI. Ну не будем затягивать, что же интересного я для тебя приготовил...


"Как переписать метод класса родителя, который мы определили в extension?"

Такой вопрос я встретил на одном интервью и сначала он поверг меня в шок, ведь нам всегда говорили обратное, что сделать этого нельзя. Собственно так я и ответил, но интервьюер все же настоял на том, что такое возможно и надо подумать. Я попробовал порассуждать от того, почему же такой код не соберется...

class MyClass { }

extension MyClass {
    func foo() {
        print("1")
    }
}

class MySecodClass: MyClass {
    override func foo() {
        print("2")
    }
}

И тут я вспомнил ту самую тему из Swift, которую не все очень любят, но кто её знает хорошо - гораздо лучше начинает понимать тонкости языка - диспетчеризация. В extension у класса у нас статическая диспетчеризация, что и не дает нам переписывать этот метод где-то ещё, потому что мы однозначно определяем место, откуда будет браться реализация функции.

Как же нам это исправить - сменить метод диспетчеризации. Это можно сделать с помощью ключевого слова @objc. Тем самым мы изменим диспетчеризацию на message dispatch. А она нам позволяет изменять имплементацию метода.

class MyClass { }

extension MyClass {
    @objc func foo() {
        print("1")
    }
}

class MySecodClass: MyClass {
    override func foo() {
        print("2")
    }
}

"Что ты можешь рассказать о непрозрачных типах и чем они отличаются от generic типов?"

Этот вопрос стоит отнести к очень важным, если ты работаешь со SwiftUI. Поэтому наверное я отношу его на данный момент к необычным, так как SwiftUI все же редко спрашивают на данный момент. А там мы используем непрозрачные типы постоянно

struct MyView: View {
        var body: some View {
                 Text("Hello, World!")
             }
     }

Думаю эти строчки узнали все любители SwiftUI)) Что же они обозначают? Начнем с базового действия - прыгнем в определение View - что же это такое

public protocol View {

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required ``View/body-swift.property`` property.
    associatedtype Body : View

    /// The content and behavior of the view.
    ///
    /// When you implement a custom view, you must implement a computed
    /// `body` property to provide the content for your view. Return a view
    /// that's composed of built-in views that SwiftUI provides, plus other
    /// composite views that you've already defined:
    ///
    ///     struct MyView: View {
    ///         var body: some View {
    ///             Text("Hello, World!")
    ///         }
    ///     }
    ///
    /// For more information about composing views and a view hierarchy,
    /// see <doc:Declaring-a-Custom-View>.
    @ViewBuilder @MainActor var body: Self.Body { get }
}

То есть это у нас "generic protocol", который обязан иметь свойство body тип Body, который в свою очередь обязан быть подписан под протокол View. Так, хорошо, то есть нам нужно реализовать это свойство. Для этого мы как раз и пишем var body: some View ... Потому что нам важно, чтобы возвращало просто что-то подписанное под протокол View.

Теперь в чем же разница generic и ключевого слова some. Основная рекомендация такова - some лучше использовать тогда, когда мы хотим просто один раз обозначить, что объект на входе функции или где-то ещё должен быть подписан под протокол Equatable например(то есть мы единожды упоминаем что что-то подписано под такой-то протокол). То есть две функции ниже схожи, просто в первом случае мы работаем с объектом подписанным под протокол, а во втором случае с чем-то удовлетворяющем протоколу Equatable.

func generic<T: Equatable>(name: T) {
    print("name")
}

func generic(name: some Equatable) {
    print("name")
}

И тут можно ещё уточнить момент про однократное использование. Если мы захотим вернуть объект типа T, то нам достаточно будет дописать логичную запись -> T. В случае some надо будет написать -> some Equatable, что несколько удлиняет строку и читаемость записи. Особенно странно выглядит запись следующего вида...

func someFunc<T: Equatable>(name: T) -> some Equatable {
    return 1
}

Тут все же палка о двух концах, что такая запись валидная. Совмещение generic и some мне кажется более логично тогда, когда входной параметр должен быть подписан на два протокола, а выходной только на один к примеру.

Тут кто-то может сказать, что вместо some(в примере с функцией generic()) мы можем написать и any, но вот такие записи уже не будут равносильны. Потому что у some есть одна особенность. Если мы в функцию с ключевым словом some на вход будем ожидать string и внутри функции будем взаимодействовать как со String, то some нам этого не даст сделать, что на самом деле логично. Потому что если мы ожидаем String, то в типе входного параметра можем указать String, а не some Equatable. А вот any в данном случае позволит нам это сделать, что на мой взгляд менее применимо на практике. То есть firstFunc() скомпилируется, а вот secondFunc() нет.

func firstFunc(name: any Equatable) {
    var temp = name
    temp = ""
}

func secondFunc(name: some Equatable) {
    var temp = name
    temp = ""
}

И на самом деле логика первого кода лично мне непонятна, поэтому запрет у some для такого случая лично для меня вполне логичен. Конечно есть случай, когда any стоит применить вместо some. Например обратимся к коду ниже

var firstMass: [some Equatable] = [1, 2, 3, "Name"]

var secondMass: [any Equatable] = [1, 2, 3, "Name"]

На firstMass наш компилятор поругается, потому что some подразумевает реализацию объектов одного типа, подписанных под протокол Equatable. В случае secondMass ключевое слово пропускает все объекты удовлетворяющие протоколу Equatable.

Наверное на этом моменте стоит остановиться и перейти дальше, потому что тут можно закопаться...

"Что будет напечатано в консоль?"

Понятное дело, что тут зависит от того, что вам показали на экране. Я видел много разных задач на разные темы. Думаю с gcd ты итак уже не один раз встретился и там просто дело практики. Я бы хотел сюда вставить задачу, которую давал сам на публичном мок собесе.

class MyClass: NSObject {
  
    var name: String
    
    override var description: String {
        return name
    }
  
    init(name: String) {
        self.name = name
    }
}

var firstMass: [MyClass] = [MyClass(name: "Andrey"), MyClass(name: "Viktor"), MyClass(name: "Lena")]
var secondMass = firstMass
firstMass.popLast()
firstMass.last?.name = "Ivan"
print(secondMass)

Тут надо понимать один момент, что массив - это value семантика. Это значит, что с самим массивом мы конечно работаем как с value типом, но в данном случае в строчке firstMass.last?.name = "Ivan" мы обратились не к массиву, а к объекту класса и поменяли по ссылке его значение. И ещё тут нужно не пропустить момент, что до этого в firstMass мы удалили последний элемент, следовательно мы обращаемся к Viktor и меняем его имя на Ivan. Но при этом количество элементов во втором массиве осталось равным трем(при ответе на этот вопрос стоит ещё упомянуть механизм COW, потому что на второй строчке var secondMass = firstMass мы копируем ссылку пока что, а не создаем полноценную копию, что очень важно). По итогу выведется [Andrey, Ivan, Lena].

Примерно это, только более простыми словами и подольше я рассказал в видео, но надеюсь и это описание было тебе доступно.

Спасибо, что прочитал статью, надеюсь тебе было интересно и полезно. Пиши, какие вопросы попадались тебе на собесе или какую тему хотел бы, чтобы я разобрал.

Комментарии (11)


  1. Krypt
    04.12.2023 10:47

    Добавить сюда баги в диспетчеризации Swift:
    https://stackoverflow.com/questions/34847318/swift-protocol-extension-method-dispatch-with-superclass-and-subclass
    У нас будет отличный список того, почему Swift - самый упоротый язык из тех, что я встречал
    Ну. по крайней мере теперь можно писать some/any Equatable, на это ушло всего каких-то 5 лет развития языка


    1. ios_dev_187 Автор
      04.12.2023 10:47

      Ну они пишут у себя в доке, что легкочитаемый, а что там можно натворить с generic они умалчивают))))


      1. Krypt
        04.12.2023 10:47

        Ну читать его действительно легко. А вот писать...


  1. Gummilion
    04.12.2023 10:47

    Интересная вещь, попробовал выполнить такой код:

    func firstFunc(name: any Equatable) {
        var temp = name
        temp = "world"
        NSLog("Hello \(temp)")
    }
    
    firstFunc(name: 3)

    Думал, сломается, но temp сначала создается как Int, а после присваивания стал String - оказывается, в Свифте тип на лету может поменяться


    1. FreeNickname
      04.12.2023 10:47

      Тип не поменялся. У Вас же написано – any Equatable. Он и остался any Equatable. В этой переменной лежит значение любого типа, реализующего Equatable, и это единственная гарантия, которую такая декларация предоставляет. А то есть вон вообще вот такой срыв покровов)

      var a: Any = "a string"
      a = 5
      a = "and a string again!"


      1. Gummilion
        04.12.2023 10:47

        Просто я полагал, что переменная при иницализации получает конкретный тип и он уже не может меняться. Выходит, что some так работает (по сути, синтаксический сахар для шаблона), а вот any - полностью динамический и конкретный тип может меняться (конечно, если удовлетворяет протоколу)


        1. ios_dev_187 Автор
          04.12.2023 10:47

          Ну вот что это синтаксический сахар для шаблона - по сути да


    1. ios_dev_187 Автор
      04.12.2023 10:47

      ну тут да, опять же потому что any написано, оно одобряет все, что подходит под Equatable


    1. Krypt
      04.12.2023 10:47

      Это нововведение в Swift 5.6, раньше действительно было нельзя написать temp = name

      Вот так пишешь приложение (особенно в первые дни языка, портируя код с ObjC), и решаешь на свой интерфейс-теперь-протокол повесить Equitable. И у тебя не компилится половина кода, потому что твой протокол больше не "concrete type", а "generic constraint" и с практической точки зрения вообще не существует


      1. FreeNickname
        04.12.2023 10:47

        В первые дни языка вообще весело было) Swift 1 -> 2 и Swift 2 -> 3 оба раза морозили всю работу на несколько дней, идущих на то, чтобы проект хотя бы снова собирался :)


        1. Krypt
          04.12.2023 10:47

          Вообще первые пару лет я Swift старательно избегал, с критерием "я не коснусь его пока они в течении 2х мажорных версий не сломают ничего мажорного".
          Формально этот критерий ещё не выполнен, ждём swift 6.

          Имхо, Swift развивается очень медленно и очень хаотично. У нас сейчас 4 способа (включая "универсальные") описать keypath, а вот писать с его использованием в не-ObjC совместимые типы нельзя до сих пор. Мне тут периодически в спорах в достоинства Swift приводят рефлексию, которой по факту в Swift нет, всё, что есть - это bridging на ObjC рефлексию