如何在 Objective-C 中安全地将 `_Nullable` 转换为 `_Nonnull`?
How do I safely cast a `_Nullable` to a `_Nonnull` in Objective-C?
当使用 -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.
如何安全地将 foo
从 NSString * _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
到目前为止我发现的最好的是泛型技巧。
本质上,您定义了一个使用泛型的接口,并且有一个方法 returns 泛型类型为 nonnull
。然后在你的宏中你使用 typeof
但是在通用类型上,这给你正确的类型。
请注意,泛型 class 从未实例化,它只是用于获取正确的类型。
@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
基本上是正确的,但自从 运行 之后我就收到了一些静态分析器警告,因为其中缺少硬 _Nonnull
保证。简而言之,我们必须 abort
如果我们收到 nil
否则当我们做这样的分配时:
@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")
我使用这个宏:
#define assumeNotNull(_value) \
({ if (!_value) abort(); __auto_type const _temp = _value; _temp; })
当然,只有在代码中进行适当的测试之后:
if (parameters) {
[obj processParameters:assumeNotNull(parameters)];
}
离开宏编译器会告诉我参数可能是 NULL
但 processParameters
需要一个 non-NULL 参数。在我的例子中,这甚至被配置为错误,而不仅仅是警告。
省略 if
检查,代码将编译,但如果我输入 NULL
,应用程序将当场崩溃。因此,人们应该只在测试后使用宏,或者如果人们绝对确定由于某种原因该值不能 NULL
并且您对此非常确定,您愿意将您的应用程序稳定性押在它上面。
如有疑问,请始终进行测试并记住,如果测试显然是不必要的(例如,之前测试过条件,如果值为 NULL
,则永远不会达到代码),编译器将在优化阶段检测到并为您删除测试。不必要的测试几乎不会成为性能问题,尤其是对于这么便宜的测试。
当使用 -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.
如何安全地将 foo
从 NSString * _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
到目前为止我发现的最好的是泛型技巧。
本质上,您定义了一个使用泛型的接口,并且有一个方法 returns 泛型类型为 nonnull
。然后在你的宏中你使用 typeof
但是在通用类型上,这给你正确的类型。
请注意,泛型 class 从未实例化,它只是用于获取正确的类型。
@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
_Nonnull
保证。简而言之,我们必须 abort
如果我们收到 nil
否则当我们做这样的分配时:
@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")
我使用这个宏:
#define assumeNotNull(_value) \
({ if (!_value) abort(); __auto_type const _temp = _value; _temp; })
当然,只有在代码中进行适当的测试之后:
if (parameters) {
[obj processParameters:assumeNotNull(parameters)];
}
离开宏编译器会告诉我参数可能是 NULL
但 processParameters
需要一个 non-NULL 参数。在我的例子中,这甚至被配置为错误,而不仅仅是警告。
省略 if
检查,代码将编译,但如果我输入 NULL
,应用程序将当场崩溃。因此,人们应该只在测试后使用宏,或者如果人们绝对确定由于某种原因该值不能 NULL
并且您对此非常确定,您愿意将您的应用程序稳定性押在它上面。
如有疑问,请始终进行测试并记住,如果测试显然是不必要的(例如,之前测试过条件,如果值为 NULL
,则永远不会达到代码),编译器将在优化阶段检测到并为您删除测试。不必要的测试几乎不会成为性能问题,尤其是对于这么便宜的测试。