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)), а результат меня изрядно озадачил:

Англійська для IT від Englishdom.
В межах курсу можна освоїти ключові ІТ-теми та почати без проблем говорити з іноземними колегами.
Дійзнайтеся більше
(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, внесло небольшую лепту в запутывание ситуации.

Онлайн-курс "Data Science with Python" від robot_dreams.
Навчіться користуватися бібліотеками Python для розв’язання задач дата-саєнтистики, обробки масивів даних та побудови ML-моделей.
Програма курсу і реєстрація

Позже, порывшись немного в сети, я обнаружил, что подобные вопросы давно обсуждаются в сообществе 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.

Онлайн-курс "2D Animation" від Skvot.
Покроково та з фідбеком від лекторки увійдіть у 2D-анімацію через вивчення софтів, інструментів та створення кейсу у портфоліо.
Програма курсу та реєстрація

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

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

PHP Developer в ScrumLaunch
Всего просмотровВсего просмотров
2434
#1
Всего просмотровВсего просмотров
2434
Founder at Shallwe, Python Software Engineer (Django/React)
Всего просмотровВсего просмотров
113
#2
Всего просмотровВсего просмотров
113
Career Consultant в GoIT
Всего просмотровВсего просмотров
95
#3
Всего просмотровВсего просмотров
95
CEO & Founder в Trustee
Всего просмотровВсего просмотров
94
#4
Всего просмотровВсего просмотров
94
Рейтинг блогеров

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

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

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