4

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

 2 years ago
source link: https://juejin.cn/post/6999460035508043807
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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 的使用习惯。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK