ru:https://highload.today/blogs/self-da-ne-tot-istoriya-neobychnogo-baga/ ua:https://highload.today/uk/blogs/self-da-ne-tot-istoriya-neobychnogo-baga/
logo
Mobile app      30/03/2021

Self, да не тот: история необычного бага

Павел Дмитриев BLOG

full-stack iOS developer компании Postindustria

Интересно, есть ли еще люди, как я, — любящие программирование за тот самый момент, когда ты осознаешь причину ошибки и понимаешь, что все с самого начала было довольно очевидно? Сразу после этого мистика улетучивается, и все расставляется по своим местам.

Сегодня я расскажу о как раз таком случае, постигшем меня на почти ровном месте. Подозреваю, что для многих развязка этого «триллера» будет довольно очевидной задолго до конца. Хотя я потратил какое-то время, пытаясь понять что происходит.

Появление проблемы

Большинство читающих эту статью знакомо с инициализацией переменных с помощью автоматически вызываемого замыкания. В свою очередь, большинство из этого большинства использовали его для создания того или иного элемента управления с последующей настройкой его полей. Такой подход позволяет обособить создание элементов управления, выделив их и визуально, и логически.

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

Ладно, думаю, хватит риторических вопросов, лучше покажу вам банальнейший фрагмент кода, который каждый из нас писал много раз:

class CustomCell: UITableViewCell {
    let actionButton: UIButton = {
        let button = UIButton(type: .system)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setTitle("Do", for: .normal)
        button.addTarget(self, action: #selector(buttonTapped(sender:)), for: .touchUpInside)

        return button
    }()

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        contentView.addSubview(actionButton)
        // добавить констрейнты
    }

    @objc func buttonTapped(sender: UIButton) {
        print("tap !")
    }
}

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

Ошибочные гипотезы

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

Проверка показала, что isUserInteractionEnabled выставлен в true по всей иерархии. Кроме того, я убедился, что кнопка добавлена в contentView ячейки, а не просто во view (поднимите руку те, кто делал такую ошибку во времена своего джунства). На всякий случай я даже временно отключил allowsSelection для таблицы, но естественно дело было не в нем.

Следующей гипотезой стало то что self в момент выполнения замыкания, создающего кнопку равен nil. Эту теорию я почти сразу откинул, так как она противоречит знаниям про жизненный цикл класса. Действительно, инициализация полей класса, для которых заданы значения, происходит до вызова конструктора, но self при этом должен быть недоступен, да и он вообще не опционал.

Разумеется, мне стало интересно, «что ты такое», и в код замыкания добавился print(type(of: self)), а результат меня изрядно озадачил:

Онлайн курс з промт інжинірингу та ефективної роботи з ШІ.
Курс-інтенсив для отримання навичок роботи з ChatGPT та іншими інструментами ШІ для професійних та особистих задач, котрі допоможуть як новачку, так і професіоналу.
Записатися на курс
(CustomCell) -> () -> CustomCell

Крутящиеся в голове мысли — «какой-то тут self странный» и «а откуда он вообще взялся» — натолкнули на правильный эксперимент.

Поменяв let на lazy var, я добился того, что код заработал, ведь ленивые переменные инициализируются уже после того как класс инициализирован, и в их замыканиях self содержит ожидаемое значение, что и подтвердил отладочный вывод:

CustomCell

«Но что это было?»

В целом, на этом этапе ошибку уже можно было считать исправленной. Понятно, почему она возникла (вместо self у нас какая-то ерунда, а так как первый параметр addTarget(_:action:for:) имеет тип Any?, метод радостно принимает почти что угодно). Очевидно, как ее избежать (добавлять обработчик там, где уже доступен «нормальный» self), но мне, как, наверно, и большинству людей на моем месте, стало интересно, что такое self в случае let.

Зубры мобильной разработки, помнящие времена Objective-C уже поняли, откуда «растут ноги» этого неправильного self: на самом деле, это instance-метод NSObject, ожидаемо возвращающий сам класс, помноженный на каррирование при объявлении методов, просуществовавшее в Swift на протяжении двух первых версий. В ходе реализации SE-0002 его (к счастью) выпилили из синтаксиса декларации функций, но в семантике методов он остался.

В обычной жизни с этим рудиментарным self столкнуться довольно сложно: его «закрывает» ключевое слово, поэтому, чтобы «достучаться» до метода NSObject, нужно использовать обратные кавычки. Сравните:

print(type(of: NSObject.self))
print(type(of: NSObject.`self`))
print(type(of: NSObject().self))
print(type(of: NSObject().`self`))

Эти четыре вызова дают нам ожидаемые результаты:

NSObject.Type
(NSObject) -> () -> NSObject
NSObject
() -> NSObject

Именно со вторым вариантом я и столкнулся изначально, только вместо NSObject попался его дальний потомок CustomCell.

Многолетняя проблема

Забавно, что Xcode одновременно помогал и мешал найти ошибку.

Помощь заключалась в попытках подставить скобки при автодополнении. IDE понимает, что это метод, и логично предполагает, что его надо вызывать. Я, к сожалению, изначально не обратил на это внимания. А вот то, что подсветка кода выделяла его тем же цветом, что и обычный self, внесло небольшую лепту в запутывание ситуации.

Позже, порывшись немного в сети, я обнаружил, что подобные вопросы давно обсуждаются в сообществе Swift. Вот, например, тема на форуме самого Swift или схожая проблема на Stack Overflow (забавно, что все наталкиваются на эту проблему именно с UIButton). Да и в багтрекере есть как минимум SR-4559 и SR-4865.

Впрочем, последние два имеют достаточно низкий приоритет, и не факт, что этот вопрос будет как-то решен в ближайшем будущем. Поэтому главная защита от подобных проблем — только расширение собственных знаний о работе «глубинных» механизмов языка и библиотек.

If you have found a spelling error, please, notify us by selecting that text and pressing Ctrl+Enter.

Онлайн-курс Frontend-разробник.
Курс на якому ти напишеш свій чистий код на JavaScript, попрацюєш із різними видами верстки, а також адаптаціями проектів під будь-які екрани. .
Зарееструватися

Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.

Топ-5 самых популярных блогеров февраля

Всего просмотровВсего просмотров
229
#1
Всего просмотровВсего просмотров
229
Всего просмотровВсего просмотров
209
#2
Всего просмотровВсего просмотров
209
QA в CodeGeeks Solutions
Всего просмотровВсего просмотров
156
#3
Всего просмотровВсего просмотров
156
Senior Project Manager at Nemesis
Всего просмотровВсего просмотров
99
#4
Всего просмотровВсего просмотров
99
Software Architect at Devlify
Всего просмотровВсего просмотров
95
#5
Всего просмотровВсего просмотров
95
Рейтинг блогеров

Ваша жалоба отправлена модератору

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: