在Objective-C中,nullable、__nullable和_Nullable有什么区别?

179
使用Xcode 6.3,引入了新的注释以更好地表达Objective-C API的意图(当然也是为了更好地支持Swift)。这些注释当然包括nonnull、nullable和null_unspecified。
但是在Xcode 7中,出现了很多警告,例如:
指针缺少空性类型说明符(_Nonnull、_Nullable或_Null_unspecified)。
除此之外,苹果还使用另一种空性说明符来标记它们的C代码(源自source):
CFArrayRef __nonnull CFArrayCreate(CFAllocatorRef __nullable allocator, const void * __nonnull * __nullable values, CFIndex numValues, const CFArrayCallBacks * __nullable callBacks);
因此,总结一下,我们现在有这三个不同的空性注释。
  • nonnullnullablenull_unspecified:用于属性和方法参数。
  • _Nonnull_Nullable_Null_unspecified:用于其他情况,如双指针。
  • __nonnull__nullable__null_unspecified:用于 C 方法。

虽然我知道为什么和在哪里使用哪种注释,但是我对应该在哪里以及为什么使用哪种类型的注释有些困惑。这是我能够收集到的:

  • 对于属性,我应该使用 nonnullnullablenull_unspecified
  • 对于方法参数,我应该使用 nonnullnullablenull_unspecified
  • 对于 C 方法,我应该使用 __nonnull__nullable__null_unspecified
  • 对于其他情况,如双指针,我应该使用 _Nonnull_Nullable_Null_unspecified

但我仍然困惑为什么我们有这么多基本上做同样事情的注释。

我的问题是:

这些注释之间的确切区别是什么,如何正确地放置它们以及为什么?


3
我看过那篇文章,但它没有解释这三种注释类型之间的区别,也没有说明为什么我们现在有了三种不同类型的注释。我真的很想知道为什么他们要继续添加第三种类型。 - Legoless
2
这真的对@Cy-4AH没有帮助,你知道的。 :) - Legoless
@Legoless,你确定你仔细阅读了吗?它准确地解释了你应该在哪里和如何使用它们,审核范围是什么,何时可以使用其他方法以获得更好的可读性、兼容性等等... 也许你不知道自己真正想问什么,但答案在链接下面显而易见。也许只是我个人感觉,进一步解释他们的目的并将简单的解释复制粘贴到这里作为答案会非常尴尬。:( - holex
2
它确实澄清了某些部分,但是我仍然不明白为什么我们不只使用第一个注释。它只解释了他们为什么从__nullable转换为_Nullable,但是如果我们有nullable,为什么我们需要_Nullable呢?而且它也没有解释为什么苹果仍然在他们自己的代码中使用__nullable。 - Legoless
4个回答

177

clang文档中得知:

空性(类型)修饰符表达了给定指针类型的值是否可以为 null(使用 _Nullable 修饰符),没有定义 null 的含义(使用 _Nonnull 修饰符),或者 null 的目的不明确(使用 _Null_unspecified 修饰符)。由于空性修饰符在类型系统内表示,因此它们比 nonnullreturns_nonnull 属性更通用,允许我们表达(例如)对非 null 指针数组的可空指针。空性修饰符写在其应用的指针右侧。

以及

在 Objective-C 中,空性修饰符有一种替代拼写方式,可用于使用上下文敏感的、无下划线关键字定义的 Objective-C 方法和属性

因此,对于方法返回和参数,您可以使用双下划线版本 __nonnull/__nullable/__null_unspecified,而不是单下划线的版本或非下划线版本。 区别在于,单下划线和双下划线版本需要放在类型定义之后,而非下划线版本需要放在类型定义之前。

因此,以下声明是等效且正确的:

- (nullable NSNumber *)result
- (NSNumber * __nullable)result
- (NSNumber * _Nullable)result

对于参数:

- (void)doSomethingWithString:(nullable NSString *)str
- (void)doSomethingWithString:(NSString * _Nullable)str
- (void)doSomethingWithString:(NSString * __nullable)str

对于属性:

@property(nullable) NSNumber *status
@property NSNumber *__nullable status
@property NSNumber * _Nullable status

然而,当涉及到双指针或返回除void以外的其他内容的块时,情况变得更加复杂,因为非下划线的变量在此处不被允许:

- (void)compute:(NSError *  _Nullable * _Nullable)error
- (void)compute:(NSError *  __nullable * _Null_unspecified)error;
// and all other combinations

与接受代码块作为参数的方法类似,请注意nonnull/nullable限定符适用于代码块而不是其返回类型,因此以下两种方式是等效的:

- (void)executeWithCompletion:(nullable void (^)())handler
- (void)executeWithCompletion:(void (^ _Nullable)())handler
- (void)executeWithCompletion:(void (^ __nullable)())handler

如果块有返回值,则必须使用下划线版本之一:

- (void)convertObject:(nullable id __nonnull (^)(nullable id obj))handler
- (void)convertObject:(id __nonnull (^ _Nullable)())handler
- (void)convertObject:(id _Nonnull (^ __nullable)())handler
// the method accepts a nullable block that returns a nonnull value
// there are some more combinations here, you get the idea
作为结论,只要编译器能够确定修饰语要分配给哪个项目,你可以使用任何一个。

3
看起来下划线版本可以在任何地方使用,所以我认为我会一直使用它们,而不是在某些地方使用带下划线的版本,在其他地方不使用。对吗? - Kartick Vaddadi
@KartickVaddadi 是的,正确的,你可以一贯地使用单下划线或双下划线版本。 - Cristik
1
在Swift中,_Null_unspecified被翻译成可选项(optional)?非可选项还是什么? - mfaani
1
在Swift中,@Honey_Null_unspecified被导入为隐式解包可选项。 - Cristik
1
@Cristik 啊哈。我猜那是它的默认值……因为当我没有指定时,我得到的是隐式解包可选项… - mfaani

33

根据clang文档

nullability(类型)修饰符表达了给定指针类型的值是否可以为 null。

大多数时候,您将使用nonnullnullable

下面列出了所有可用的标识符。取自这篇文章

  • null_unspecified: 这是默认值。 它桥接到一个 Swift 隐式解包可选项。
  • nonnull: 值不会为 nil。它桥接到一个 Swift 常规引用。
  • nullable: 值可以为 nil。它桥接到一个 Swift 可选项。
  • null_resettable: 在读取时值永远不会为 nil,但可以将其设置为 nil 来重置它。只适用于属性。

上述标识符的表示方式在您将它们用于属性或函数/变量的上下文中也有所不同:

Pointers vs. Properties notation

文章作者还提供了一个很好的例子:

// property style
@property (nonatomic, strong, null_resettable) NSString *name;

// pointer style
+ (NSArray<NSView *> * _Nullable)interestingObjectsForKey:(NSString * _Nonnull)key;

// these two are equivalent!
@property (nonatomic, strong, nullable) NSString *identifier1;
@property (nonatomic, strong) NSString * _Nullable identifier2;

31

来自Swift博客的消息:

这个功能最初在Xcode 6.3中发布,使用了关键字__nullable和__nonnull。由于可能与第三方库发生冲突,我们在Xcode 7中将它们改为您在此处看到的_Nullable和_Nonnull。但是,为了与Xcode 6.3兼容,我们预定义了宏__nullable和__nonnull以扩展到新名称。


5
简而言之,单下划线版本和双下划线版本是相同的。 - Kartick Vaddadi
4
在Xcode 7.0发布说明中也有记录:"双下划线的nullability修饰符(__nullable、__nonnull和__null_unspecified)已被重命名为单下划线加大写字母的形式:_Nullable、_Nonnull和_Null_unspecified。编译器预定义了宏,将旧的双下划线名称映射到新名称以实现源代码兼容性。(21530726)" - Cosyn

14

非常方便

NS_ASSUME_NONNULL_BEGIN 

并以...结尾

NS_ASSUME_NONNULL_END 

这将使代码级别的“nullibis”变得无关紧要:-)因为我们可以合理地假设一切都是非空的(或nonnull_nonnull__nonnull),除非另有说明。

不幸的是,也存在例外情况...

  • typedef不被认为是__nonnull(请注意,“nonnull”似乎不起作用,必须使用它丑陋的同义词)
  • id *需要显式的nullibi,但是该规则太繁琐了(_Nullable id * _Nonnull <-猜猜这是什么意思...)
  • NSError **始终被认为是可空的

因此,由于异常情况和不一致的关键字具有相同的功能,也许最好的方法就是使用丑陋版本的__nonnull / __nullable / __null_unspecified,并在编译器发出警告时进行交换...?也许这就是为什么它们存在于Apple的头文件中的原因?

有趣的是,某些东西把它放到了我的代码中... 我讨厌代码中的下划线(老派的Apple C++风格的人),所以我非常确定我没有输入这些,但它们出现了(其中之一的例子):

typedef void ( ^ DidReceiveChallengeBlock ) ( NSURLSessionAuthChallengeDisposition disposition,
                                          NSURLCredential * __nullable credential );

更有趣的是,它插入__nullable的位置是错误的...(啊!)

我真希望我可以使用非下划线版本,但显然编译器不允许这样做,因为这会被标记为一个错误:

typedef void ( ^ DidReceiveChallengeBlock ) ( NSURLSessionAuthChallengeDisposition disposition,
                                          NSURLCredential * nonnull  credential );

2
非下划线字符只能直接在左括号后面使用,例如(nonnull ... 为了让生活更有趣,我敢肯定。 - Elise van Looij
如何使id<>非空?我觉得这个答案包含了很多知识,但缺乏清晰度。 - fizzybear
1
@fizzybear 承认我通常采用相反的方法。指针是我的好朋友,自从90年代初以来,我就没有遇到过“null”/“nil”指针问题。我希望我能让整个可为空/nilable的事情消失。但归正题,真正的答案在回答中的前6行。至于你的问题:这不是我会做的事情(没有批评),所以我不知道。 - Cerniuk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接