Swift 范型可选值与 OC 桥接

·  阅读 183
Swift 范型可选值与 OC 桥接

问题

先看一个问题:

  • 对于 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 类型:

image

将 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

分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改