在实际编程中,很多时候,我们都需要使用比Int
,String
这类简单类型更复杂的类型,例如,需要两个Double
表达的2D
坐标位置;需要两个String
表达的人的姓和名等等。因此,如果我们的程序里真的可以使用叫做location
或name
的类型,程序的可读性就会提高很多。于是,Swift
允许我们根据实际的需要,定义自己的类型系统。并把这种自定义的类型,叫做named types
。
根据实际的应用场景,Swift
提供了4种不同的named types
:struct
,class
,enum
和protocol
。首先,我们来看表达值类型(value type)的struct
。
如何来理解这个value type
呢?在前面我们也提到过,有时,我们需要把没有任何意义的Double
,近一步抽象成一个点坐标的location
。而无论是Double
还是location
,它们所表达的,都是“某种值”这样的概念,而这,就是值类型的含义。
定义一个struct
先来看一个例子,我们要计算平面坐标里某个点距点A的距离是否小于200,算起来很简单,勾股定理就搞定了:
let centerX = 100.0
let centerY = 100.0
let distance = 200.0
func inRange(x: Double, y: Double) -> Bool {
// sqrt(n) pow(x, n)
let distX = x - centerX
let distY = y - centerY
let dist =
sqrt(pow(distX, 2) + pow(distY, 2))
return dist < distance
}
其中sqrt(n)
用来计算n的平方根,pow(x, n)
用来计算x的n次方,它们都是Swift提供的库函数。定义好inRange
之后,我们就可以像:
inRange(100, y: 500)
inRange(300, y: 800)
来调用inRange
。但是这样有一个不足,当我们需要比较很多个点和Center的距离的时候,这些数字并不能明确告诉我们它们代表的位置的意义,甚至我们都无法知道它们代表一个数字。如果我们可以像这样来比较位置:
inRange(location1)
inRange(myHome)
相比数字,它们看上去就会直观的多,而这,就是我们需要自定义struct类型最直接的原因。
初始化struct
我们可以像这样定义一个struct类型:
struct StructName {/* struct memberes*/}
struct Location {
let x: Double
var y: Double
}
在我们的例子里,struct members
就是两个Double
来表示一个位置的xy坐标。定义好struct类型之后,我们就可以像这样定义location
变量并访问和修改location members
:
var pointA = Location(x: 100, y: 200)
pointA.x
pointA.y
pointA.y = 500
有了这些内容之后,我们就可以修改之前的inRange函数了,让它接受一个Location类型的参数:
func inPointRange(point: Location) -> Bool {
// sqrt(n) pow(x, n)
let distX = point.x - centerX
let distY = point.y - centerY
let dist = sqrt(pow(distX, 2) + pow(distY, 2))
return dist < distance
}
然后,我们用来比较位置的代码,就易懂多了:
inPointRange(pointA)
Struct initializer
除了使用:
var pointA = Location(x: 100, y: 200)
这样的方式初始化Location
之外,我们几乎可以使用任意一种我们“希望”的方式,例如:
var pointA = Location("100,200")
为了实现这个功能,我们需要给Location添加自定义的initializer:
struct Location {
let x: Double
var y: Double
// Initializer
init(stringPoint: String) {
// "100,200"
let xy = stringPoint.characters.split(",")
x = atof(String(xy.first!))
y = atof(String(xy.last!))
}
}
这样,我们就可以使用特定格式的字符串,初始化Location
了。但是,这时,我们会看到编译器告诉我们之前pointA
的定义发生了一个错误:
这是由于struct initializer
创建规则导致的。
Initialization rules
Memberwise initializer
对于我们一开始的Location
定义:
struct Location {
let x: Double
var y: Double
}
它没有自定义任何init方法,Swift就会自动创建一个struct memberwise initializer
。因此,我们可以逐个成员的去定义一个Location
。
var pointA = Location(x: 100, y: 200)
“Memberwise只是默认为我们提供了一个按成员初始化的方法,它并不会自动为成员设置默认值。”
特别提示
在我们自定义了init
方法之后,Swift
就不会自动为我们创建memberwise initializer
了,这也就是编译器会报错的原因,不过我们可以手工打造一个:
struct Location {
let x: Double
var y: Double
// Initializer
init(x: Double, y: Double) {
self.x = x;
self.y = y;
}
init(stringPoint: String) {
// "100,200"
let xy = stringPoint.characters.split(",")
x = atof(String(xy.first!))
y = atof(String(xy.last!))
}
}
我们手工打造的memberwise
版本和String
大同小异。只是,由于我们在init
的参数中,使用了和member
同样的名字,在init
的实现中,我们使用了关键字self
来表示要创建的struct
本身,来帮助编译器区分member x
和参数x
,这和之前我们用pointA.x
的道理是一样的。
这里,有一个小细节。之前我们讲函数的时候,说过函数的第一个参数是省略outername
的,但是在init
里,第一个参数的outername
是不会省略的,我们必须指定每一个参数的outername
。
Member default values
如果我们希望给structmember设置一个默认值,而不用每次创建的时候去指定它们,我们可以像下面这样:
struct Location {
let x = 100.0
var y = 100.0
}
在这里,如果我们可以明确给成员设置默认值,就可以省掉type annotation
的部分了,type inference
会正确的推导成员的类型。
Default initializer
如果我们为struct
的每一个成员都设置了默认值,并且,我们没有自定义init
方法,Swift
就会自动为我们生成一个default initializer
。有了它,我们就可以这样创建Location
了:
let center = Location()
“如果,我们没有为每一个member
设置默认值而直接使用default initializer
,编译器会报错。”
特别提示
和memberwise initializer
一样,如果我们自定义了init
方法,那么这个默认的initializer就不存在了
。但是,我们同样可以手工打造一个:
struct Location {
let x = 100.0
var y = 100.0
// Default initializer
init() {}
// Initializer
init(x: Double, y: Double) {
self.x = x;
self.y = y;
}
init(stringPoint: String) {
// "100,200"
let xy = stringPoint.characters.split(",")
x = atof(String(xy.first!))
y = atof(String(xy.last!))
}
}
Default initializer最简单,看上去,就像一个什么都不做的方法一样。
Methods in struct
假设我们希望让Location水平移动一段距离,我们可以这样定义一个函数:
var pointA = Location(x: 100, y: 200)
func moveHorizental(dist: Double, inout point: Location) {
point.x = point.x + dist
}
moveHorizental(10.0, point: PointA)
尽管达成了目的,但这样做有很多问题。首先,x
是一个Location
内部的成员,这个代表水平坐标的名字无需被Location
的使用者关心;其次,移动坐标点本身就是一个只和Location
自身计算有关的行为,我们应该像下面这样来完成这个任务:
var pointA = Location(x: 100, y: 200)
pointA.moveHorizental(10.0)
这就是我们要为struct
类型定义methods
的原因,它让和类型相关的计算表现的更加自然。定义method很简单,和定义函数是类似的:
struct Location {
let x = 100.0
var y = 100.0
mutating func moveHorizental(dist: Double) {
self.x = self.x + dist;
}
}
由于作为一个值类型,Swift
默认不允许我们在method
里修改成员的值,如果我们要修改它,需要在对应的方法前面使用mutating
关键字。之后,我们就可以这样:
pointA.moveHorizental(10.0)
来水平移动pointA了。
Struct extension
如果我们使用的Location
不是自己定义的,但是我们仍旧希望在自己的项目里扩展Location
的操作,Swift
也能帮我们达成,这个机制,叫做extension
。例如,我们希望给Location
添加垂直移动的方法:
extension Location {
mutating func moveVertical(dist: Double) {
self.y += dist
}
}
这里,同样我们要使用mutating
。之后,在我们的项目里,就可以针对Location
类型的变量,使用moveVertical
方法了:
pointA.moveVertical(10.0)
接下来,我们再举一个更明显的例子,我们甚至可以扩展Swift
的String
类型,例如判断一个字符串中字符的个数是奇数还是偶数:
extension String {
func isEven() -> Bool {
return self.characters.count % 2 == 0 ? true : false
}
}
"An even string".isEven()
Struct is a value type
在一开始,我们就讲到struct
是一个value type
,它用来表达某种和“值”有关的概念。除了语义上的描述之外,value type还有一个特点,就是当它被拷贝的时候,会把“整个值"拷贝过去
。
var pointA = Location(x: 100, y: 200)
var pointB = pointA
pointB.y = 500.0
pointA.y
从结果里,我们就能看到,所谓的“拷贝整个值”,就是指修改pointB
完整复制了pointA
的内容,而不是和A共享同一个内容。
Struct无处不在
其实,在Swift
,我们已经使用了很多struct
,甚至是哪些我们叫做“基础类型”的Int
,String
。在Playground
里,如果我们按住Command
键单击某个类型,例如:Double
,就会看到,它是一个struct
。
public struct Double {
public var value: Builtin.FPIEEE64
/// Create an instance initialized to zero.
public init()
public init(_bits v: Builtin.FPIEEE64)
/// Create an instance initialized to `value`.
public init(_ value: Double)
}
这和我们之前用过的诸如C、Objective-C是完全不同的。
作为Swift中的“named type”,从语法上来说,class和struct有很多相似的地方,例如:
struct PointValue {
var x: Int
var y: Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
class PointRef {
var x: Int
var y: Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
都可以用来自定义类型
;
都可以有properties
;
都可以有methods
;
而实际上,作为一个reference type
,class具有很多struct不具备的表达能力
,这些能力,都源自于它们要表达的内容不同。class
表达一个“对象
”,而struct
表达一个值
。我们通过一个例子来简单的理解下这种区别。
Value Type vs Reference Type
第一个区别是:class
类型没有默认的init
方法。如果我们不指定它,Swift
编译器会报错。
为什么要如此呢?因为,class
并不简单表达一个“值”
的概念。Swift
要求我们明确通过init
方法说明“打造”一个对象的过程。相反,struct
表达一个自定义的“值
”,在没有特别说明的情况下,一个值的初始化当然是把它的每一个member都按顺序初始化
。
第二个区别是:class
和struct
对“常量
”的理解是不同的。我们分别定义一个PointValue
和PointRef
的常量:
let p1 = PointVal(x: 0, y: 0)
let p2 = PointRef(x: 0, y: 0)
p1.x = 10
p2.x = 10
同样是常量,但是编译器会对p1.x = 10报错:
这是因为,p1
作为一个值类型
,常量的意义当然是:“它的值不能被改变”。但是p2
作为一个引用类型,常量的意义则变成了,它不能再引用其他的PointRef
对象。但是,它可以改变其引用的对象自身。
这就是引用类型代表的“对象”和值类型代表的“值本身”之间的区别,理解了这种区别,我们才能正确的理解struct和class的用法。
"相等的值
"还是“相等的引用
”?
如果我们定义下面两个变量:
let p2 = PointRef(x: 0, y: 0)
let p3 = PointRef(x: 0, y: 0)
尽管它们都表示同一个坐标点,但是它们相等么?针对引用类型,为了区分“同样的值”和“同样的对象”,Swift定义了Identity Operator:===和!==。
// Identity operator
if p2 === p3 {
print("They are the same object")
}
if p2 !== p3 {
print("They are not the same object")
}
它们分别用于比较两个引用类型是否引用相同的对象,返回的是一个Bool类型。
Method
之于struct
和class
看似都是在struct
和class
中定一个函数,但是method
之于它们,也有着不同的含义。如果struct
中的method
要修改其成员,我们要明确把它定义为mutating
:
struct PointVal {
var x: Int
var y: Int
mutating func moveX(x: Int) {
self.x += x
}
}
这是因为,在Swift
看来一个PointVal
的值,和我们在程序中使用的诸如:123这样的字面值是没有本质区别的,一个字面值理论上是不应该有修改其自身值的方法的。
“通常你需要修改一个struct
的本意,是需要一个新的值。”
特别提示
但是类对象不同,它的数据成员对他来说,只是一个用于描述其特征的属性,我们当然可以为其定义修改的方法:
class PointRef {
var x: Int
var y: Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
func moveX(x: Int) {
self.x += x
}
}
赋值 Vs 复制
我们要讲到的struct
和class
的最后一个区别是:
var p1 = PointVal(x: 0, y: 0)
var p4 = p1
p4.x = 10
p1.x // p1.x still 0
var p2 = PointRef(x: 0, y: 0)
var p5 = p2
p2.x = 10
p5.x // p5.x = 10
值类型的变量赋值,会把变量的值完整的拷贝,因此修改p4.x
不会影响p1.x
;
引用类型的变量赋值,会复制一个指向相同对象的引用,因此修改p2.x
会影响p5.x
;
不再只是“值替身”的enum
很多时候,我们需要用一组特定的值,来表达一个公共的含义。例如用1,2,3, 4表示东、南、西、北:
let EAST = 1
let SOUTH = 2
let WEST = 3
let NORTH = 4
或者用一个字符串表示一年的月份:
let months = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"]
在上面这些例子里,无论是用数字表示方向,还是用字符串表示月份,它们都有一个共同的问题:我们让一个类型承载了本不属于他的语意。因此我们无法安全的避免“正确的类型,却是无意义的值”这样的问题。例如:数字5表示什么方向呢?Jan.可以用来表示一月么?还有JAN呢?因此,面对“把一组有相关意义的值定义成一个独立的类型”这样的任务,Swift为我们提供了一个叫做:enumeration
的工具。
Enumeration
并不是一个新生事物,几乎任何一种编程语言都有和enumeration
类似的语法概念。但是Swift
对enumeration
做了诸多改进和增强,它已经不再是一个简单的“值的替身“。它可以有自己的属性、方法,还可以遵从protocol
。
定义一个Enumeration
我们这样来来定义一个enum:
enum {
case value
case value
}
例如,我们可以把开始提到过的方向和月份定义成enum:
enum Direction {
case EAST
case SOUTH
case WEST
case NORTH
}
enum Month {
case January, Februray, March, April, May, June, July,
August, September, October, November, December
}
这样,我们就用enum
定义了两个新的类型,用来表示“方向”和“月份”,它们是两个有限定值的类型。然后,我们可以像下面这样,使用它们代表的值:
let north = Direction.NORTH
let jan = Month.January
直观上看,使用enum
比直接使用数字和字符串有很多“天生”的好处:一来我们可以借助Xcode的auto complete避免输入错误;二来,使用enum是类型安全的,需要使用方向和月份内容的时候,不会发生“类型正确,值却无意义的情况”。
理解Enumeration
的“各种”value
在Swift
里,enum
的值,可以通过不同的方式表达出来。而不像Objective-C,只能通过一个整数来替代。
Enumeration case
自身就是它的值
例如在上面的例子里,当我们当我们使用Direction.NORTH
时,我们就已经在访问一个enum
的值了,它的case
就是它的值本身,我们无需特意给它找一个“值替身”来表示。另外,如果通过type inference
可以推导出enum
的类型,我们可以在读取值的时候,省掉enum
的名字:
func direction(val: Direction) -> String {
switch val {
case .NORTH, .SOUTH:
return "up down"
case .EAST, .WEST:
return "left right"
}
}
这个例子里,有两个地方值得注意:
因为val
的类型可以通过type inference
推导出是Direction
,因此,在case
里,我们可以省略掉enum
的名字;
对于一个enum
来说,它全部的值就是它所有的case
,因此在一个switch...case...
里,只要列举了enum
所有的case
,它就被认为是exhausitive
的,因此,我们可以没有default
分支;
Raw values
和Objective-C
不同,Swift
的enum
默认不会为它的case
“绑定”一个整数值。如果你一定要这么做,你可以手工进行“绑定”。而这样“绑定”来的值,叫做raw values
。
enum: Type {
case value
case value
}
enum Direction: Int {
case EAST
case SOUTH
case WEST
case NORTH
}
当我们这样定义Direction之后,Swift就会依次把EAST / SOUTH / WEST / NORTH“绑定”上0 / 1 / 2 / 3。我们也可以像下面这样给所有的case单独指定值:
enum Direction: Int {
case EAST = 2
case SOUTH = 4
case WEST = 6
case NORTH = 8
}
或者,我们可以给所有的case指定一个初始值:
enum Month: Int {
case January = 1, Februray, March, April, May, June, July,
August, September, October, November, December
}
这样,Swift就会自动为其他月份“绑定”对应的整数值了。如果我们要读取enum的raw value,可以访问case的rawProperty方法:
let north = Direction.NORTH.rawValue
let jan = Month.January.rawValue
或者,我们可以通过一个rawValue
来生成一个enum
的value
值:
let north = Direction(rawValue: 4)
if let n = Direction(rawValue: 4) {
print("North!")
}
但是,不一定所有传入的值都是一个合法的rawValue
。因此,Direction(rawValue:)
是一个failable initializer
,它返回的类型是一个Optional<Dierection>
。
Associated values
Raw value
的各种机制和方式,传统且易于理解。但是,这并不是给enum
“绑定”值的唯一办法,在Swift
里,我们甚至可以给每一个case
“绑定”不同类型的值。我们管这样的值叫做associated values
。
例如,我们定义一个表达HTTP action的enum
:
enum HTTPAction {
case GET
case POST(String)
case PUT(Int, String)
}
我们在每一个需要有associated value
的case
后面放上和case
对应的值的类型,就可以了。然后,我们可以这样来使用HTTPAction
:
var action1 = HTTPAction.GET
var action2 = HTTPAction.POST("BOXUE")
switch action1 {
case .GET:
print("HTTP GET")
case let .POST(msg):
print("\(msg)")
case .DELETE(let id, let value):
print("\(id)=\(value)")
}
这个例子里,有两点是应该注意的:
不是每一个case
必须有associated value
,例如.GET
就只有自己的enum value
;
当我们想“提取”associated value
的所有内容时,我们可以把let
或var
写在case
后面,例如.POST
的用法;
当我们想分别“提取associated value
中的某些值时,我们可以把let或var写在associated value
里面,例如.DELETE的用法;
Optional
是一个enumeration
其实,有了associated value
之后就不难想象,Swift中的Optional,是基于enum实现的了。可以把一个Optional理解为包含两个case的enum,一个是.None,表示空值;一个是.Some用来表示非空的值。下面这两种定义optional的方式是一样的:
var address: String? = nil
var address1: Optional<String> = nil
如果我们按住option然后点击Optional,就可以看到Xcode提示我们Optional
是一个enum
。
var address: Optional<String> = .Some("Beijing")
switch address {
case .None:
print("No address")
case let .Some(addr):
print("\(addr)")
}
而当address为.None时,它和nil是相等的:
address = .None
if address == nil {
print(".None is equal to nil")
}
自定义 properties
struct Location {
var x = 100.0
var y = 100.0
}
class PointRef {
var x: Int
var y: Int
}
enum Direction: Int {
case EAST = 2
case SOUTH = 4
case WEST = 6
case NORTH = 8
}
其中,我们已经在struct
和class
中使用了自定义类型的属性。实际上,除了像定义变量一样的使用属性,Swift
为自定义类型的属性提供了更多功能
Stored properties
顾名思义,这种属性是用来真正存储值的,就像之前我们为Location
和PointRef
定义的x
和y
一样,它们有以下特点:
可以分别使用let
或var
定义成常量或变量;
init
方法要确保每一个stored property
被初始化;
可以在定义property
的时候指定初始值;
实际占用内存空间;
使用obj.property
的形式读取;
使用obj.property = val
的形式赋值;
只有struct
和class
可以定义stored property
;
每一个stored property
都表示了某一个自定义类型对象的某种特点,但是有些属性是需要访问的时候被计算出来的,而不是定义之后一成不变的。这种属性叫做computed properties
。
Computed properties
顾名思义,作为properties
,它用来表示对象的某种属性。但是,它的值在每次被访问的时候,要被计算出来,而不是内存中读取出来。我们来看一个例子:
struct MyRect {
var origin: Point
var width: Double
var height: Double
}
var rect1 = MyRect(origin: Point(1, 1), width: 200, height: 100)
我们使用原点以及宽高定义了一个矩形。除了这些“原始”的矩形属性之外,当我们想访问矩形的“中心”时,我们就可以定义一个computed property:
struct MyRect {
var origin: Point
var width: Double
var height: Double
var center: Point {
let centerX = origin.x + self.width / 2
let centerY = origin.Y + self.height / 2
return Point(x: centerX, y: centerY)
}
}
这样,我们就能“动态”读取到一个矩形的中心了:
rect1.center
rect1.height = 200
rect1.center
```
从`Playground`结果里,我们可以看到`center`会随着`height`的改变而改变。在我们的这个例子里我们只是读取了一个`computed property`,我们可以对`computed property`赋值么?
答案是可以的,但是由于`computed property`并不实际占用内存,因此我们要把传入的值“拆”给`class`的各种`stored properties`。并且,一旦我们需要给`computed property`赋值,我们就要在定义它的时候,明确它的`get`和`set`方法,像下面这样:
```
struct MyRect {
var origin: Point
var width: Double
var height: Double
var center: Point {
get {
let centerX = origin.x + self.width / 2
let centerY = origin.Y + self.height / 2
return Point(x: centerX, y: centerY)
}
set(newCenter) {
self.origin.x = newCenter.x - self.width / 2
self.origin.y = newCenter.y - self.height / 2
}
}
}
```
对于上面的例子,有几点值得注意:
我们使用get和set关键字分别表示一个`computed propterty`的读取和赋值操作;
当我们对一个`computed property`赋值的时候,由于它被拆分成多个`stored property`,因此我们在拆分的过程中总要做一些假设。在我们的例子里,我们假设当`center`变化时,矩形的宽高不变,移动了原点。
然后,我们可以通过下面的代码来测试结果:
```
var center = rect1.center
rect1.origin
center.x += 100
rect1.center = center
rect1.origin
```
从`Playground`的结果可以看到,`rect1`的`orgin`向右移动了`100`。以上就是`computed property`的用法,接下来,我们回到`stored property`,如果我们想在`stored property`赋值的时候自动过滤掉“非法值”,或者在`stored property`赋值后自动更新相关的`property`怎么办呢?在`Swift`里,我们可以给`stored property`添加`observer`。
###Property observer
`Swift`给`stored property`提供了两个`observer`:`willSet`和`didSet`,它们分别在`stored property`被赋值前和后被调用。定义`observer`和定义`computed property`是类似的。首先我们来看`willSet`,我们在`width`被赋值之前,向控制台打印一个信息:
```
struct MyRect {
var origin: Point
var width: Double {
willSet(newWidth) {
print("width will be updated")
}
}
}
rect1.width = 300
这时,打开控制台,就可以看到willSet的输出了。接下来,我们来看didSet的用法,如果我们希望width大于0的时候才更新并且保持宽高相等,可以这样:
struct MyRect {
var origin: Point
var width: Double {
didSet(oldWidth) {
if width <= 0 {
width = oldWidth
}
else {
self.height = width
}
}
}
}
rect1.width=-300
rect1.height
rect1.width=300
rect1.height
```
从结果我们就可以看到,只有在`width大于0`的时候,`width`和`height`才会被更新。在上面的这两个例子里,有以下几点是我们要特别强调的:
在`didSet``里,我们可以直接使用width读取MyRect的width属性`,但是我们必须使用`self`读取其它的属性;
`willSet`和`didSet`不在对象的`init`过程中生效,仅针对一个已经完整初始化的对象在对属性赋值的时候生效;
如果我们不指定`willSet`和`didSet`的参数,`Swift`默认使用`newValue`和`oldValue`作为它们的参数;
###Type property
首先我们定义一个`enum`表示各种形状:
```
enum Shape {
case RECT
case TRIANGLE
case CIRCLE
}
```
然后,我们可以给`MyRect`添加一个属性表示它对应的形状,由于这个属性对于MyRect的所有对象都是一样的,都应该是`Shape.RECT`,这时,我们就可以为`MyRect`添加一个`type property`:
```
struct MyRect {
var origin: Point
var width: Double
var height: Double
// Type property
static let shape = Shape.RECT
}
```
我们使用`static`关键字为`named type`定义`type property`,定义好之后,我们不能通过具体的对象访问`type property`,而是要直接使用类型的名字。例如:
```
// WRONG!!! rect1.shape
let shape = MyRect.shape
```
这就是`type property`的用法,它可以帮助我们很方便的描述一个类型所有对象的属性。