互操作性
与Objective-C API交互
互操作性让Swift与Objective-C双向互为接口的能力,使你能在一种语言中访问并使用另一个文件中用另一种语言编写的代码。在开始把Swift整合进开发工作流之前,先理解如何利用互操作性来改善、改进和增强Cocoa应用的开发方式是个好主意。
关于互操作性的一个重要的方面是它能让你在编写Swift代码时使用Objective-C的API。导入Objective-C框架之后,你就可以用原生的Swift语法初始化其中的类并与之交互。
初始化
要在Swift中初始化一个Objective-C的类,你可以用Swift的语法调用它的某个构造方法。
Objective-C的构造方法以init
开头,如果构造方法接受一个或多个参数则是以initWith:
开头。当Objective-C的构造方法被导入Swift的时候,init
前缀变为init
关键词,表示此方法是Swift的构造方法。如果构造方法接受参数,那么With
会被去掉,方法选择器(selector)的其他部分会被分开成为相应的命名参数。
比如下面的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: CGRectZero, style: .Grouped)
注意,你无需调用alloc
;Swift会帮你正确处理。另外需要注意的是,在调用Swift风格的构造方法时,任何位置都没有出现“init”。
你可以在为常量或变量赋值的时候,显示的指定类型;你也可以省略类型声明,让Swift根据构造方法推断(infer)出类型。
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类的工厂方法以快捷构造方法(convenient initializer)的形式导入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中,构造方法直接返回它们初始化的对象。要在初始化失败时告知调用者,Objective-C的构造方法会返回nil
。在Swift中,这个模式成为一种语言特性,叫做“可失败构造方法”(failable initializer)。
系统框架中的很多Objective-C构造方法已经被修改为可以指示初始化是否会失败。你也可以在你自己的Objective-C类中使用在空值和可选类型章节中介绍的可空性标记(nullability annotions)指示构造方法是否会初始化失败。根据构造方法是否会初始化失败的指示,Objective-C的构造方法会被导入为init(...)
—如果不会初始化失败,或者init?(...)
—如果可能会初始化失败。否则,Objective-C的构造方法会被导入为init!(...)
。
例如,UIImage(contentOfFile:)
构造方法在初始化UIImage
对象的时候,如果给定的路径下图片文件不存在,就会初始化失败。你可以使用可选类型绑定(optional binding)在初始化成功时提取可失败构造方法的结果。
if let image = UIImage(contentOfFile: "MyImage.png") {
// 图片载入成功
} else {
// 无法载入图片
}
访问属性
用@property
语法声明的Objective-C属性,以如下几种方式导入为Swift属性:
- 包含空值指示参数(
nonnull
,nullable
和null_resettable
)的属性,被导入为对应的可选类型或非可选类型的Swift属性,详见空值和可选类型。 - 包含
readonly
(只读)参数的属性,被导入为只包含取值方法(gette)({getter}
)的Swift计算属性(computed property)。 - 包含
weak
(弱)参数的属性,被导入为用weak
关键词标注(weak var
)的Swift属性。 - 包含除
weak
外的其他所属关系参数(也就是assign
,copy
,strong
或unsafe_unretained
)的属性,被导入为适当存储的Swift属性。 - 属性的原子性参数(
atomic
和nonatomic
)不会反映在Swift参数中。但当导入的属性在Swift中访问时,Objective-C实现中保证的原子性会被保留。 - 属性的访问方法(accessor)参数(
getter=
和setter=
)在Swift中被忽略。
在Swift中访问Objective-C对象的属性使用点语法(dot syntax),属性名后面无需加括号。
例如,你可以用如下代码为UITextField
对象的textColor
和text
属性赋值:
myTextField.textColor = UIColor.darkGrayColor()
myTextField.text = "Hello world"
提示
darkGrayColor()
的后面是包含括号的,因为darkGrayColor()
是UIColor
的类方法,不是一个属性。
Objective-C中,不包含参数,但是有返回值的方法可以像Objective-C属性一样用点语法调用。但是这些方法是作为实例方法导入Swift的,只有使用@property
声明的Objective-C属性才会被导入为Swift属性。方法的导入和调用在方法的使用章节介绍。
方法的使用
你可以使用点语法在Swift中调用Objective-C方法。
当Objective-C方法导入Swift之后,Objective-C方法选择器(selector)的第一部分变成Swift的方法名,出现在括号外面。第一个参数紧随开括号出现,是没有参数名的。方法选择器的其他部分分别作为对应命名参数出现在括号中。在调用时,所有的参数都是必须的。
例如,在Objective-C中这样的代码:
[myTableView insertSubview: mySubview atIndexPath: 2];
在Swift中,是这样的:
myTableView.insertSubview(mySubView, atIndexPath: 2)
即使你调用的方法没有参数,你依然需要在最后包含一个空括号。
myTableView.layoutIfNeeded()
id的兼容性
Swift有一种AnyObject
类型,用来代表某个对象。它和Objective-C中的id
类型很相似。Swift把id
导入为AnyObject
,使你能够在编写类型安全的Swift代码的同时,保留不确定类型对象(untyped object)的灵活性。
例如,像id
一样,你可以为AnyObject
类型的常量或变量赋任何类型的值。你还可以为变量重新赋一个另一种类型值。
var myObject : AnyObject = UITableViewCell()
myObject = NSDate()
你也可以无需进行类型转化,就能对AnyObject
类型的值调用任何Objective-C方法,访问任何属性。包括使用@objc
注解(attribute)的兼容Objective-C的方法。
(译者按:本书中的annotation都被翻译成注解。Swift和Objective-C的注解与Java的注解看起来有点类似,但是区别也非常大,请注意分别。另外,在适当的时候,部分名字以@
开头的attributes也被翻译成注解。)
let futureDate = myObject.dateByAddingTimeInterval(10)
let timeSinceNow = myObject.timeIntervalSinceNow
无法识别的方法选择器和可选调用链
因为声明为AnyObject
的对象的类型要到运行时才被确定,所以可能会导致你不经意地编写了不安全的代码。不论是在Swift中还是在Objective-C中,试图调用一个不存在的方法,都会触发“无法识别的方法选择器”(unrecognized selector)错误。
例如,下面这行代码能够顺利编译通过,但是在运行时会触发“无法识别的方法选择器”(unrecognized selector)错误:
myObject.charactorAtIndex(5)
// 程序崩溃,myObject不能执行这个方法
Swift使用可选类型来应对这种不安全的行为。当你对AnyObject
类型的值调用方法的时候,方法调用的行为与隐式解包可选类型(implicit unwrapped optionals)差不多。你可以用像在调用协议中的可选方法时使用的可选调用链(Optional Chaining)一样,来对AnyObject
对象可选地触发一个方法。
提示
访问
AnyObject
对象的属性,总是返回可选类型值。如果属性本来就是返回一个可选类型,那么它的返回值就回编程一个双重包装的可选类型(doubly-wrapped optional type),例如AnyObject?!
。
例如,在下面列出的代码中,第一行和第二行没有被执行,因为count
属性和characterAtIndex:
方法在NSDate
对象中不存在。myCount
常量被推断为可选Int
类型,然后被赋值为nil
。你也可以使用if
-let
语句来有条件地提取对象可能无法响应的方法的执行结果,就像第三行代码展示的那样。
// myObject是AnyObject类型,包含了一个NSDate值
let myCount = myObject.count
// myCount是Int?类型,值为nil
let myChar = myObject.characterAtIndex?(5)
// myChar是unichar?类型,值为nil
if let fifthCharacter = myObject.characterAtIndex?(5) {
print("Found \(fifthCharacter) at Index 5")
}
// 条件分支没有被执行
提示
尽管Swift在对类型为
AnyObject
的值调用方法时,并不会要求强制解包,不过这是安全应对未知行为的途径之一。
AnyObject向下类型转换
当AnyObject
对象的底层类型已经知道,或可以被推断出来时,我们通常会把它向下转换为更加特定的类型。但是因为AnyObject
可能代表任何类型的对象,类型转换并不能保证成功。
你可以使用条件类型转换操作符(as?
)来进行转换操作。它将返回目标类型的可选值(optional value):
let userDefaults = NSUserDefaults.standardUserDefaults()
let lastRefreshDate : AnyObject? = userDefaults.objectForKey("LastRefreshDate")
if let date = lastRefreshDate as? Date {
print("\(date.timeIntervalSinceReferenceDate)")
}
如果你确知对象的类型,你也可以使用强制类型转换操作符(as!
)。
let myDate = lastReferenceDate as! NSDate
let timeInterval = myDate.timeIntervalSinceReferenceDate
不过,如果强制类型转换失败了,将会触发一个运行时错误:
let myDate = lastReferenceDate as! NSString // 错误
空值和可选类型
(译者注:nullability这里翻译为空值,主要是为了不发明大家看不懂的新词。nullability的直译的话,意为“可否为空值的一种能力”。后文中nullbility将不会使用直译,我会根据上下文环境尽量避免晦涩,多用意译。)
在Objective-C中,你使用原生的指针来操作可能为NULL
(在Objective-C中被称为nil
)的引用。在Swift中,所有的值—包括结构体和对象的引用—总是为非空的。取而代之,你需要将可能为空的值封装到一个可选类型(optional type)中。当你需要表示值不存在的时候,你可以使用nil
。要了解更多关于可选类型的信息,你可以参阅“Swift编程语言(Swift 2.2版)”的“可选类型”章节。
Objective-C可以使用空值标记(nullability annotation)来标识参数、属性或返回值能否包含NULL
或nil
。单独的类型声明可以使用_Nullable
和_Nonnull
标记,单独的属性声明可以使用nullable
,nonnull
和null_resettable
标记,或者你也可以使用NS_ASSUME_NONNULL_BEGIN
和NS_ASSUME_NONNULL_END
宏把一段代码标记成不为空(nonnull)。如果类型没有空值标记信息,Swift就无法区分可选和非可选的引用了,它们将被作为隐式解包可选类型(implicit unwrapped optional)被导入。
- 无论是用
_Nonnull
标记或者包围在非空宏被声明为不为空的类型,都作为非可选类型导入Swift。 - 使用
_Nullable
标记为可空(nullable)的类型,作为可选类型导入Swift。 - 没有使用空值标记的类型,作为隐式解包可选类型导入Swift。
例如,来看看的这段Objective-C声明:
@property (nullable) id nullableProperty;
@property (nonnull) id nonNullProperty;
@property id unannotatedProperty;
#NSASSUME_NONNULL_BEGIN
- (id)returnsNonNullValue;
- (void)takesNonNullParameter:(id)value;
#NSASSUME_NONNULL_END
- (nullable id) returnsNullableValue;
- (void) takesNullableParameter:(nullable id)value;
- (id)returnsUnannotatedValue;
- (void)takesUnannotatedParameter:(id)value;
它们是这样被导入Swift的:
var nullableProperty : AnyObject?
var nonNullProperty : AnyObject
var unannotatedProperty : AnyObject!
func returnsNonNullValue() -> AnyObject
func takesNonNullParameter(value: AnyObject)
func returnsNullableValue() -> AnyObject?
func takesNullableParameter(value: AnyObject?)
func returnsUnannotatedValue() -> AnyObject!
func takesUnannotatedParameter(value: AnyObject!)
大部分Objective-C系统框架,包括Foundation,都已经包含了空值标记,你可以用习惯的方法安全地处理各类值。
轻量级泛型
Objective-C中,使用轻量级泛型进行参数化的声明的NSArray
,`NSSet
和NSDictionary
类型,在导入Swift的时候,参数的类型信息会被保留。
例如,下面的这段Objective-C的属性声明:
@property NSArray<NSDate *> *dates;
@property NSSet<NSString *> *words;
@property NSDictionary<NSURL *, NSData *> *cachedData;
是这样导入Swift的:
var dates : [NSDate]
var words : Set<String>
var cachedData : [NSURL : NSData]
提示
除了这些Foundation框架中的集合类之外,Objective-C的轻量级泛型会被Swift忽略。其他任何使用了轻量级泛型的类型在导入Swift的时候,都会被去参数化。
扩展
Swift的扩展(Extension)与Objective-C的扩展(Category)类似。扩展可以为已有的类、结构体和枚举,甚至是那些在Objective-C中定义的类型,增加新的行为。无论是系统框架中定义的类,还是自定义类型,你都可以为它们定义扩展。你只需导入适当的模块,用类、结构体或枚举的名字来使用它们,就像在Objective-C中一样。
比如,你可以像下面这样扩展UIBezierPath
类,利用边长和起始点来创建一个简单的等边三角形贝塞尔曲线。
extension UIBezierPath {
convenience init(triangleSideLength: CGFloat, origin: CGPoint) {
self.init()
let squareRoot = CGFloat(sqrt(3.0))
let altitude = (squareRoot * triangleSideLegth) / 2
moveToPoint(origin)
addLineToPoint(CGPoint(x: origin.x + triangleSideLength, y: origin,y))
addLineToPoint(CGPoint(x: origin.x + triangleSideLength / 2, y: origin.y + altitude))
closePath()
}
}
你也可以使用扩展增加属性(包括类属性和静态属性)。不过这些属性必须是计算属性;扩展无法为类、结构体或枚举增加带存储的属性。
下面这个扩展的例子展示了如何为CGRect
结构体增加一个计算属性area
:
extension CGRect {
var area : CGFloat {
return width * height
}
}
let rect = CGRect(x: 0.0, y: 0.0, width: 10.0, height: 50.0)
let area = rect.area
你还可以使用扩展来增加协议支持,而无需创建子类。如果协议是在Swift中定义的,那么不管结构体和枚举是在Swift还是在Objective-C中定义的,你都可以通过扩展让它们支持该协议。
你不能使用扩展来覆盖Objective-C类型的已有的方法或属性。
闭包
用@convention(block)
注解标记的Objective-C的块(block)在导入为Swift闭包时,使用Objective-C的调用惯例。例如,下面的这个Objective-C的块变量:
void (^completionBlock)(NSData *, NSError *) = ^(NSData *data, NSError *error) {
// ...
}
导入Swift后,它是这样的:
let completionBlock: (NSData, NSError) -> Void = { (data, error) in
// ...
}
Swift的闭包与Objective-C的块是兼容的,你可以给接受块参数的Objective-C方法传一个Swift的闭包。如果Swift的闭包和函数有相同的类型签名,你甚至可以直接传一个Swift函数名。
闭包与块有着相似的值捕获语义(capture semantics),但是有一个关键的地方不一样:变量是可以被更改的,而不是被复制的。换句话说,Objective-C中,需要使用__block
标记的变量的行为,在Swift中是默认行为。
捕获self的时候避免强引用循环
在Objective-C中,如果你需要在块中捕获self
,你需要考量一下内存管理机制。
块会对捕获的对象,包括self
,保持强引用。如果self
也对块保持了强引用,比如复制属性,这样就会造成强引用循环。要避免这个问题,你可以让块捕获self
的一个弱引用:
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
[strongSelf doSomething];
}
与Objective-C的块一样,Swift也会对捕获的任何对象,包括self
,保持一个强引用。要避免强引用循环,你需要在闭包的捕获列表中,把self
标记为unowned
:
self.closure = ^{ [unowned self] in
self.doSomething()
}
要了解更多信息,请参考“Swift编程语言(Swift 2.2版)”的“解决闭包的强引用循环”章节。
对象比较
在比较两个对象的时候,Swift中有两种不同的比较方法。第一种,是相等(==
),比较的是两个对象的内容。第二种,是相同(===
),比较的是常量或变量对象是否为同一个实例。
派生自NSObject
类的对象,Swift为之提供了默认的==
和===
操作符实现,并且接受Equatable
协议。默认的==
操作符实现是调用isEqual:
方法。而默认的===
实现则是检查指针是否相同。你不应该覆盖导入自Objective-C的类型的比较相等或相同的操作符。
NSObject
类中定义的isEqual:
的基本实现时指针相等检查。你可以在子类中覆盖isEqual:
方法,让Swift和Objective-C的API能够基于对象包含的内容而不失对象的身分来进行相等性比较。要了解更多关于如何实现比较的逻辑,请参考Cocoa Core Competencies的对象比较部分。
提示
Swift会为相等操作符自动创建对应的不想等操作符(
!=
和!==
)。你不应该覆盖着两个操作符。
哈希
Swift为派生自NSObject
类的对象提供了符合Hashable
协议所需的hashValue
属性的默认实现。hashValue
属性的默认实现是调用hash
属性。
提供了自定义isEqual
方法的NSObject
的子类也必须提供它们自己的hash
属性实现。
Swift类型的兼容性
当你创建了一个派生自Objective-C类的Swift类,这个类以及它的与Objective-C兼容的成员—属性、方法、下标(subscript,译者注:也就是从一组值中用索引取值的方法)以及构造方法等—会自动对Objective-C可用。但是并不支持Swift特有的特性,例如下面列出的这些:
- 泛型
- 元组
- 在Swift中定义,且原始值类型不为
Int
的枚举类型 - 在Swift中定义的结构体
- 在Swift中定义的顶层方法
- 在Swift中定义的全局变量
- 在Swift中定义的类型别名
- Swift风格的可变参数
- 嵌套类型
- 柯里化函数(译者按:Curried functions的翻译参考了维基百科)
把Swift的API翻译成Objective-C与把Objective-C的API翻译成Swift时很类似,只是过程相反:
- Swift中的可选类型被标记为
__nullable
。 - Swift中的非可选类型被标记为
__nonnull
。 - Swift中的常量存储属性和计算属性转换为Objective-C的只读属性。
- Swift中的变量存储属性转换为可读写的Objective-C属性。
- Swift中的类方法(type method)转换成Objective-C的类方法(class method)。
- Swift中的构造方法和实例方法转换成Objective-C的实例方法。
- Swift中可能抛出错误的方法转换成Objective-C中带有末尾
NSError **
参数并且在方法名中加上AndReturnError:
。如果Swift方法并没有指定返回类型,那么对应的Objective-C方法会包含BOOL
返回类型。
例如,下面这段Swift代码:
class Jukebox: NSObject {
var library: Set<String>
var nowPlaying: String?
var isCurrentlyPlaying: Bool {
return nowPlaying != nil
}
init(song: String...) {
self.library = Set<String>(song)
}
func playSong(named name: String) throws {
// 播放音乐或在音乐不可用时抛出错误
}
}
上述代码导入Objective-C时会转换成如下定义:
@interface Jukebox : NSObject
@property(nonatomic, copy) NSSet<NSString *> * __nonnull library;
@property(nonatomic, copy) NSString * __nullable nowPlaying;
@property(nonatomic, readonly) BOOL isCurrentlyPlaying;
- (nonnull instancetype)initWithSongs:(NSArray<NSString *> * __nonnull)songs OBJC_DESIGNATED_INITIALIZER;
- (BOOL)playSong:(NSString *__nonnull)name error:(NSError * __nullable * __null_unspecified)error;
@end
提示
你无法在Objective-C中创建Swift类的子类。
配置Swift暴露给Objective-C的接口
在某些情况下,你可能需要精细控制如何将Swift API暴露给Objective-C。你可以在Swift类中使用@objc
注解来修改暴露给Objective-C的接口中符号的名字(译者按:也就是属性名、方法名等)。
例如,如果Swift类的名字包含了Objective-C不支持的字符,你可以提供一个在Objective-C中使用的名字。如果你要为一个Swift方法提供一个Objective-C名字,你可以使用Objective-C的选择器(selector)语法。不要忘记在选择器的各个接受参数的位置后面加上冒号(:
)。
@objc(Squirrel)
class 松鼠 : NSObject {
@objc(initWithName:)
init(名字: String) {
// ...
}
@objc(hideNuts:inTree:)
func 收藏(几: Int, 个坚果到大树 大树: 树) {
// ...
}
}
当你对Swift的类使用@objc(
name)
注解的时候,这个类会在Objective-C中可用,并且不在任何名字空间内。因此,这个注解在你迁移一个可以存档(archivable)的Objective-C类到Swift的时候也很有用。因为可以存档的对象会保存它们的类名到存档文件(archive)中,你应该使用@objc(
name)
注解来指定与Objective-C类一样的名字,这样,旧的存档文件才能被新建的Swift类解包(unarchive)。
提示
相对的,Swift也可以用
@nonobjc
注解,让Swift中的声明在Objective-C中不可用。你可以用这个方法来解决桥接方法(bridging methods)的循环引用,这样就可以覆盖用@objc
注解标记的类中的方法。如果一个Objective-C的方法被Swift的方法覆盖,但该方法无法在Objective-C中表示,例如,通过指定一个参数是可变的,那个方法必须标记为@nonobjc
。
使用动态分配
当Swift API被Objective-C运行时导入时,它无法确保属性、方法、下标或构造器能被动态分配(dynamic dispatch)Swift编译器可能依然会去虚化(devirtualize)或内嵌(inline)访问成员,绕过Objective-C运行时,优化代码的性能。
你可以用dynamic
修饰符标记成员的声明,访问这个成员的时候,能保证它总是动态分配的。需要动态分配的场景很少。不过如果你知道某个API的实现会被运行时替换,那么你就必须使用dynamic
修饰符。例如,你可以使用Objective-C运行时环境的method_exchangeImplementations
方法在程序运行时替换到一个方法的实现。如果Swift编译器内嵌了方法的实现,或者把它的访问去虚化了,那么新的实现将不被使用。
提示
使用
dynamic
修饰符标记的声明无法同时使用@nonobjc
注解。
Objective-C的方法选择器
Objective-C的方法选择器是一种可以引用Objective-C方法名的类型。在Swift中,Objective-C的方法选择器用Selector
结构体表示。你可以用一个字符串来构造一个方法选择器,例如let mySelector: Selector = "tappedButton:"
。因为字符串可以自动转换为方法选择器,所以你可以给所有接受方法选择器参数的方法传入字符串。
import UIKit
class MyViewController : UIViewController {
let myButton = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
override init?(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil, bundle: nilBundleOrNil)
myButton.addTarget(self, action: "tappedButton:", forControlEvents: .TouchUpInside)
}
func tappedButton(sender:UIButton) {
print("tapped button")
}
required init?(coder:NSCoder) {
super.init(coder: coder)
}
}
如果你的Swift类继承自Objective-C类,类中的所有的方法和属性都可以用作Objective-C的方法选择器。反之,如果你的Swift类并没有继承自Objective-C类,那么你需要使用@objc
注解标注你想用作方法选择器的成员,就像在Swift类型的兼容性中描述的那样。
用performSelector来发送消息
(译者注:Objective-C的发送消息,在其他语言中叫做方法调用,两者有所区别,但是基本作用相同。)
你可以使用performSelctor(_:)
及其变体向Objective-C兼容的对象发送消息。
performSelector
API可以向某个线程或在一段时间的延迟后发送的消息,是没有返回值的。同步执行的performSelector
API返回一个隐式解包的非托管的可选类型实例(Unmanaged<AnyObject>!
),因为执行方法选择器得到返回值的类型、所属关系在编译时无法知晓。你可以阅读非托管对象章节了解更多信息。
let string: NSString = "Hello, Cocoa!"
let selector: Selector = "lowercaseString"
if let result = string.performSelector(selector) {
print(result.takeUnretainedValue())
}
// 打印出 "hello, cocoa!"
向对象发送一个无法识别的方法选择器,会导致接收者去调用doesNotRecognizeSelector(_:)
,这个方法默认引发一个NSInvalidArgumentException
异常。
let array: NSArray = ["delta", "alpha", "zulu"]
let invalidSelecto: Selector = "invalid"
array.performSelector(invalidSelector) // 引发一个异常
在Objective-C运行时向对象直接发送消息在本质上是不安全的,因为编译器无法对发送消息得到的结果作保证,甚至无法保证对象可以接收某个消息。因此,通常是不鼓励使用performSelector
API的,除非你的代码对Objective-C运行时提供的动态方法解析有特殊需求。否则,先把一个对象转换成AnyObject
,然后使用可选调用链来调用方法会安全很多。详见id的兼容性。
编写具备Objective-C行为的Swift类
互操作性使你能定义具备Objective-C行为的Swift类。你可以在编写Swift类的时候继承Objective-C的类,接受Objective-C的协议,并利用其他Objective-C的功能。这意味着你可以创建基于你熟悉的、有着明确行为的Objective-C类创建子类,同时,你可以使用Swift的现代、强大的语言特征来增强它。
继承Objective-C的类
在Swift中,你可以定义Objective-C类的子类。要创建一个继承自Objective-C类的Swift类,你可以在Swift的类名后架上冒号(:
),再加上Objective-C类的名字。
import UIKit
class MySwiftViewController : UIViewController {
// 定义这个类
}
你可以获得用Objective-C定义的亲类的所有功能。如果你要为亲类中的同名方法提供你自己的实现,你需要使用override
关键字。
NSCoding
NSCoding
协议要求符合改协议的类型实现init(coder:)
构造方法。接受NSCoding
协议的类必须实现这个方法,该类的子类如果有一个以上自定义构造方法、或包含没有初始值的属性,那么它也必须接受NSCoding
协议。Xcode提供了下面这个自动修正(fix-it)功能,提供了一个默认实现:
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
对于从StoryBoard中加载的对象,或者用NSUserDefaults
或NSKeyedArchiver
存档至磁盘中的对象,你必须为这个构造函数提供一个完整的实现。不过,你可能不需要为那些无法或不会通过这种途径初始化的类型实现这个构造方法。
支持协议
Objective-C的协议会导入为Swift的协议,如果需要的话,类可以通过在其亲类名后,加上逗号分隔的协议名列表,以支持协议。
class MySwiftViewController : UIViewController, UITableViewDelegate, UITableViewDataSource {
// 定义这个类
}
在Swift代码中,要定义一个符合单个协议的类型,可以直接使用协议名作为其类型(对比Objective-C中的id<SomeProtocal>
)。要在Swift代码中定义一个符合多个协议的类型,可以使用复合协议(protocal composition),形如protocal<SomeProtocal, AnotherProtocal>
(对比Objective-C中的id<SomeProtocal, AnotherProtocal>
)。
var textFieldDelegate: UITableViewDelegate
var tableViewController: protocal<UITableViewDataSource, UITableViewDelegate>
提示
因为Swift中的类和协议使用同一个名字空间,因此,Objective-C中的
NSObject
协议在Swift中被映射为NSObjectProtocal
。
编写构造方法和析构方法
Swift的编译器会确保你的构造方法不会遗留任何没有初始化的属性以增强代码的安全性和可预测性。另外,与Objective-C不同的是,Swift不需要额外调用内存分配方法。即便你在使用Objective-C的类,你也只需要使用原生的Swift构造语法—Swift会把Objective-C的构造方法转换成Swift的构造方法。你可以阅读“Swift编程语言(Swift 2.2版)”的“构造方法”章节,了解更多关于如何实现你自己的构造方法的信息。
当你需要在对象被回收时做一些额外的清理工作,你可以实现一个析构方法,而不是使用dealloc
方法。Swift的析构方法会在实例被回收前自动调用。Swift会在触发子类的析构方法后,自动调用亲类的析构方法。当你操作的是Objective-C类,或者你的Swift类继承自Objective-C类,Swift一样也会调用该类的亲类的dealloc
方法。你可以阅读“Swift编程语言(Swift 2.2版)”的“析构方法”章节了解更多关于如何实现你自己的析构方法的信息。
在Objective-C API中使用Swift的类名
Swift的类是根据它们编译进的模块来确定名字空间的,即便是用于Objective-C的代码中时也一样。和所有的类都在全局名字空间中,因而不能包含两个同名类的Objective-C不一样,Swift的类可以通过它们所处的模块进行区分。例如,在名为MyFramework
的框架中,名为DataManager
的类的全名MyFramework.DataManager
。Swift应用的编译目标(Target)也是一个模块,因此在一个叫做MyGreatApp
的应用中,名为Observer
的类的全名为MyGreatApp.Observer
。
为了保留在Objective-C中使用的Swift类的名字空间,Swift的类暴露给Objective-C运行时环境的时候,暴露的是全名。因此,当你使用以字符串的形式操作Swift的类名的时候,你必须使用类的全名。例如,当你需要创建一个基于文档的Mac应用的时候,你需要在应用的Info.plist
文件中指定NSDocument
子类的类名。如果你是用Swift编写应用,那么你必须使用这个文档子类的全名,也就是要包含应用或框架的模块名。
在下面这个示例中,NSClassFromString
函数用来通过字符串形式的类名来获取类的引用。要获取一个Swift类,你就必须使用包含了应用名的类的全名:
let myPersonClass: AnyClass? = NSClassFromString("MyGreatApp.Person")
与Interface Builder整合
(译者注:Interface Builder作为一个Xcode的一个组件,我没有给出翻译。一方面翻译后听起来很别扭;另一方面,也不方便用户对照查找。Interface Builder字面上的意思“界面构建器”。)
Swift编译器支持能让你的Swift类与Interface Builder交互的注解。和在Objective-C中一样,你可以使用外连(outlets),动作(actions)和实时渲染。
(译者注:outlet还没有统一的翻译,这里暂时翻译成外连。后文中可能会根据情况,保留英文原文。)
使用外连和动作
外连和动作能把你的源代码和Interface Builder中的用户界面对象连接起来。要在Swift中使用外连和动作,你可以在属性和方法声明的前面分别加上@IBOutlet
和@IBAction
注解。@IBOutlet
也可以用来声明外连集合(outlet collection)—只需指定类型为数组。
当你在Swift中声明外连的时候,你需要把类型设置为隐式解包可选类型(implicit unwrapped optional)。这样,storyboard就能在初始化之后,通过运行时把外连和界面连接起来了。当你的类通过storyboard或xib文件初始化完成后,你就可以认为外连已经连接好了。
例如,下面这段Swift代码定义了一个包含有一个外连,一个外连集合和一个动作的类:
class MyViewController : UIViewController {
@IBOutlet weak var button: UIButton!
@IBOutlet var textFields: [UITextField]!
@IBAction func buttonTapped(_: AnyObject) {
print("button tapped!")
}
}
因为sender
参数在buttonTapped
方法中没有用到,因此参数名可以忽略。
实时渲染
你可以使用另外两个注解—@IBDesignable
和@IBInspectable
—来使用实时渲染,在Interface Builder中与自定义视图(View)进行交互。当你创建了一个派生自UIView
或NSView
类的自定义视图的时候,你就可以在类声明的前面加上@IBDesignable
注解。当你把这个自定义视图加入Interface Builder中之后(通过在inspector面板中,设置视图对应的自定义类),Interface Builder就会在画板(Canvas)上对你的视图进行实时渲染。
你还可以对支持自定义的属性使用@IBInspectable
注解。在你把自定义视图加入Interface Builder中之后,你就能在Inspector面板中编辑这些属性了。
@IBDesignable
class MyCustomView: UIView {
@IBInspectable var textColor: UIColor
@IBInspectable iconHeight: CGFloat
// ...
}
设置属性的参数
(译者注:注意区分属性的参数(attributes)区别与方法的参数(attributes))
在Objective-C中,属性有一系列可以指定关于属性行为的额外信息的参数。在Swift中,你用不同的方式来指定这些属性参数。
强引用和弱引用
Swift的属性默认是强引用。你需要使用weak
关键词来表明属性指向的存储属性值的对象使用的是弱引用。这个关键词只能被用于可选类型的属性。详情请参阅“Swift编程语言(Swift 2.2版)”的“参数”(Attributes)章节。
读写与只读
在Swift中,没有readwrite
和readonly
参数。当声明存储属性时,使用let
来使之成为只读,使用var
来使之可以读写。当声明计算属性的时候,只提供取值方法(getter)使之成为只读,同时提供取值方法(getter)和赋值方法(setter)使之可以读写。要了解更多信息,请查看“Swift编程语言(Swift 2.2版)”的“属性”章节。
复制语义
在Swift中,Objective-C的copy
属性参数被转译为@NSCopying
。属性的类型需要复合NSCopying
协议。要了解更多信息,请查看“Swift编程语言(Swift 2.2版)”的“参数”(Attributes)章节。
实现Core Data的托管对象子类
(译者注:这里Managed Object翻译成托管对象。如果你学过.NET,请不要把这里的托管对象与.NET里的托管对象混淆,两者概念不一样。)
Core Data为NSManagedObject
的子类提供了属性的底层存储和实现的支持。Core Data还提供了在对多关系(to-many relationship)中增加和删除对象的实例方法的实现。你可以使用@NSManaged
注解来告诉Swift编译器,Core Data会在运行时提供存储和实现。
你需要在你的托管类的子类中,为所有与Core Data模型相关的属性和关系有关的属性和方法声明加上@NSManaged
注解。比如,假设有一个名为“Person”的Core Data实体(Entity)有一个字符串属性“name”,以及一个对多的关系“friends”:
以下是NSManagedObject
子类的Person
对应的Swift代码:
import CoreData
class Person : NSManagedObject {
@NSManaged var name: String
@NSManaged var friends: NSSet
@NSManaged func addFriendsObject(friend: Person)
@NSManaged func removeFriendsObject(friend: Person)
@NSManaged func addFriends(friends: NSSet)
@NSManaged func addFriends(friends: NSSet)
}
name
和friends
属性声明都使用了@NSManaged
注解,用来指示Core Data在运行时为它们提供了存储和实现。因为friends
属性是对多关系,Core Data还提供了一系列对应的访问方法实现。
要把用Swift编写的NSManagedObject
子类用于Core Data的模型实体,你需要打开Xcode的模型实体检查面板,在Class文本框中输入类名,并从Module下拉列表中选择“Current Product Module”。
使用Cocoa数据类型
作为与Objective-C互操作性功能的一部分,Swift提供了便利而高效的利用Cocoa数据类型的方法。
Swift会自动把部分Objective-C的数据类型转换成Swift的数据类型、把部分Swift数据类型转换成Objective-C数据类型。有些数据类型在Swift和Objective-C中时可以通用的。可以转换或通用的数据类型,我们称之为桥接(bridged)类型。例如,在Swift代码中,你可以把Array
类型的值传给一个接受NSArray
对象的方法。你也可以在桥接的类型之间进行类型转换。当你在桥接的类型之间进行转换时—用as
或直接为常量或变量显式地指定类型—Swift会自动对类型进行桥接。
Swift还为Foundation数据类型提供了快捷接口层,让这些数据类型的使用语法,感觉起来与Swift语言的其它部分更加自然、统一地结合。
字符串
Swift会自动桥接String
类型和NSString
类。这意味着如果你在任何地方需要使用NSString
,那么你也可以使用String
类型替代。它同时具有String
类型的字符串变量插入(interpolation)功能和其它的Swift字符串API,以及NSString
类提供的大量功能。因此,你几乎永远都不需要在你的Swift代码中直接使用NSString
类。事实上,在Swift导入Objective-C的API时,会把所有的NSString
类型替换为String
类型。当你的Objective-C代码导入Swift类的时候,在导入的API中,所有的String
类型都会被替换为NSString
类型。
要使用字符串桥接,你只需要导入Foundation模块。比如,你可以访问Swift字符串的capitalizedString
属性—这是来自NSString
类的属性。Swift会自动桥接String
到NSString
对象以访问这个属性。这个属性甚至会直接返回Swift的String
类,因为它在导入的过程中已经进行了类型转换。
import Foundation
let greeting = "hello, world!"
let capitalizedGreeting = greeting.capitalizedString
// capitalizedString: String = Hello, World!
如果你确实需要使用NSString
对象,你可以在需要的时候通过类型转换把它转成Swift的String
类型值。因为String
类型值随时都可以由NSString
对象转换而成,因此在进行类型转换时无需使用可选类型转换操作符(as?
)。你也可以显式地指定变量或常量的类型,并为它赋一个字符串字面量(string literal)来创建一个NSString
对象。
import Foundation
let myString : NSString = "123"
if let integerValue = Int(myString as String) {
print("\(myString) is the integer \(integerValue)")
}
// 打印出"123 is the integer 123"
提示
Swift的
String
结构体类型的实例无法用AnyObject
类型来表示,因为AnyObject
只能用来代表类的实例。不过,当Foundation的桥接启用后,Swift的String
值会被桥接到NSString
类的实例,这样就可以被赋值给AnyObject
类型的常量或变量了。
本地化
在Objective-C中,通常使用NSLocalizedString
系列的宏定义来创建本地化字符串的。这个系列的宏包括NSLocalizedString
,NSLicalizedStringFromTable
,NSLocalizedStringFromTableInBundle
和NSLocalizedStringWithDefaultValue
。在Swift中,你可以使用一个方法—NSLocalizedString(key:tableName:bundle:value:comment:)
—来提供整个NSLocalizedString
系列的宏的功能。NSLocalizedString
为tableName
,bundle
和value
参数提供了默认值。你可以在以前使用宏的地方使用这个方法。
数字类型
Swift会自动桥接原生的数字类型,例如Int
和Float
到NSNumber
类型。这个桥接功能让你可以用这些类型创建NSNumber
对象:
let n = 42
let m : NSNumber = n
这也允许你把Int
类型值传给接受NSNumber
对象作为参数的方法。需要注意的是,因为NSNUmber
能够包含多种数字类型的对象,所以你不能把它传给接受某种特定的数字类型,比如Int
值的方法。
以下所有的类型都会自动桥接到NSNumber
:
Int
UInt
Float
Double
Bool
提示
Swift的数字结构体类型,比如
Int
,UInt
,Float
,Double
和Bool
的实例无法用AnyObject
类型来表示,因为AnyObject
只能用来代表类的实例。不过,当Foundation的桥接启用后,Swift的数字类型值会被桥接到NSNumber
类的实例,这样就可以被赋值给AnyObject
类型的常量或变量了。
集合类
Swift会分别把NSArray
,NSSet
和NSDictionary
桥接到Array
,Set
和Dictionary
类。这意味着你可以在利用Swift强大的算法的同时,用自然的语法来操作集合类,并能同时使用Foundation和Swift的集合类型。
数组
Swift自动桥接Array
和NSArray
类。当你把参数化的NSArray
对象桥接到Swift数组的时候,得到的数组的类型是[ObjectType]
。如果NSArray
对象没有参数化类型,它会桥接到类型为[AnyObject]
的Swift数组。
例如下面的这段Objective-C声明:
@property NSArray<NSDate *> *dates;
- (NSArray<NSDate *> *)datesBeforeDate:(NSDate *)date;
- (void)addDatesParsedFromTimestamps:(NSArray<String *> *)timestamps;
Swift导入后会是下面这样:
var dates : [NSDate]
func datesBeforeDate(date: NSDate) -> [NSDate]
func addDatesParsedFromTimestamps(timestamps: [String])
如果一个对象是Objective-C或Swift类的实例,或者可以被桥接到一个类,那么它就与AnyObject
兼容。你可以桥接所有的NSArray
对象到Swift的数组,因为所有的Objective-C对象都与AnyObject
兼容。因为所有的NSArray
对象都可以桥接到Swift的数组,所以Swift编译器在导入Objective-C的API时会把NSArray
类替换为[AnyObject]
。
在你把NSArray
对象桥接到Swift数组之后,你就可以把数组的类型转换成更加明确的类型。与把NSArray
对象转换成[AnyObject]
类型不同的是,把AnyObject
向下转换成一个更加明确的类型并不能保证成功。在运行之前,编译器是无法确知数组的所有元素都能被转换成你所指定的类型的。因此,你需要使用条件类型转换操作符as?
来将[AnyObject]
向下转换为[SomeType]
,在你确信转换可以成功时,也可以使用无条件类型转换操作符as!
。例如,如果你知道Swift数组只包含NSView
类(或NSView
的子类)的实例,你就可以把包含AnyObject
类型元素的数组向下转换成NSView
对象数组了。如果在运行时,Swift数组中有任何一个元素不是NSView
对象,类型转换的结果就会返回nil
。
let swiftArray = fundationArray as [AnyObject]
if let downcastedArray = swiftArray as? [NSView] {
// downcastedArray只含有NSView对象的话,会执行到这里
}
你也可以直接在for循环中,把NSArray
对象转换成特定类型的Swift数组:
for view in foundationArray as! [NSView] {
// view的类型为NSView
}
当你把Swift的数组桥接到NSArray
对象时,Swift数组的所有成员必须与AnyObject
兼容。例如,一个类型为[Int]
的Swift数组包含的时Int
结构体元素。虽然Int
类型并不是一个类的实例,但是Int
类型可以被桥接至NSNumber
类,因此Int
类型依然与AnyObject
兼容的。所以你可以把类型为[Int]
的Swift数组桥接到NSArray
对象。如果Swift数组的元素类型和AnyObject
不兼容,那么当你把它桥接到NSArray
对象时,会引起一个运行时错误。
提示
作为性能优化的一项措施,把一个集合无条件向下类型转换成另一个类型更加特殊的集合时,例如,
NSArray as! [NSView]
,数组成员的类型检查可能会被延迟到它们被访问的时候才会进行。因此,无条件类型转换为不兼容的类型可能会看起来成功了,直到某个元素的类型转换失败触发一个运行时错误。把一个集合对象通过条件类型转换转成另一个类型更加特殊的集合时,例如,
NSArray as? [NSView]
,会立刻对所有的数组元素进行类型检查。如果有任何数组元素在进行类型转换时出错,就会返回nil
。
你也可以通过与前面提到的桥接规则相同的方式,直接用Swift的数组字面量创建一个NSArray
对象。当你显式地把一个常量或变量指定为NSArray
类型,并给它赋值一个数组字面量时,Swift会创建一个NSArray
对象而不是Swift数组。
let schoolSupplies : NSArray = ["Pencil", "Eraser", "Notebook"]
// schoolSupplies是一个包含NSString对象的NSArray对象。
上例中,Swift的数组字面量包含了三个String
字面量。因为String
类型可以桥接至NSString
类,所以数组字面量会被桥接至NSArray
对象,对schoolSupplies
的赋值就成功了。
当你在Objective-C代码中使用Swift类或协议的时候,所有的Swift数组都会在导入的API中被替换为NSArray
。如果你把一个NSArray
对象传给一个接受不同类型元素的数组的Swift API时,就会引发一个运行时错误。如果Swift API返回的Swift数组不能被桥接至NSArray
,也会引发运行时错误。
数集
(译者注:Set通常会翻译为集合,但是这样翻译可能会和Collection(集合)混淆,因此我这里把Set翻译为数集。在程序语言中的数集的成员并不局限于数字,通常可以包含任何对象,与数组(array)类似的,请不要因为Set的翻译中包含“数”这个字而引起误会。)
除了数组之外,Swift会自动把Set
类型桥接为NSSet
类。当你把参数化的NSSet
对象桥接至Swift数集时,得到的数集对象的类型为Set<ObjectType>
。如果NSSet
对象没有参数化,它会被桥接到类型为Set<NSObject>
的Swift数集。
(译者按:因为数集实际上声明为Set<Element : Hashable>
,因为有了Hashable
这个约束,所以映射的时候不是AnyObject
,而是NSObject
,这一点区别于数组。)
例如,下面这段Objective-C声明:
@property NSSet<NSStirng *> *words;
- (NSSet<NSString *> *)wordsMatchingPredicate:(NSPredicate *)predicate;
- (void)removeWords:(NSSet<NSString *> *)words;
导入Swift之后是这样的:
var words: Set<String>
func wordsMatchingPredicate(predicate: NSPredicate) -> Set<String>
func removeWords(words: Set<String>)
提示
和
NSArray
不一样,不包含参数话类型的NSSet
对象在桥接到Swift数集时,类型参数会被影射为NSObject
而不是AnyObject
,因为Swift的Set
所包含元素的类型必须符合Hashable
协议。
你可以把任意NSSet
对象桥接至Swift数集,因为所有的Objective-C对象都可以被桥接至AnyObject
。因为所有的NSSet
对象都可以桥接至Swift数集,因此Swift编译器在导入Objective-C API的时候,会把NSSet
类替换为Set<NSObject>
。类似的,当你在Objective-C代码中使用Swift类或协议的时候,Swift的数集对象会被重映射至NSSet
对象。
把NSSet
对象桥接至Swift数集的之后,你可以把这个集合向下类型转换为一个更特殊的数集类型。和Swift数组的向下类型转换一样,Swift数集的向下类型转换并不保证成功,使用as?
操作符把Set<NSObject>
向下转换为更特殊的类型时,会返回一个可选类型值。
let swiftSet = foundationSet as Set<NSObject>
if let downcastedSwiftSet = swiftSet as? Set<UITouch> {
// downcastedSwiftSet只包含UITouch对象
}
你也可以通过与前面提到的桥接规则相同的方式,用一个Swift数组字面量创建一个NSSet
对象,当你显式地把一个常量或变量指定为NSSet
类型,并给它赋值一个数组字面量时,Swift会创建一个NSSet
对象而不是Swift数集。
let amenities: NSSet = ["Sauna", "Steam Room", "Jacuzzi"]
// amenities是一个包含NSString对象的NSSet对象
字典
Swift也会自动桥接Dictionary
类型和NSDictionary
类。当你把参数化的NSDictionary
对象转换成一个Swift字典时,得到的字典的类型是[NSObject: AnyObject]
(译者按,原文为[ObjectType]
,应该是错误的。)。如果NSDictionary
对象没有参数化,那么它会被桥接至类型为[NSObject: AnyObject]
的Swift字典类型。
例如,下面这段Objective-C声明:
@property NSDictionary<NSURL *, NSData *> *cachedData;
- (NSDictionary<NSURL *, NSNumber *> *)fileSizesForURLsWithSuffix:(NSString *)suffix;
- (void)setCacheExpirations:(NSDictionary<NSURL *, NSDate *>)expirations;
导入Swift之后是这样的:
var cachedData: [NSURL: NSData]
func fileSizesForURLsWithSuffix(suffix: String) -> [NSURL: NSNumber]
func setCacheExpirations(expirations: [NSURL: NSDate])
提示
与
NSSet
类似,没有参数话的NSDictionary
对象在桥接到Swift字典类型时,键的类型是NSObject
而不是AnyObject
,因为Swift的Dictionary
类型要求其键的类型必须符合Hashable
协议。
你可以把任何NSDictionary
对象桥接到Swift字典,因为所有的Objective-C对象都与AnyObject
兼容。回忆一下,如果对象是Objective-C或Swift的类的实例,或可以被桥接到一个类,那么它就与AnyObject
兼容。因为所有的NSDictionary
对象可以被桥接到Swift字典,所以Swift编译器会在导入Objective-C API的时候把NSDictionary
类替换为[NSObject: AnyObject]
。类似的,当你在Objecitve-C代码中使用Swift的类或协议时,Swift字典会被重映射为NSDictionary
对象。
把NSDictionary
对象桥接至Swift字典的之后,你可以把这个集合向下类型转换为一个更特殊的字典类型。和Swift数组和数集的向下类型转换一样,Swift字典的向下类型转换并不保证成功,使用as?
操作符把[NSObject: AnyObject]
向下转换为更特殊的类型时,会返回一个可选类型值。
当你反过来进行类型转换的时候—从Swift字典转换成NSDictionary
对象—键和值必须都是类的实例,或者可以被桥接到某个类的实例。
你也可以通过与前面提到的桥接规则相同的方式,用一个Swift字典字面量创建一个NSDictionary
对象,当你显式地把一个常量或变量指定为NSDictionary
类型,并给它赋值一个字典字面量时,Swift会创建一个NSDictionary
对象而不是Swift字典。
let medalRankings: NSDictionary = ["Gold": "1st Place", "Silver": "2nd Place", "Bronze": "3rd Place"]
// medalRankings是一个包含NSString成员对象的NSDictionary对象。
错误
Swift自动把ErrorType
桥接至NSError
类。会发生错误的Objective-C方法,被导入为可抛出异常的Swift方法。能抛出异常的Swift方法,会遵循Objective-C的错误处理惯例,被导入为会发生错误的Objective-C方法。
有@objc
注解,并符合ErrorType
协议的Swift枚举类型,会在生成的头文件中创建一个NS_ENUM
声明以及错误领域(error domain)所对应的字符串常量。例如,下面这个Swift枚举类型声明:
@objc public enum CustomError: Int, ErrorType {
case A, B, C
}
在生成的头文件中会生成下述对应的Objective-C声明:
// Project-Swift.h
typedef Swift_ENUM(NSInteger, CustomError) {
CustomErrorA = 0,
CustomErrorB = 1,
CustomErrorC = 2,
};
static NSString * const CustomErrorDomain = @"Project.CustomError";
请参阅错误处理章节了解更多关于如何在Swift和Objective-C API中使用错误处理的信息。
Foundation数据类型
Swift为在Foundation框架中定义的数据类型提供了快捷的封装接口。用这个封装层来操作CGSize
和CGPoint
的时候,语法感觉起来更加自然,与其它Swift代码更加一致。比如,你可以使用下面这种语法来创建CGSize
结构体:
let size = CGSize(width: 20, height: 40)
你也可以通过这个封装层自然地调用Foundation的函数和结构体。
let rect = CGRect(x: 50, y: 50, width: 100, height: 100)
let width = rect.width // 等价于 CGRectGetWidth(rect)
let maxY = rect.maxY // 等价与 CGRectGetMaxY(rect)
Swift会把NSUInteger
和NSInteger
桥接至Int
。在Foudation API中,这两种类型都被导入为Int
。在Swift中,在所有可能的时候都会使用Int
以保持一致性,不过UInt
类型在某些一定要使用无符号整数类型的地方还是可用的。
Foundation函数
NSLog
在Swift中还是可以用来向控制台输出日志。你可以使用于Objective-C一样的语法来调用它。
NSLog("%.7f", pi)
// 把 "3.1415927" 输出到控制台
Swift自身也提供了一个打印函数print(_:)
。这个函数支持Swift的字符串插入,用法简单、强大而且通用。这个函数不会输出到系统控制台,但是可以满足常规的打印需求。
NSAssert
系列函数没有被引入Swift。取而代之的是assert
函数。
Core Foundation
Core Foundation类型会自动被导入为功能完备的Swift类。只要包含了内存管理的注解,Swift会自动管理Core Foundation对象的内存,包括你自己在代码中创建的Core Foundation对象。在Swift中,你可以互换使用Foundation和Core Foundation的无缝桥接(toll-free bridge)类型对。部分无缝桥接的Core Foundation类型,如果你先把它转换成Foundation类型,那么你还可以把它映射为Swift标准库中的类型。
(译者按:请注意区分Foundation与Core Foundation之间的无缝桥接(toll-free bridging),以及Swift与Objective-C之间的桥接(bridging)。)
重映射的类型
当Swift导入Core Foundation类型时,编译器会对类型名进行重映射。编译器把Ref从名字的末尾去除了,因为所有的Swift类都是引用类型,因此这个词尾是多余的。
Core Foundation的CFTypeRef
类型则完全被重映射为AnyObject
类型。在你的代码中,所有以前使用CFTypeRef
的地方,现在都要使用AnyObject
。
受内存管理的对象
从包含内存管理注解的API中返回的Core Foundation对象,Swift会自动对它们进行内存管理—你不需要自己手动调用CFRetain
,CFRelease
或CFAutoRelease
函数。
如果你需要从你自己的C函数或Objective-C方法中返回Core Foundation对象,那么你就需要为它们添加CF_RETURN_RETAINED
或CF_RETURN_NOT_RETAINED
宏作为注解,以自动插入内存管理调用。你也可以根据Core Foundation的所属关系(ownership)策略,使用CF_IMPLICIT_BRIDGING_ENABLED
和CF_IMPLICIT_BRIDGING_DISABLED
宏来包围C函数声明,以通过命名方式来暗示内存管理机制。
如果你只需要用到包含了内存管理注解,且不会间接返回Core Foundation对象的API的话,你可以跳过本节的其它部分。否则请继续阅读关于如何操作不受内存管理对象的内容。
不受管理的对象
当Swift导入不包含内存管理注解的API时,编译器无法对返回的Core Foundation对象进行自动内存管理。Swift会把这些返回的Core Foundation对象封装进Unmanaged<Instance>
结构体中。所有间接返回的Core Foundation对象也是不受内存管理的。比如,下面这个未注解的C函数:
CFStringRef StringByAddingTwoString(CFStringRef s1, CFStringRef s2)
Swift会这样导入它:
func StringByAddingTwoString(_:CFString!, _:CFString!) -> Unmanaged<CFString>! {
// ...
}
当你从一个无注解的API接收到一个不受内存管理的对象时,在操作它之前,你应该马上把它转换成一个受内存管理的对象。这样,Swift帮你进行内存管理。Unmanaged<Instance>
结构体提供了两个把不受内存管理的对象转换成受内存管理的对象的方法—takeUnretainedValue()
和takeRetainedValue()
。这两个方法都会返回原始的、解包过的类型的对象。你需要根据调用的API返回的对象是否为保留(retain)过,来选择使用哪个方法。
例如,假设上述C函数在返回之前没有保留(retain)过CFString
对象。那么在操作这个对象之前,你需要使用takeUnretainedValue()
方法。
let memoryManagedResult = StringByAddingTwoStrings(str1, str2).takeUnretainedValue()
// memoryManagedResult 是一个受内存管理的CFString对象。
你也可以对不受内存管理的对象调用retain()
,release()
和autorelease()
方法,不过这种方式不受推荐。
要了解更多信息,请参阅“Memory Management Programming Guide for Core Foundation”文档。
使用Cocoa设计模式
要编写设计优秀,适应性良好的应用的一条准则是使用成熟的Cocoa设计模式。许多设计模式都依赖在Objective-C中定义的类。得益于Swift与Objective-C的互操作性,你可以在你的Swift代码中使用这些设计模式。而且在很多情况下,你还可以使用Swift的语言特性来增强或简化已有的Cocoa设计模式,使它们更加强大且更加易用。
委托
(译者注:这里把delegation翻译成委托,如果你学过.NET,请不要与.NET的委托混淆,两者是完全不同的概念。)
无论是在Swift还是Objective-C中,委托(delegation)模式通常是通过定义了交互方式的协议来表示的。类通过遵守协议来实现委托模式。在Objective-C中,在你向委托对象发送一个它可能无法响应的消息之前,你需要先询问委托对象是否可以响应这个方法选择器(selector)。在Swift中,你可以使用可选调用链来对可能为nil
的对象触发一个可选的协议方法,然后通过使用if-let
语法获得可能的返回结果。后面列出的代码展示了如下几个过程:
- 检查
myDelegate
是否不为nil
。 - 检查
myDelegate
是否实现了window:willUseFullScreenContentSize:
方法。 - 如果1和2都为真,那么调用这个方法并将结果赋值给
fullScreenSize
。 - 打印方法的返回值。
class MyDelegate: NSObject, NSWindowDelegate {
func window(window: NSWindow, willUseFullScreenContentSize proposedSize: NSSize) -> NSSize {
return proposedSize
}
}
var myDelegate: NSWindowDelegate? = MyDelegate()
if let fullScreenSize = myDelegate?.window?(myWindow, willUseFullScreenContentSize:mySize) {
print(NSStringFromSize(fullScreenSize))
}
懒惰初始化
一个懒惰属性(lazy property)是指其底层的值在首次访问的时候才被初始化的属性。懒惰属性在初始化时需要复杂运算,或者需要进行大运算量的设置,或者是实例初始化完成前无法确知的情况下非常有用。
在Objective-C中,属性可以覆盖取值方法的默认实现(synthesized getter),让属性对应的实例变量的值为nil
的时候,才有条件地进行初始化:
@property NSXMLDocument *XMLDocument;
- (NSXMLDocument *)XMLDocument {
if (_XMLDocument == nil) {
_XMLDocument = [[NSXMLDocument alloc] initWithContentOfURL:[[NSBundle mainBundle] URLForResource:@"/path/to/resource" withExtension:@"xml"] options:0 error:nil];
}
return _XMLDocument;
}
在Swift中,带有初始值的存储属性(stored property)可以使用lazy
修饰符声明,以让计算初始值的表达式在属性首次访问的时候才进行运算:
lazy var XMLDocument: NSXMLDocument = try! NSXMLDocument(contentOfURL:NSBundle.mainBundle().URLForResource("document", withExtension:"xml")!, options:0)
懒惰属性在初次访问时才进行计算,而此时实例已经完成初始化,因此默认的初始化表达式可以访问实例的常量或变量属性:
var pattern: String
lazy var regex: NSRegularExpression = try! NSRegularExpression(pattern: self.pattern, options:[])
对于需要在初始化之后需要进行额外设置步骤的值,你可以给属性赋一个能够自我执行的,可以返回完整初始化值的闭包作为默认初始值:
lazy var ISO8601DateFormatter : NSDateFormatter = {
let formatter = NSDateFormatter()
formatter.localze = NSLocale(localeIdentifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
return formatter
}()
提示
如果一个懒惰属性尚未被初始化,且同时被多个线程访问,那么程序并不保证属性只被初始化一次。
要了解更多信息,请查看“Swift编程语言(Swift 2.2版)”的“懒惰存储属性”部分。
错误处理
在Cocoa中,可能产生错误的方法会把NSError
指针作为方法的最后一个参数,发生错误的时候,这个指针会指向一个NSError
对象。Swift会自动把可能产生错误的Objective-C方法转译成可以抛出一个错误的方法,以配合Swift的原生错误处理功能使用。
提示
处理(consume)错误的方法(method),比如委托方法或接受一个包含
NSError
参数的完成代码块(completion handler)作为参数的方法,在导入Swift的时候不会被转译成可以抛出错误的方法。
举个例子,请看下面来自NSFileManager
类的Objective-C方法:
- (BOOL)removeItemAtURL:(NSURL *)URL
error:(NSError *)error;
导入Swift时会变成这样:
func removeItemAtURL(URL:NSURL) throws
注意removeItemAtURL(_:)
方法在导入Swift之后的返回类型变为Void
,没有了error
参数,但是包含了一个throws
声明。
如果Objective-C方法的最后一个非代码块参数的类型为NSError **
,Swift就会把它替换为throws
关键词,以指示这个方法能抛出一个错误。如果Objective-C方法的错误参数是第一个参数,Swift会试图通过删除方法选择器第一部分的WithError
或AndReturnError
词尾(如果存在的话)来进一步简化方法名。如果得到的方法名已经被另一个方法所使用,那么这个方法名将不会改变。
如果能产生错误的Objective-C方法返回一个BOOL
值,用于指示方法调用的成败,Swift会把方法的返回值改为Void
。类似的,如果能产生错误的Objective-C方法通过返回一个nil
值用来指示方法调用失败,那么Swift会把方法的返回类型改为返回一个非可选的类型。
否则,如果无法通过惯例来确定返回值,那么方法的返回值会保持原样。
提示
对Objective-C的方法声明使用
NS_SWIFT_NOTHROW
宏来创建NSError
,以防止方法在导入Swift时变成抛出错误的方法。
捕捉和处理错误
在Objective-C中,错误处理是可选的(opt-in),这意味着除非你传入了错误指针,否则方法调用产生的错误就会被忽略。在Swift中,调用抛出错误的方法时,必须进行显式的错误处理。
下面这个例子展示了在Objective-C中是如何处理方法调用时产生的错误的:
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *fromURL = [NSURL fileURLWithPath:@"/path/to/old"];
NSURL *toURL = [NSURL fileURLWithPath:@"/path/to/new"];
NSError *error = nil;
BOOL success = [fileManager moveItemAtURL:fromURL toURL:toURL error:&error];
if (!success) {
NSLog(@"Error: %@", error.domain);
}
下面是Swift中的等效代码:
let fileManager = NSFileManager.defaultManager()
let fromURL = NSURL(fileURLWithPath:"/path/to/old")
let toURL = NSURL(fileURLWithPath:"/path/to/new")
do {
try fileManager.moveItem(fromURL, toURL: toURL)
} catch let error as NSError {
print("Error: \(error.domain)")
}
另外,你可以使用catch
语句来匹配特定的错误代码,来区分不同的错误情况:
do {
try fileManager.moveItemAtURL(fromURL, toURL: toURL)
} catch NSCocoaError.FileNoSuchFileError {
print("Error: no such file exists")
} catch NSCocoaError.FileReadUnsupportedSchemeError {
print("Error: unsupported scheme (should be 'file://')")
}
把错误转换成可选类型值
在Objective-C中,如果你只关心错误是否发生,而不关心发生的是什么错误,你可以传NULL
给错误参数。在Swift中,你可以使用try?
把一个抛出错误的表达式转换成一个返回可选类型值的方法,你可以随后检查返回值是否为nil
。
例如,NSFileManager
的实例方法:URLForDirectory(_:inDomain:appropriateForURL:create:)
,返回一个位于特定搜索路径和文件域(domain)下的URL;如果合适的URL不存在且无法创建,则产生一个错误。在Objective-C中,方法调用的成败可以通过检查返回值是否为一个NSURL
对象来确定。
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *tmpURL = [fileManager URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:NULL];
if (tmpURL != nil) {
// ...
}
你可以使用如下Swift代码实现相同功能:
let fileManager = NSFileManager.defaultManager()
if let tmpURL = try? fileManager.URLForDirectory(.CachesDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true) {
// ...
}
抛出错误
如果Objective-C方法中产生了错误,该方法的错误指针就会指向这个错误:
// 错误发生时
if (errorPtr) {
*errorPtr = [NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorCannotOpenFile
userInfo:nil];
}
如果在Swift方法中发生了错误,这个错误就会被抛出,并自动传递给调用者:
// 错误发生时
throw NSError(domain: NSURLErrorDomain, code:NSURLErrorCannotOpenFile, userInfo:nil)
如果Objective-C代码调用了一个抛出错误的Swift方法,错误会自动传递给Objective-C桥接方法的错误指针参数。
例如,NSDocument
类的readFromFileWrapper(_:ofType:)
方法。在Objective-C中,这个方法的最后一个参数的类型时NSError **
。当你在Swift中创建NSDocument
的子类,覆盖这个方法时,方法会自动替换错误参数,并用抛出代替。
class SerializedDocument : NSDocument {
static let ErrorDomain = "com.example.error.serialized-document"
var representedObject: [String: AnyObject] = [:]
override func readFromFileWrapper(fileWrapper:NSFileWrapper, ofType typeName: String) throws {
guard let data = fileWrapper.regularFileContents else {
throw NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotOpenFile, userInfo:nil)
}
if case let JSON as [String : AnyObject] = try NSJSONSerialization.JSONObjectWithData(data, options: []) {
self.representedObject = JSON
} else {
throw NSError(domain: SerializedDocument.ErrorDomain, code: -1, userInfo: nil)
}
}
}
如果方法无法使用文档的文件内容创建一个对象,它会抛出一个NSError
对象。如果方法是在Swift代码中调用的,错误会被传递给它的调用作用域(calling scope)。如果方法是在Objective-C代码中调用的,错误就会传递给错误指针参数。
(译者按:原文中这段文字于本节开头第一段重复,这里不再翻译。)
提示
尽管Swift的错误处理与Objective-C的异常处理类似,但是这两个是完全不同的功能。如果Objective-C方法在运行时抛出异常,Swift会触发一个运行时错误。在Swift中是无法直接从Objective-C的异常中恢复的。所有的异常都必须在Swift调用的Objective-C代码实现中处理。
(译者按:Swift和Objective-C的错误(error)类似于其他语言(例如Java,C#,Ruby,Python等)的异常,而Objective-C的异常(exception)则类似于其他语言的运行时错误(runtime error)。)
键值观察
键值观察(key-value observing)是一种能在指定的属性发生变化的时候通知其他对象的机制。只要Swift类是继承自NSObject
的,它就可以使用键值观察。你可以通过如下三个步骤在Swift中实现键值观察。
为你要观察的属性加上
dynamic
修饰符。要了解更多关于dynamic
的信息,请查看使用动态分配章节。class MyObjectToObserve : NSObject {
dynamic var myDate = NSDate()
func updateDate() {
myDate = NSDate()
}
}
创建一个全局上下文变量。
private var myContext = 0
为键路径(key-path)创建一个观察者,覆盖
observeValueForKeyPath:ofObject:change:context:
方法,并在deinit
方法中移除观察者。class MyObserver : NSObject {
var objectToObserve = MyObjectToObserve()
override init() {
super.init()
objectToObserve.addObserver(self, forKeyPath:"myDate", options: .New, context: &myContext)
}
override func observeValueForKeyPath(keyPath: String?, ofObject: AnyObject?, change: [String:AnyObject], context:context) {
if context == &myContext {
if let newValue = change?[NSKeyValueChangeNewKey] {
print("Date changed: \(newValue)")
}
}
else {
super.observeValueForKeyPath(keyPath, ofObject:object, change: change, context:context)
}
}
deinit {
objectToObserve.removeObserver(self, forKeyPath:"myDate", context: &myContext)
}
}
撤销
在Cocoa中,你可以使用NSUndoManager
注册一个操作,以允许用户撤掉该项操作。你可以像在Objective-C中一样,在Swift中使用Cocoa的撤销架构。
应用的响应链中的对象—也就是OS X中NSResponder
和iOS中UIResponder
的子类—都包含一个只读的undoManager
属性,返回一个可选的NSUndoManager
对象,这个对象管理应用的撤销栈(undo stack)。用户每触发一个动作,比如在控件中输入文本,或从列表中删除一个选中的行,就可以向撤销管理器(undo manager)注册一个撤销操作,以允许用户撤销这个操作的影响。撤销操作会记录回滚(conteract)操作所需的步骤,例如把控件的文本设定为其原始值,或把删除的项目重新加回列表中。
NSUndoManager
支持两种注册撤销操作的方式:一种是“简单撤销”(simple undo),这种方式会执行一个带有一个对象参数方法。另一种是“基于动作对象的撤销”(invocation-based undo),这种方式能用可接受任意数量、任意类型参数的NSInvocation
对象执行撤销操作。
(译者按:这里把Invocation翻译成“动作对象”。在Cocoa中,Invocation是一个封装为对象的方法,可以由其他对象用它触发一个预定义的动作。)
比如,有一个简单的Task
模型,在ToDoListController
中被用来展示一个需要完成的任务的列表:
class Task {
var text: String
var completed: Bool = false
init(text:String) {
self.text = text
}
}
class ToDoListController : NSViewController, NSTableViewDataSource, NSTableViewDelegate {
@IBOutlet var tableView: NSTableView!
var tasks: [Task] = []
// ...
}
Swift中定义的属性,你可以在willSet
观察方法中,以self
作为target
,相应的Objective-C的赋值方法作为selector
,并用当前的属性值作为object
,创建一个撤销操作:
@IBOutlet var notesLabel: NSTextView!
var notes: String {
willSet {
undoManager?.registerUndoWithTarget(self, selector: "setNotes:", object: self.title)
undoManager?.setActionName(NSLocalizedString("todo.notes.update", comment:"Update Notes"))
}
didSet {
notesLabel.string = notes
}
}
对于接受多个参数的方法,你可以使用NSInvocation
创建一个撤销操作,用于触发一个接受多个参数的方法,把应用恢复成操作前的状态:
@IBOutlet var remainingLabel: NSTextView!
func markTask(task: Task asComplete complete: Bool) {
if let target = undoManager?.prepareWithInvocationTarget(self) as TodoListController {
target.markTask(task, asComplete: !complete)
undoManager?.setActionName(NSLocalizedString("todo.task.mark", comment:"Mark As Complete"))
}
task.completed = completed
tableView.reloadData()
let numberRemaining = tasks.filter { $0.complete }.count
remainingLabel.string = String(format: NSLocalizedString("todo.taks.remaining", comment:"Tasks Remaining: %d"), numberRemaining)
}
prepareWithInvocationTarget(_:)方法返回指定
target的代理对象。通过把它转成
ToDoListViewController,使它能直接响应
markTask(_:asCompleted)`调用。
要了解更多信息,请参阅“The Undo Architecture Programming Guide”文档。
目标-动作
目标-动作(target-action)模式是Cocoa中一个常用的设计模式,这种模式下,在事件发生时,一个对象会发送消息给另一个对象。目标-动作模式在Swift和Objective-C中极其相似。在Swift中,你使用Selector
类型来表示Objective-C的方法选择器。要查看关于目标-动作的Swift代码实例,请查看Objective-C的方法选择器章节。
单例模式
单例模式能提供一个全局可用的、共享的对象实例。你可以通过创建一个单例,为整个应用共享的资源或服务提供一个统一的访问点。如,用来播放音效的音频通道、或管理HTTP请求的网络管理对象。
在Objective-C中,你可以通过把实例的初始化过程放在dispatch_once
函数调用中,以确保单例对象只有一个实例,因为这个函数会执行一次代码块,并能确保在程序的生命周期内只执行一次:
+ (instancetype)sharedInstance {
static id _sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[self alloc] init];
});
return _sharedInstance;
}
在Swift中,你只需使用一个静态属性就能确保这个属性只被懒惰初始化一次,即使是在多个线程中被同时访问:
class Singleton {
static let sharedInstance = Singleton()
}
如果你需要在初始化时做一些额外的设置,你可以为这个全局常量赋值闭包的执行结果:
class Singleton {
static let sharedInstance : Singleton = {
let instance = Singleton()
// 设置代码
return instance
}()
}
要了解更多信息,请查看“Swift编程语言(Swift 2.2版)”的“类型属性”章节。
自省
在Objective-C中,你可以使用isKindOfClass:
方法来检查对象是否属于某个类,用conformsToProtocal:
方法来检查对象是否遵守某个协议。在Swift中,你使用is
操作符来检查对象的类型,或者用as?
操作符进行向下类型转换,以实现同样的功能。
你可以使用is
操作符检查实例的类型是否为某个子类。如果实例属于该子类,is
操作符返回true
,否则返回false
。
if object is UIButton {
// object的类型是UIButton
}
else {
// object的类型不是UIButton
}
你也可以使用as?
试着向下类型转换转成子类的对象。as?
操作符会返回一个可选类型对象,然后就可以用if-let
语句把值赋给一个常量。
if let button = object as? UIButton {
// 对象成功被转成UIButton类型,并被赋值给了button
}
else {
// object无法转换成UIButton类型
}
要了解更多关于“类型转换”的信息,请查看“Swift编程语言(Swift 2.2版)”。
检查和转换成协议对象,与检查和转换类的对象使用完全相同的语法。下面就是一个使用as?
操作符检查对象是否遵守协议的一个例子:
if let dataSource = object as? UITableViewDataSource {
// 对象遵守UITableViewDataSource协议,并复制给了dataSource
}
else {
// 对象不遵守UITableViewDataSource协议
}
注意,在类型转换后,dataSource
的类型是UITableViewDataSource
,因此你只能对它调用在UITableViewDataSource
协议中定义的方法和属性。要执行其他操作,你就必须把它转换回其他类型。
要了解更多信息,请查看“Swift编程语言(Swift 2.2版)”的“协议”章节。
序列化
序列化能让你编码或解码应用中的对象,并把它们保存为平台无关的格式,如JSON或属性列表(property list)。你可以把这些格式的数据写入文件,或者传递给本地或网络上的其他进程。
在Objective-C中,你可以使用Foundation框架的NSJSONSerialization
和NSPropertyListSerialization
类,用一个解码过的JSON或属性列表值—通常是NSDictionary
例如,下面的这个Venue
结构体,包含一个String
类型的name
属性,一个CLLocationCoordinate2D
类型的coordinate
属性,和一个内嵌的Category
枚举类型的category
属性:
import Foundation
import CoreLocation
struct Venue {
enum Category: String{
case Entertainmant
case Food
case Nightlife
case Shopping
}
var name: String
var coordinate: CLLocationCoordinate2D
var category: Category
}
假设某个应用能处理Venue
实例。应用在和网络服务器通讯的时候,获得了JSON格式的地点数据,如下:
{
"name": "Caffe Macs",
"coordinates": {
"lat": 37.330576,
"lng": -122.029739
},
"category": "Food"
}
你可以编写一个可失败的构造方法(failable initializer),这个方法接受一个类型为[String: AnyObject]
的attributes
参数,对应通过NSJSONSerialization
或NSPropertyListSerialization
类返回的值。
init?(attributes: [String: AnyObject]) {
guard let name = attributes["name"] as? String,
let coordinates = attributes["coordinates"] as? [String: Double],
let latitude = coordinate["lat"],
let longitude = coordinate["lng"],
let category = Caterogy(rawValue: attributes["category"] as String ?? "Invalid")
else {
return nil
}
self.name = name
self.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
self.category = category
}
包含多个可选值绑定表达式(optional-binding expression)的guard
语句保证了attributes
参数从预期的文件格式中获得了所有必要的信息。如果任意一个可选类型值的绑定表达式在为常量赋值时失败了,guard
语句会立刻停止执行其他语句,并转跳到else
分支,返回nil
。
你现在就可以用一组JSON格式的数据,通过NSJSONSerialization
类创建的对象,传给Venue
的构造方法来创建venue
对象了:
let JSON = "{\"name\": \"Caffe Macs\", \"coordinates\": {\"lat\": 37.330576, \"lng\": -122.029739 }, \"category\": \"Food\"}"
let data = JSON.dataUsingEncoding(NSUTF8StringEncoding)!
let attributes = try! NSJSONSerialization.JSONObjectWithData(data, options: []) as! [String: AnyObject]
let venue = Venue(attributes: attrubutes)!
print(venue.name)
// 打印出“Caffe Macs”
验证序列化数据
在前面的例子中,Venue
的构造函数在所有必须的信息都提供的情况下,可选地返回一个实例。否则,构造器会简单的返回nil
。
如果可以知道一组给定的数据为何不能创建有效实例的原因的话,通常会很有用。要实现这一点,你需要把这个可失败的构造方法重构为一个可抛出错误的构造方法:
enum ValidationError : ErrorType {
case Missing(String)
case Invalid(String)
}
init(attributes [String: AnyObject]) throws {
guard let name = attributes["name"] as? String else {
throw ValidationError.Missiong("name")
}
guard let coordinates = attrubutes["coordinates"] as? [String: Double] else {
throw ValidationError.Missing("coordinate")
}
guard let latitude = coordinate["lat"],
let longitude = coordinate["lng"] else {
throw ValidationError.Invalid("coordinate")
}
guard let categoryName = attributes["category"] as? String else {
throw ValidationError.Missing("category")
}
guard let category = Category(rawValue: categoryName) else {
throw ValidationError.Invalid("category")
}
self.name = name
self.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
self.category = category
}
这个构造方法没有只用一条guard
语句处理所有attributes
的值,而是分别检查了每个值,并在值不存在或不合法的时候抛出一个错误。
例如,如果JSON没有包含键为name
的值,那么这个构造方法会抛出ValicationError.Missing
枚举值,并把对应的"name"
字段作为枚举的关联值(associated value):
{
"coordinates": {
"lat": 37.77492,
"lng": -122.419
},
"category": "Shopping"
}
let JSON = "{\"coordinates\": {\"lat\": 37.77492, \"lng\": -122.419 }, \"category\": \"Shopping\"}"
let data = JSON.dataUsingEncoding(NSUTF8StringEncoding)!
let attributes = try! NSJSONSerialization.JSONObjectWithData(data, options: []) as! [String: AnyObject]
do {
let venue = try Venue(attributes: attributes)
} catch ValidationError.Missing(let field) {
print("Missing Field: \(field)")
}
// 打印 “Missing Field: name”
如果JSON包含了所有必需的字段,但是"category"
键的值不匹配Category
枚举中定义的值的rawValue
,那么这个构造方法会抛出ValicationError.Invalid
枚举值,并把对应的"category"
字段作为枚举的关联值:
{
"name": "Moscone West",
"coordinates": {
"lat": 37.7842,
"lng": -122.4016
},
"category": "Convention Center"
}
let JSON = "{\"name\": \"Moscone West\", \"coordinates\": {\"lat\": 37.7842, \"lng\": -122.4016 }, \"category\": \"Convention Center\"}"
let data = JSON.dataUsingEncoding(NSUTF8StringEncoding)!
let attributes = try! NSJSONSerialization.JSONObjectWithData(data, options: []) as! [String: AnyObject]
do {
let venue = try Venue(attributes: attributes)
} catch ValidationError.Invalid(let field) {
print("Invalid Field: \(field)")
}
// 打印 “Invalid Field: category
API可用性
某些类和方法并非在应用支持的所有版本和所有平台上都可用。要保证应用能够应对任何功能缺失,你需要检查API的可用性。
在Objective-C中,我们使用respondesToSelector:
和instancesRespondToSelector:
方法来检查类方法或实例方法的可用性。如果不检查,方法调用会抛出NSInvalidArgumentException
,“unrecognized selector sent to instance”(向实例发送了无法识别的方法选择器)异常。例如,CLLocationManager
的requestWhenInUseAuthorization
实例方法从iOS 8.0和OS X 10.0才开始可用:
if ([CLLocationManager instancesRespondToSelector:@selector(requestWhenInUseAuthorization)]) {
// 方法可用
}
else {
// 方法不可用
}
在Swift中,调用一个不是在所有目标平台和版本上都可用的方法会引发一个编译时错误。
下面是前述例子的Swift代码:
let locationManager = CLLocationManager()
locationManager.requestWhenInUseAuthorization()
// 错误:只在iOS 8.0或更新的系统中可用。
如果应用的目标iOS版本早于8.0,目标OS X的版本早于10.0,requestWhenInUseAuthorization()
方法是不可用的,那么编译器会汇报一个错误。
Swift代码可以在运行时检查API可用性。可用性检查可以在if
,guard或
when`等条件语句中使用。
再回到前面的例子,你可以使用if
语句检查可用性,让requestWhenAvailableAuthorization
方法在运行时可用时才执行:
let locationManager = CLLocationManager()
if #available(iOS 8.0, OSX 10.0, *) {
locationManager.requestWhenInUseAuthorization()
}
你也可以使用guard
语句检查可用性,除非当前的运行平台满足要求,否则跳出代码块。这种方法简化了判断不同平台能力的处理逻辑。
let locationManager = CLLocationManager()
guard #available(iOS 8.0, OSX 10.0, *) { else return }
locationManager.requestWhenInUseAuthorization()
每个平台的参数都包含下面列出的其中一个平台名,后面跟上相应的版本号。最后一个参数是星号(*
),用来支持未来可能出现的新平台。
平台名:
iOS
iOSApplicationExtension
OSX
OSXApplicationExtension
watchOS
watchOSApplicationExtension
tvOS
tvOSApplicationExtension
所有的Cocoa API都已经包含了可用性信息,因此你可以确信你编写的代码能按照预期在支持的平台上正确运行。
你也可以使用@available
注解为你的API标注可用性信息。@available
注解使用与#available
运行时检查相同的语法,以一系列逗号分隔的平台和版本号要求作为参数。
例如:
@available(iOS 8.0, OSX 10.0, *)
func useShinyNewFeature() {
// ...
}
提示
有
@available
注解的方法可以安全的使用对其指定的平台要求可用的API,而无需额外进行显式地API可用性检查。
处理命令行参数
在OS X上,你通常是通过单击Dock或LaunchPad上的应用图标、或双击Finder中的应用图标来打开一个应用的。不过,你也可以从终端用程序打开一个应用,并给它传递命令行参数。
你可以通过访问Process.arguments
类属性来得到一个包含了所有在启动时指定的命令行参数的列表。这种方式与访问NSProcessInfo.processInfo()
的argument
属性是等效的。
$ /path/to/app --argumentName value
for argument in Process.arguments {
print(argument)
}
// 打印 “/path/to/app”
// 打印 “--argumentName”
// 打印 “value”
提示
Process.arguments
的第一个元素总是可执行文件的路径。启动时指定的命令行参数是从Process.arguments[1]
开始的。
与C语言API交互
作为与Objective-C互操作性的一部分,Swift保持了与C语言的部分类型和特性的兼容性。Swift还提供了操作常见的C语言结构和模式的方法,以便你在编码时需要用到它。
基本类型
Swift提供了与C语言基本整数类型等价的类型—比如,char
,int
,float
,和double
。不过这些类型不会通过隐式转换转成Swift的整型,如Int
。因此,只有在你的代码对这些类型有特殊需求时才使用它们,否则,请总是使用Int
。
C类型 | Swift类型 |
bool | CBool |
char , signed char | CChar |
unsigned char | CUnsignedChar |
short | CShort |
unsigned short | CUnsignedShort |
int | CInt |
unsigned int | CUnsignedInt |
long | CLong |
unsigned long | CUnsignedLong |
long long | CLongLong |
unsigned long long | CUnsignedLongLong |
wchar_t | CWideChar |
char16_t | CChar16 |
char32_t | CChar32 |
float | CFloat |
double | CDouble |
枚举类型
Swift会把用NS_ENUM
宏创建的C枚举类型导入为以Int
作为其原始值类型的Swift的枚举类型。不管是在系统框架还是在自定义代码中创建的枚举,C枚举类型的成员名的前缀在导入Swift的时候会被移除。
例如,下面这个枚举类型是用NS_ENUM
声明的C枚举。
typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
UITableViewCellStyleDefault,
UITableViewCellStyleValue1,
UITableViewCellStyleValue2,
UITableViewCellStyleSubtitle
};
Swift会把它导入成这样:
enum UITableViewCellStyle: Int {
case Default
case Value1
case Value2
case Subtitle
}
当你使用枚举值的时候,你在值的名字前加上一个点(.
)
let cellStyle: UITableViewCellStyle = .Default
不是用NS_ENUM或NS_OPTIONS宏标记的C的枚举类型会被Swift导入为结构体。C枚举的每个成员都被导入为一个类型为该结构体的全局只读计算属性—而不是成为Swift结构体的成员。
例如,下面这个C枚举不是用NS_ENUM
宏声明的。
typedef enum Error {
ErrorNone = 0,
ErrorFileNotFound = -1,
ErrorInvalidFormat = -2,
};
Swift会把它导入成这样:
struct Error: RawRepresentable, Equatable {
}
var ErrorNone: Error { get }
var ErrorFileNotFound: Error { get }
var ErrorInvalidFormat: Error { get }
Swift会为导入的C枚举类型自动生成遵守Equatable
协议的实现代码。
选项集
Swift也会把用NS_OPTIONS
宏标记的C语言枚举类型,导入为Swift的选项集(option set)。选项集与枚举类型一样,导入后会移除选项值名字中的前缀。
比如,下面这个Objective-C选项声明:
typedef NSOPTIONS(NSUInteger, NSJSONReadingOptions) {
NSJSONReadingOptionsMutableContainers = (1UL << 0),
NSJSONReadingOptionsMutableLeaves = (1UL << 1),
NSJSONReadingOptionsAllowFragments = (1UL << 2)
};
Swift会把它导入成这样:
struct NSJSONReadingOptions : OptionSetType {
init(rawValue: UInt)
static var MutableContainers : NSJSONReadingOptions { get }
static var MutableLeaves : NSJSONReadingOptions { get }
static var AllowFragments : NSJSONReadingOptions { get }
}
在Objective-C中,选项集是一个位掩码的整数值。你使用按位或(bitwise OR)操作符(|
)来组合选项值,用按位与(bitwise AND)操作符(&
)来检查选项值。你可以用一个常量或表达式创建一个新的选项集,空的选项集用常数零(0
)来表示。
在Swift中,选项集用遵守OptionalSetType
协议的结构体表示,选项用静态成员表示。选项集的行为与Swift的Set
集合类型类似。你可以使用insert(_:)
或unionInPlace(_:)
方法添加选项值,使用remove(_:)
或substractInPlace(_:)
方法移除选项值,使用contains(_:)
方法检查选项值。你可以使用一个数组字面量创建一个选项集,使用类似枚举类型的点(.
)前缀来访问选项值。空的选项值可以用一个空的数组字面量([]
)或默认构造方法创建。
let options: NSDataBase64EncodingOptions = [
.Encoding76CharacterLineLength,
.EncodingEndLineWithLineFeed
]
let string = data.base64EncodedStringWithOptions(options)
共用体
Swift仅部分支持C语言的union
(共用体)类型。导入包含共用体的C的聚合数据类型(C aggregates)时,Swift是无法访问不受支持的字段的。不过如果C或Objective-C API的参数或返回值包含共用体类型,那么它们是可以在Swift中使用的。
(译者按:C aggregates翻译为“聚合数据类型”参考了维基百科)
位域
Swift把结构体的位域(bit field),比如Foundation框架的NSDecimal
类型中能找到的那些,导入为计算属性。当访问对应某个位域的计算属性时,Swift会自动把该值与兼容的Swift类型互相转换。
(译者按:位域是一种特殊的结构体。)
未命名结构体和未命名共用体字段
C的结构体和共用体类型可能会定义未命名的结构体和共用体类型的字段。Swift不支持未命名的结构体,因此这些字段被导入为形如__Unnamed_fieldName
这种名字的嵌套类型。
例如,如下C名为Pie
的结构体包含一个未命名的结构体类型crust
的字段,和一个未命名的共用体类型的filling
字段:
struct Pie {
struct { bool flakey; } crust;
union { int fruit; int meat; } filling;
}
当这个结构体导入Swift时,crust
属性的类型为Pie.__Unnamed_crust
,而filling
属性的类型则为Pie.__Unnamed_filling
。
指针
只要有可能,Swift就会避免让你直接使用指针。不过,如果你确实需要直接访问内存,那么你还是有很多指针类型可以使用的。下面的几个表格使用Type
作为类型名的占位符,用来展示指针类型的映射语法。
对于返回类型、变量和参数中的指针,应用如下映射规则:
C语法 | Swift语法 |
const Type | UnsafePointer<Type> |
Type | UnsafeMutablePointer<Type> |
对于类的指针,应用如下映射规则:
C语法 | Swift语法 |
Type const | UnsafePointer<Type> |
Type __strong | UnsafeMutablePointer<Type> |
Type ** | AutoreleasingUnsafeMutablePointer<Type> |
常指针
当函数声明了接受一个UnsafePointer<Type>
参数时,它可以接受下面这几种类型的参数:
nil
,传入一个空指针。UnsafePointer<Type>
,UnsafeMutablePointer<Type>
,或AutoreleasingUnsafeMutablePointer<Type>
值,在必要的时候会转换成UnsafePointer<Type>
。String
值,如果Type
是Int8
或UInt8
,字符串会自动转换成UTF8编码,保存到缓存中,直到调用结束。- 左手侧操作数的类型为
Type
的inout
表达式,会传入左手侧标识符的地址。 [Type]
值,会传入数组开始位置的指针,数组的生命周期被延长至整个调用的周期。
假设你声明了如下函数:
func takeAPointer(x:UnsafePointer<Float>) {
// ...
}
你可以通过下面几种方式来调用它:
var x: Float = 0.0
var p: UnsafePointer<Float> = nil
takeAPointer(nil)
takeAPointer(p)
takeAPointer(&x)
takeAPointer([1.0, 2.0, 3.0])
当函数声明了接受一个UnsafePointer<Void>
参数,它可以接受与UnsafePointer<Type>
相同的操作数,Type
可以是任何类型。
假设你声明了如下函数:
func takeAVoidPointer(x:UnsafePointer<Void>) {
// ...
}
你可以通过下面几种方式来调用它:
var x: Float = 0.0, y: Int = 0
var p: UnsafePointer<Float> = nil, q: UnsafePointer<Int> = nil
takeAVoidPointer(nil)
takeAVoidPointer(p)
takeAVoidPointer(q)
takeAVoidPointer(&x)
takeAVoidPointer(&y)
takeAVoidPointer([1.0, 2.0, 3.0] as [Float])
let intArray = [1, 2, 3]
takeAVoidPointer(intArray)
可变指针
当函数声明了接受一个UnsafeMutablePointer<Type>
参数时,它可以接受下面这几种类型的参数:
nil
,传入一个空指针。UnsafeMutablePointer<Type>
值。- 左值的类型为
Type
的inout
表达式,会传入左值的地址。 inout [Type]
值,会传入数组开始位置的指针,数组的生命周期被延长至整个调用的周期。
假设你声明了如下函数:
func takeAMutablePointer(x:UnsafeMutablePointer<Float>) {
// ...
}
你可以通过下面几种方式来调用它:
var x: Float = 0.0
var p: UnsafeMutablePointer<Float> = nil
var a: [Float] = [1.0, 2.0, 3.0]
takeAPointer(nil)
takeAPointer(p)
takeAPointer(&x)
takeAPointer(&a)
当函数声明了接受一个UnsafeMutablePointer<Void>
参数,它可以接受与UnsafeMutablePointer<Type>
相同的操作数,Type
可以是任何类型。
假设你声明了如下函数:
func takeAMutableVoidPointer(x:UnsafeMutablePointer<Void>) {
// ...
}
你可以通过下面几种方式来调用它:
var x: Float = 0.0, y: Int = 0
var p: UnsafeMutablePointer<Float> = nil, q: UnsafeMutablePointer<Int> = nil
var a: [Float] = [1.0, 2.0, 3.0], b: [Int] = [1, 2, 3]
takeAMutableVoidPointer(nil)
takeAMutableVoidPointer(p)
takeAMutableVoidPointer(q)
takeAMutableVoidPointer(&x)
takeAMutableVoidPointer(&y)
takeAMutableVoidPointer(&a)
takeAMutableVoidPointer(&b)
自动释放指针
当函数声明了接受一个AutoreleasingUnsafeMutablePointer<Type>
参数时,它可以接受下面这几种类型的参数:
nil
,传入一个空指针。AutoreleasingUnsafeMutablePointer<Type>
值。inout
表达式,其操作数会通过基础拷贝到一个临时的不拥有的(nonowning)缓存区。缓存区的地址接着传给被调用者,返回时,缓存区内的值被加载、保留(retain)并重新赋值给操作数。
注意,上面的列表中不包含数组。
假设你声明了如下函数:
func takeAnAutoreleasingUnsafeMutalbePointer(x:AutoreleasingUnsafeMutalbePointer<NSDate?>) {
// ...
}
你可以通过下面几种方式来调用它:
var x: NSDate? = nil
var p: AutoreleasingUnsafeMutalbePointer<NSDate?> = nil
takeAPointer(nil)
takeAPointer(p)
takeAPointer(&x)
被指向的类型不会被桥接。例如,NSString **
导入Swift的时候,是AutoreleasingUnsafeMutalbePointer<NSString?>
,不是AutoreleasingUnsafeMutalbePointer<String?>
。
函数指针
C函数指针会作为闭包导入Swift,闭包遵循C函数指针的调用惯例,并以@convention(c)
注解标记。例如,一个类型为int(*)(void)
的C函数指针导入Swift后成为@convention(c) () -> Int32
。
调用接受函数指针作为参数的函数时,你可以传一个顶层(top-level)的Swift函数,一个闭包字面量或nil
。以Core Foundation的CFArrayCreateMutable(_:_:_:)
函数作为例子,CFArrayCreateMutable(_:_:_:)
接受一个CFArrayCallBacks
结构体,这个结构体在初始化时需要函数指针用于回调:
func customCopyDescription(p: UnsafePointer<Void>) -> Unmanaged<CFString>! {
// 返回一个Unmanaged<CFString>!值
}
var callbacks = CFArrayCallBacks(
version: 0,
retain: nil,
release: nil,
copyDescription: customCopyDescription,
equal: { (p1, p2) -> DarwinBoolean in
// 返回Bool值
}
)
var mutableArray = CFArrayCreateMutable(nil, 0, &callbacks)
上面这个例子中,CFArrayCallBacks
构造方法用nil
作为retain
和release
参数,customCopyDescription
函数作为copyDescription
参数,一个闭包字面量作为equal
参数。
数据类型大小计算
在C语言中,sizeof
操作符可以返回任何变量和数据类型的大小。在Swift中,你可以使用sizeof
函数来获取给定类型的大小,或者sizeofValue
函数来获取给定值的大小。不过和C的sizeof
操作符不同的是,Swift的sizeof
和sizeofValue
函数不包含内存对齐必需的内存衬底(padding)。例如,Darwin中的timeval
结构体,在C语言中得到的大小是16字节,在Swift中得到的大小是12字节:
print(sizeof(timeval))
// 打印 “12”
要获得计算了数据结构内存对齐,与C语言的sizeof
操作符的输出一致的值,你可以使用strideof
或strideofValue
函数替代:
print(strideof(timeval))
// 打印 “16”
例如,setsockopt
函数能接受timeval
值的指针和大小作为参数,用作作套接字(socket)接收超时选项(SO_RCVTIMEO),这里的大小值就是通过strideof
来计算的:
let socketfd = socket(AF_INET, SOCK_STREAM, 0)
let optval = timeval(tv_sec: 30, tv_usec: 0)
let optlen = socklen_t(strideof(timeval))
if setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &optval, optlen) == 0 {
// ...
}
提示
只有符合C函数引用调用惯例的Swift函数能被用作函数指针参数。与C的函数指针一样,有
@convention(c)
注解的Swift函数类型不会捕获周围作用域的上下文环境。要了解更多信息,请参阅“Swift编程语言(Swift 2.2版)”的“类型参数”章节。
可变参数函数
在Swift中你可以使用getVaList(_:)
或withVaList(_:_:)
函数,调用C语言中的可变参数函数,例如vasprintf
。getVaList(_:)
函数接受一个`CVarArgType类型的数组,并返回一个
CVaListPointer值;而
withVaList(::)则把参数值作为闭包的传输传入,而不是直接返回这些值。得到的
CVaListPointer值随后传给C语言可变参数函数的
va_list参数。例如,下面是一个如何在Swift中调用
vasprintf`函数的例子:
func sprintf(format: String, _ args: CVarArgType...) -> String? {
return withVaList(args) { va_list in
var buffer: UnsafeMutablePointer<Int8> = nil
return format.withCString { CString in
guard vasprintf(&buffer, CString, va_list) != 0 else {
return nil
}
return String.fromCString(buffer)
}
}
}
print(sprintf("√2 ≡ %g", sqrt(2.0))!)
// 打印出 "√2 ≡ 1.41421"
全局常量
在C和Objective-C源码中定义的全局常量会被Swift编译器自动导入为Swift的全局常量。
预处理指令
Swift编译器不包含预处理器。作为替代,它能利用编译时(compile-time)参数,构建配置(build configuration)和语言特性来实现相同的功能。因此预处理指令不会导入Swift。
简单的宏
在C和Objective-C中用#define
指令创建一个基本常量的地方,在Swift中你可以通过定义全局常量来代替。例如,这个常量定义:#define FADE_ANIMATION_DURATION 0.35
,在Swift中用let FADE_ANIMATION_DURATION = 0.35
来表示。因为简单的类似常量的宏定义在Swift中会直接映射为全局变量,所以编译器会自动导入C和Objective-C源码中的简单的宏定义。
复杂的宏
C和Objective-C中使用的复杂的宏,在Swift中没有对应的概念。复杂的宏不是定义常量的宏,包括:参数化的宏、类似函数的宏。在C和Objective-C中使用复杂宏一般是为了避开类型检查的约束,或者是为了避免重复输入大量的模板(boilerplate)代码。然而宏会让除错和重构变得更加困难。在Swift中,你可以使用函数和泛型来达到同样的效果,而且不会产生副作用(without any compromises)。因此,C和Objective-C中的复杂宏在Swift代码中是不可用的。
构建配置
Swift代码和Objective-C代码通过不同的手段进行条件编译。Swift代码可以基于对构建配置的评估来实现条件编译。构建配置包含true
和false
字面值,命令行编译参数,以及下表中的这些平台检测函数。你还可以在命令行中使用-D <#flag#>
来设定编译参数。
函数 | 可用的参数 |
os() | OSX ,iOS ,watchOS ,tvOS |
arch() | x86_64 ,arm ,arm64 ,i386 |
提示
arch(arm)
编译配置在ARM 64设备上不会返回true
。arch(i386)
编译配置在代码为32位的iOS模拟器编译时返回true
。
一个简单的条件编译语句形式如下:
#if <#编译配置#>
<#语句#>
#else
<#语句#>
#endif
其中语句部分包含零条或更多条的合法Swift语句,可以包含表达式、一般语句和控制流语句。你可以使用&&
和||
操作符为一个条件编译语句增加额外的构建配置;用!
操作符来对构建配置取反;用#elseif
来增加一个条件语句块:
#if <#编译配置#> && !<#编译配置#>
<#语句#>
#elseif <#编译配置#>
<#语句#>
#else
<#语句#>
#endif
与C语言预处理起的条件编译指令不同的是,条件编译语句包围的代码块必需是自包含的,而且在语法上是合法的。这是因为所有的Swift代码都会经过语法检查—甚至是那些不会被编译的代码。