11 ноября 2014, 10:36

О неявном приведении типов

Давно я не писал о кодерском...

На прошлой неделе тестировщик показал мне баг в нашем 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;

Ну а как же Свифт? Свифт — молодец:

А теперь представьте, сколько подобного кода может быть у вас :-)

Поделиться
Запинить