Objective-C 字面量

简介

Clang 同步引入了三个新特性:NSNumber 字面量提供了一种从标量字面量表达式创建 NSNumber 的语法;集合字面量为创建数组和字典提供了一种简写方式;对象下标提供了一种使用下标操作 Objective-C 对象的方式。苹果编译器版本的用户可以从苹果 LLVM 编译器 4.0 开始使用这些特性。开源 LLVM.org 编译器版本的用户可以从 clang v3.1 开始使用这些特性。

这些语言添加简化了常见的 Objective-C 编程模式,使程序更简洁,并提高了容器创建的安全性。

本文档介绍了这些特性在 clang 中的实现方式,以及如何在自己的程序中使用它们。

NSNumber 字面量

框架类 NSNumber 用于将标量值包装在对象中:带符号和无符号整数 (char, short, int, long, long long),浮点数 (float, double) 和布尔值 (BOOL, C++ bool)。包装在对象中的标量值也称为装箱值。

在 Objective-C 中,任何以 '@' 字符为前缀的字符、数字或布尔字面量将计算为指向一个使用该值初始化的 NSNumber 对象的指针。C 的类型后缀可用于控制数字字面量的大小。

示例

以下程序演示了 NSNumber 字面量的规则

void main(int argc, const char *argv[]) {
  // character literals.
  NSNumber *theLetterZ = @'Z';          // equivalent to [NSNumber numberWithChar:'Z']

  // integral literals.
  NSNumber *fortyTwo = @42;             // equivalent to [NSNumber numberWithInt:42]
  NSNumber *fortyTwoUnsigned = @42U;    // equivalent to [NSNumber numberWithUnsignedInt:42U]
  NSNumber *fortyTwoLong = @42L;        // equivalent to [NSNumber numberWithLong:42L]
  NSNumber *fortyTwoLongLong = @42LL;   // equivalent to [NSNumber numberWithLongLong:42LL]

  // floating point literals.
  NSNumber *piFloat = @3.141592654F;    // equivalent to [NSNumber numberWithFloat:3.141592654F]
  NSNumber *piDouble = @3.1415926535;   // equivalent to [NSNumber numberWithDouble:3.1415926535]

  // BOOL literals.
  NSNumber *yesNumber = @YES;           // equivalent to [NSNumber numberWithBool:YES]
  NSNumber *noNumber = @NO;             // equivalent to [NSNumber numberWithBool:NO]

#ifdef __cplusplus
  NSNumber *trueNumber = @true;         // equivalent to [NSNumber numberWithBool:(BOOL)true]
  NSNumber *falseNumber = @false;       // equivalent to [NSNumber numberWithBool:(BOOL)false]
#endif
}

讨论

NSNumber 字面量只支持 '@' 后面的字面量标量值。因此,@INT_MAX 可以工作,但 @INT_MIN 无法工作,因为它们的定义如下所示

#define INT_MAX   2147483647  /* max value for an int */
#define INT_MIN   (-2147483647-1) /* min value for an int */

INT_MIN 的定义不是一个简单的字面量,而是一个带括号的表达式。带括号的表达式可以使用 装箱表达式 语法,将在下一节中介绍。

因为 NSNumber 目前不支持包装 long double 值,所以使用 long double NSNumber 字面量(例如 @123.23L)将被编译器拒绝。

以前,BOOL 类型只是 signed char 的一个 typedef,YESNO 是宏,分别展开为 (BOOL)1(BOOL)0。为了支持 @YES@NO 表达式,这些宏现在在 <objc/objc.h> 中使用新的语言关键字定义

#if __has_feature(objc_bool)
#define YES             __objc_yes
#define NO              __objc_no
#else
#define YES             ((BOOL)1)
#define NO              ((BOOL)0)
#endif

编译器将 __objc_yes__objc_no 隐式转换为 (BOOL)1(BOOL)0。这些关键字用于区分 BOOL 和整数字面量。

Objective-C++ 也支持 @true@false 表达式,它们等效于 @YES@NO

装箱表达式

Objective-C 提供了一种新的语法用于装箱 C 表达式

@( <expression> )

支持标量(数字、枚举、BOOL)、C 字符串指针和一些 C 结构(通过 NSValue)的表达式

// numbers.
NSNumber *smallestInt = @(-INT_MAX - 1);  // [NSNumber numberWithInt:(-INT_MAX - 1)]
NSNumber *piOverTwo = @(M_PI / 2);        // [NSNumber numberWithDouble:(M_PI / 2)]

// enumerated types.
typedef enum { Red, Green, Blue } Color;
NSNumber *favoriteColor = @(Green);       // [NSNumber numberWithInt:((int)Green)]

// strings.
NSString *path = @(getenv("PATH"));       // [NSString stringWithUTF8String:(getenv("PATH"))]
NSArray *pathComponents = [path componentsSeparatedByString:@":"];

// structs.
NSValue *center = @(view.center);         // Point p = view.center;
                                          // [NSValue valueWithBytes:&p objCType:@encode(Point)];
NSValue *frame = @(view.frame);           // Rect r = view.frame;
                                          // [NSValue valueWithBytes:&r objCType:@encode(Rect)];

装箱枚举

Cocoa 框架经常使用枚举定义常量值。虽然枚举值是整数值,但它们不能直接用作装箱字面量(这避免了与未来的 '@' 为前缀的 Objective-C 关键字发生冲突)。相反,枚举值必须放在装箱表达式中。以下示例演示了使用包含装箱枚举值的字典配置 AVAudioRecorder

enum {
  AVAudioQualityMin = 0,
  AVAudioQualityLow = 0x20,
  AVAudioQualityMedium = 0x40,
  AVAudioQualityHigh = 0x60,
  AVAudioQualityMax = 0x7F
};

- (AVAudioRecorder *)recordToFile:(NSURL *)fileURL {
  NSDictionary *settings = @{ AVEncoderAudioQualityKey : @(AVAudioQualityMax) };
  return [[AVAudioRecorder alloc] initWithURL:fileURL settings:settings error:NULL];
}

表达式 @(AVAudioQualityMax)AVAudioQualityMax 转换为整型,并相应地装箱该值。如果枚举具有 固定底层类型,如

typedef enum : unsigned char { Red, Green, Blue } Color;
NSNumber *red = @(Red), *green = @(Green), *blue = @(Blue); // => [NSNumber numberWithUnsignedChar:]

那么将使用固定底层类型来选择正确的 NSNumber 创建方法。

装箱枚举类型的值将产生一个 NSNumber 指针,其创建方法取决于枚举的底层类型,可以是 固定底层类型 或一个能够表示枚举所有成员值的编译器定义的整数类型

typedef enum : unsigned char { Red, Green, Blue } Color;
Color col = Red;
NSNumber *nsCol = @(col); // => [NSNumber numberWithUnsignedChar:]

装箱 C 字符串

'@' 符号为前缀的 C 字符串字面量表示一个 NSString 字面量,就像以 '@' 符号为前缀的数字字面量表示一个 NSNumber 字面量一样。当带括号的表达式的类型为 (char *)(const char *) 时,装箱表达式的结果是指向包含等效字符数据的 NSString 对象的指针,该数据被认为以 ‘\0’ 结尾并以 UTF-8 编码。以下示例将 C 样式的命令行参数转换为 NSString 对象。

// Partition command line arguments into positional and option arguments.
NSMutableArray *args = [NSMutableArray new];
NSMutableDictionary *options = [NSMutableDictionary new];
while (--argc) {
    const char *arg = *++argv;
    if (strncmp(arg, "--", 2) == 0) {
        options[@(arg + 2)] = @(*++argv);   // --key value
    } else {
        [args addObject:@(arg)];            // positional argument
    }
}

与所有 C 指针一样,字符指针表达式可以涉及任意的指针运算,因此程序员必须确保字符数据有效。将 NULL 作为字符指针传递将在运行时引发异常。在可能的情况下,编译器将拒绝装箱表达式中使用的 NULL 字符指针。

装箱 C 结构

装箱表达式支持构造 NSValue 对象。它说 C 结构可以使用,唯一的要求是:结构应该用 objc_boxable 属性标记。为了支持旧版本的框架和/或第三方库,您可能需要通过 typedef 添加该属性。

struct __attribute__((objc_boxable)) Point {
    // ...
};

typedef struct __attribute__((objc_boxable)) _Size {
    // ...
} Size;

typedef struct _Rect {
    // ...
} Rect;

struct Point p;
NSValue *point = @(p);          // ok
Size s;
NSValue *size = @(s);           // ok

Rect r;
NSValue *bad_rect = @(r);       // error

typedef struct __attribute__((objc_boxable)) _Rect Rect;

NSValue *good_rect = @(r);      // ok

容器字面量

Objective-C 现在支持一种新的表达式语法用于创建不可变的数组和字典容器对象。

示例

不可变数组表达式

NSArray *array = @[ @"Hello", NSApp, [NSNumber numberWithInt:42] ];

这将创建一个包含 3 个元素的 NSArray。数组字面量的逗号分隔子表达式可以是任何 Objective-C 对象指针类型表达式。

不可变字典表达式

NSDictionary *dictionary = @{
    @"name" : NSUserName(),
    @"date" : [NSDate date],
    @"processInfo" : [NSProcessInfo processInfo]
};

这将创建一个包含 3 个键值对的 NSDictionary。字典字面量的值子表达式必须是 Objective-C 对象指针类型,就像数组字面量一样。键子表达式必须是实现了 <NSCopying> 协议的 Objective-C 对象指针类型。

讨论

容器中的键和值都不能为 nil。如果编译器可以在编译时证明键或值为 nil,那么将发出警告。否则,将在运行时发生错误。

使用数组和字典字面量比目前常用的可变创建形式更安全。数组字面量表达式扩展为调用 +[NSArray arrayWithObjects:count:],这将验证所有对象都不为 nil。可变形式 +[NSArray arrayWithObjects:] 使用 nil 作为参数列表终止符,这会导致数组对象格式错误。字典字面量以类似的方式使用 +[NSDictionary dictionaryWithObjects:forKeys:count:] 创建,这将验证所有对象和键,不像 +[NSDictionary dictionaryWithObjectsAndKeys:],它也使用 nil 参数作为参数列表终止符。

对象下标

Objective-C 对象指针值现在可以使用 C 的下标运算符。

示例

以下代码演示了使用对象下标语法操作 NSMutableArrayNSMutableDictionary 对象

NSMutableArray *array = ...;
NSUInteger idx = ...;
id newObject = ...;
id oldObject = array[idx];
array[idx] = newObject;         // replace oldObject with newObject

NSMutableDictionary *dictionary = ...;
NSString *key = ...;
oldObject = dictionary[key];
dictionary[key] = newObject;    // replace oldObject with newObject

下一节将解释下标表达式如何映射到访问器方法。

下标方法

Objective-C 支持两种下标表达式:数组样式下标表达式使用整型下标;字典样式下标表达式使用 Objective-C 对象指针类型下标。每种下标表达式都使用预定义的选择器映射到消息发送。这种设计的优点是灵活:类设计者可以通过声明方法或采用协议来自由地引入下标。此外,由于方法名称是根据下标类型选择的,因此可以使用数组和字典样式对同一个对象进行下标操作。

数组样式下标

当下标操作数具有整型时,表达式将被重写为使用两个不同的选择器之一,具体取决于元素是正在读取还是写入。当表达式使用整型索引读取元素时,如以下示例所示

NSUInteger idx = ...;
id value = object[idx];

它将被转换为对 objectAtIndexedSubscript: 的调用。

id value = [object objectAtIndexedSubscript:idx];

当表达式使用整型索引写入元素时

object[idx] = newValue;

它将被转换为对 setObject:atIndexedSubscript: 的调用。

[object setObject:newValue atIndexedSubscript:idx];

这些消息发送然后像显式消息发送一样进行类型检查和执行。用于 objectAtIndexedSubscript: 的方法必须声明为具有整型参数和一些 Objective-C 对象指针类型的返回值。用于 setObject:atIndexedSubscript: 的方法必须声明其第一个参数具有某种 Objective-C 指针类型,其第二个参数具有整型。

索引的含义由声明类决定。编译器将强制将索引转换为它用于类型检查的方法的适当参数类型。对于 NSArray 的实例,使用范围外的索引读取元素 [0, array.count) 将引发异常。对于 NSMutableArray 的实例,使用此范围内的索引分配元素将替换该元素,但使用此范围外的索引分配元素将引发异常;没有语法提供用于插入、追加或删除可变数组的元素。

一个类不需要声明两种方法才能利用此语言功能。例如,类 NSArray 仅声明 objectAtIndexedSubscript:,因此对元素的赋值将无法进行类型检查;此外,它的子类 NSMutableArray 声明 setObject:atIndexedSubscript:

字典样式下标

当下标操作数具有 Objective-C 对象指针类型时,表达式将被重写为使用两个不同的选择器之一,具体取决于元素是正在读取还是写入。当表达式使用 Objective-C 对象指针下标操作数读取元素时,如以下示例所示

id key = ...;
id value = object[key];

它将被转换为对 objectForKeyedSubscript: 方法的调用。

id value = [object objectForKeyedSubscript:key];

当表达式使用 Objective-C 对象指针下标写入元素时

object[key] = newValue;

它将被转换为对 setObject:forKeyedSubscript: 的调用。

[object setObject:newValue forKeyedSubscript:key];

setObject:forKeyedSubscript: 的行为是特定于类的;但一般来说,如果一个值已经与一个键相关联,它应该替换一个现有值,否则它应该为该键添加一个新值。没有语法提供用于从可变字典中删除元素。

讨论

当 C 下标运算符的基操作数具有 Objective-C 对象指针类型时,就会出现 Objective-C 下标表达式。由于这可能会与对该值的指针算术冲突,因此这些表达式仅在现代 Objective-C 运行时下支持,该运行时明确禁止此类算术。

目前,仅支持整型或 Objective-C 对象指针类型的下标。在 C++ 中,如果一个类类型具有单个转换为整型或 Objective-C 指针类型的转换函数,则可以使用该类类型,在这种情况下,将应用该转换并将分析继续进行,方法与适当的方法相同。否则,表达式格式错误。

Objective-C 对象下标表达式始终是左值。如果表达式出现在简单赋值运算符 (=) 的左侧,则将按如下所述写入元素。如果表达式出现在复合赋值运算符(例如 +=)的左侧,则程序格式错误,因为读取元素的结果始终是 Objective-C 对象指针,并且在这些指针上不允许使用任何二元运算符。如果表达式出现在任何其他位置,则将按如下所述读取元素。获取下标表达式的地址(或在 C++ 中将引用绑定到它)是一个错误。

程序可以使用类型为 id 的 Objective-C 对象指针进行对象下标。应用正常的动态消息发送规则;编译器必须看到下标方法的某些声明,并且将选择首先看到的声明。

注意事项

使用字面量或装箱表达式语法创建的对象不能保证由运行时进行唯一化,但也不能保证它们是新分配的。因此,对对象字面量位置执行直接比较的结果(使用 ==, !=, <, <=, >>=)是不确定的。这通常是代码中的一个简单错误,该代码原本打算调用 isEqual: 方法(或 compare: 方法)。

此注意事项也适用于编译时字符串字面量。从历史上看,字符串字面量(使用 @"..." 语法)已在链接期间跨翻译单元进行唯一化。这是编译器的一个实现细节,不应依赖它。如果使用此类代码,请改用全局字符串常量(NSString * const MyConst = @"...")或使用 isEqual:

语法添加

为了支持上面描述的新语法,Objective-C @-表达式语法具有以下新的产生式

objc-at-expression : '@' (string-literal | encode-literal | selector-literal | protocol-literal | object-literal)
                   ;

object-literal : ('+' | '-')? numeric-constant
               | character-constant
               | boolean-constant
               | array-literal
               | dictionary-literal
               ;

boolean-constant : '__objc_yes' | '__objc_no' | 'true' | 'false'  /* boolean keywords. */
                 ;

array-literal : '[' assignment-expression-list ']'
              ;

assignment-expression-list : assignment-expression (',' assignment-expression-list)?
                           | /* empty */
                           ;

dictionary-literal : '{' key-value-list '}'
                   ;

key-value-list : key-value-pair (',' key-value-list)?
               | /* empty */
               ;

key-value-pair : assignment-expression ':' assignment-expression
               ;

注意:@true@false 仅在 Objective-C++ 中支持。

可用性检查

程序通过使用 clang 的 __has_feature 检查来测试新功能。以下是它们用法的示例

#if __has_feature(objc_array_literals)
    // new way.
    NSArray *elements = @[ @"H", @"He", @"O", @"C" ];
#else
    // old way (equivalent).
    id objects[] = { @"H", @"He", @"O", @"C" };
    NSArray *elements = [NSArray arrayWithObjects:objects count:4];
#endif

#if __has_feature(objc_dictionary_literals)
    // new way.
    NSDictionary *masses = @{ @"H" : @1.0078,  @"He" : @4.0026, @"O" : @15.9990, @"C" : @12.0096 };
#else
    // old way (equivalent).
    id keys[] = { @"H", @"He", @"O", @"C" };
    id values[] = { [NSNumber numberWithDouble:1.0078], [NSNumber numberWithDouble:4.0026],
                    [NSNumber numberWithDouble:15.9990], [NSNumber numberWithDouble:12.0096] };
    NSDictionary *masses = [NSDictionary dictionaryWithObjects:objects forKeys:keys count:4];
#endif

#if __has_feature(objc_subscripting)
    NSUInteger i, count = elements.count;
    for (i = 0; i < count; ++i) {
        NSString *element = elements[i];
        NSNumber *mass = masses[element];
        NSLog(@"the mass of %@ is %@", element, mass);
    }
#else
    NSUInteger i, count = [elements count];
    for (i = 0; i < count; ++i) {
        NSString *element = [elements objectAtIndex:i];
        NSNumber *mass = [masses objectForKey:element];
        NSLog(@"the mass of %@ is %@", element, mass);
    }
#endif

#if __has_attribute(objc_boxable)
    typedef struct __attribute__((objc_boxable)) _Rect Rect;
#endif

#if __has_feature(objc_boxed_nsvalue_expressions)
    CABasicAnimation animation = [CABasicAnimation animationWithKeyPath:@"position"];
    animation.fromValue = @(layer.position);
    animation.toValue = @(newPosition);
    [layer addAnimation:animation forKey:@"move"];
#else
    CABasicAnimation animation = [CABasicAnimation animationWithKeyPath:@"position"];
    animation.fromValue = [NSValue valueWithCGPoint:layer.position];
    animation.toValue = [NSValue valueWithCGPoint:newPosition];
    [layer addAnimation:animation forKey:@"move"];
#endif

代码还可以使用 __has_feature(objc_bool) 来检查数字字面量支持的可用性。这检查新的 __objc_yes / __objc_no 关键字,这些关键字允许使用 @YES / @NO 字面量。

要检查是否支持装箱表达式,请使用 __has_feature(objc_boxed_expressions) 功能宏。