Давно я не писал о кодерском...
На прошлой неделе тестировщик показал мне баг в нашем iOS-приложении: после ввода текста интерфейс должен был переходить в другой режим, но ничего не происходило. Повторялось это стабильно. «Ок, посмотрю», — сказал я и пошёл фиксить на своём устройстве.
Попытался воспроизвести — никак. Пошёл за девайсом, на котором баг воспроизвёлся, но после перезапуска на нём всё заработало правильно. Странно. Попробовал ещё несколько раз — без толку. Полез смотреть код, перепроверил несколько раз всю логику — вроде всё верно. Потратил больше часа и не смог найти проблему. «Как же так, я же только что видел, что он воспроизводится!» Вспомнив про фазу луны, решил запустить в последний раз, и — ура! — проблема повторилась. Начал дебажить. Смотрю — в коде есть метод:
- (BOOL)isEnabled {
return (_text);
}
Здесь _text — это строка, и она содержит правильный текст. Адрес у неё 0x15f11800, а метод возвращает NO.
Конечно, я знал, что так неявно возвращать BOOL не нужно, и сам так никогда не пишу. Но в чужом коде глаз за такое не зацепился. Написал тест:
BOOL BOOLCast() {
long long address = 0x15f11800;
return address;
}
int main(int argc, const char * argv[]) {
NSLog(@"\%@\n", BOOLCast() ? @"YES" : @"NO");
return 0;
}
Выводит:
NO
Xcode не выдаёт никаких ворнингов, анализатор — тоже. Забавно, что такая конструкция сработает правильно:
if (_text)
Дело в том, что в Objective-C тип BOOL объявлен как signed char, поэтому адрес 0x15f11800 обрезается и приводится к нулю. Другими словами, 0x15f11800 делится нацело на размер BOOL. То есть баг воспроизводится каждый раз, когда последний байт адреса оказывается нулевым.
368 121 856 / 256 = 1 437 976
Коллега сказал, что в C++ такая штука будет работать правильно с типом bool, так как инициализация происходит по-другому. Проверяем:
#include <iostream>
typedef signed char BOOL;
bool boolCast(void) {
long long i = 0x15f11800;
return i;
}
BOOL BOOLCast(void) {
long long i = 0x15f11800;
return i;
}
int main(int argc, const char * argv[]) {
printf("bool: %d\nBOOL: %d\n", boolCast(), BOOLCast());
return 0;
}
bool: 1
BOOL: 0
В Objective-C тоже есть тип bool, и результат аналогичен:
#import <Foundation/Foundation.h>
bool boolCast() {
long long i = 0x15f11800;
return i;
}
BOOL BOOLCast() {
long long i = 0x15f11800;
return i;
}
int main(int argc, const char * argv[]) {
NSLog(@"\nbool: %d\nBOOL: %d\n", boolCast(), BOOLCast());
return 0;
}
bool: 1
BOOL: 0
Однако Apple советует везде использовать BOOL. В любом случае, чтобы не зависеть от платформы, языка и архитектуры, лучше явно сравнивать с nil. Лишние символы спасут от непонятных багов:
return _text != nil;
Ну а как же Свифт? Свифт — молодец:
А теперь представьте, сколько подобного кода может быть у вас :-)