『Apple API』从 NSDictitonary 中学接口定义

学习最规范的接口定义,最好的方式就是看官方 API。平时常用的 NSDictionary 中,也有很多以前未注意到的地方。当然,除了看如何使用以外,更为重要的是理解、总结这样设计的原因,在开发过程中也注意实践。

相关文章:

环境信息:
macOS 10.12.3
Xcode 8.2.1
iOS 10.2


泛型

从其他语言转过来的开发者应该对泛型比较熟悉,而 OC 中,仅提供了弱泛型,并不像 Swift,JAVA 等语言,有强大的泛型支持,只泛型而言,就能玩出很多花样。 OC 中的泛型,感觉更多的是给开发者看,而不是给编译器看。

NSDictionary<KeyType, ObjectType>

在声明字典的时候,可以写作:

NSDictionary<NSString *, MyModel *> *dic = nil;

其中,NSString * 对应的就是 KeyType,而 MyModel 对应的则为 ObjectType

所以,这中泛型就很明确了,先使用 KeyTypeObjectType 占位,当声明指明类型时,再为确定的类型。使用时,编译器也能给出对应的代码提示,若放入不合类型的对象,也会出现相应警告。

__covariant 与 __contravariant

在看上面的代码时,可以看到 NSDictionary 中实际声明的是:NSDictionary<__covariant KeyType, __covariant ObjectType>,即协变与逆变。

若声明两个字典:

// NSMutableString : NSMutableString
NSDictionary<NSMutableString *, NSMutableString *> *dic1 = @{@"dic1" : @1};
// NSMutableString : NSMutableString
NSDictionary<NSMutableString *, NSMutableString *> *dic2 = nil;
// NSString : NSString
NSDictionary<NSString *, NSString *> *dic3 = nil;

// 将 NSMutableString 赋值给 NSString(子类 -> 父类)
// 编译通过,无警告
dic3 = dic1;

// 将 NSString 赋值给 NSMutableString(父类 -> 子类)
// warning: Incampatible pointer types assgining to ...
// 警告:无法将类型为 <NSString :  NSString> 的字典赋值给 <NSMutableString : NSMutableString>
dic2 = dic3;

因为 NSDictionary 的 key 与 value 都为 __covariant(协变),所以可以将子类型转到父类型(里氏替换)。那么,如果想要支持父类转为子类能(应该尽量避免这种情况)?可以将 __covariant 改为 __contravariant(逆变)。

@property (readonly, copy) NSArray<KeyType> *allKeys;
– (NSEnumerator<ObjectType> *)objectEnumerator;

这两种分别是泛型在属性与方法中的运用,与之前类似,如果属性或方法指明了类型,编译器可以在读写时给出相应提示与警告。

__kindof

__kindof 关键字并未在 NSDictionary 的头文件中出现,不过它也给类型限制做出了一些贡献。

这个关键字可以在 UIView 的头文件中找到:

@property (nonatomic,readonly,copy) NSArray<__kindof UIView *> *subviews;
- (nullable __kindof UIView *)viewWithTag:(NSInteger)tag;

记得在以前通过 tag 找 view 时,如果接收者为 UIButton,则需要写为:

// 进行强转
UIButton *button = (UIButton *)[self.view viewWithTag:1000];

而现在,__kindof 解决了该问题,只要接收者是 UIView 或其子类即可,不需要强转,其含义与 isKindOfClass: 方法类似。

const

在 Swift 中,方法的参数默认是不可写的,而 OC 在这一点上恰恰相反。如果要声明参数不可写,应该这样:

+ (instancetype)dictionaryWithObjects:(const ObjectType [])objects forKeys:(const KeyType [])keys count:(NSUInteger)cnt;

其中,objectskeys 均声明了 const,说明在方法实现时,是不会修改到这两个对象的。

值得注意的是,const 的位置依然和 * 的位置有关系,详细解释可以查看 StackOverflow 的高票回答

NS_NOESCAPE

可以看到 NSDictionary 在定义枚举器时,给 block 参数加上了 NS_NOESCAPE 修饰:

- (void)enumerateKeysAndObjectsUsingBlock:(void (NS_NOESCAPE ^)(KeyType key, ObjectType obj, BOOL *stop))block;

该修饰符与 Swift 中的 @noescape 对应,用于修饰在方法内能执行完毕的 block(Swift:闭包):

- (void)runBlock:(NS_NOESCAPE dispatch_block_t)block {
    block();
}

关于该修饰符的作用,在 swift-evolution 上描述得非常清楚:

A closure argument is guaranteed to be executed (if executed at all) before the function returns. This enables the compiler to perform various optimizations, such as omitting unnecessary capturing/retaining/releasing of self.

如果闭包能在方法返回之前调用,那么该修饰符可以让编译器做很多优化,比如剔除对 self 的捕获、持有、释放等。

关于 block 对变量的捕获以及更多详细内容,可以参考我之前的文章:Block 梳理与疑问

NS_REQUIRES_NIL_TERMINATION

要求最后一个值为 nil

- (instancetype)initWithObjectsAndKeys:(id)firstObject, ... NS_REQUIRES_NIL_TERMINATION;

// 调用
NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys:@"1", @"1", nil];

若未使用 nil 结尾,warning: Missing sentinel in method dispatch。若运行,则 crash。

这种类似的方法,除了 NSDictionaryNSArray 也有。来看看为什么这里要求用 nil 结尾:

- (void)testParameters:(id)first, ... {
    if (!first) {
        return;
    }
    
    va_list list;
    
    va_start(list, first);
    
    // 遍历,va_arg 读取对象,并将指针指向下一个对象
    for (id obj = first; obj; obj = va_arg(list, id)) {
        NSLog(@"%@", obj);
    }
    
    va_end(list);
}

从以上代码的 for 中可以看出,循环的跳出条件,即指针指向的对象为 nil。若不用 nil 结尾,就无法正确判断结尾。所以,nil 结尾是非常有必要的,使用 NS_REQUIRES_NIL_TERMINATION 让编译器在编译阶段警告也就非常人性化了。

再来看看 NS_REQUIRES_NIL_TERMINATION 宏的定义:

#define NS_REQUIRES_NIL_TERMINATION __attribute__((sentinel(0,1)))

对于 sentinel 函数 gcc 官方文档为:

This function attribute ensures that a parameter in a function call is an explicit NULL. The attribute is only valid on variadic functions. By default, the sentinel is located at position zero, the last parameter of the function call. If an optional integer position argument P is supplied to the attribute, the sentinel must be located at position P counting backwards from the end of the argument list.

该属性函数用于确保函数调用时的参数为 NULL。并且,该属性仅在可变参数函数中有效。默认情况下,哨兵位(sentinel)在第 0 位,对应着可变参数的最后一位。如果给 sentinel 传入整数 P,则 sentinel 处于参数列表的倒数第 P 位。

所以,NS_REQUIRES_NIL_TERMINATION 为确保可变参数列表的最后一位为 NULL

NS_REQUIRES_SUPER

要求当前方法必须调用 super。该修饰符未在 NSDictionary 中出现,不过用的地方也很多:

// UIView

- (void)updateConstraints NS_REQUIRES_SUPER;

如果在子类中未调用 super,则编译警告。找到这个宏,是因为我本来想找如何让子类强行重写父类方法的解决方案,但是最后却以失败告终,目前能找到的方法是在父类中写一个异常,而没办法在编译时期就给出报错。

发表评论

电子邮件地址不会被公开。