【译】:Objective-C id 和 Swift Any

前言:在正式发布Swift3.0版本后,苹果官方于2016年10月12日更新了一篇关于讲解Objective-C的id和Swift中Any类型的博客:Objective-C id as Swift Any。旨在指导开发者正确认识和使用idAny关键字。如果有同样和我对两者的使用不是很清楚或者正在着手将项目代码从Swift2.0迁移到Swift3.0版本的小伙伴可以们阅读一下由本人翻译的译文。以下是译文全部内容:

2016年10月12日

Objective-C 中的 id 和 Swift 中的 Any

在使用Objective-C API方面Swift 3的接口表现的比之前版本更强大。例如Swift 2将Objective-C中的id类型映射成Swift中的AnyObject类型,通常只能保存类这种类型的值。 Swift 2还为AnyObject提供了对一些桥接值类型(例如StringArrayDictionarySet和一些数字)的隐式转换,以方便我们可以很容易的像使用NSStringNSArray或其他基础的集合类一样使用Swift中的原生类型。这些转换与语言的其他部分不一致,使得很难理解什么可以用作AnyObject,导致会出现错误。

在Swift 3中,Objective-C中的id类型现在映射成了Swift中的Any类型,它可以代表任何类型的值,无论是类、枚举、结构体还是任何其他Swift类型。 这种变化使得Swift中的Objective-C API更加灵活,因为Swift定义的值类型可以传递给Objective-C API并作为Swift中的类型获取,从而无需手动“框选”类型(本人理解为转换、解包)。 这些好处还扩展到集合类:Objective-C中的集合类型NSArrayNSDictionaryNSSet,以前只接受AnyObject类型的元素,现在可以保存任何类型的元素。 对于Swift中哈希类的集合,例如DictionarySet,有一个新类型AnyHashable可以保存任何遵守Swift中Hashable协议的类型的值。 总之,从Swift 2到Swift 3一些类型的映射关系变化如下图:

《【译】:Objective-C id 和 Swift Any》 图片摘自苹果官方博客

通常情况下,你的代码不需要为适应这种变化做出大量的修改。 Swift 2中的代码存在的AnyObject的值类型可以借助隐式转换变成Any继续在Swift 3中工作。 但是,有些地方你需要更改声明的变量和方法类型才能获得在Swift 3的编译通过。另外,如果你的代码显式使用AnyObject或Cocoa中的类,如NSStringNSArrayNSDictionary,你将需要引入更多的显式转换使用作为NSString或作为字符串,因为对象和值类型之间的隐式转换在Swift 3中是不允许的。Xcode中的自动迁移器将进行最小的更改,以保证你的代码从Swift 2 到3能够编译成功,但情况并不总是有利的。 本文将指出你可能需要做的一些更改,以及在更改代码时将id变成Any使用需要注意的一些问题。

重写方法和遵守协议

新建一个继承自Objective-C类的子类并且重写它的方法,或者是遵守一个Objective-C中的协议,当父类的方法中使用了Objective-C中id类型时,子类方法的类型此时应该被修改。一个常见的例子是NSObject类的isEqual:方法和NSCopying协议的copyWithZone:方法。在Swift 2中,你可以像下面一样新建一个遵守NSCopying协议并继承自NSObject的子类:

// Swift 2
class Foo: NSObject, NSCopying {
      override func isEqual(_ x: AnyObject?) -> Bool { ... }
      func copyWithZone(_ zone: NSZone?) -> AnyObject { ... }
}

在Swift 3中,除了将方法的命名从copyWithZone(_ :)更改为copy(with :)之外,你还需要将这些方法接受参数的类型从AnyObject改为Any。如下所示:

// Swift 3
class Foo: NSObject, NSCopying {
      override func isEqual(_ x: Any?) -> Bool { ... }
      func copy(with zone: NSZone?) -> Any { ... }
}

非类型集合

属性列表,JSON和用户信息字典在Cocoa框架中很常见,Cocoa框架将这些表示为非类型化集合。 在Swift 2中处理这类数据需要用到AnyObjectNSObject来构建ArrayDictionarySet,并且依靠隐式桥接转换来处理值的类型:

// Swift 2
struct State {
    var name: String
    var abbreviation: String
    var population: Int

    var asPropertyList: [NSObject: AnyObject] {
        var result: [NSObject: AnyObject] = [:]
        // Implicit conversions turn String into NSString here…
        result["name"] = self.name
        result["abbreviation"] = self.abbreviation
        // …and Int into NSNumber here.
        result["population"] = self.population
        return result
    }
}
let california = State(name: "California",
                       abbreviation: "CA",
                       population: 39_000_000)
NSNotification(name: "foo", object: nil,
               userInfo: california.asPropertyList)

或者,你可以使用Cocoa框架中的集合类,例如NSDictionary

// Swift 2
struct State {
    var name: String
    var abbreviation: String
    var population: Int

    var asPropertyList: NSDictionary {
        var result = NSMutableDictionary()
        // Implicit conversions turn String into NSString here…
        result["name"] = self.name
        result["abbreviation"] = self.abbreviation
        // …and Int into NSNumber here.
        result["population"] = self.population
        return result.copy()
    }
}
let california = State(name: "California",
                       abbreviation: "CA",
                       population: 39_000_000)
// NSDictionary then implicitly converts to [NSObject: AnyObject] here.
NSNotification(name: "foo", object: nil,
               userInfo: california.asPropertyList)

在Swift 3中,隐式转换已经不支持,因此上述两段代码都不会按原样工作。 Xcode中的迁移器可能会建议你使用as挨个进行类型转换,以保证此代码能够正常工作,但有一个更好的解决方案。 Swift现在导入Cocoa API接受AnyAnyHashable类型的集合,所以我们可以用[AnyHashable:Any]代替[NSObject:AnyObject]NSDictionary申明集合类型,而不需要更改任何其他代码:

// Swift 3
struct State {
    var name: String
    var abbreviation: String
    var population: Int

    // Change the dictionary type to [AnyHashable: Any] here...
    var asPropertyList: [AnyHashable: Any] {
        var result: [AnyHashable: Any] = [:]
        // No implicit conversions necessary, since String and Int are subtypes
        // of Any and AnyHashable
        result["name"] = self.name
        result["abbreviation"] = self.abbreviation
        result["population"] = self.population
        return result
    }
}
let california = State(name: "California",
                       abbreviation: "CA",
                       population: 39_000_000)
// ...and you can still use it with Cocoa API here
Notification(name: "foo", object: nil,
             userInfo: california.asPropertyList)

AnyHashable类型

Swift的Any类型可以保存任何类型,但是DictionarySet需要的键的类型是要求遵守Hashable协议的类型,所以Any表示的太广泛。 从Swift 3开始,Swift标准库提供了一个新的类型AnyHashable。 与Any类似,它充当所有Hashable类型的父类,因此StringInt和其他hashable类型的值都可以隐式地用作AnyHashable值,AnyHashable类型的值可以使用isas !动态检查或者使用as?动态转换运算符。 当从Objective-C导入无类型的NSDictionaryNSSet对象时可以使用AnyHashable,当然在纯Swift中构建异构集合或字典时AnyHashable也很有用。

未链接上下文的显式转换

在某些确定的情况下,Swift不能自动桥接C和Objective-C。 例如,一些C和Cocoa API使用id *指针作为“out”“in-out”参数,并且由于Swift不能静态地确定指针的使用方式,因此它不能对内存中的值自动执行桥接转换 。 在这种情况下,指针仍将显示为UnsafePointer <AnyObject>。 如果您需要使用到这些不能自动桥接转换的API,您可以使用显式桥接转换,在代码中使用as Typeas AnyObject显式转换。

// ObjC
@interface Foo
- (void)updateString:(NSString **)string;
- (void)updateObject:(id *)obj;
@end
// Swift
func interactWith(foo: Foo) -> (String, Any) {
    var string = "string" as NSString // explicit conversion
    foo.updateString(&string) // parameter imports as UnsafeMutablePointer<NSString>
    let finishedString = string as String

    var object = "string" as AnyObject
    foo.updateObject(&object) // parameter imports as UnsafeMutablePointer<AnyObject>
    let finishedObject = object as Any

    return (finishedString, finishedObject)
}

另外,Objective-C中的协议在Swift中仍然是类约束(及只有类才可以遵守协议),所以你不能让Swift中的结构体或枚举直接遵守Objective-C中的协议或者是使用轻量级的泛型类。 当您需要使用到这些协议和API时应该像这样String as NSStringArray as NSArray进行显式转换。

AnyObject属性查找

Any没有与AnyObject相同的返回对象的描述信息的方法。在Swift 2中Any类型的对象查找属性或者是给一个无类型的Objective-C对象发送消息可能会导致奔溃。 例如下面这个使用Swift 2语法的代码:

// Swift 2
func foo(x: NSArray) {
    // Invokes -description by magic AnyObject lookup
    print(x[0].description)
}

Swift 3中description不再是的Any类型的对象的方法(通常我们重写这个方法以获得关于对象的一些描述信息)。你可以这样做x[0] as AnyObjectx[0]的值转换成AnyObject类型来获取它的描述:

// Swift 3
func foo(x: NSArray) {
    // Result of subscript is now Any, needs to be coerced to get method lookup
    print((x[0] as AnyObject).description)
}

或者,将值强制转换成你期望的具体类型:

func foo(x: NSArray) {
    // Cast to the concrete object type you expect
    print((x[0] as! NSObject).description)
}

Objective-C中的Swift中的值类型

Any可以包含任何结构体,枚举,元组或其他你定义的Swift类型。在Swift3中Objective-C的桥接可以将任何Swift中的类型的值转换成Objective-C的id类型的兼容的对象。 这使得更容易在Cocoa集合中存储userInfo、字典和其他自定义Swift类型的对象。 例如,在Swift 2中,您需要将数据类型更改为类,或者手动加载它们,以将它们的值附加到NSNotification中:

// Swift 2
struct CreditCard { number: UInt64, expiration: NSDate }

let PaymentMade = "PaymentMade"

// We can't attach CreditCard directly to the notification, since it
// isn't a class, and doesn't bridge.
// Wrap it in a Box class.
class Box<T> {
    let value: T
    init(value: T) { self.value = value }
}

let paymentNotification =
    NSNotification(name: PaymentMade,
                   object: Box(value: CreditCard(number: 1234_0000_0000_0000,
                                                 expiration: NSDate())))

使用Swift 3,我们不需要Box这个类,可以将对象直接附加到通知中:

// Swift 3
let PaymentMade = Notification.Name("PaymentMade")

// We can associate the CreditCard value directly with the Notification
let paymentNotification =
    Notification(name: PaymentMade,
                 object: CreditCard(number: 1234_0000_0000_0000,
                                    expiration: Date()))

在Objective-C中,CreditCard值将显示为一个兼容idNSObject对象(这里有疑问),使用Swift的EquatableHashableCustomStringConvertible如果存在原始的Swift类型,它将实现isEqual:hash和描述。 在Swift中,可以通过将值动态地转换回其原始类型来检索该值:

// Swift 3
let paymentCard = paymentNotification.object as! CreditCard
print(paymentCard.number) // 1234000000000000

请注意,在Swift 3.0中,一些常见的Swift和Objective-C结构类型将桥接为不透明对象(不透明对象什么鬼?),而不是惯用的Cocoa中的对象。例如IntUIntDoubleBool桥接到NSNumber,其他大小的数字类型,例如Int8UInt16等只桥接为不透明对象。可变结构如CGRectCGPointCGSize也作为不透明对象桥接,即使大多数Cocoa API使用的是NSValue包装的实例。如果你看到一些类似unrecognized selector sent to _SwiftValue的错误,这表明Objective-C代码试图调用一个不透明的Swift值类型的方法,你可能需要手动包装该类的实例而不是使用Objective-C转换的类型实例。

还有一个特殊问题是Optionals。 Swift中的 Any可以保存任何东西,包括一个Optional,所以可以将一个可选类型的对象传递给Objective-C API,而不是首先检查它,即使API被声明为一个非空的id,很可能会造成涉及_SwiftValue的运行时错误,而不是编译时错误。 Xcode 8.1 beta中包含的Swift 3.0.1对Objective-C中的结构体和可选类型做了数字类型处理,以解决NSNumberNSValue和可选桥接中的上述限制:

* SE–0139: Bridge Numeric Types to NSNumber and Cocoa Structs to NSValue
* SE–0140: Warn when Optional converts to Any, and bridge Optional As Its Payload Or NSNull
(以上两篇是Github上swift-evolution中讲解Swift类型转换的文章,有兴趣的同学可以看看)

为了避免向前兼容性问题,你不应该依赖_SwiftValue类的不透明对象的实现,因为未来版本的Swift可能允许更多的Swift类型桥接到惯用的Objective-C类。

Linux可移植性

在Linux上使用Swift Core Swift运行的Swift程序库使用一个Swift本地编写的Foundation版本,没有Objective-C运行时桥接。id映射成Any允许Core Libraries直接使用本地Swift Any和标准库值类型,同时使用Objective-C Foundation实现保持与Apple平台上的代码兼容。由于Swift在Linux上不与Objective-C交互操作,因此不支持桥接转换,例如字符串为NSString或值为AnyObject。希望在Cocoa和Swift Core Libraries之间移植的Swift代码应该只使用值类型。

学习更多

id映射成Any是Swift语言改进的一个很好的例子,受到用户对早期版本的Swift的反馈的启发,并通过来自开放的Swift Evolution过程的回顾完善。如果你想更多地了解id映射Any背后的动机和设计决策,原始的Swift Evolution提议可以在GitHub的swift-evolution仓库中找到:

* SE-0072: Fully eliminate implicit bridging conversions from Swift
* SE–0116: Import Objective-C id as Swift Any type
* SE–0131: Add AnyHashable to the standard library

最后,Swift是一种更加一致的语言,当使用Swift时,Cocoa API变得更强大。
< 所有博客文章

以上就是苹果官方Objective-C id as Swift Any博客的全部内容,由于本人理解有限以及时间仓促导致译文中难免存在瑕疵,如果大家有发现欢迎在评论区留言指出,本人将在第一时间修改过来;喜欢我的文章,可以关注我以此促进交流学习; 如果觉得此文戳中了你的G点请随手点赞;转载请注明出处,谢谢支持。

    原文作者:老板娘来盘一血
    原文地址: https://www.jianshu.com/p/7759ac839b8c
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞