源码

Swift和Objective-C混编

翻译自苹果官方文档

和Objective-C交互

互用性是指,在Swift和Objective-C之间可以建立一个互通接口,不管是Swift生成接口给Objective-C对接,还是Objective-C生成接口给Swift对接。既然你决定开始用Swift来开发,那么有必要理解一下怎么运用互用性来重定义、提高、改进你写Cocoa app的方式。

互用性重要性之一是,在Swift中调用Objective-C的API。在你import一个Objective-C框架之后,你就可以用Swift的语法来实例化里面的类,继而使用它们。

初始化

要在Swift里初始化一个Objective-C类,需要用Swift的初始化语法来调Objective-C的初始化方法。

Objective-C初始化方法都以init开头,或者,如果有一个或多个参数,会以initWith:开头。在Swift文件里如果要调用Objective-C初始化方法,那么init前缀会变成Swift初始化方法。如果此时初始化方法带有参数,会去掉with,而其他参数会根据情况划分到各个参数中。

Objective-C初始化方法的声明:

- (instancetype)init;
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style;

转为Swift的初始化声明:

init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }

实例化对象的过程,更能看出Objective-C和Swift语法的不同:

Objective-C:

UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];

Swift:

let myTableView: UITableView = UITableView(frame: .zero, style: .grouped)

不用调用alloc,Swift替你处理了。还有,调用Swift风格的初始化函数,不会到处出现init。

当给变量或者常量赋值的时候,你可以指明一个类型,或者可以不指明这个类型,让Swift根据初始化方法自动推导出类型。

let myTextField = UITextField(frame: CGRect(x: 0.0, y: 0.0, width: 200.0, height: 40.0))

这里的UITableView和UITextField和你在Objective-C里实例化出来的对象是一样的,Objective-C里怎么用,这里就怎么用,根据各自的类型,获取属性、调用方法都一样。

类工厂方法和方便初始化方法

为了保持一致性和简单,Objective-C的类工厂方法引入Swift后,会改为方便初始化方法。这样,使用这些方法就像使用初始化方法一样。

例如,下面这个就是Objective-C中的一个工厂方法:

UIColor *color = [UIColor colorWithRed:0.5 green:0.0 blue:0.5 alpha:1.0];

在Swift中,要这样调用:

let color = UIColor(red: 0.5, green: 0.0, blue: 0.5, alpha: 1.0)

可失败的初始化

Objective-C中初始化函数直接返回初始化的结果,如果初始化失败,就会直接返回nil。在Swift中,这个模式由语言的一个特性来实现,叫可失败的初始化(原文:failable initialization)。

很多系统框架里的初始化方法都有标识初始化能不能失败。在你自己的Objective-C类里可以使用可空性(原文:nullability)来标识是不是能失败,Nullability and Optionals里有描述。在Objective-C里不可失败的话,在Swift里是init(...),可失败的话,用init?(...)。否则,都用init!(...)。

例如,UIImage(contentsOfFile:)初始化方法,如果提供的路径下面没有图片文件,就会初始化失败。如果可失败初始化函数初始化成功了,你可以用可选绑定去解包返回的结果。(译者注:public init?(contentsOfFile path: String))

if let image = UIImage(contentsOfFile: "MyImage.png") {
    // loaded the image successfully
} else {
    // could not load the image
}

获取属性

在Objective-C中用@property声明的属性,Swift会做下面的处理:

已经用nonnull、nullable和 null_resettable修饰的属性,按照Nullability and Optionals说的,转成可选和非可选属性。

readonly属性会转化为Swift中的计算型属性,也即只有一个getter方法({ get })。

weak属性转化为Swift中的weak属性(weak var)。

对于不是weak的所有者属性(原文:ownership property)(即assign、 copy、strong、 unsafe_unretained)都会转成相对应的存储属性。

class修饰的属性(译者注:Objective-C在Xcode9引入了一个新的属性class,应该就是为了对接Swift吧),转为在Swift中类型属性。

Objective-C中的原子性修饰符(atomic和nonatomic)在Swift中没有对应的修饰符,但是原子性所提供的功能在Swift里是依然存在的。(译者注:Atomicity property attributes (atomic and nonatomic) are not reflected in the corresponding Swift property declaration, but the atomicity guarantees of the Objective-C implementation still hold when the imported property is accessed from Swift.这句翻译的不是太好,请指教!)

存取属性(getter=和setter=)在Swift被忽略掉。(译者注:今天才知道getter=和setter=也是属性)

在Swift中,获取Objective-C 对象的属性值,用点语法直接带上属性名字,不需要括号。

例如,设置UITextField的textColor和text属性:

myTextField.textColor = .darkGray
myTextField.text = "Hello world"

Objective-C中用点语法调用无参数有返回值的方法,形式和获取属性很像(译者注:方法除了点,还有括号,其实并不一样)。虽然调用形式一样,方法在Swift中还是会转换为方法,只有Objective-C中用@property声明的变量才能转为Swift中的属性。关于方法的引入和调用可以参考方法的使用

方法的使用

在Swift中,用点语法来调用Objective-C方法。

Objective-C方法引入到Swift后,方法的第一部分,变成方法名,在括号前面。第一个参数在括号里,没有参数名。剩下的参数对应各自的参数名排在括号里。方法的所有组成部分都是需要的,少了任何部分,调用地址就是不对的。

例如Objective-C中:

[myTableView insertSubview:mySubview atIndex:2];

在Swift中,会是这样:

init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }

0

调用一个没有参数的方法,依然要带上括号。

init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }

1

id兼容

Objective-C中的id类型会转为Swift中的Any类型。在编译时和运行时,将Swift的值或者对象(译者注:Swift里引入了结构体,所以不光是类引用类型,也包括结构体值类型,所以,总是出现“值或者对象”)传给Objective-C的类型为id参数时,编译器会去执行一个全局桥接转换操作(原文:universal bridging conversion operation)。当将一个Objective-C中的id传给Swift的Any参数时,运行时会自动桥接回Swift的类引用或者值类型。(译者注:换句话说就是,id和Any可以转换,并且转换由系统完成)

init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }

2

(译者注:这个例子代码说明啥?不是特别清楚,求指教!)

向下转换Any(译者注:从一个比较宽的类型,转化为一个比较具体的类型)

如果知道Any里实际是什么类型,那么将其向下转化到一个更具体的类型比较有用。由于Any类型可以包含任意类型,所以,向下转型到一个更具体的类型不一定都能成功。

可以试试可选类型转化操作符(as?),返回一个包裹着转化结果的可选值:

init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }

3

如果你能确定对象的类型,那么可以用强制向下类型转换(as!)。

init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }

4

如果强制向下转换失败,会触发一个运行时错误:

init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }

5

动态方法查找

Swift还有一个AnyObject类型,用来表示某些对象类型(译者注:和Any不同的是,Any还可以存储值类型),并且这个类型还有特别的能力,可以将@objc修饰的方法转为动态查找。通过这个特性,对Objective-C返回的id类型的值可以更好的操作与维护。(译者注:AnyObject能力比较强大,因为Swift是强类型,不指定具体类型是不能调用方法的,但是AnyObject不一样,可以调用任意方法。如果一个对象的方法或者属性标记为@objc,那么这些方法就变成可以动态查找的方法。因为Objective-C的方法是采用动态查找实现的,所以,只有这样,这些方法才能提供给Objective-C使用。@objc后面有详细叙述)

例如,可以给AnyObject类型的常量或者变量赋值任意的对象,这个变量又可以再赋值一个不同类型的对象。你不需要转换为具体的类型,直接通过AnyObject类型就可以调用Objective-C具体类的方法和属性。

init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }

6

不能识别的方法和可选链

因为直到运行时才能知道AnyObject的值到底是什么类型,所以,就有可能写出不安全的代码。无论是Swift还是Objective-C,试图去调用一个不存在的方法都会触发一个找不到方法(译者注:unrecognized selector)的错误。

例如,下面的代码不会有编译时的警告,但是在运行时会报错:

init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }

7

Swift可以用可选的方式来避免不安全的行为。调用AnyObject类型的方法时,实际进行了一个隐式解包。可以用可选链语法来调用AnyObject类型的方法。

例如下面的代码,第一行和第二行不会执行,因为NSDate对象没有count属性和character(at:)方法。常量myCount会被推导为可选Int类型,值为nil。可以用if let语句来解包方法返回的结果,如第三行所示。

init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }

8

注意

虽然Swift没有强制要求,AnyObject类型的值调用方法时,一定要解包,但还是推荐解包,以此来避免不可预知的行为。(译者注:Although Swift does not require forced unwrapping when calling methods on values of type AnyObject, it is recommended as a way to safeguard against unexpected behavior.不是太理解,求指教!)

可空和可选

Objective-C中,通过原始指针(原文:raw pointer)来引用对象,指针可能是NULL(在Objective-C中是nil)。在Swift中,所有的结构体类型,或者对象类型都不会为空(译者注:不会直接是nil,而是一个可选值)。如果要表示这个值可以为空,那么要将这个值包装为可选类型。关于可选值的更多信息,请看Swift编程语言(Swift 4.0.3)中可选值。

Objective-C中可以用可空性标识来表示参数、属性、返回值是不是可以为NULL或者nil。单个的类型声明,我们可以用_Nullable和_Nonnull标识,单个的属性声明可以用nullable, nonnull和null_resettable标识,或者用宏NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END来标识整个区域的可空性。如果没有给一个类型设置可空性修饰,Swift无法区分是可选还是非可选,那么统一设置为隐式解包可选类型。(译者注:对于隐式解包可选类型加个说明,隐式解包的可选类型主要用在一个变量/常量在定义瞬间完成之后值一定会存在的情况,在使用时,不需要解包,直接使用,系统自动解包)

  • 用_Nonnull标识,或者在整个非空宏区域里的类型,声明为不可空,在Swift里都转为不可选。

  • 声明为_Nullable的可空类型,在Swift里转为可选类型。

  • 没有任何可空性标识的类型,在Swift里都转为隐式解包可选类型。

例如,下面的Objective-C声明:

init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }

9

下面是在Swift中的样子:

UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];

0

大部分的系统框架,包括Foundation,都有可空性标识,所以在和这些值打交道时,可以保持类型安全和语言的习惯

桥接可选类型到非空对象

Swift将可选值桥接为Objective-C的非空对象,如果可选值里有值,那么把这个值传递给Objective-C对象,如果可选值是nil,那么会传递一个NSNull实例给Objective-C对象。例如,可以将一个Swift可选类型直接传入Objective-C接受非空id参数的API,也可以将包含可选项的数组([T?])桥接为NSArray。

下面的代码可以看出,怎么根据实际所包含的值来桥接String?实例到Objective-C中。

UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];

1

valueFromSwift参数类型是id,所以在Swift中要传入Any类型,如下所示。一般情况下,给参数为Any函数传入一个可选类型并不普遍,所以,需要将传入logSomeValue(_:)类方法的可选值进行显式的转换为Any类型,避免编译器警告。

UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];

2

协议限定类(Protocol-Qualified Classes)

实现了一个或者多个协议的Objective-C类,在Swift中会转换成协议组合类型。例如以下的Objective-C中定义的view controller属性

UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];

3

Swift中会是这样:

UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];

4

Objective-C中协议限定元类(protocol-qualified metaclasses),在Swift中转换为协议元类型(protocol metatypes)。例如以下Objective-C方法对特定的类执行操作(原文:Objective-C protocol-qualified metaclasses are imported by Swift as protocol metatypes. For example, given the following Objective-C method that performs an operation on the specified class):

UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];

5

Swift中的样子:

UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];

6

轻量级泛型

用轻量级泛型参数化(译者注:generic parameterization泛型参数化,意思是,给泛型指定了类型,也即尖括号里有具体的类型。所以后面说到的参数化,都是指已经给泛型指定了具体的类型)。声明的Objective-C类型,内容的类型信息在Swift会保留下来。例如下面的Objective-C属性声明:(译者注:Objective-C里面的泛型实际是Xcode7引入的一个语法糖,称为轻量级泛型。Stack Overflow上有这个讨论)

UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];

7

Swift中会是这样:

UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];

8

一个Objective-C中参数化的类,引入Swift后,转换为泛型类,有同样数量的类型参数(译者注:type parameters类型参数,这里的意思是,泛型尖括号里的类型)。所有Objective-C中的泛型类型参数,在Swift中,会将类型参数转换为像(T: Any)类似的类的形式。如果Objective-C里泛型参数化指定了一个限制类(译者注:class qualification限制类,意思是,这个类起到了一个限制的作用,下面说道的protocol qualification协议限制,也是一样的理解,表示,这个协议对这个泛型起到一个限制作用),在Swift中也会转换这个限制:这个类必须是这个限制类的子类。如果Objective-C离泛型参数化指定了一个限制协议,在Swift中也会转换这个限制:这个类也必须遵守指定的协议。对于没有任何限制的Objective-C类型,Swift会推导给出限制,如下,Objective-C类和类别的声明:

UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];

9

(0)

本文由 投稿者 创作,文章地址:https://blog.isoyu.com/archives/swiftheobjective-chunbian.html
采用知识共享署名4.0 国际许可协议进行许可。除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。最后编辑时间为:5月 21, 2018 at 08:24 下午

热评文章

发表回复

[必填]

我是人?

提交后请等待三秒以免造成未提交成功和重复