【问题标题】:How do I safely cast a `_Nullable` to a `_Nonnull` in Objective-C?如何在 Objective-C 中安全地将 `_Nullable` 转换为 `_Nonnull`?
【发布时间】:2017-07-15 19:28:48
【问题描述】:

当使用-Wnullable-to-nonnull-conversion 进行编译时,我们会收到带有以下代码的适当警告:

NSString * _Nullable maybeFoo = @"foo";
^(NSString * _Nonnull bar) {  
}(maybeFoo);

Tests.m:32:7: error: implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull' [-Werror,-Wnullable-to-nonnull-conversion]
    }(maybeFoo);
      ^
1 error generated.

如何安全地将 fooNSString * _Nullable 转换为 NSString * _Nonnull

目前为止我最好的解决方案

我想出的最好的就是这个宏:

#define ForceUnwrap(type, nullableExpression) ^type _Nonnull () { \
  type _Nullable maybeValue___ = nullableExpression; \
  if (maybeValue___) { \
    return (type _Nonnull) maybeValue___; \
  } else { \
    NSLog(@"Attempted to force unwrap a null: " #nullableExpression); \
    abort(); \
  } \
}()

使用如下:

NSString * _Nullable maybeFoo = @"foo";
if (maybeFoo) {
    NSString * _Nonnull foo = ForceUnwrap(NSString *, maybeFoo);
    ^(NSString * _Nonnull bar) {
    }(foo);
}

如果分配给错误类型的变量会产生错误:

NSString * _Nullable maybeFoo = @"foo";
if (maybeFoo) {
    NSNumber * _Nonnull foo = ForceUnwrap(NSString *, maybeFoo);
    ^(NSNumber * _Nonnull bar) {
    }(foo);
}

Tests.m:40:29: error: incompatible pointer types initializing 'NSNumber * _Nonnull' with an expression of type 'NSString * _Nonnull' [-Werror,-Wincompatible-pointer-types]
        NSNumber * _Nonnull foo = ForceUnwrap(NSString *, maybeFoo);
                            ^     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

如果转换为错误的类型会产生错误:

NSString * _Nullable maybeFoo = @"foo";
if (maybeFoo) {
    NSNumber * _Nonnull foo = ForceUnwrap(NSNumber *, maybeFoo);
    ^(NSNumber * _Nonnull bar) {
    }(foo);
}

Tests.m:40:35: error: incompatible pointer types initializing 'NSNumber * _Nullable' with an expression of type 'NSString * _Nullable' [-Werror,-Wincompatible-pointer-types]
        NSNumber * _Nonnull foo = ForceUnwrap(NSNumber *, maybeFoo);
                                  ^                       ~~~~~~~~
Tests.m:27:16: note: expanded from macro 'ForceUnwrap'
type _Nullable maybeValue___ = nullableExpression; \
               ^               ~~~~~~~~~~~~~~~~~~
1 error generated.

不幸的是,如果您需要转换为具有多个参数的泛型类型,则必须求助于preprocessor hacks

NSDictionary<NSString *, NSString *> * _Nullable maybeFoo = 
[NSDictionary<NSString *, NSString *> new];
if (maybeFoo) {
  NSDictionary<NSString *, NSString *> * _Nonnull foo =
#define COMMA ,
  ForceUnwrap(NSDictionary<NSString * COMMMA NSString *>, maybeFoo);
#undef COMMA
  ^(NSDictionary<NSString *, NSString *> * _Nonnull bar) {
  }(foo);
}

我尝试过的方法不起作用

maybeFoo 直接分配给NSString * _Nonnull 不起作用。它产生与以前相同的错误:

NSString * _Nullable maybeFoo = @"foo";
if (maybeFoo) {
  NSString * _Nonnull foo = maybeFoo;
  ^(NSString * _Nonnull bar) {  
  }(foo);
}

Tests.m:30:35: error: implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull' [-Werror,-Wnullable-to-nonnull-conversion]
        NSString * _Nonnull foo = maybeFoo;
                                  ^
1 error generated.

maybeFoo 转换为NSString * _Nonnull 是不安全的,因为如果maybeFoo 的类型发生变化,编译器不会中断:

NSNumber * _Nullable maybeFoo = @"foo";
if (maybeFoo) {
  NSString * _Nonnull foo = (NSString * _Nonnull) maybeFoo;
  ^(NSString * _Nonnull bar) {  
  }(foo);
}
// no errors!

我也尝试在转换时使用__typeof__,但__typeof__ 带有可空性说明符,因此当您尝试转换为__typeof__(maybeFoo) _Nonnull 时会遇到可空性冲突:

NSString * _Nullable maybeFoo = @"foo";
if (maybeFoo) {
    NSString * _Nonnull foo = (__typeof__(maybeFoo) _Nonnull) maybeFoo;
    ^(NSString * _Nonnull bar) {
    }(foo);
}

Tests.m:30:57: error: nullability specifier '_Nonnull' conflicts with existing specifier '_Nullable'
        NSString * _Nonnull foo = (__typeof__(maybeFoo) _Nonnull) maybeFoo;
                                                        ^
Tests.m:30:35: error: implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull' [-Werror,-Wnullable-to-nonnull-conversion]
        NSString * _Nonnull foo = (__typeof__(maybeFoo) _Nonnull) maybeFoo;
                                  ^
2 errors generated.

一切都使用深度静态分析器运行,并使用 Xcode 8.2.1 编译,并带有以下标志:

-Wnon-modular-include-in-framework-module 
-Werror=non-modular-include-in-framework-module
-Wno-trigraphs
-Werror
-Wno-missing-field-initializers
-Wno-missing-prototypes
-Wunreachable-code
-Wno-implicit-atomic-properties
-Wno-arc-repeated-use-of-weak
-Wduplicate-method-match
-Wno-missing-braces
-Wparentheses
-Wswitch
-Wunused-function
-Wno-unused-label
-Wno-unused-parameter
-Wunused-variable
-Wunused-value
-Wempty-body
-Wuninitialized
-Wno-unknown-pragmas
-Wno-shadow
-Wno-four-char-constants
-Wno-conversion
-Wconstant-conversion
-Wint-conversion
-Wbool-conversion
-Wenum-conversion
-Wshorten-64-to-32
-Wpointer-sign
-Wno-newline-eof
-Wno-selector
-Wno-strict-selector-match
-Wundeclared-selector
-Wno-deprecated-implementations
-Wno-sign-conversion
-Wno-infinite-recursion
-Weverything
-Wno-auto-import
-Wno-objc-missing-property-synthesis
-Wno-cstring-format-directive
-Wno-direct-ivar-access
-Wno-double-promotion

【问题讨论】:

  • 你没有。不要为 Objective-C 使用该属性。它们适用于 Swift。 Objective-C 对 nil 行为有明确定义的信息。
  • 它们仅适用于 Swift。编译器不会更改任何生成的代码,具体取决于引用的可空性。
  • Again,正如 Amin 所说,“非空”在 ObjC 中不是一个东西。观察;使用尽可能多的标志编译以下内容。 -Wall,-Weverything,-Wpedantic,-Werror。 NSString * __nonnull s = nil; 编译良好。 ObjC 永远不会强制执行可空性。 nil 对象指针的行为是基本且成熟的。
  • @Caswell,你错了。如果您使用-Wnulllable-to-nonnull-conversion 编译,NSString * _Nonnull s = nil; 将收到警告。
  • 对不起,阿明,但这只是无知。当然编译器输出不会改变。如果这是唯一重要的事情,您可以忽略大多数警告。但是,如果您想编写干净的代码以防止您在实际使用它们之前出现很多问题,那么可空性当然会为您的代码增加很多安全性。不仅在 Swift 中。

标签: ios objective-c xcode nullable objective-c-nullability


【解决方案1】:

到目前为止,我发现的最好的方法是使用泛型。

本质上,您定义了一个使用泛型的接口,并具有一个将泛型类型返回为nonnull 的方法。然后在您的宏中使用 typeof 但在泛型类型上,这会为您提供正确的类型。

请注意,泛型类永远不会被实例化,它只是用来获取正确的类型。

@interface RBBBox<__covariant Type>

- (nonnull Type)asNonNull;

@end

#define RBBNotNil(V) \
    ({ \
        NSCAssert(V, @"Expected '%@' not to be nil.", @#V); \
        RBBBox<__typeof(V)> *type; \
        (__typeof(type.asNonNull))V; \
    })

不过,这不是我的想法。来源:https://gist.github.com/robb/d55b72d62d32deaee5fa

【讨论】:

  • 我更新了您答案中的代码,并添加了一些关于它在新答案中引起的可能的静态分析器警告的附加说明。 stackoverflow.com/a/46123981/9636
【解决方案2】:

Michael Ochs' answer 基本上是正确的,但由于缺乏硬性_Nonnull 保证,我后来遇到了一些静态分析器警告。简而言之,如果我们收到nil,我们必须abort,否则当我们执行这样的任务时:

@interface Foo : NSObject
+ (NSString * _Nullable)bar;
@end

int main(int argc, char * argv[]) {
  NSString * _Nonnull bar = RBBNotNil([Foo bar]);
}

Release 配置中(在我的情况下,归档时),静态分析器会抱怨您试图将_Nullable 值分配给_Nonnull 左值。我收到了这样的警告:

nil assigned to a pointer which is expected to have non-null value

这是我的更新版本:

// We purposefully don't have a matching @implementation.
// We don't want +asNonnull to ever actually be called
// because that will add a lot of overhead to every RBBNotNil
// and we want RBBNotNil to be very cheap.
// If there is no @implementation, then if the +asNonnull is
// actually called, we'll get a linker error complaining about
// the lack of @implementation.
@interface RBBBox <__covariant Type>

// This as a class method so you don't need to
// declare an unused lvalue just for a __typeof
+ (Type _Nonnull)asNonnull;

@end

/*!
 * @define RBBNotNil(V)
 * Converts an Objective-C object expression from _Nullable to _Nonnull. 
 * Crashes if it receives a nil! We must crash or else we'll receive
 * static analyzer warnings when archiving. I think in Release mode,
 * the compiler ignores the _Nonnull cast.
 * @param V a _Nullable Objective-C object expression
 */
#define RBBNotNil(V) \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wgnu-statement-expression\"") \
({ \
__typeof__(V) __nullableV = V; \
NSCAssert(__nullableV, @"Expected '%@' not to be nil.", @#V); \
if (!__nullableV) { \
    abort(); \
} \
(__typeof([RBBNotNil<__typeof(V)> asNonnull]))__nullableV; \
}) \
_Pragma("clang diagnostic pop")

【讨论】:

  • 对,我通常在发布模式下启用断言,否则您在发布时运行的代码与在调试时不同。但我想如果你没有那个,这个测试永远不会完成,因此 clang 会再次发出警告。
【解决方案3】:

我使用这个宏:

#define assumeNotNull(_value) \
    ({ if (!_value) abort(); __auto_type const _temp = _value; _temp; })

当然,只有在代码中进行适当的测试后:

if (parameters) {
    [obj processParameters:assumeNotNull(parameters)];
}

忽略宏,编译器会告诉我参数可能是NULL,但processParameters 需要一个非NULL 参数。就我而言,这甚至被配置为错误,而不仅仅是警告。

省略if 检查,代码将编译,但如果我输入NULL,应用程序将当场崩溃。因此,应该只在测试后使用宏,或者如果绝对确定该值由于某种原因不能为NULL,并且您对此非常确定,那么您愿意将您的应用程序稳定性押在它上面。

如果有疑问,请始终测试并记住,如果测试显然是不必要的(例如,之前测试过条件并且如果值为NULL,则永远不会达到代码),编译器将在期间检测到优化阶段并为您删除测试。不必要的测试几乎不会是性能问题,尤其是在廉价的测试中。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-12-30
    • 2011-03-15
    • 2017-09-30
    • 1970-01-01
    • 2019-12-27
    • 1970-01-01
    • 2012-08-12
    • 2022-11-30
    相关资源
    最近更新 更多