iOS 混编|为 Objective-C 添加枚举宏,改善混编体验

·  阅读 2808
iOS 混编|为 Objective-C 添加枚举宏,改善混编体验

关键词:NS_ENUM、NS_OPTIONS、NS_CLOSED_ENUM、NS_TYPED_ENUM、NS_TYPED_EXTENSIBLE_ENUM、NS_STRING_ENUM、NS_EXTENSIBLE_STRING_ENUM、@unknown default

前言

使用 Objective-C 的你,是否对 NS_CLOSED_ENUMNS_STRING_ENUM/NS_EXTENSIBLE_STRING_ENUMNS_TYPED_ENUM/NS_TYPED_EXTENSIBLE_ENUM 这几个枚举宏感到陌生呢?笔者对修饰 NSNotificationName 的 NS_EXTENSIBLE_STRING_ENUM 宏比较好奇,便展开了探索,于是就有了本文。

typedef NSString *NSNotificationName NS_EXTENSIBLE_STRING_ENUM;
UIKIT_EXTERN NSNotificationName const UIApplicationDidEnterBackgroundNotification;
UIKIT_EXTERN NSNotificationName const UIApplicationWillEnterForegroundNotification;
UIKIT_EXTERN NSNotificationName const UIApplicationDidFinishLaunchingNotification;
复制代码

在 Xcode 13 中,Apple 已经将其改为使用 NS_TYPED_EXTENSIBLE_ENUM 声明。

优雅地声明类型常量枚举

在 Objective-C 中,我们经常会使用 NSString 类型常量来当作 NSDictionary 的 key,例如:

// Dicitonary keys
FOUNDATION_EXTERN NSString * const DCDictionaryKeyTitle;
FOUNDATION_EXTERN NSString * const DCDictionaryKeySubtitle;
FOUNDATION_EXTERN NSString * const DCDictionaryKeyCount;

// 使用
NSDictionary<NSString *, id> *dict = @{......};

NSString *title    = dict[DCDictionaryKeyTitle]; 
NSString *subtitle = dict[DCDictionaryKeySubtitle]; 
NSInteger count    = [dict[DCDictionaryKeyCount] integerValue];
复制代码

混编时,在 Swift 中的使用方式为:

// Objective-C 的常量被自动转换成 Swift 常量
let DCDictionaryKeyTitle    : String
let DCDictionaryKeySubtitle : String
let DCDictionaryKeyCount    : String

// 使用
let dict:[String : Any] = [DCDictionaryKeyTitle    : "a title",
                           DCDictionaryKeySubtitle : "a subTitle",
                           DCDictionaryKeyCount    : 66]

let title    = dict[DCDictionaryKeyTitle]    as? String 
let subtitle = dict[DCDictionaryKeySubtitle] as? String 
let count    = dict[DCDictionaryKeyCount]    as? Int
复制代码

你可以查看编译器为 Objective-C 接口生成的 Swift 接口,参考:查看编译器为 Objective-C 接口生成的 Swift 接口

这样的写法虽然是没有错的,但却存在着问题:

  1. dict 的 key 的类型是 String,所以我们其实可以使用任意的字符串当作索引。一般情况下,开发者使用这个 dict 时会去查文件看看有哪些 key 可以使用。但不可避免的是,开发者也能会直接使用字符串如 dict["title"] 来取值,如果不小心拼错的话编译器也不会给警告的,这样就增加了不可预期的错误的风险。
  2. 一个小问题,就是代码看起来比较冗长,不符合 Swift 的使用习惯。在 Swift 中我们通常会把这种常量枚举用一个具有字符串原始值的 Enum 或者 Struct 定义,这样我们就能直接使用 .title 而不是 DCDictionaryKeyTitle,以彰显 Swift 的简洁。

Apple 也发现了这个问题。在 Xcode 8 中,Apple 为 Objective-C 提供了全新的宏 NS_STRING_ENUMNS_EXTENSIBLE_STRING_ENUM,让字符串类型常量在 Swift 中使用起来更优雅简洁更符合 Swift 的使用习惯。

首先,使用 typedef 对类型常量进行分组,并指定一个类型(如 DCDictionaryKey),涉及到使用该类型常量的地方都改为使用 DCDictionaryKey,而不是 String。然后,在后面添加上宏 NS_STRING_ENUM

typedef NSString *DCDictionaryKey NS_STRING_ENUM;

FOUNDATION_EXTERN DCDictionaryKey const DCDictionaryKeyTitle;
FOUNDATION_EXTERN DCDictionaryKey const DCDictionaryKeySubtitle;
FOUNDATION_EXTERN DCDictionaryKey const DCDictionaryKeyCount;

// 使用
NSDictionary<DCDictionaryKey, id> *dict = @{......};

NSString *title    = dict[DCDictionaryKeyTitle]; 
NSString *subtitle = dict[DCDictionaryKeySubtitle]; 
NSInteger count    = [dict[DCDictionaryKeyCount] integerValue];
复制代码

在 OC 中使用起来没多大变化,但在 Swift 中可就不一样了,真够 Swift!

// Objective-C 的常量被自动转换成 Swift Struct
struct DCDictionaryKey : Hashable, Equatable, RawRepresentable {
    init(rawValue: String)
}
extension DCDictionaryKey {
    static let title    : DCDictionaryKey
    static let subtitle : DCDictionaryKey
    static let count    : DCDictionaryKey
}

// 使用
let dict:[DCDictionaryKey : Any] = [.title    : "a title",
                                    .subtitle : "a subTitle",
                                    .count    : 66]

let title    = dict[.title]    as? String
let subtitle = dict[.subtitle] as? String
let count    = dict[.count]    as? Int

// 这时候如果我们之间使用字符串 "title" 当作 key 的话,编译器会报错
let title    = dict["title"]   as? String // Error: Cannot convert value of type 'String' to expected argument type 'DCDictionaryKey'. Replace '"title"' with 'DCDictionaryKey(rawValue: "title") ?? <#default value#>
复制代码

Foundation 库的 NSNotificationName、NSRunLoopMode 等,或者 SDWebImage 的 SDWebImageContextOption 就是这样处理的。

typedef NSString *NSNotificationName NS_EXTENSIBLE_STRING_ENUM;
UIKIT_EXTERN NSNotificationName const UIApplicationDidEnterBackgroundNotification;
UIKIT_EXTERN NSNotificationName const UIApplicationWillEnterForegroundNotification;
UIKIT_EXTERN NSNotificationName const UIApplicationDidFinishLaunchingNotification;
复制代码

Objective-C 枚举宏

Apple|Grouping Related Objective-C Constants 中,Apple 详细列举了 NS_ENUMNS_CLOSED_ENUMNS_OPTIONSNS_TYPED_ENUMNS_TYPED_EXTENSIBLE_ENUM 等宏的使用场景,用好它们以改善在混编时在 Swift 中的编程体验。另外,Apple 建议弃用 NS_STRING_ENUM/NS_EXTENSIBLE_STRING_ENUM 而改用 NS_TYPED_ENUM/NS_TYPED_EXTENSIBLE_ENUM

  • NS_ENUM:用于简单的枚举
  • NS_CLOSED_ENUM:用于不会变更枚举成员的简单的枚举(简称 “冻结枚举” )
  • NS_OPTIONS:用于选项枚举
  • NS_TYPED_ENUM:用于类型常量枚举
  • NS_TYPED_EXTENSIBLE_ENUM:用于可扩展的类型常量枚举

NS_ENUM

用于声明简单的枚举,这个大家都很熟悉了,将作为 enum 导入到 Swift 中。。

// Declare in Objective-C
typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
    UITableViewCellStyleDefault,
    UITableViewCellStyleValue1,
    UITableViewCellStyleValue2,
    UITableViewCellStyleSubtitle
};

// In Swift, the UITableViewCellStyle enumeration is imported like this:
enum UITableViewCellStyle : Int {
    case `default` = 0
    case value1 = 1
    case value2 = 2
    case subtitle = 3
}

// Use it in Swift
let style = UITableViewCellStyle.default
复制代码

这个知识点看似没用,实则大大有用。在 Objective-C 中,除了使用 NS_ENUM 宏,还可以像如下等方式声明枚举。它或许是你或同事的编码习惯,又或许是历史遗留的代码。虽然这样的写法并没有错,但 Generated Swift Interface 却不尽如人意,导致在 Swift 中使用时只能使用原始的完整的枚举名称。

// Declare in Objective-C
typedef enum: NSUInteger {
    UITableViewCellStyleDefault,
    UITableViewCellStyleValue1,
    UITableViewCellStyleValue2,
    UITableViewCellStyleSubtitle
} UITableViewCellStyle;

// Generated Swift Interface
struct UITableViewCellStyle : Equatable, RawRepresentable {
    init(_ rawValue: UInt)
    init(rawValue: UInt)
    var rawValue: UInt
}

// Use it in Swift
let style = UITableViewCellStyleDefault
复制代码

《Effective Objective-C 2.0》5. 用枚举表示状态、选项、状态码 中也提到了使用 NS_ENUM 和 NS_OPTIONS 来声明枚举类型的优点。如果你的工程处于混编阶段,不妨将 Objective-C 中的枚举类型改为 NS_ENUM 和 NS_OPTIONS 声明,以优化 Swift 编程体验吧。

NS_CLOSED_ENUM

用于声明不会变更枚举成员的简单的枚举(简称 “冻结枚举” ),对应 Swift 中的 @frozen 关键字,将作为 @frozen enum 导入到 Swift 中。冻结枚举对于希望在 switch 语句中匹配有限状态集的时候非常有用,这个有限状态集是一个完整的集合,覆盖了所有情况,将来不会再有其他新的情况。

例如,NSComparisonResult 枚举用于指定如何排序,在两个数比大小时,无非就 <、=、> 三种情况,所以非常适合使用冻结枚举。

// Declare in Objective-C
typedef NS_CLOSED_ENUM(NSInteger, NSComparisonResult) {
    NSOrderedAscending = -1L,
    NSOrderedSame,
    NSOrderedDescending
};

// In Swift, the NSComparisonResult enumeration is imported like this:
@frozen enum NSComparisonResult : Int {
    case orderedAscending = -1
    case orderedSame = 0
    case orderedDescending = 1
}
复制代码

使用 NS_ENUM 和 NS_CLOSED_ENUM 枚举宏在导入到 Swift 时生成的是实际 Enum 类型,而其它枚举宏都是生成 Struct 类型。

相比较于非冻结枚举,冻结枚举降低了灵活性,但提升了性能。一旦枚举被标记为冻结枚举,那么在未来版本的库中就不能通过添加、删除或重新排序枚举的 case,否则会破坏 ABI 兼容性。

Swift 中的 default 与 @unknown default

  • 对于非冻结枚举,你需要使用 default 或者 @unknown default 来处理未知的 case(未来可能新增枚举类型),否则会得到编译器警告 Switch covers known cases, but 'enumType' may have additional unknown values,但 Xcode 的 fix 方案是使用 @unknown default
  • 而对于冻结枚举,使用 @unknown default 无论如何都会得到编译器警告。
    • 如果你穷举了所有 case,将得到警告 Case is already handled by previous patterns; consider removing it,因为冻结枚举已经约定好将来不会添加新的枚举成员,所以 @unknown default case 永远不会执行。虽然这里使用 default 不会得到警告,但也是不会执行的。
    • 如果你没有穷举所有 case,将得到警告 Switch must be exhaustive,使用 @unknown default 必须穷举所有 case。

简单来说 default@unknown default 都可以用来处理已知以及未知的情况。区别在于,使用 @unknown default,如果你没有穷举所有枚举类型,或者未来有新增枚举类型,那么编译器会给出警告提示。关于选择应该是,对于非冻结枚举,如果你想穷举所有 case,并希望未来有新增枚举类型时得到编译器警告,那么就使用 @unknown default。也就是说,@unknown default 应该只匹配未来加入的枚举 case

关于冻结枚举与非冻结枚举,可参阅:

NS_OPTIONS

用于声明选项枚举,这个大家也都很熟悉了,将作为 struct 导入到 Swift 中。需要注意的地方在上文 NS_ENUM 中已经提到了,尽量使用 NS_OPTIONS 来声明选项枚举。

// Declare in Objective-C
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

// In Swift, the UIViewAutoresizing type is imported like this:
public struct UIViewAutoresizing: OptionSet {
    public init(rawValue: UInt)
    
    public static var flexibleLeftMargin: UIViewAutoresizing { get }
    public static var flexibleWidth: UIViewAutoresizing { get }
    public static var flexibleRightMargin: UIViewAutoresizing { get }
    public static var flexibleTopMargin: UIViewAutoresizing { get }
    public static var flexibleHeight: UIViewAutoresizing { get }
    public static var flexibleBottomMargin: UIViewAutoresizing { get }
}

// Use it in Swift
let style = UIViewAutoresizing([.flexibleWidth, .flexibleHeight])
复制代码

NS_TYPED_ENUM

用于声明类型常量枚举,将作为 struct 导入到 Swift 中,可大大改善 Objective-C 类型常量在 Swift 中的使用方式。它不局限于字符串类型常量,NS_STRING_ENUM 可以用它替代。

用法是使用 typedef 对类型常量进行分组,并指定一个类型(如下 TrafficLightColor),然后在后面添加上宏 NS_TYPED_ENUM。

使用 NS_STRING_ENUM 宏,在逻辑上你不能在 Swift 中使用 extension 扩展新的常量集,虽然这是允许的。如果你需要做此支持,请使用 NS_TYPED_EXTENSIBLE_ENUM。

// Store the three traffic light color options as 0, 1, and 2.
typedef long TrafficLightColor NS_TYPED_ENUM;
 
FOUNDATION_EXTERN TrafficLightColor const TrafficLightColorRed;
FOUNDATION_EXTERN TrafficLightColor const TrafficLightColorYellow;
FOUNDATION_EXTERN TrafficLightColor const TrafficLightColorGreen;

// In Swift, the TrafficLightColor type is imported like this:
struct TrafficLightColor : Hashable, Equatable, RawRepresentable {
    init(rawValue: Int)
}
extension TrafficLightColor {
    static let red: TrafficLightColor
    static let yellow: TrafficLightColor
    static let green: TrafficLightColor
}

// Use it in Swift
let color = TrafficLightColor.red
复制代码

NS_TYPED_EXTENSIBLE_ENUM

用于声明可扩展的类型常量枚举。与 NS_TYPED_ENUM 的区别是生成的 struct 多了一个忽略参数标签的构造器。

// declared
typedef long FavoriteColor NS_TYPED_EXTENSIBLE_ENUM;
FOUNDATION_EXTERN FavoriteColor const FavoriteColorBlue;

// imported
struct FavoriteColor : Hashable, Equatable, RawRepresentable {
    init(_ rawValue: Int)
    init(rawValue: Int)
}
extension FavoriteColor {
    static let blue: FavoriteColor
}

// extended
extension FavoriteColor {
    static var green: FavoriteColor {
        return FavoriteColor(1) // blue is 0, green is 1, and new favorite colors could follow
    }
}
复制代码

最后,让我们看一下 NS_STRING_ENUM/NS_EXTENSIBLE_STRING_ENUMNS_TYPED_ENUM/NS_TYPED_EXTENSIBLE_ENUM 的宏定义,它们的替换宏都为 _NS_TYPED_ENUM/_NS_TYPED_EXTENSIBLE_ENUM。我们优先使用 NS_TYPED_ENUM/NS_TYPED_EXTENSIBLE_ENUM 以保持代码统一性。

#define _NS_TYPED_ENUM _CF_TYPED_ENUM
#define _NS_TYPED_EXTENSIBLE_ENUM _CF_TYPED_EXTENSIBLE_ENUM

// Note: NS_TYPED_ENUM is preferred to NS_STRING_ENUM
#define NS_STRING_ENUM _NS_TYPED_ENUM
// Note: NS_TYPED_EXTENSIBLE_ENUM is preferred to NS_EXTENSIBLE_STRING_ENUM
#define NS_EXTENSIBLE_STRING_ENUM _NS_TYPED_EXTENSIBLE_ENUM

#define NS_TYPED_ENUM _NS_TYPED_ENUM
#define NS_TYPED_EXTENSIBLE_ENUM _NS_TYPED_EXTENSIBLE_ENUM
复制代码

小结

通过阅读本文,你是否对 Objective-C 的枚举宏有了进一步的了解呢?用好它们以改善在混编时在 Swift 中的编程体验。使用 NS_ENUMNS_OPTIONS 来声明简单枚举和选项枚举,以优化 Swift 编程体验。NS_CLOSED_ENUM 用于声明不会变更枚举成员的冻结枚举,对应 Swift 中的 @frozen 关键字,以降低灵活性的代价,换取了性能上的提升。NS_STRING_ENUM/NS_EXTENSIBLE_STRING_ENUMNS_TYPED_ENUM/NS_TYPED_EXTENSIBLE_ENUM 用于声明字符串常量/类型常量枚举(建议统一使用前者,弃用后者),这在混编时在 Swift 中使用起来更简洁优雅更符合 Swift 的使用习惯。

参考

推荐阅读

分类:
iOS
标签: