Swift编译器之迷惑行为一

OptionSet 是 Swift 中比较常用的一个内置协议,它的存在算是填补了既要强类型,又要支持按位取与的空白,是对 OC 中 NS_OPTIONS 的桥接。从声明上可以看到,OptionSet 协议本身又服从 SetAlgebra 协议,后者只定义了常见的集合操作方法,如 union remove insert 等,而前者在 extension 中给了默认实现:

public protocol OptionSet : RawRepresentable, SetAlgebra {

    /// The element type of the option set.
    ///
    /// To inherit all the default implementations from the `OptionSet` protocol,
    /// the `Element` type must be `Self`, the default.
    associatedtype Element = Self

    /// Creates a new option set from the given raw value.
    ///
    /// This initializer always succeeds, even if the value passed as `rawValue`
    /// exceeds the static properties declared as part of the option set. This
    /// example creates an instance of `ShippingOptions` with a raw value beyond
    /// the highest element, with a bit mask that effectively contains all the
    /// declared static members.
    ///
    ///     let extraOptions = ShippingOptions(rawValue: 255)
    ///     print(extraOptions.isStrictSuperset(of: .all))
    ///     // Prints "true"
    ///
    /// - Parameter rawValue: The raw value of the option set to create. Each bit
    ///   of `rawValue` potentially represents an element of the option set,
    ///   though raw values may include bits that are not defined as distinct
    ///   values of the `OptionSet` type.
    init(rawValue: Self.RawValue)
}

Swift 对 OC 中使用 NS_OPTIONS 声明的类型自动做了转换,如常用的正则表达式的选项:

typedef NS_OPTIONS(NSUInteger, NSMatchingOptions) {  
   NSMatchingReportProgress         = 1 << 0,       /* Call the block periodically during long-running match operations. */
   NSMatchingReportCompletion       = 1 << 1,       /* Call the block once after the completion of any matching. */
   NSMatchingAnchored               = 1 << 2,       /* Limit matches to those at the start of the search range. */
   NSMatchingWithTransparentBounds  = 1 << 3,       /* Allow matching to look beyond the bounds of the search range. */
   NSMatchingWithoutAnchoringBounds = 1 << 4        /* Prevent ^ and $ from automatically matching the beginning and end of the search range. */
};

对应的 Swift 声明:

    public struct MatchingOptions : OptionSet {

        public init(rawValue: UInt)

        public static var reportProgress: NSRegularExpression.MatchingOptions { get } /* Call the block periodically during long-running match operations. */

        public static var reportCompletion: NSRegularExpression.MatchingOptions { get } /* Call the block once after the completion of any matching. */

        public static var anchored: NSRegularExpression.MatchingOptions { get } /* Limit matches to those at the start of the search range. */

        public static var withTransparentBounds: NSRegularExpression.MatchingOptions { get } /* Allow matching to look beyond the bounds of the search range. */

        public static var withoutAnchoringBounds: NSRegularExpression.MatchingOptions { get } /* Prevent ^ and $ from automatically matching the beginning and end of the search range. */
    }

另外,为了开发者的方便,SetAlgebra 服从了 ExpressibleByArrayLiteral 使得它可以从一个数组字面量初始化构造一个对象:

public protocol SetAlgebra<Element> : Equatable, ExpressibleByArrayLiteral {

    associatedtype Element

}

因此我们通常可以这样来使用 OptionSet

let options: UIView.AutoresizingMask = [.flexibleHeight, .flexibleWidth]  
let options: UIView.AutoresizingMask = []  

等同与之前 OC 的写法:

UIViewAutoresizing option = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;  
UIViewAutoresizing option = UIViewAutoresizingNone;  

然而,使用数组初始化有时会出现一些迷惑行为,比如下图中的 crash 问题,使用的是开源的 URLNavigator 库。

对应的堆栈如下:

#0    0x0000000125822388 in swift_getAssociatedTypeWitnessSlowImpl(swift::MetadataRequest, swift::TargetWitnessTable<swift::InProcess>*, swift::TargetMetadata<swift::InProcess> const*, swift::TargetProtocolRequirement<swift::InProcess> const*, swift::TargetProtocolRequirement<swift::InProcess> const*) ()
#1    0x00000001258210d8 in swift_getAssociatedTypeWitness ()
#2    0x000000012562cf70 in OptionSet<>.init() ()
#3    0x00000001050ed292 in protocol witness for SetAlgebra.init() in conformance UIRectCorner ()
#4    0x00000001053129c9 in protocol witness for SetAlgebra.init() in conformance NSRegularExpressionOptions ()
#5    0x000000012564af54 in SetAlgebra.init<τ_0_0>(_:) ()
#6    0x0000000106c57356 in protocol witness for SetAlgebra.init<τ_0_0>(_:) in conformance NSRegularExpressionOptions ()
#7    0x000000012564b218 in SetAlgebra<>.init(arrayLiteral:) ()
#8    0x0000000106c55423 in URLMatcher.replaceRegex(_:_:_:) at /Users/hztanxin/Code/MusicDating/Pods/URLNavigator/Sources/URLMatcher/URLMatcher.swift:110
#9    0x0000000106c550e4 in URLMatcher.normalizeURL(_:) at /Users/hztanxin/Code/MusicDating/Pods/URLNavigator/Sources/URLMatcher/URLMatcher.swift:103
#10    0x0000000106c53ae6 in URLMatcher.match(_:from:) at /Users/hztanxin/Code/MusicDating/Pods/URLNavigator/Sources/URLMatcher/URLMatcher.swift:50
#11    0x0000000106c4e8b7 in Navigator.handler(for:context:) at /Users/hztanxin/Code/MusicDating/Pods/URLNavigator/Sources/URLNavigator/Navigator.swift:62

其中 #7 是数组初始化的方法调用,一直到 #4 都正常,但是 #3 出现了一个不该出现的符号 UIRectCorner。两个风牛马不相及的类型不知为何会产生联系,从编译出来的二进制反汇编看,#4 的方法实现体就是跳转到 #3 的实现体。

但是有时候又是好的,比如所依赖的库某几个从静态库改为源代码(或者全用静态库);有时侯 Debug 与 Release 打包出来的结果也不一样。当它是好的时侯,编译出来的方法的实现体是完整的,而不是一个跳转。

目前没有思路与检测手段,只有出问题时可以显式地用 .init() 方法初始化绕过此问题:

guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return string }

/// 改为

guard let regex = try? NSRegularExpression(pattern: pattern, options: .init()) else { return string }