Рубріки: Mobile app

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

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

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

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

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

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

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

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

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

(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.

Останні статті

Токсичные коллеги. Как не стать одним из них и прекратить ныть

В благословенные офисные времена, когда не было большой войны и коронавируса, люди гораздо больше общались…

07.12.2023

Делать что-то впервые всегда очень трудно. Две истории о начале карьеры PM

Вот две истории из собственного опыта, с тех пор, когда только начинал делать свою карьеру…

04.12.2023

«Тыжпрограммист». Как люди не из ІТ-отрасли обесценивают профессию

«Ты же программист». За свою жизнь я много раз слышал эту фразу. От всех. Кто…

15.11.2023

Почему чат GitHub Copilot лучше для разработчиков, чем ChatGPT

Отличные новости! Если вы пропустили, GitHub Copilot — это уже не отдельный продукт, а набор…

13.11.2023

Как мы используем ИИ и Low-Code технологии для разработки IT-продукта

Несколько месяцев назад мы с командой Promodo (агентство инвестировало в продукт более $100 000) запустили…

07.11.2023

Университет или курсы. Что лучше для получения IT-образования

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

19.10.2023