3.4. 可空性检查

本文档是对可空性检查的高级描述。这些检查旨在使用此 RFC 中描述的注释: https://discourse.llvm.org/t/rfc-nullability-qualifiers/35672 (Mailman)

让我们考虑以下 2 类

1) 可空

如果指针 p 带有可空注释,并且没有显式空检查或断言,我们应该在以下情况下发出警告

在可空指针上进行分支与在未指定为空的指针上进行分支相同。

从可空到非空的显式转换

__nullable id foo;
id bar = foo;
takesNonNull((_nonnull) bar); // should not warn here (backward compatibility hack)
anotherTakesNonNull(bar); // would be great to warn here, but not necessary(*)

由于 bar 始终对应于同一个符号,因此难以实现检查器以使转换只抑制第一次调用而不抑制第二次调用。出于这个原因,在第一次实现中,在发生矛盾转换后,我会将 bar 视为可空未指定,这样所有警告都会被抑制。将符号视为可空未指定也有一个优点,即如果 takesNonNull 函数体被内联,则在符号被解引用时不会发出警告。如果在初始版本之后我有时间,我可能会花额外的时间尝试找到一个更复杂的方法,在该方法中,我们会产生第二个警告 (*)。

2) 非空

__nonnull id takesNonnull(__nonnull id x) {
    if (x == nil) {
        // Defensive backward compatible code:
        ....
        return nil; // Should the analyzer cover this piece of code? Should we require the cast (__nonnull)nil?
    }
    ....
}

有以下方向

其他需要注意的事项

3.4.1. 内联

在内联体中,符号可能需要以不同的方式处理。例如,考虑在存在内联的情况下从非空到可空的转换

id obj = getNonnull();
takesNullable(obj);
takesNonnull(obj);

void takesNullable(nullable id obj) {
   obj->ivar // we should assume obj is nullable and warn here
}

如果没有特殊处理,当 takesNullable 被内联时,分析器在 obj 符号被解引用时不会发出警告。一种解决方案是将 takesNullable 重新分析为顶级函数以获取可能的违规。另一种方法是,在内联后从参数中推断出可空性信息,但这种方法不够健壮(例如,可能存在更多具有不同可空性的参数,但在给定路径中,两个参数最终可能成为同一个符号,或者可能存在嵌套函数,它们对同一个符号的可空性有不同的看法)。因此,符号将保持为非空以避免误报,但 takesNullable 参数的函数也将分别进行分析,而不会进行内联。

3.4.2. 多级指针上的注释

跟踪指向指针的指针的多级注释将使检查器更加复杂,因为这样需要为每个符号跟踪可空性限定符的向量。这不是一个很大的问题,因为一旦顶级指针被解引用,内部指针的符号将具有可空性信息。只有当多级指针被传递到具有多级注释的参数的函数时,才能观察到缺乏多级注释跟踪。因此,目前检查器只支持顶级可空性限定符。

int * __nonnull * __nullable p;
int ** q = p;
takesStarNullableStarNullable(q);

3.4.3. 实现说明

跟踪什么?

  • 检查器将跟踪内存区域,并将限定符信息附加到每个相关区域,该信息是可空、非空或空未指定(或被矛盾以抑制特定区域的警告)。

  • 在分支中,如果可空指针被认为是非空的,检查器将像处理用非空注释的指针一样处理它。

  • 当存在从空未指定到非空或可空的显式转换时,我会信任该转换。

  • 未注释的指针与用可空性未指定限定符注释的指针处理方式相同,除非区域被 ASSUME_NONNULL 宏包装。

  • 我们可能希望为顶级函数的入口点实现一个回调,在那里会进行指针可空性假设。