问题
先看一个问题:
-
对于 UserDefaults 的写操作,比较一下方法 1 与方法 2 有什么区别,执行结果有什么差异:
- 方法 1:
func userDefaultsTestWithDirectValue() { UserDefaults.standard.setValue(nil, forKey: "user-defaults-test") } userDefaultsTestWithDirectValue() 复制代码
- 方法 2:
func setValue<T>(_ value: T) { UserDefaults.standard.setValue(value, forKey: "user-defaults-test") } func userDefaultsTestWithGeneric() { let value: Int? = nil setValue(value) } userDefaultsTestWithGeneric() 复制代码
解析
直接赋值
像 UserDefaults 这样的 OC 类在 Swift 中是通过桥接访问的,因此对桥接类传递可选值时需要进行桥接转换,如 UserDefaults 的以下方法,value 传递给 OC 方法调用时对应的类型为 Optional<Anyobject>
:
func set(_ value: Any?, forKey defaultName: String)
复制代码
当 value 值为 nil 时,Swift 会先转换为 Optional.none 再传递给 OC,对应 OC 的值为 nil,通过生成 Swift 中间代码可以对此进行印证。
执行命令转换以下代码:swiftc -emit-silgen -O UserDefaults.swift
// UserDefaults.swift
func setValue() {
UserDefaults.standard.setValue(nil, forKey: "user-defaults-test")
}
复制代码
生成结果如下:
sil_stage raw
import Builtin
import Swift
import SwiftShims
import Foundation
func setValue()
// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
%2 = integer_literal $Builtin.Int32, 0 // user: %3
%3 = struct $Int32 (%2 : $Builtin.Int32) // user: %4
return %3 : $Int32 // id: %4
} // end sil function 'main'
// setValue()
sil hidden [ossa] @$s12UserDefaults8setValueyyF : $@convention(thin) () -> () {
bb0:
%0 = metatype $@objc_metatype UserDefaults.Type // users: %2, %1
%1 = objc_method %0 : $@objc_metatype UserDefaults.Type, #UserDefaults.standard!getter.foreign : (UserDefaults.Type) -> () -> UserDefaults, $@convention(objc_method) (@objc_metatype UserDefaults.Type) -> @autoreleased UserDefaults // user: %2
%2 = apply %1(%0) : $@convention(objc_method) (@objc_metatype UserDefaults.Type) -> @autoreleased UserDefaults // user: %3
%3 = upcast %2 : $UserDefaults to $NSObject // users: %37, %34, %33
%4 = alloc_stack $Optional<Any> // users: %21, %7, %6, %5
inject_enum_addr %4 : $*Optional<Any>, #Optional.none!enumelt // id: %5
switch_enum_addr %4 : $*Optional<Any>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6
bb1: // Preds: bb0
%7 = unchecked_take_enum_data_addr %4 : $*Optional<Any>, #Optional.some!enumelt // users: %16, %8
%8 = open_existential_addr immutable_access %7 : $*Any to $*@opened("EBCE2CDC-B606-11EC-9A75-ACDE48001122") Any // users: %12, %10, %9
%9 = alloc_stack $@opened("EBCE2CDC-B606-11EC-9A75-ACDE48001122") Any // type-defs: %8; users: %15, %14, %12, %10
copy_addr %8 to [initialization] %9 : $*@opened("EBCE2CDC-B606-11EC-9A75-ACDE48001122") Any // id: %10
// function_ref _bridgeAnythingToObjectiveC<A>(_:)
%11 = function_ref @$ss27_bridgeAnythingToObjectiveCyyXlxlF : $@convention(thin) <τ_0_0> (@in_guaranteed τ_0_0) -> @owned AnyObject // user: %12
%12 = apply %11<@opened("EBCE2CDC-B606-11EC-9A75-ACDE48001122") Any>(%9) : $@convention(thin) <τ_0_0> (@in_guaranteed τ_0_0) -> @owned AnyObject // type-defs: %8; user: %13
%13 = enum $Optional<AnyObject>, #Optional.some!enumelt, %12 : $AnyObject // user: %17
destroy_addr %9 : $*@opened("EBCE2CDC-B606-11EC-9A75-ACDE48001122") Any // id: %14
dealloc_stack %9 : $*@opened("EBCE2CDC-B606-11EC-9A75-ACDE48001122") Any // id: %15
destroy_addr %7 : $*Any // id: %16
br bb3(%13 : $Optional<AnyObject>) // id: %17
bb2: // Preds: bb0
%18 = enum $Optional<AnyObject>, #Optional.none!enumelt // user: %19
br bb3(%18 : $Optional<AnyObject>) // id: %19
// %20 // users: %36, %34
bb3(%20 : @owned $Optional<AnyObject>): // Preds: bb2 bb1
dealloc_stack %4 : $*Optional<Any> // id: %21
%22 = string_literal utf8 "user-defaults-test" // user: %27
%23 = integer_literal $Builtin.Word, 18 // user: %27
%24 = integer_literal $Builtin.Int1, -1 // user: %27
%25 = metatype $@thin String.Type // user: %27
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%26 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %27
%27 = apply %26(%22, %23, %24, %25) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // users: %32, %29
// function_ref String._bridgeToObjectiveC()
%28 = function_ref @$sSS10FoundationE19_bridgeToObjectiveCSo8NSStringCyF : $@convention(method) (@guaranteed String) -> @owned NSString // user: %30
%29 = begin_borrow %27 : $String // users: %31, %30
%30 = apply %28(%29) : $@convention(method) (@guaranteed String) -> @owned NSString // users: %35, %34
end_borrow %29 : $String // id: %31
destroy_value %27 : $String // id: %32
%33 = objc_method %3 : $NSObject, #NSObject.setValue!foreign : (NSObject) -> (Any?, String) -> (), $@convention(objc_method) (Optional<AnyObject>, NSString, NSObject) -> () // user: %34
%34 = apply %33(%20, %30, %3) : $@convention(objc_method) (Optional<AnyObject>, NSString, NSObject) -> ()
destroy_value %30 : $NSString // id: %35
destroy_value %20 : $Optional<AnyObject> // id: %36
destroy_value %3 : $NSObject // id: %37
%38 = tuple () // user: %39
return %38 : $() // id: %39
} // end sil function '$s12UserDefaults8setValueyyF'
// _bridgeAnythingToObjectiveC<A>(_:)
sil [serialized] @$ss27_bridgeAnythingToObjectiveCyyXlxlF : $@convention(thin) <τ_0_0> (@in_guaranteed τ_0_0) -> @owned AnyObject
// String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
sil [serialized] [always_inline] [readonly] [_semantics "string.makeUTF8"] @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String
// String._bridgeToObjectiveC()
sil [_semantics "convertToObjectiveC"] @$sSS10FoundationE19_bridgeToObjectiveCSo8NSStringCyF : $@convention(method) (@guaranteed String) -> @owned NSString
// Mappings from '#fileID' to '#filePath':
// 'UserDefaults/UserDefaults.swift' => 'UserDefaults.swift'
复制代码
- 先看方法 bb0 的最后一行:
switch_enum_addr %4 : $*Optional<Any>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6
复制代码
对于传给 setValue 方法的 value 值有一个对枚举值的判断,如果为 .some 代表可解包继续执行 bb1 方法,如果为 .none 则走 bb2 方法。
- 从倒数第二行可以看到传给 switch 的值是 Optional.none!enumelt,说明后面将执行 bb2 方法。
inject_enum_addr %4 : $*Optional<Any>, #Optional.none!enumelt // id: %5
复制代码
- 再看 bb2 方法,它做的事情就是调用 bb3 方法
- bb3 方法中执行 setValue 方法的是这一行:
%34 = apply %33(%20, %30, %3) : $@convention(objc_method) (Optional<AnyObject>, NSString, NSObject) -> ()
复制代码
%20 代表value 值,类型为 Optional<AnyObject>
, 值为 Optional.none!enumelt,该值在 OC 中对应的值可以通过 OC 的 Category 覆盖原方法得到验证,新建以下文件,执行时可以看到 value 接收到的值是 nil:
// UserDefaults.m
#import <Foundation/Foundation.h>
@implementation NSUserDefaults (Optional)
- (void)setObject:(id)value forKey:(NSString *)defaultName {
NSLog(@"%@", value);
}
@end
复制代码
UserDefaults 直接写入 nil 时可以成功执行。
由此可见 Swift 直接传递 nil 给 OC,OC 中接收到的也是 nil.
范型与可选值
但是如果方法加入范型时,处理又会有点不同,对之前的方法做些改造,在 setValue 方法上加入范型 T,注意不是 T?,然后再传入 nil:
func setValue<T>(_ value: T) {
UserDefaults.standard.setValue(value, forKey: "user-defaults-test")
}
let value: Int? = nil
setValue(value)
复制代码
此时 UserDefaults 的 setValue 方法接收到的 value 值不再是 Optional.none!enumelt(即 nil),而是 Optional.some!enumelt,即程序认为 value 值是可解包的,即使它的实际值是 nil,那么 nil 解包之后在 OC 中对应什么值呢?就是 NSNull 单例对象,我们再从中间代码做个验证。
- 对以上方法转换为中间代码如下:
sil_stage raw
import Builtin
import Swift
import SwiftShims
import Foundation
func setValue<T>(_ value: T)
@_hasStorage @_hasInitialValue let value: Int? { get }
// value
sil_global hidden [let] @$s12UserDefaults5valueSiSgvp : $Optional<Int>
// main
sil [ossa] @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
alloc_global @$s12UserDefaults5valueSiSgvp // id: %2
%3 = global_addr @$s12UserDefaults5valueSiSgvp : $*Optional<Int> // users: %6, %5
%4 = enum $Optional<Int>, #Optional.none!enumelt // user: %5
store %4 to [trivial] %3 : $*Optional<Int> // id: %5
%6 = load [trivial] %3 : $*Optional<Int> // user: %8
%7 = alloc_stack $Optional<Int> // users: %11, %10, %8
store %6 to [trivial] %7 : $*Optional<Int> // id: %8
// function_ref setValue<A>(_:)
%9 = function_ref @$s12UserDefaults8setValueyyxlF : $@convention(thin) <τ_0_0> (@in_guaranteed τ_0_0) -> () // user: %10
%10 = apply %9<Int?>(%7) : $@convention(thin) <τ_0_0> (@in_guaranteed τ_0_0) -> ()
dealloc_stack %7 : $*Optional<Int> // id: %11
%12 = integer_literal $Builtin.Int32, 0 // user: %13
%13 = struct $Int32 (%12 : $Builtin.Int32) // user: %14
return %13 : $Int32 // id: %14
} // end sil function 'main'
// setValue<A>(_:)
sil hidden [ossa] @$s12UserDefaults8setValueyyxlF : $@convention(thin) <T> (@in_guaranteed T) -> () {
// %0 "value" // users: %7, %1
bb0(%0 : $*T):
debug_value_addr %0 : $*T, let, name "value", argno 1 // id: %1
%2 = metatype $@objc_metatype UserDefaults.Type // users: %4, %3
%3 = objc_method %2 : $@objc_metatype UserDefaults.Type, #UserDefaults.standard!getter.foreign : (UserDefaults.Type) -> () -> UserDefaults, $@convention(objc_method) (@objc_metatype UserDefaults.Type) -> @autoreleased UserDefaults // user: %4
%4 = apply %3(%2) : $@convention(objc_method) (@objc_metatype UserDefaults.Type) -> @autoreleased UserDefaults // user: %5
%5 = upcast %4 : $UserDefaults to $NSObject // users: %28, %25, %24
%6 = alloc_stack $T // users: %12, %11, %9, %7
copy_addr %0 to [initialization] %6 : $*T // id: %7
// function_ref _bridgeAnythingToObjectiveC<A>(_:)
%8 = function_ref @$ss27_bridgeAnythingToObjectiveCyyXlxlF : $@convention(thin) <τ_0_0> (@in_guaranteed τ_0_0) -> @owned AnyObject // user: %9
%9 = apply %8<T>(%6) : $@convention(thin) <τ_0_0> (@in_guaranteed τ_0_0) -> @owned AnyObject // user: %10
%10 = enum $Optional<AnyObject>, #Optional.some!enumelt, %9 : $AnyObject // users: %27, %25
destroy_addr %6 : $*T // id: %11
dealloc_stack %6 : $*T // id: %12
%13 = string_literal utf8 "user-defaults-test" // user: %18
%14 = integer_literal $Builtin.Word, 18 // user: %18
%15 = integer_literal $Builtin.Int1, -1 // user: %18
%16 = metatype $@thin String.Type // user: %18
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%17 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %18
%18 = apply %17(%13, %14, %15, %16) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // users: %23, %20
// function_ref String._bridgeToObjectiveC()
%19 = function_ref @$sSS10FoundationE19_bridgeToObjectiveCSo8NSStringCyF : $@convention(method) (@guaranteed String) -> @owned NSString // user: %21
%20 = begin_borrow %18 : $String // users: %22, %21
%21 = apply %19(%20) : $@convention(method) (@guaranteed String) -> @owned NSString // users: %26, %25
end_borrow %20 : $String // id: %22
destroy_value %18 : $String // id: %23
%24 = objc_method %5 : $NSObject, #NSObject.setValue!foreign : (NSObject) -> (Any?, String) -> (), $@convention(objc_method) (Optional<AnyObject>, NSString, NSObject) -> () // user: %25
%25 = apply %24(%10, %21, %5) : $@convention(objc_method) (Optional<AnyObject>, NSString, NSObject) -> ()
destroy_value %21 : $NSString // id: %26
destroy_value %10 : $Optional<AnyObject> // id: %27
destroy_value %5 : $NSObject // id: %28
%29 = tuple () // user: %30
return %29 : $() // id: %30
} // end sil function '$s12UserDefaults8setValueyyxlF'
// _bridgeAnythingToObjectiveC<A>(_:)
sil [serialized] @$ss27_bridgeAnythingToObjectiveCyyXlxlF : $@convention(thin) <τ_0_0> (@in_guaranteed τ_0_0) -> @owned AnyObject
// String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
sil [serialized] [always_inline] [readonly] [_semantics "string.makeUTF8"] @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String
// String._bridgeToObjectiveC()
sil [_semantics "convertToObjectiveC"] @$sSS10FoundationE19_bridgeToObjectiveCSo8NSStringCyF : $@convention(method) (@guaranteed String) -> @owned NSString
// Mappings from '#fileID' to '#filePath':
// 'UserDefaults/UserDefaults.swift' => 'UserDefaults.swift'
复制代码
- 在 main 方法中,value 变量的值为 Optional.none!enumelt
%4 = enum $Optional<Int>, #Optional.none!enumelt // user: %5
复制代码
- 执行 UserDefaults 的 setValue 方法时,value 的类型为 Optional.some!enumelt:
%10 = enum $Optional<AnyObject>, #Optional.some!enumelt, %9 : $AnyObject // users: %27, %25
复制代码
这里编译器认为 value 是可解包的,即范型 T 为非可选值类型,因此程序运行时需要对 value,即对 nil 进行解包,通过上面 UserDefaults 的 Category 方法,可以打印出解包之后的值为 NSNull 类型:
将 NSNull 对象传递给 UserDefaults 写入时将会发生崩溃。
- 对上面方法再做个改造:
func setOptionalValue<T>(_ value: T?) {
UserDefaults.standard.setValue(value, forKey: "user-defaults-test")
}
复制代码
此时 value 类型为 T?,如果传入 nil 时,编译器将会把它转换为 Optional.none!enumelt,此时 OC 将接收到 nil 值,而不会再发生崩溃。
由此可见在使用范型时,尤其是桥接至 OC 对象时,如果标记为 T 类型,有可能会被编译器转换为可解包类型,即 Optional.some,即使传入的是 nil,这种情况下最好标记为 T? 类型。
Photo by Robin Schreiner on Unsplash