Men的博客

欢迎光临!

0%

原来的代码+原来的思想+Swift. 哪里出错了呢?
最理想的情况当然就是从一个100%的Swift项目开始.如果你能做到,很好.但是多数情况下,我们有着已存在的代码,却很想开始使用Swift语言,那我们应该从哪里开始呢?

为什么?
回过来说一下:你为什么想用Swift写程序呢?原因有很多:Swift语言是编程语言的新星;有着更棒的语法;Swift是苹果向我们推荐的iOS开发语言.
在将来,Swift的关注度会继续增加,并且Swift将成为开发OSX和iOS程序最适合,最受支持,最简便的编程语言.

SwiftAwesomeness

正如上图,这就是未来Swfit的趋势,那你又能做什么来从Swift方式开始呢?

Swift方式
有许多方面需要考虑,但我们先讨论下两大内容: 安全性和值语义.

安全性
Nil是Objective-C中很棒的存在:你可以发消息给nil,系统运行时会不会Crash,继续保持响应.

然而Swift中的nil是很不一样的,通常其类型机制会阻止你试图使用nil调用方法或者访问一个内容为nil的属性.虽然你可以避开这个类型机制,但这样做就如同在C语言中使用指向null的指针一样糟糕:你将容易在运行时掉入陷阱,使你的应用Crash.

在Swift中,所有都是类型安全的.一个String类型的字符串就是字符串,且不会为nil.理解上类似于C++的引用而不是C中的指针,因为它们永远不会为nil.

可选变量
在可选变量中,nil是可以使用的.一个可选类型的字符串变量可以是一个字符串,也可以为nil.你每次都要检查是什么.不然你可以强制解包这个可选变量,或者改变它成一个隐式解包的可选变量,这意味着成为一个可选类型但表现跟一个真正的值一样,一旦它是nil,这个app就会Crash

unwarp
Cocoa里充满了可选类型的变量,这代表着每次你所持有的数据,都不得不检查内部真正的内容

这在想法上是一个巨大的提升,你要明白不应该有机会向nil发送消息.在一个强类型语言机制里,要么它是nil,要么就是某个值.如果它在程序运行时未知的,就需要检查,不要试着强制解包它

将可选变量想做一个箱子:箱子里可以什么都没有(为nill),也可以有个值.但你在解包或者拆箱这个可选变量之前总是需要检查它,就像你会问在这个可选变量内部到底是什么?

box
在Swift已经有大量的其他例子显示了Swift语言的安全性:初始化方法,更少未定义的行为,内存安全.Nil安全是Swift语言面世以来相当常见的对语言安全性的体现.

值类型
值类型在Swift随处可见.显而易见,Obejctvie-C有着像NSInteger的基本数据类型,和像CGRect结构体.但是许多像NSString,NSArray等等都是类,为引用类型.

在Swfit中,一个完全不同的地方就在于它的标准库都有超过80个结构体,只有4个类,可以通过浏览头文件看到.

字符串,数字,集合这些类型在Swift都是值类型.这就意味着如果你有一个可变的Swfit字符串,把它传入一个函数,你将得到的是字符串的拷贝.再次说明下,这可不是一件糟糕的事:我们在一直使用Objective-C中,有copy和mutableCopy.这对于多数普遍类型的新默认行为是一个巨大的提升.

桥接
Swift被设计出来当然要能与Objective-C很好协同工作,由于Cocoa是为Objective-C建立的,所以这是很有必要的.所有这些Cocoa APIs 几乎都能被Swfit调用,这意味着你自定义的Objective-C类也能很好桥接到Swift类

这里就有问题出现了:以Swift开始,添加Objective-C,如何正像期望一样调用从Objective-C桥接到Swfit方法

Swift 到 Objective-C
Swiftz中有许多特性比如原生的结构体,加强的枚举等等,不能完全地桥接到Objective-C,这就意味着你使用Swift最新特性来写最新和最好的Swift框架,许多框架你就不能在Objective-C上使用.

即使你限制自己只用Swift一些能兼容的特性写程序,你也不能让Swift的类继承自Objective-C的类.你可以借鉴table views或collection views的模式,使用代理和布局对象来解决这个问题,但你要始终记得如果你Swfit的API需要继承,默认在Objective-C不可见的.

如果你用@objc
标记了你的类和协议,那么他们在Obejctive-C在就是可用的.dynamic
修饰符也显示使用了@objc
使得在Objective-C语言上可用,但它也使得你所限定的属性或者方法使用了Objective-C动态分发.

Swift 到 Objective-C

如果你想要使用swizzle或者其他动态特性,你将需要使用dynamic
标识符,仅仅@objc
不能保证它是使用objc_msgSend()
方法使得可能方法仍是编译过的或者内联的.
再一次强调:只有具有兼容性的特性才能工作.如果你在Swfit枚举对象中实现一个方法,它不会桥接过去,如果你的枚举存储不是int而是其他类型,它也不会桥接过去.

Objective-C 到 Swift: Nullability
Objective-C转向Swift有许多好的方面来优化.你给在Objective-C中属性,参数,和返回值的类型添加标注.
_Null_unspecified(default)

桥接为一个在Swift显示解包的可选值
_Nonnull

变量值不会为nil;桥接为一个常规的引用
_Nullable

变量值可以为nil;桥接为一个可选变量
如果你标注了你Obejctive-C代码, 在Swift中将会有很好的类型转变.即使你没有接触过Swfit,当你写Objective-C时这些标注会在你代码完成后出现.并且如果你声明一个方法参数为_Nonnull
而你传入了nil,你就会得到一个编译警告.
这是很好的练习来开始添加这些标注.在你使用已存在的API时会有所有帮助,让你更容易地开始使用Swfit.

Objective-C 到 Swift: Lightweight Generics
轻量泛型是Swfit2新出来的特性.那些NSArray,NSDictionary,NSSet能存储任何原来的NSObject对象的集合类型需要大量的转换.在Objective-C中不会出现这样的问题,但记得在Swfit的所有都安全相关,正确的转换需要大量的类型检查.你应该先检测类型,而不是强制转换.
现在有了泛型,这意味着你写的Objective-C代码会是这样的:
NSArray<NSString *> * _Nonnull
这是一个将存储NSString对象的NSArray.有着Nullability标注,你会明白这数组本身不能为nil;你将总能得到一个数组.如果你之前写过Java或C++你应该会熟悉这样的泛型写法.

而这个桥接到Swfit后会是这个样子的:[String]
一个简洁的Swfit的存储String的数组.

小小的提醒: 轻量泛型只对基础集合的array, dictionary, sets 起作用

需要跨的坑
我简易完全用Swfit开始新项目,如果你需要第三方库,这不会影响你用Swfit还是Obejctive-C,你都能调用.如果你已经有了存在的项目代码,想开始引入Swfit代码,尝试Obejctive-C向Swfit方法的保证桥接.例如已经实例化的视图控制器,视图在Swfit工作正常;它们都起源于NSObject,所以需要的话你能访问它们通过Objective-C.

Bridge

还有其他方面:Swfit泛型,枚举的存储不仅仅是整型,内嵌类型,结构体等等,这些需要等到你完全入门100%Swift,不要灰心,那天会比你所想的提前到来.

在那之前,继续友好地使用Obejctive-C,做好使用Swfit的准备.下面资源可以帮助你完成这一转变.

在Swfit中,什么时候用结构体,什么时候用类?

到底是用类的做法优于用结构体,还是用结构体的做法优于类。函数式编程倾向于值类型,面向对象编程更喜欢类。
在Swift 中,类和结构体有许多不同的特性。下面是两者不同的总结:

类支持继承,结构体不支持。
类是引用类型,结构体是值类型
并没有通用的规则决定结构体和类哪一个更好用。一般的建议是使用最小的工具来完成你的目标,但是有一个好的经验是多使用结构体,除非你用了继承和引用语义。想要了解更多,点击这里。

注意:在运行时,结构体的在性能方面更优于类,原因是结构体的方法调用是静态绑定,而类的方法调用是动态实现的。这就是尽可能得使用结构体代替类的又一个好的原因。

class 与 struct 的主要区别:类是引用类型,而结构体是值类型。

class 引用类型
//类 - 引用类型

class 未成型人类 {
var 身高 = 168
init() { }

init(身高: Int) {
self.身高 = 身高
}
}

let 小华 = 未成型人类()

let 大明 = 未成型人类(身高: 200)
let 黑仔 = 大明
let 高佬 = 大明
高佬.身高 += 10
print(大明.身高) // = 210

let 小高 = 高佬
小高.身高 += 10
print(大明.身高) // = 220

struct 值类型
//结构体 - 值类型

struct 克隆人 {
var 身高 = 180
init() { }
init(身高: Int) {
self.身高 = 身高
}
}

var 小A = 克隆人()
小A.身高 = 122
let cokeA = 小A
print(cokeA.身高) // = 122
print(小A.身高) // = 122

let 小B = 克隆人()
print(小A.身高) // = 180
什么时候使用类? 什么时候使用结构体?
注意:虽说赋值时候值类型表面上经常要执行拷贝现象,但 Swift 在管理所有的值拷贝会确保性能最优化,所以我们没有必要去回避赋值来确保性能最优化。
/* 什么时候使用类? 什么时候使用结构体?
一般情况下,可以:
1⃣ 把类看成是物体
如: 人、车、动物、商店等..一般跟生物沾边多
比如一个人有少属性,虽有不同人种,但是他们很多属性都是一样的,使用类的继承关系,很好描述人这一类东东…

2⃣ 把结构体看作值
如:位置(经纬度)、坐标、温度等..一般跟物理沾边多
结构体比类更加 “轻量级”,因为结构体的内存空间是开在系统的栈中,而类则开在系统的堆中。当然对于小程序来说,可以忽略不计。

还有什么时候使用也不是绝对的,因为他们都有很多类似的地方,也看个人习惯思维爱好之类。何况现在 Swift

designated initializer
convenience initializer
required 关键字
protocol中的initializer
Designated Initializer

当我们创建一个class的时候,如果这个class拥有未初始化的非Optional属性,则必须提供一个构造器(称为designated initializer)。这句话有两个重点:
1.未初始化;2.非Optional属性。
比较一下下面几种写法:

//A
class Person {
let name : String = “”
}
//B
class Person {
let name : String
}
//C
class Person {
var name : String?
}
//D
class Person {
let name : String?
}
A,C都可以正常编译,B,D会报错(error: class ‘Person’ has no initializers)。因为Swift不允许非Optional变量在没有初始化的情况下使用或者访问,这样会造成潜在的风险,于是Swift编译器对构造器做了严格的限制:类必须提供一个designated initializer,并且初始化所有未初始化的非Optional属性

A,B两种写法中,let换成var结果是一样的。但是D比较特殊,在Swift中,常量在访问和使用之前必须初始化(哪怕是Optional)

var p : String?
print(p)
//正常编译
let p : String?
print(p)
//报错:error: constant ‘p’ used before being initialized
由此可见我们上面的描述并不准确,应当是未初始化的非Optional属性或Optinal常量。不过一般我们不会声明一个Optional常量,另外这么说也是在太过拗口,所以就简单称之为未初始化的非Optional属性吧(下文也会这样表述)。

同样,在子类初始化过程中,Swift也做了严格的限制

class Person {
let name : String
init(name : String){
self.name = name
}
}
class Engineer: Person {
var title : String
init(name: String, title: String) {
self.title = title
super.init(name: name)
}
}
首先,Engineer必须调用super.init,如果你这样写:

init(name: String, title: String) {
self.title = title
self.name = name
}
不好意思,self.name是常量,不可修改。即便我们将let name : String改为var name : String,仍会报错,而且是两个:

error: super.init isn’t called on all paths before returning from initializer
error: use of ‘self’ in property access ‘name’ before super.init initializes self
从这两个错误信息可以看出,在子类的构造器中,父类的成员变量必须通过父类的designated initializer进行初始化。也就是说,我们必须调用super.init(第一个error),而且在调用super.init之前不能使用和访问任何父类的成员变量(第二个error)。

其次,super.init必须在子类所有未初始化的非Optional属性初始化之后调用。

这跟我们以前所了解的不同,例如在JAVA的构造器中我们会先调用super.init。Swift之所以这样做,同样是为了安全性。
假设我们要求在初始化一个Person之后,立刻输出一些信息用来debug:

class Person {
var name : String
init(name : String){
self.name = name
printDescription()
}

func printDescription() -> () {
print(“name=(name)”)
}
}
class Engineer: Person {
var title : String
init(name: String, title: String) {
self.title = title
super.init(name: name)
}

override func printDescription() -> () {
print(“name=(name), title=(title)”)
}
}
如果在Engineer的init中,我们首先调用super.init,那么我们就会在初始化title之前执行子类的printDescription方法,这是Swift所不允许的。

Convenience Initializer
一个类可以提供多个designated initializer,但是designated initializer不可以调用designated initializer。
如果需要提供一个依赖designated initializer的构造器,需要添加关键字convenience,称为convenience initializer。
在上面的例子中,假设我们从文件中读取人的信息,读取到的是“名字;职位;联系方式”格式的字符串。我们需要写一个构造器方法从这串文字中解析出相应的信息。

class Person {
let name : String
init(name : String){
self.name = name
}

convenience init(convertFromLiteral value : String){
var name = “”
let components = value.componentsSeparatedByString(“;”)
for component in components{
if(name.characters.count == 0){
name = component
break
}
}
self.init(name: name)
}
}
因为新的构造器调用了self.init,因此需要添加关键字convenience,否则会报错:error: designated initializer for ‘Person’ cannot delegate (with ‘self.init’); did you mean this to be a convenience initializer?

一个类可以拥有多个convenience initializer,而且convenience initializer可以调用其他convenience initializer。
convenience initializer有以下约束:

必须调用当前类的designated initializer,调用父类的designated initializer是不行的。
只有当前类可以使用(子类也可以使用,不过需要特殊处理。见下文)。
Required 关键字
如果父类中的构造器需要在子类中重写,可以使用required关键字。子类必须重写父类中标记为required的构造器方法(不管是designated initializer还是convenience initializer)。
上面的例子中,init(convertFromLiteral value : String)并不适合子类,所以我们加上required关键字,编译器会提示我们Engineer没有实现required initializer。

class Engineer: Person {
var title : String

init(name: String, title: String) {
self.title = title
super.init(name: name)
}

required convenience init(convertFromLiteral value : String){
var name = “”
var title = “”
let components = value.componentsSeparatedByString(“;”)
for component in components{
if(name.characters.count == 0){
name = component
}else if(title.characters.count == 0){
title = component
break
}
}
self.init(name: name, title: title)
}
}
上文提到了convenience initializer只有当前类才能使用,Swift不允许子类通过父类构造器进行初始化是显而易见的。convenience initializer实际上更像一个方法,如果我们将某个convenience initializer所依赖的designated initializer标记为required,就相当于告诉了编译器我们的子类一定重写了这个designated initializer。也就是说,如果我们的子类通过这个convenience initializer进行初始化,最终会执行子类中重写的designated initializer。

说这么一大堆,用代码看的更清楚:

class Person {
let name : String
required init(name : String){
self.name = name
}

convenience init(convertFromLiteral value : String){
var name = “”
let components = value.componentsSeparatedByString(“;”)
for component in components{
if(name.characters.count == 0){
name = component
break
}
}
self.init(name: name)
}
}

class Engineer: Person {
var title : String

required init(name: String) {
self.title = “unknow”
super.init(name: name)
}

init(name: String, title: String) {
self.title = title
super.init(name: name)
}
}

let peter = Person.init(convertFromLiteral: “peter;”)
print(peter.name)
let tom = Engineer.init(convertFromLiteral: “tom;”)
print(tom.name + “, “ + tom.title)
将父类中convenience initializer所依赖的designated initializer标记为required,我们就可以通过该convenience initializer初始化子类了。

Protocol中的Initializer
Swift提供了很多包涵构造器的protocol,如StringLiteralConvertible,DictionaryLiteralConvertible,ArrayLiteralConvertible等。

如果我们想通过如下方式初始化,我们就需要用到StringLiteralConvertible

let peter : Person = “Peter”
代码如下:

class Person: StringLiteralConvertible{
let name: String
init(name value: String) {
self.name = value
}

required convenience init(stringLiteral value: String) {
self.init(name: value)
}

required convenience init(extendedGraphemeClusterLiteral value: String) {
self.init(name: value)
}

required convenience init(unicodeScalarLiteral value: String) {
self.init(name: value)
}
}

let peter : Person = “Peter”
是不是很cool?

需要注意的是,Swfit很好的控制了构造器的作用域(默认情况下,convenience initializer是子类不可见的),但是protocol中的方法是子类可见的。所以protocol中的构造器方法都必须声明为required。
如果实现protocol的是一个final类,那么则没必要再是required,因为它不会有子类。
等一下,如果Person是第三方库中的类呢?
你可能会说没问题啊,我们可以通过extension实现StringLiteralConvertible

class Person: StringLiteralConvertible{
let name: String
init(name value: String) {
self.name = value
}

}

extension Person: StringLiteralConvertible{
required convenience init(stringLiteral value: String) {
self.init(name: value)
}

required convenience init(extendedGraphemeClusterLiteral value: String) {
self.init(name: value)
}

required convenience init(unicodeScalarLiteral value: String) {
self.init(name: value)
}
}

let peter : Person = “Peter”
真是这样吗?Sorry!!!

首先,required initializer必须在类中声明。所以上面的required关键字要不得
其次,protocol中的构造器方法必须声明为required,除非Person是final的。

参考:https://segmentfault.com/a/1190000004023852

协议(Protocols)
我已经提及了structs和enums之间的相似性。除了附加方法的能力之外,Swift也允许你在枚举中使用协议(Protocols)和协议扩展(Protocol Extension)。
Swift协议定义一个接口或类型以供其他数据结构来遵循。enum当然也不例外。我们先从Swift标准库中的一个例子开始.
CustomStringConvertible是一个以打印为目的的自定义格式化输出的类型。
protocol CustomStringConvertible {
var description: String { get }
}
该协议只有一个要求,即一个只读(getter)类型的字符串(String类型)。我们可以很容易为enum实现这个协议。
enum Trade: CustomStringConvertible {
case Buy, Sell
var description: String {
switch self {
case Buy: return “We’re buying something”
case Sell: return “We’re selling something”
}
}
}
let action = Trade.Buy
print(“this action is (action)”)
// prints: this action is We’re buying something
一些协议的实现可能需要根据内部状态来相应处理要求。例如定义一个管理银行账号的协议。
protocol AccountCompatible {
var remainingFunds: Int { get }
mutating func addFunds(amount: Int) throws
mutating func removeFunds(amount: Int) throws
}
你也许会简单地拿struct实现这个协议,但是考虑应用的上下文,enum是一个更明智的处理方法。不过你无法添加一个存储属性到enum中,就像var remainingFuns:Int。那么你会如何构造呢?答案灰常简单,你可以使用关联值完美解决:
enum Account {
case Empty
case Funds(remaining: Int)
enum Error: ErrorType {
case Overdraft(amount: Int)
}
var remainingFunds: Int {
switch self {
case Empty: return 0
case Funds(let remaining): return remaining
}
}
}
为了保持代码清爽,我们可以在enum的协议扩展(protocl extension)中定义必须的协议函数:
extension Account: AccountCompatible {

mutating func addFunds(amount: Int) throws {
var newAmount = amount
if case let .Funds(remaining) = self {
newAmount += remaining
}
if newAmount < 0 {
throw Error.Overdraft(amount: -newAmount)
} else if newAmount == 0 {
self = .Empty
} else {
self = .Funds(remaining: newAmount)
}
}

mutating func removeFunds(amount: Int) throws {
try self.addFunds(amount * -1)
}

}
var account = Account.Funds(remaining: 20)
print(“add: “, try? account.addFunds(10))
print (“remove 1: “, try? account.removeFunds(15))
print (“remove 2: “, try? account.removeFunds(55))
// prints:
// : add: Optional(())
// : remove 1: Optional(())
// : remove 2: nil
正如你所看见的,我们通过将值存储到enum cases中实现了协议所有要求项。如此做法还有一个妙不可言的地方:现在整个代码基础上你只需要一个模式匹配就能测试空账号输入的情况。你不需要关心剩余资金是否等于零。

同时,我们也在账号(Accout)中内嵌了一个遵循ErrorType协议的枚举,这样我们就可以使用Swift2.0语法来进行错误处理了。这里给出更详细的使用案例教程。

扩展(Extensions)
正如刚才所见,枚举也可以进行扩展。最明显的用例就是将枚举的case和method分离,这样阅读你的代码能够简单快速地消化掉enum内容,紧接着转移到方法定义:
enum Entities {
case Soldier(x: Int, y: Int)
case Tank(x: Int, y: Int)
case Player(x: Int, y: Int)
}
现在,我们为enum扩展方法:
extension Entities {
mutating func move(dist: CGVector) {}
mutating func attack() {}
}
你同样可以通过写一个扩展来遵循一个特定的协议:
extension Entities: CustomStringConvertible {
var description: String {
switch self {
case let .Soldier(x, y): return “(x), (y)”
case let .Tank(x, y): return “(x), (y)”
case let .Player(x, y): return “(x), (y)”
}
}
}
枚举泛型(Generic Enums)
枚举也支持泛型参数定义。你可以使用它们以适应枚举中的关联值。就拿直接来自Swift标准库中的简单例子来说,即Optional类型。你主要可能通过以下几种方式使用它:可选链(optional chaining(?))、if-let可选绑定、guard let、或switch,但是从语法角度来说你也可以这么使用Optional:
let aValue = Optional.Some(5)
let noValue = Optional.None
if noValue == Optional.None { print(“No value”) }
这是Optional最直接的用例,并未使用任何语法糖,但是不可否认Swift中语法糖的加入使得你的工作更简单。如果你观察上面的实例代码,你恐怕已经猜到Optional内部实现是这样的5:
// Simplified implementation of Swift’s Optional
enum MyOptional {
case Some(T)
case None
}
这里有啥特别呢?注意枚举的关联值采用泛型参数T作为自身类型,这样可选类型构造任何你想要的返回值。
枚举可以拥有多个泛型参数。就拿熟知的Either类为例,它并非是Swift标准库中的一部分,而是实现于众多开源库以及
其他函数式编程语言,比如Haskell或F#。设计想法是这样的:相比较仅仅返回一个值或没有值(née Optional),你更期望返回一个成功值或者一些反馈信息(比如错误值)。
// The well-known either type is, of course, an enum that allows you to return either
// value one (say, a successful value) or value two (say an error) from a function
enum Either<T1, T2> {
case Left(T1)
case Right(T2)
}
最后,Swift中所有在class和struct中奏效的类型约束,在enum中同样适用。
// Totally nonsensical example. A bag that is either full (has an array with contents)
// or empty.
enum Bag<T: SequenceType where T.Generator.Element==Equatable> {
case Empty
case Full(contents: T)
}
递归 / 间接(Indirect)类型
间接类型是 Swift 2.0 新增的一个类型。 它们允许将枚举中一个 case 的关联值再次定义为枚举。举个例子,假设我们想定义一个文件系统,用来表示文件以及包含文件的目录。如果将文件和目录定义为枚举的 case,则目录 case 的关联值应该再包含一个文件的数组作为它的关联值。因为这是一个递归的操作,编译器需要对此进行一个特殊的准备。Swift 文档中是这么写的:

枚举和 case 可以被标记为间接的(indrect),这意味它们的关联值是被间接保存的,这允许我们定义递归的数据结构。
所以,如果我们要定义 FileNode 的枚举,它应该会是这样的:

enum FileNode {
case File(name: String)
indirect case Folder(name: String, files: [FileNode])
}
此处的 indrect 关键字告诉编译器间接地处理这个枚举的 case。也可以对整个枚举类型使用这个关键字。作为例子,我们来定义一个二叉树:

indirect enum Tree<Element: Comparable> {
case Empty
case Node(Tree,Element,Tree)
}
这是一个很强大的特性,可以让我们用非常简洁的方式来定义一个有着复杂关联的数据结构。

使用自定义类型作为枚举的值
如果我们忽略关联值,则枚举的值就只能是整型,浮点型,字符串和布尔类型。如果想要支持别的类型,则可以通过实现 StringLiteralConvertible 协议来完成,这可以让我们通过对字符串的序列化和反序列化来使枚举支持自定义类型。

作为一个例子,假设我们要定义一个枚举来保存不同的 iOS 设备的屏幕尺寸:

enum Devices: CGSize {
case iPhone3GS = CGSize(width: 320, height: 480)
case iPhone5 = CGSize(width: 320, height: 568)
case iPhone6 = CGSize(width: 375, height: 667)
case iPhone6Plus = CGSize(width: 414, height: 736)
}
然而,这段代码不能通过编译。因为 CGPoint 并不是一个常量,不能用来定义枚举的值。我们需要为想要支持的自定义类型增加一个扩展,让其实现 StringLiteralConvertible 协议。这个协议要求我们实现三个构造方法,这三个方法都需要使用一个String类型的参数,并且我们需要将这个字符串转换成我们需要的类型(此处是CGSize)。

extension CGSize: StringLiteralConvertible {
public init(stringLiteral value: String) {
let size = CGSizeFromString(value)
self.init(width: size.width, height: size.height)
}

public init(extendedGraphemeClusterLiteral value: String) {
let size = CGSizeFromString(value)
self.init(width: size.width, height: size.height)
}

public init(unicodeScalarLiteral value: String) {
let size = CGSizeFromString(value)
self.init(width: size.width, height: size.height)
}
}
现在就可以来实现我们需要的枚举了,不过这里有一个缺点:初始化的值必须写成字符串形式,因为这就是我们定义的枚举需要接受的类型(记住,我们实现了 StringLiteralConvertible,因此String可以转化成CGSize类型)

enum Devices: CGSize {
case iPhone3GS = “{320, 480}”
case iPhone5 = “{320, 568}”
case iPhone6 = “{375, 667}”
case iPhone6Plus = “{414, 736}”
}
终于,我们可以使用 CGPoint 类型的枚举了。需要注意的是,当要获取真实的 CGPoint 的值的时候,我们需要访问枚举的是 rawValue 属性。

let a = Devices.iPhone5
let b = a.rawValue
print(“the phone size string is (a), width is (b.width), height is (b.height)”)
// prints : the phone size string is iPhone5, width is 320.0, height is 568.0
使用字符串序列化的形式,会让使用自定义类型的枚举比较困难,然而在某些特定的情况下,这也会给我们增加不少便利(比较使用NSColor / UIColor的时候)。不仅如此,我们完全可以对自己定义的类型使用这个方法。

对枚举的关联值进行比较
在通常情况下,枚举是很容易进行相等性判断的。一个简单的 enum T { case a, b } 实现默认支持相等性判断 T.a == T.b, T.b != T.a

然而,一旦我们为枚举增加了关联值,Swift 就没有办法正确地为两个枚举进行相等性判断,需要我们自己实现 == 运行符。这并不是很困难:

enum Trade {
case Buy(stock: String, amount: Int)
case Sell(stock: String, amount: Int)
}
func ==(lhs: Trade, rhs: Trade) -> Bool {
switch (lhs, rhs) {
case let (.Buy(stock1, amount1), .Buy(stock2, amount2))
where stock1 == stock2 && amount1 == amount2:
return true
case let (.Sell(stock1, amount1), .Sell(stock2, amount2))
where stock1 == stock2 && amount1 == amount2:
return true
default: return false
}
}
正如我们所见,我们通过 switch 语句对两个枚举的 case 进行判断,并且只有当它们的 case 是匹配的时候(比如 Buy 和 Buy)才对它们的真实关联值进行判断。

自定义构造方法
在 静态方法 一节当中我们已经提到它们可以作为从不同数据构造枚举的方便形式。在之前的例子里也展示过,对出版社经常误用的苹果设备名返回正确的名字:

enum Device {
case AppleWatch
static func fromSlang(term: String) -> Device? {
if term == “iWatch” {
return .AppleWatch
}
return nil
}
}
我们也可以使用自定义构造方法来替换静态方法。枚举与结构体和类的构造方法最大的不同在于,枚举的构造方法需要将隐式的 self 属性设置为正确的 case。

enum Device {
case AppleWatch
init?(term: String) {
if term == “iWatch” {
self = .AppleWatch
}
return nil
}
}
在这个例子中,我们使用了可失败(failable)的构造方法。但是,普通的构造方法也可以工作得很好:

enum NumberCategory {
case Small
case Medium
case Big
case Huge
init(number n: Int) {
if n < 10000 { self = .Small }
else if n < 1000000 { self = .Medium }
else if n < 100000000 { self = .Big }
else { self = .Huge }
}
}
let aNumber = NumberCategory(number: 100)
print(aNumber)
// prints: “Small”
对枚举的 case 进行迭代
一个特别经常被问到的问题就是如何对枚举中的 case 进行迭代。可惜的是,枚举并没有遵守SequenceType协议,因此没有一个官方的做法来对其进行迭代。取决于枚举的类型,对其进行迭代可能也简单,也有可能很困难。在StackOverflow上有一个很好的讨论贴。贴子里面讨论到的不同情况太多了,如果只在这里摘取一些会有片面性,而如果将全部情况都列出来,则会太多。

对 Objective-C 的支持
基于整型的枚举,如 enum Bit: Int { case Zero = 0; case One = 1 } 可以通过 @objc 标识来将其桥接到 Objective-C 当中。然而,一旦使用整型之外的类型(如 String)或者开始使用关联值,我们就无法在 Objective-C 当中使用这些枚举了。

有一个名为_ObjectiveCBridgeable的隐藏协议,可以让规范我们以定义合适的方法,如此一来,Swift 便可以正确地将枚举转成 Objective-C 类型,但我猜这个协议被隐藏起来一定是有原因的。然而,从理论上来讲,这个协议还是允许我们将枚举(包括其实枚举值)正确地桥接到 Objective-C 当中。

但是,我们并不一定非要使用上面提到的这个方法。为枚举添加两个方法,使用 @objc 定义一个替代类型,如此一来我们便可以自由地将枚举进行转换了,并且这种方式不需要遵守私有协议:

enum Trade {
case Buy(stock: String, amount: Int)
case Sell(stock: String, amount: Int)
}

// 这个类型也可以定义在 Objective-C 的代码中
@objc class OTrade: NSObject {
var type: Int
var stock: String
var amount: Int
init(type: Int, stock: String, amount: Int) {
self.type = type
self.stock = stock
self.amount = amount
}
}

extension Trade {

func toObjc() -> OTrade {
switch self {
case let .Buy(stock, amount):
return OTrade(type: 0, stock: stock, amount: amount)
case let .Sell(stock, amount):
return OTrade(type: 1, stock: stock, amount: amount)
}
}

static func fromObjc(source: OTrade) -> Trade? {
switch (source.type) {
case 0: return Trade.Buy(stock: source.stock, amount: source.amount)
case 1: return Trade.Sell(stock: source.stock, amount: source.amount)
default: return nil
}
}
}
这个方法有一个的缺点,我们需要将枚举映射为 Objective-C 中的 NSObject 基础类型(我们也可以直接使用 NSDictionary),但是,当我们碰到一些确实需要在 Objective-C 当中获取有关联值的枚举时,这是一个可以使用的方法。

枚举底层
Erica Sadun 写过一篇很流弊的关于枚举底层的博客,涉及到枚举底层的方方面面。在生产代码中绝不应该使用到这些东西,但是学习一下还是相当有趣的。在这里,我准备只提到那篇博客中一条,如果想了解更多,请移步到原文:

枚举通常都是一个字节长度。[…]如果你真的很傻很天真,你当然可以定义一个有成百上千个 case 的枚举,在这种情况下,取决于最少所需要的比特数,枚举可能占据两个字节或者更多。
Swift 标准库中的枚举
在我们准备继续探索枚举在项目中的不同用例之前,先看一下在 Swift 标准库当中是如何使用枚举可能会更诱人,所以现在让我们先来看看。

Bit 这个枚举有两个值,One 和 Zero。它被作为 CollectionOfOne 中的 Index 类型。
FloatingPointClassification 这个枚举定义了一系列 IEEE 754 可能的类别,比如 NegativeInfinity, PositiveZero 或 SignalingNaN。
Mirror.AncestorRepresentation 和 Mirror.DisplayStyle 这两个枚举被用在 Swift 反射 API 的上下文当中。
Optional 这个就不用多说了
Process 这个枚举包含了当前进程的命令行参数(Process.argc, Process.arguments)。这是一个相当有趣的枚举类型,因为在 Swift 1.0 当中,它是被作为一个结构体来实现的。
实践用例
我们已经在前面几个小节当中看过了许多有用的枚举类型。包括 Optional,Either, FileNode 还有二叉树。然而,还存在很多场合,使用枚举要胜过使用结构体和类。一般来讲,如果问题可以被分解为有限的不同类别,则使用枚举应该就是正确的选择。即使只有两种 case,这也是一个使用枚举的完美场景,正如 Optional 和 Either 类型所展示的。

以下列举了一些枚举类型在实战中的使用示例,可以用来点燃你的创造力。

错误处理
说到枚举的实践使用,当然少不了在 Swift 2.0 当中新推出的错误处理。标记为可抛出的函数可以抛出任何遵守了 ErrorType 空协议的类型。正如 Swift 官方文档中所写的:

Swift 的枚举特别适用于构建一组相关的错误状态,可以通过关联值来为其增加额外的附加信息。
作为一个示例,我们来看下流行的JSON解析框架 Argo。当 JSON 解析失败的时候,它有可能是以下两种主要原因:

JSON 数据缺少某些最终模型所需要的键(比如你的模型有一个 username 的属性,但是 JSON 中缺少了)
存在类型不匹配,比如说 username 需要的是 String 类型,而 JSON 中包含的是 NSNull6。
除此之外,Argo 还为不包含在上述两个类别中的错误提供了自定义错误。它们的 ErrorType 枚举是类似这样的:

enum DecodeError: ErrorType {
case TypeMismatch(expected: String, actual: String)
case MissingKey(String)
case Custom(String)
}
所有的 case 都有一个关联值用来包含关于错误的附加信息。

一个更加通用的用于完整 HTTP / REST API 错误处理的ErrorType应该是类似这样的:

enum APIError : ErrorType {
// Can’t connect to the server (maybe offline?)
case ConnectionError(error: NSError)
// The server responded with a non 200 status code
case ServerError(statusCode: Int, error: NSError)
// We got no data (0 bytes) back from the server
case NoDataError
// The server response can’t be converted from JSON to a Dictionary
case JSONSerializationError(error: ErrorType)
// The Argo decoding Failed
case JSONMappingError(converstionError: DecodeError)
}
这个 ErrorType 实现了完整的 REST 程序栈解析有可能出现的错误,包含了所有在解析结构体与类时会出现的错误。

如果你看得够仔细,会发现在JSONMappingError中,我们将Argo中的DecodeError封装到了我们的APIError类型当中,因为我们会用 Argo 来作实际的 JSON 解析。

更多关于ErrorType以及此种枚举类型的示例可以参看官方文档。

观察者模式
在 Swift 当中,有许多方法来构建观察模式。如果使用 @objc 兼容标记,则我们可以使用 NSNotificationCenter 或者 KVO。即使不用这个标记,didSet语法也可以很容易地实现简单的观察模式。在这里可以使用枚举,它可以使被观察者的变化更加清晰明了。设想我们要对一个集合进行观察。如果我们稍微思考一下就会发现这只有几种可能的情况:一个或多个项被插入,一个或多个项被删除,一个或多个项被更新。这听起来就是枚举可以完成的工作:

enum Change {
case Insertion(items: [Item])
case Deletion(items: [Item])
case Update(items: [Item])
}
之后,观察对象就可以使用一个很简洁的方式来获取已经发生的事情的详细信息。这也可以通过为其增加 oldValue 和 newValue 的简单方法来扩展它的功能。

状态码
如果我们正在使用一个外部系统,而这个系统使用了状态码(或者错误码)来传递错误信息,类似 HTTP 状态码,这种情况下枚举就是一种很明显并且很好的方式来对信息进行封装7 。

enum HttpError: String {
case Code400 = “Bad Request”
case Code401 = “Unauthorized”
case Code402 = “Payment Required”
case Code403 = “Forbidden”
case Code404 = “Not Found”
}
结果类型映射(Map Result Types)
枚举也经常被用于将 JSON 解析后的结果映射成 Swift 的原生类型。这里有一个简短的例子:

enum JSON {
case JSONString(Swift.String)
case JSONNumber(Double)
case JSONObject([String : JSONValue])
case JSONArray([JSONValue])
case JSONBool(Bool)
case JSONNull
}
类似地,如果我们解析了其它的东西,也可以使用这种方式将解析结果转化我们 Swift 的类型。

UIKit 标识
枚举可以用来将字符串类型的重用标识或者 storyboard 标识映射为类型系统可以进行检查的类型。假设我们有一个拥有很多原型 Cell 的 UITableView:

enum CellType: String {
case ButtonValueCell = “ButtonValueCell”
case UnitEditCell = “UnitEditCell”
case LabelCell = “LabelCell”
case ResultLabelCell = “ResultLabelCell”
}
单位
单位以及单位转换是另一个使用枚举的绝佳场合。可以将单位及其对应的转换率映射起来,然后添加方法来对单位进行自动的转换。以下是一个相当简单的示例:

enum Liquid: Float {
case ml = 1.0
case l = 1000.0
func convert(amount amount: Float, to: Liquid) -> Float {
if self.rawValue < to.rawValue {
return (self.rawValue / to.rawValue) * amount
} else {
return (self.rawValue * to.rawValue) * amount
}
}
}
// Convert liters to milliliters
print (Liquid.l.convert(amount: 5, to: Liquid.ml))
另一个示例是货币的转换。以及数学符号(比如角度与弧度)也可以从中受益。

游戏
游戏也是枚举中的另一个相当好的用例,屏幕上的大多数实体都属于一个特定种族的类型(敌人,障碍,纹理,…)。相对于本地的 iOS 或者 Mac 应用,游戏更像是一个白板。即开发游戏我们可以使用全新的对象以及全新的关联创造一个全新的世界,而 iOS 或者 OSX 需要使用预定义的 UIButtons,UITableViews,UITableViewCells 或者 NSStackView.

不仅如此,由于枚举可以遵守协议,我们可以利用协议扩展和基于协议的编程为不同为游戏定义的枚举增加功能。这里是一个用来展示这种层级的的简短示例:

enum FlyingBeast { case Dragon, Hippogriff, Gargoyle }
enum Horde { case Ork, Troll }
enum Player { case Mage, Warrior, Barbarian }
enum NPC { case Vendor, Blacksmith }
enum Element { case Tree, Fence, Stone }

protocol Hurtable {}
protocol Killable {}
protocol Flying {}
protocol Attacking {}
protocol Obstacle {}

extension FlyingBeast: Hurtable, Killable, Flying, Attacking {}
extension Horde: Hurtable, Killable, Attacking {}
extension Player: Hurtable, Obstacle {}
extension NPC: Hurtable {}
extension Element: Obstacle {}
字符串类型化
在一个稍微大一点的 Xcode 项目中,我们很快就会有一大堆通过字符串来访问的资源。在前面的小节中,我们已经提过重用标识和 storyboard 的标识,但是除了这两样,还存在很多资源:图像,Segues,Nibs,字体以及其它资源。通常情况下,这些资源都可以分成不同的集合。如果是这样的话,一个类型化的字符串会是一个让编译器帮我们进行类型检查的好方法。

enum DetailViewImages: String {
case Background = “bg1.png”
case Sidebar = “sbg.png”
case ActionButton1 = “btn1_1.png”
case ActionButton2 = “btn2_1.png”
}
对于 iOS 开发者,R.swift这个第三方库可以为以上提到的情况自动生成结构体。但是有些时候你可能需要有更多的控制(或者你可能是一个Mac开发者8)。

API 端点
Rest API 是枚举的绝佳用例。它们都是分组的,它们都是有限的 API 集合,并且它们也可能会有附加的查询或者命名的参数,而这可以使用关联值来实现。

这里有个 Instagram API 的简化版:

enum Instagram {
enum Media {
case Popular
case Shortcode(id: String)
case Search(lat: Float, min_timestamp: Int, lng: Float, max_timestamp: Int, distance: Int)
}
enum Users {
case User(id: String)
case Feed
case Recent(id: String)
}
}
Ash Furrow的Moya框架就是基本这个思想,使用枚举对 rest 端点进行映射。

链表
Airspeed Velocity有一篇极好的文章说明了如何使用枚举来实现一个链表。那篇文章中的大多数代码都超出了枚举的知识,并涉及到了大量其它有趣的主题9,但是,链表最基本的定义是类似这样的(我对其进行了一些简化):

enum List {
case End
indirect case Node(Int, next: List)
}
每一个节点(Node) case 都指向了下一个 case, 通过使用枚举而非其它类型,我们可以避免使用一个可选的 next 类型以用来表示链表的结束。

Airspeed Velocity 还写过一篇超赞的博客,关于如何使用 Swift 的间接枚举类型来实现红黑树,所以如果你已经阅读过关于链表的博客,你可能想继续阅读这篇关于红黑树的博客。

设置字典(Setting Dictionaries)
这是 Erica Sadun 提出的非常非常机智的解决方案。简单来讲,就是任何我们需要用一个属性的字典来对一个项进行设置的时候,都应该使用一系列有关联值的枚举来替代。使用这方法,类型检查系统可以确保配置的值都是正确的类型。

关于更多的细节,以及合适的例子,可以阅读下她的文章。

局限
与之前类似,我将会用一系列枚举的局限性来结束本篇文章。

提取关联值
David Owens写过一篇文章,他觉得当前的关联值提取方式是很笨重的。我墙裂推荐你去看一下他的原文,在这里我对它的要旨进行下说明:为了从一个枚举中获取关联值,我们必须使用模式匹配。然而,关联值就是关联在特定枚举 case 的高效元组。而元组是可以使用更简单的方式来获取它内部值,即 .keyword 或者 .0。

// Enums
enum Ex { case Mode(ab: Int, cd: Int) }
if case Ex.Mode(let ab, let cd) = Ex.Mode(ab: 4, cd: 5) {
print(ab)
}
// vs tuples:
let tp = (ab: 4, cd: 5)
print(tp.ab)
如果你也同样觉得我们应该使用相同的方法来对枚举进行解构(deconstruct),这里有个 rdar: rdar://22704262 (译者注:一开始我不明白 rdar 是啥意思,后来我 google 了下,如果你也有兴趣,也可以自己去搜索一下)

相等性
拥有关联值的枚举没有遵守 equatable 协议。这是一个遗憾,因为它为很多事情增加了不必要的复杂和麻烦。深层的原因可能是因为关联值的底层使用是使用了元组,而元组并没有遵守 equatable 协议。然而,对于限定的 case 子集,如果这些关联值的类型都遵守了 equatable 类型,我认为编译器应该默认为其生成 equatable 扩展。

// Int 和 String 是可判等的, 所以 Mode 应该也是可判等的
enum Ex { case Mode(ab: Int, cd: String) }

// Swift 应该能够自动生成这个函数
func == (lhs: Ex.Mode, rhs: Ex.Mode) -> Bool {
switch (lhs, rhs) {
case (.Mode(let a, let b), .Mode(let c, let d)):
return a == c && b == d
default:
return false
}
}
元组(Tuples)
最大的问题就是对元组的支持。我喜欢使用元组,它们可以使很多事情变得更简单,但是他们目前还处于无文档状态并且在很多场合都无法使用。在枚举当中,我们无法使用元组作为枚举的值:

enum Devices: (intro: Int, name: String) {
case iPhone = (intro: 2007, name: “iPhone”)
case AppleTV = (intro: 2006, name: “Apple TV”)
case AppleWatch = (intro: 2014, name: “Apple Watch”)
}
这似乎看起来并不是一个最好的示例,但是我们一旦开始使用枚举,就会经常陷入到需要用到类似上面这个示例的情形中。

迭代枚举的所有case
这个我们已经在前面讨论过了。目前还没有一个很好的方法来获得枚举中的所有 case 的集合以使我们可以对其进行迭代。

默认关联值
另一个会碰到的事是枚举的关联值总是类型,但是我们却无法为这些类型指定默认值。假设有这样一种情况:

enum Characters {
case Mage(health: Int = 70, magic: Int = 100, strength: Int = 30)
case Warrior(health: Int = 100, magic: Int = 0, strength: Int = 100)
case Neophyte(health: Int = 50, magic: Int = 20, strength: Int = 80)
}
我们依然可以使用不同的值创建新的 case,但是角色的默认设置依然会被映射。

Swift枚举基本概念

Swift中的枚举比OC中的枚举强大, 因为Swift中的枚举是一等类型,
它可以像类和结构体一样增加属性和方法
enum Method{
case 枚举值
}
2. 枚举的基本使用
我们注意到,swift 的每个枚举项前面,都使用一个 case 关键字来标识。除了每行声明一个枚举项,也可以将这些枚举项放在一行中声明,每项之间用逗号分隔。
enum Menthod1 {
case Add, Sub, Mul, Div
}
可以使用枚举类型变量或常量接收枚举值
var m: Method = .Add
注意: 如果变量或常量没有指定类型, 那么前面必须加上该值属于哪个枚举类型
// var m1 = .Add //直接报错
var m1 = Method.Add //正确写法
3.原始值
OC中枚举的本质就是整数, 所以OC中的枚举是有原始值的,默认是从0开始
而Swift中的枚举默认是没有原始值的, 但是可以在定义时告诉系统让枚举有原始值和OC中的枚举一样, 也可以指定原始值, 后面的值默认+1 , 但必须指定枚举的类型, 并且一定是 Int 类型
Swift中的枚举除了可以指定整形以外还可以指定其它类型, 但是如果指定其它类型, 必须给所有枚举值赋值, 因为不能自动递增
// 指定了枚举原始值类型为 Int , 所以并且给第一个枚举值定义了原始值, 则后面的值默认 +1
enum Method3: Int{
case Add = 5, Sub, Mul, Div
}
// 自定的枚举原始值的类型不是 Int , 所以后面的值不会默认 +1,
// 后面的也需要手动赋值
enum Method4: Double{
case Add = 5.0, Sub = 6.0, Mul = 6.1, Div = 8.0
}
原始值转换为枚举值
enum Method5: String{
case Add = “add”, Sub = “sub”, Mul = “mul”, Div = “div”
}
获取枚举原始值
rawValue代表将枚举值转换为原始值, 注意老版本中转换为原始值的方法名叫toRaw
Method4.Sub.rawValue
print(Method4.Sub.rawValue)
// 输出结果: 6.0
通过原始值创建枚举值
原始值区分大小写
返回的是一个可选值,因为原始值对应的枚举值不一定存在
老版本中为fromRaw(“add”)
let m2 = Method5(rawValue: “add”)
print(m2)

func chooseMethod(op:String){
// 由于返回是可选类型, 所以有可能为nil, 最好使用可选绑定
if let opE = Method5(rawValue: op){
switch (opE){
case .Add:
print(“加法”)
case .Sub:
print(“减法”)
case .Mul:
print(“除法”)
case .Div:
print(“乘法”)
}
}
}
4. 枚举相关值
在 Swift 中,我们还可以定义这样的枚举类型,它的每一个枚举项都有一个附加信息,来扩充这个枚举项的信息表示,这又叫做关联值。加入我们有一个枚举类型 Shape 来表示形状。
enum lineSegmentDescriptor{
case StartAndEndPattern(start:Double, end:Double)
case StartAndLengthPattern(start: Double, length:Double)
}

var lsd = lineSegmentDescriptor.StartAndLengthPattern(start: 0.0, length: 100.0)
lsd = lineSegmentDescriptor.StartAndEndPattern(start: 0.0, end: 50.0)

print(lsd)
// 输出结果: StartAndEndPattern(0.0, 50.0)
利用switch提取枚举关联值
switch lsd {
case let .StartAndEndPattern(s, e):
print(“start = (s) end = (e)”)
case .StartAndLengthPattern(let s, let l):
print(“start = (s) lenght = (l)”)
}
// 输出结果: start = 0.0 end = 50.0

嵌套枚举(Nesting Enums)

如果你有特定子类型的需求,可以对enum进行嵌套。这样就允许你为实际的enum中包含其他明确信息的enum。以RPG游戏中的每个角色为例,每个角色能够拥有武器,因此所有角色都可以获取同一个武器集合。而游戏中的其他实例则无法获取这些武器(比如食人魔,它们仅使用棍棒)。
enum Character {
enum Weapon {
case Bow
case Sword
case Lance
case Dagger
}
enum Helmet {
case Wooden
case Iron
case Diamond
}
case Thief
case Warrior
case Knight
}

属性(Properties)

尽管增加一个存储属性到枚举中不被允许,但你依然能够创建计算属性。当然,计算属性的内容都是建立在枚举值下或者枚举关联值得到的。
enum Device {
case iPad, iPhone
var year: Int {
switch self {
case iPhone: return 2007
case iPad: return 2010
}
}

静态方法(Static Methods)

你也能够为枚举创建一些静态方法(static methods)。换言之通过一个非枚举类型来创建一个枚举。在这个示例中,我们需要考虑用户有时将苹果设备叫错的情况(比如AppleWatch叫成iWatch),需要返回一个合适的名称。
enum Device {
case AppleWatch
static func fromSlang(term: String) -> Device? {
if term == “iWatch” {
return .AppleWatch
}
return nil
}
}
print (Device.fromSlang(“iWatch”))

可变方法(Mutating Methods)

方法可以声明为mutating。这样就允许改变隐藏参数self的case值了3。
enum TriStateSwitch {
case Off, Low, High
mutating func next() {
switch self {
case Off:
self = Low
case Low:
self = High
case High:
self = Off
}
}
}
var ovenLight = TriStateSwitch.Low
ovenLight.next()
// ovenLight 现在等于.On
ovenLight.next()
// ovenLight 现在等于.Off

小结

枚举声明的类型是囊括可能状态的有限集,且可以具有附加值。通过内嵌(nesting),方法(method),关联值(associated values)和模式匹配(pattern matching),枚举可以分层次地定义任何有组织的数据。
现在我们已经对这个定义更加清晰了。确实,如果我们添加关联值和嵌套,enum就看起来就像一个封闭的、简化的struct。相比较struct,前者优势体现在能够为分类与层次结构编码。

存储属性

Swift中的存储属性就是以前学习OC中的普通属性
在结构体或者类中定义的属性, 默认就是存储属性
struct Person {
var name: String
var age: Int
}
var p:Person = Person(name: “cdh”, age: 20)

常量存储属性

常量存储属性只能在定义时或构造时修改
构造好一个对象之后不能对常量存储属性进行修改
struct Person1 {
var name: String
var age: Int
let card: String // 身份证
}
var p1: Person1 = Person1(name: “cdh”, age: 20, card: “123456”)
p1.name = “CDH”
p1.age = 50
//构造好对象之后不能修改常量存储属性
//以下写法是错误的
//p1.card = “56789”

结构体和枚举是值类型
因此不能修改结构体常量中的属性
不能修改结构体/枚举常量对象中的值, 因为他指向的对象是一个常量
类是引用类型
可以修改类常量中属性的值, 因为他指向的对象不是一个常量
struct Person2 {
var name: String
var age: Int
}
let p2: Person2 = Person2(name: “cdh”, age: 20)
//因为结构体是值类型, 所以不能修改结构体常量中的属性
//不能修改结构体/枚举常量对象中的值, 因为他指向的对象是一个常量
//以下写法错误
//p2.name = “CDH” //不能修改结构体常量对象的值
//以下写法错误
//p2 = Person2(name: “CDH”, age: 50)

class Person3 {
var name: String = “cdh”
var age: Int = 20
}
let p3:Person3 = Person3()
//可以修改类常量中属性的值, 因为他指向的对象不是一个常量
p3.name = “CDH”
//不可以修改类常量的指向
//以下写法是错误的
//p3 = Person4()

延迟存储属性

Swift语言中所有的存储属性必须有初始值, 也就是当构造完一个对象后, 对象中所有的存储属性必须有初始值
但是也有例外, 其中延迟存储属性可以将属性的初始化推迟到该属性第一次被调用的时候
这个延时属性对应于 OC中的懒加载的概念
苹果的设计思想:希望所有的对象在使用时才真正加载到内存中
和OC不同的是swift有专门的关键字来实现懒加载
lazy关键字可以用于定义某一个属性懒加载
懒加载应用场景:
有可能不会用到
依赖于其它值
class Line {
var start:Double = 0.0
var end: Double = 0.0
// 如果不是lazy属性,定义的时候对象还没有初始化,所以不能访问self
// 如果加上lazy,代表使用时才会加载,也就是使用到length属性时才会调用self
// 而访问一个类的属性必须通过对象方法
// 所以访问时对象已经初始化完成了,可以使用self
lazy var length: Double = self.getLenght()
// 通过闭包懒加载
lazy var container: Array = {
print(“懒加载”)
var arrM = []
return arrM as [AnyObject]
}()
func getLenght() ->Double
{
print(“懒加载”)
return end - start
}
}
var line = Line()
line.end = 150.0
print(“创建对象完毕”)
print(line.length)
var arrM = line.container
arrM.append(“1”)
arrM.append(5)
print(arrM)
//输出结果:
//创建对象完毕
//懒加载
//150.0
//懒加载
//[1, 5]

计算属性

1.Swift中的计算属性不直接存储值
跟存储属性不同,没有任何的”后端存储与之对应”
2.计算属性用于计算, 可以实现setter和getter这两种计算方法
3.枚举不可以有存储属性, 但是允许有计算属性
setter 对象.属性 = 值
getter var value = 对象.属性
struct Rect {
var origion: (x: Double, y: Double) = (0, 0)
var size: (w: Double, h: Double) = (0, 0)
// 由于center的值是通过起点和宽高计算出来的, 所以没有必要提供一个存储属性
var center: (x: Double, y: Double) {
get{
return (origion.x + size.w/2, origion.y + size.h/2)
}
set{
// 注意: 计算属性不具备存储功能, 所以不能给计算属性赋值
// 如果赋值会发生运行时错误
// 注意: setter可以自己传递一个参数, 也可以使用系统默认的参数newValue
// 如果要使用系统自带的参数, 必须删除自定义参数
origion.x = newValue.x - size.w / 2
origion.y = newValue.y - size.h / 2
}
}
}
var r = Rect()
r.origion = (0, 0)
r.size = (100, 100)
print(“center.x = (r.center.x) center.y = (r.center.y)”)
//输出结果: center.x = 50.0 center.y = 50.0
r.center = (100, 100)
print(“origion.x = (r.origion.x) origion.y = (r.origion.y)”)
//输出结果: origion.x = 50.0 origion.y = 50.0
print(“center.x = (r.center.x) center.y = (r.center.y)”)
//输出结果:center.x = 100.0 center.y = 100.0

只读计算属性

对应OC中的readonly属性
所谓的只读属性就是只提供了getter方法, 没有提供setter方法
class Line1 {
var start:Double = 0.0
var end: Double = 0.0
// 只读属性, 只读属性必须是变量var, 不能是常量let
// 例如想获取长度
// 只能通过计算获得, 而不需要外界设置, 可以设置为只读计算属性
var length: Double{
// 只读属性的简写, 可以省略get{}
return end - start
}
}
var line1 = Line1()
line1.end = 100
print(line1.length)
//输出结果: 100.0
//错误,不可设置只读属性
line1.length = 100

属性观察器

类似OC中的KVO
可以用于监听属性什么时候被修改, 只有属性被修改才会调用
有两种属性观察器:
1.willSet, 在设置新值之前调用
2.didSet, 在设置新值之后调用
可以直接为除计算属性和lazy属性之外的存储属性添加属性观察器
但是可以在继承类中为父类的计算属性提供属性观察器
因为在计算属性中也可以监听到属性的改变
所以给计算属性添加属性观察器没有任何意义
class Line2 {
var start:Double = 0.0{
willSet{
print(“willSet newValue = (newValue)”)
}
didSet{
print(“didSet oldValue = (oldValue)”)
}
}
var end: Double = 0.0
}
var l2 = Line2()
l2.start = 10.0
//输出结果:
//willSet newValue = 10.0
//didSet oldValue = 0.0
//开发中使用didSet较多

HTML页面加载和解析流程

  1. 用户输入网址(假设是个html页面,并且是第一次访问),浏览器向服务器发出请求,服务器返回html文件。

  2. 浏览器开始载入html代码,发现标签内有一个标签引用外部CSS文件。

  3. 浏览器又发出CSS文件的请求,服务器返回这个CSS文件。

  4. 浏览器继续载入html中部分的代码,并且CSS文件已经拿到手了,可以开始渲染页面了。

  5. 浏览器在代码中发现一个标签引用了一张图片,向服务器发出请求。此时浏览器不会等到图片下载完,而是继续渲染后面的代码。

  6. 服务器返回图片文件,由于图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码。

  7. 浏览器发现了一个包含一行Javascript代码的