nil, Nil, NULL, NSNull. Всё — ничто

в 21:16, , рубрики: Cocoa, ios development, objective-c, разработка под iOS, метки: , ,

Понятие отсутствия чего-либо — ничего — это не только философская, но и вполне улититарная единица: людям, как и созданным ими вычислительным машинам, часто приходится оперировать «пустыми», выражающими лишь несуществование чего-либо значениями, будь то отсутствие денег на банковской карте, about:blank, чёрные дыры или grep "${rootpswrd}" /etc/passwd.

Для выражения подобного «несуществования» в языках программирования используется большое количество различных мнемоник. Мы же рассмотрим те из них, что используются в сверхпопулярном (за прошедшую пятилетку, но, будем верить, что и на долгие лета́ вперёд) языке Objective-C.

Унификация

В Objective-C существует несколько различных типов что ни на есть самого настоящего ничего. Причиной этому служит «многослойность» языка, объединяющая и процедурную парадигму языка Си, и объектно-ориентированность Smalltalk.
Си определяет ничто как 0 (ноль) для большинства базовых (примитивных) типов и как NULL исключительно для указателей.

На самом деле, в контексте указателей применим как NULL, так и 0, ввиду того что первый — не более чем макрос-обёртка для второго:

#define NULL ((void *)0)

Однако не следует использовать NULL в качестве замены 0 в тех местах, где ноль — алгебраическое значение:

int c = b*a;
if (c != NULL) {
    printf("Neither b nor a is equal to 0n");
}

Objective-C (как настройка на Си) сохранил все формы выражения ничего своего прародителя, добавив к ним ещё один: nil. nil это указатель на нулевой объект:

#if !defined(nil)
    #define nil (id)0
#endif

То есть, технически, он эквивалентен NULL.

Пожалуй, ни одна программа на Objective-C не обходится без использования фреймворка Foundation, который в свою очередь также привносит в язык ещё одно (уже четвертое по счёту) представление ничего — класс NSNull, имеющий один-единственный (не считая унаследованных) метод +null, который возвращает синглтон класса NSNull.

Но и это ещё не всё: в дополнение к четырём вышеперечисленным мнемоникам, Foundation определяет макрос Nil (не путать с nil) — нулевой указатель типа Class:

#if !defined(Nil)
    #define Nil (Class)0
#endif

Пример использования Nil в реальной жизни придумать сложно: манипуляциями с классами в runtime занимаются лишь немногие гики. Так или иначе, вот небольшой кусок кода, инициализирующий новый класс, унаследованный от NSString, с именем NSArray (да, это не очепятка, пояснение ниже) в рантайме:

#inlcude <objc/runtime.h>
	
Class foo = objc_allocateClassPair([NSString class], "NSArray", 0);
/* 
    Так как класс с именем NSArray уже существует, функция вернёт
    значение Nil — ошибка
*/
if (Nil != foo) {
    int check = class_addMethod(foo, @selector(makeWorldBetter), (IMP)_makeWorldBetter, "v@:"));
    if (check)
        objc_registerClassPair(foo);
}

Кое-что о nil

Рассмотрим пару случаев, когда программисту на Objective-C может понадобиться nil:

  1. Иногда разработчики той или иной функции (метода) допускают возможность, что не все входные параметры могут быть заданы пользователем. Например, метод -capitalizedStringWithLocale: класса NSString принимает в качестве аргумента локаль (объект класса NSLocale), либо nil — в последнем случае при изменении регистра строки будет использоваться каноническая декомпозиция (NFD) Unicode-символов независимо от настроек локали на компьютере пользователя (то есть, данный метод будет эквивалентен методу -capitalizedString).
  2. Инициализация и «опустошение» объектов (например, для того, чтобы сообщить об ошибке):
    - (NSDictionary*)dictionaryForLicenseFile:(NSString *)path
    {
        NSData *licenseFile = [NSData dataWithContentsOfFile:path];
        /* 
            «Достаточно, вы мне плюнули в душу!» 
        */
        if (!licenseFile)
            return nil;
    			
        return [self dictionaryForLicenseData:licenseFile];
    }  	
    

    Следует, кстати, заметить, что распространённая среди разработчиков практика инициализации ivars нулевыми значениями в методах семейства -init* не имеет смысла, так как их значения уже были приравнены к нолю ещё в методе -alloc:

    … memory for all other instance variables is set to 0.

Одним из самых замечательных свойств ничего (то есть, nil) в Objective-C является возможность посылать ему совершенно любые сообщения, ответом на которые неизменно будет всё тот же nil:

    id foo = nil;
    int zero = [foo bar: @"Hello, Habr!"];

Часто эта особенность используется для сокращения количества проводимых проверок над объектами:

/* In a galaxy far, far away… */
if (foo != nil && [foo testValue: 0x90]) { … }
	
/* Программисты же из Млечного Пути могут писать просто */
if ([foo testValue: 0x31с040с3]) { … } 

Некоторые начинающие разработчики, узнав об этой особенности nil, переносят её и на ненулевые объекты, забывая проверять, существует ли вызываемый метод у данного класса, и неизбежно ловят исключения (exception), если его нет:

   /* 
      «Да хоть NSPersistentStoreCoordinator» ©  Brad Cox & Tom Love
  */
  NSData *signature = [NSString stringWithFormat: @"%lu", 0xCAFEBABE];
  /*
      Исключение вылетит —  без @catch не поймаешь: никакого метода -bytes у класса NSString нет
   */
  bytes = [signature bytes];
И ладно бы этим промышляли только начинающие разработчики…

Несколько месяцев назад я обнаружил подобный баг в системе проверки лицензии приложения The Tagger, этот баг позволял любому пользователю (а не только тому, кто умеет работать напильником) обойти ограничение триальной версии, просто подсунув программе невалидный файл .lic (точнее .ttl).
Само собой разумеется, я уведомил об этом разработчика, но ответа так и не получил.
К слову, за код проверки отвечала Cocoa-версия популярного класса AquaticPrime (которой, обычно, разработчики предпочитают CoreFoundation-версию).

К тому же, не стоит совсем забывать о проверках объектов на равенство nil: очень многие методы из стандартных фреймворков, да и (готов поспорить) некоторые ваши собственные, просто не расчитаны на работу с «нулевыми» объектами. Взять хотя бы +[NSArray arrayWithObjects:] — попробуйте создать массив из трёх объектов с nil в начале списка и посмотрите на результат. Ещё один пример: +[NSString stringWithString:], вызвав который с nil в качестве аргумента, получите исключение.

NSNull: ничто или нечто?

NSNull используется внутри фреймворка Foundation и некоторых других для того, чтобы обойти ограничение стандартных коллекций, вроде NSArray или NSDictionary, заключающееся в том, что они не могут содержать в себе значения nil.

NSNull — это своего рода обёртка над NULL и nil, позволяющая хранить их в объектах-коллекциях Objective-C.

Может быть не совсем понятно, зачем нужно хранить где-то нулевые объекты. Рассмотрим пример: в вашем iOS-приложении вам нужно распарсить некий JSON-файл, в котором есть кусок наподобие этого

{
 	"keys": ["X"], 
 	"command": "set_action_motion",
 	"args": {
	        "action": "vi_left_delete",
	        "motion": null
 	},
 	"context": [{"key": "setting.command_mode", "modes": [] }]
}

Скорее всего вы не будете разбирать JSON «руками», а воспользуетесь готовой библиотекой (вроде JSONKit), которая в момент преобразует его в объекты стандартных классов Objective-C. Однако, значения argc["motion"] и context[0]["modes"] равны ничему, то есть null и пустому массиву соответственно! Эти-то значения и будут считаны в объекты класса NSNull в результате работы парсера.

Опять 11001b

Непоследок сводная таблица:

Обозначение Легким движением руки… Поснение
0 0 Ноль — он везде ноль
NULL (void *)0 Нулевой указатель в языке Си
nil (id)0 Нулевой указатель на объект Objective-C
Nil (Class)0 Нулевой указатель типа Class в Objective-C
NSNull [NSNull null] Синглтон класса NSNull — обёртки над nil и Null

Литература

P.S. я не стал оформлять этот топик как первод, потому как довольно сильно в итоге отошёл от оригинальной статьи. Надеюсь, в лучшую сторону.

Автор: ericbro

Источник

Поделиться

* - обязательные к заполнению поля