在實際編程中,很多時候,我們都需要使用比Int
,String
這類簡單類型更復雜的類型,例如,需要兩個Double
表達的2D
坐標位置;需要兩個String
表達的人的姓和名等等。因此,如果我們的程序里真的可以使用叫做location
或name
的類型,程序的可讀性就會提高很多。于是,Swift
允許我們根據(jù)實際的需要,定義自己的類型系統(tǒng)。并把這種自定義的類型,叫做named types
。
根據(jù)實際的應用場景,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提供的庫函數(shù)。定義好inRange
之后,我們就可以像:
inRange(100, y: 500)
inRange(300, y: 800)
來調(diào)用inRange
。但是這樣有一個不足,當我們需要比較很多個點和Center的距離的時候,這些數(shù)字并不能明確告訴我們它們代表的位置的意義,甚至我們都無法知道它們代表一個數(shù)字。如果我們可以像這樣來比較位置:
inRange(location1)
inRange(myHome)
相比數(shù)字,它們看上去就會直觀的多,而這,就是我們需要自定義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
有了這些內(nèi)容之后,我們就可以修改之前的inRange函數(shù)了,讓它接受一個Location類型的參數(shù):
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")
為了實現(xiàn)這個功能,我們需要給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
的定義發(fā)生了一個錯誤:
這是由于struct initializer
創(chuàng)建規(guī)則導致的。
Initialization rules
Memberwise initializer
對于我們一開始的Location
定義:
struct Location {
let x: Double
var y: Double
}
它沒有自定義任何init方法,Swift就會自動創(chuàng)建一個struct memberwise initializer
。因此,我們可以逐個成員的去定義一個Location
。
var pointA = Location(x: 100, y: 200)
“Memberwise只是默認為我們提供了一個按成員初始化的方法,它并不會自動為成員設(shè)置默認值。”
特別提示
在我們自定義了init
方法之后,Swift
就不會自動為我們創(chuàng)建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
的參數(shù)中,使用了和member
同樣的名字,在init
的實現(xiàn)中,我們使用了關(guān)鍵字self
來表示要創(chuàng)建的struct
本身,來幫助編譯器區(qū)分member x
和參數(shù)x
,這和之前我們用pointA.x
的道理是一樣的。
這里,有一個小細節(jié)。之前我們講函數(shù)的時候,說過函數(shù)的第一個參數(shù)是省略outername
的,但是在init
里,第一個參數(shù)的outername
是不會省略的,我們必須指定每一個參數(shù)的outername
。
Member default values
如果我們希望給structmember設(shè)置一個默認值,而不用每次創(chuàng)建的時候去指定它們,我們可以像下面這樣:
struct Location {
let x = 100.0
var y = 100.0
}
在這里,如果我們可以明確給成員設(shè)置默認值,就可以省掉type annotation
的部分了,type inference
會正確的推導成員的類型。
Default initializer
如果我們?yōu)?code>struct的每一個成員都設(shè)置了默認值,并且,我們沒有自定義init
方法,Swift
就會自動為我們生成一個default initializer
。有了它,我們就可以這樣創(chuàng)建Location
了:
let center = Location()
“如果,我們沒有為每一個member
設(shè)置默認值而直接使用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
假設(shè)我們希望讓Location水平移動一段距離,我們可以這樣定義一個函數(shù):
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
內(nèi)部的成員,這個代表水平坐標的名字無需被Location
的使用者關(guān)心;其次,移動坐標點本身就是一個只和Location
自身計算有關(guān)的行為,我們應該像下面這樣來完成這個任務:
var pointA = Location(x: 100, y: 200)
pointA.moveHorizental(10.0)
這就是我們要為struct
類型定義methods
的原因,它讓和類型相關(guān)的計算表現(xiàn)的更加自然。定義method很簡單,和定義函數(shù)是類似的:
struct Location {
let x = 100.0
var y = 100.0
mutating func moveHorizental(dist: Double) {
self.x = self.x + dist;
}
}
由于作為一個值類型,Swift
默認不允許我們在method
里修改成員的值,如果我們要修改它,需要在對應的方法前面使用mutating
關(guān)鍵字。之后,我們就可以這樣:
pointA.moveHorizental(10.0)
來水平移動pointA了。
Struct extension
如果我們使用的Location
不是自己定義的,但是我們?nèi)耘f希望在自己的項目里擴展Location
的操作,Swift
也能幫我們達成,這個機制,叫做extension
。例如,我們希望給Location
添加垂直移動的方法:
extension Location {
mutating func moveVertical(dist: Double) {
self.y += dist
}
}
這里,同樣我們要使用mutating
。之后,在我們的項目里,就可以針對Location
類型的變量,使用moveVertical
方法了:
pointA.moveVertical(10.0)
接下來,我們再舉一個更明顯的例子,我們甚至可以擴展Swift
的String
類型,例如判斷一個字符串中字符的個數(shù)是奇數(shù)還是偶數(shù):
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
,它用來表達某種和“值”有關(guān)的概念。除了語義上的描述之外,value type還有一個特點,就是當它被拷貝的時候,會把“整個值"拷貝過去
。
var pointA = Location(x: 100, y: 200)
var pointB = pointA
pointB.y = 500.0
pointA.y
從結(jié)果里,我們就能看到,所謂的“拷貝整個值”,就是指修改pointB
完整復制了pointA
的內(nèi)容,而不是和A共享同一個內(nèi)容。
Struct無處不在
其實,在Swift
,我們已經(jīng)使用了很多struct
,甚至是哪些我們叫做“基礎(chǔ)類型”的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不具備的表達能力
,這些能力,都源自于它們要表達的內(nèi)容不同。class
表達一個“對象
”,而struct
表達一個值
。我們通過一個例子來簡單的理解下這種區(qū)別。
Value Type vs Reference Type
第一個區(qū)別是:class
類型沒有默認的init
方法。如果我們不指定它,Swift
編譯器會報錯。
為什么要如此呢?因為,class
并不簡單表達一個“值”
的概念。Swift
要求我們明確通過init
方法說明“打造”一個對象的過程。相反,struct
表達一個自定義的“值
”,在沒有特別說明的情況下,一個值的初始化當然是把它的每一個member都按順序初始化
。
第二個區(qū)別是: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
對象。但是,它可以改變其引用的對象自身。
這就是引用類型代表的“對象”和值類型代表的“值本身”之間的區(qū)別,理解了這種區(qū)別,我們才能正確的理解struct和class的用法。
"相等的值
"還是“相等的引用
”?
如果我們定義下面兩個變量:
let p2 = PointRef(x: 0, y: 0)
let p3 = PointRef(x: 0, y: 0)
盡管它們都表示同一個坐標點,但是它們相等么?針對引用類型,為了區(qū)分“同樣的值”和“同樣的對象”,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
中定一個函數(shù),但是method
之于它們,也有著不同的含義。如果struct
中的method
要修改其成員,我們要明確把它定義為mutating
:
struct PointVal {
var x: Int
var y: Int
mutating func moveX(x: Int) {
self.x += x
}
}
這是因為,在Swift
看來一個PointVal
的值,和我們在程序中使用的諸如:123這樣的字面值是沒有本質(zhì)區(qū)別的,一個字面值理論上是不應該有修改其自身值的方法的。
“通常你需要修改一個struct
的本意,是需要一個新的值。”
特別提示
但是類對象不同,它的數(shù)據(jù)成員對他來說,只是一個用于描述其特征的屬性,我們當然可以為其定義修改的方法:
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
的最后一個區(qū)別是:
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"]
在上面這些例子里,無論是用數(shù)字表示方向,還是用字符串表示月份,它們都有一個共同的問題:我們讓一個類型承載了本不屬于他的語意。因此我們無法安全的避免“正確的類型,卻是無意義的值”這樣的問題。例如:數(shù)字5表示什么方向呢?Jan.可以用來表示一月么?還有JAN呢?因此,面對“把一組有相關(guān)意義的值定義成一個獨立的類型”這樣的任務,Swift為我們提供了一個叫做:enumeration
的工具。
Enumeration
并不是一個新生事物,幾乎任何一種編程語言都有和enumeration
類似的語法概念。但是Swift
對enumeration
做了諸多改進和增強,它已經(jīng)不再是一個簡單的“值的替身“。它可以有自己的屬性、方法,還可以遵從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
比直接使用數(shù)字和字符串有很多“天生”的好處:一來我們可以借助Xcode的auto complete避免輸入錯誤;二來,使用enum是類型安全的,需要使用方向和月份內(nèi)容的時候,不會發(fā)生“類型正確,值卻無意義的情況”。
理解Enumeration
的“各種”value
在Swift
里,enum
的值,可以通過不同的方式表達出來。而不像Objective-C,只能通過一個整數(shù)來替代。
Enumeration case
自身就是它的值
例如在上面的例子里,當我們當我們使用Direction.NORTH
時,我們就已經(jīng)在訪問一個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
“綁定”一個整數(shù)值。如果你一定要這么做,你可以手工進行“綁定”。而這樣“綁定”來的值,叫做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就會自動為其他月份“綁定”對應的整數(shù)值了。如果我們要讀取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
的各種機制和方式,傳統(tǒng)且易于理解。但是,這并不是給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
的所有內(nèi)容時,我們可以把let
或var
寫在case
后面,例如.POST
的用法;
當我們想分別“提取associated value
中的某些值時,我們可以把let或var寫在associated value
里面,例如.DELETE的用法;
Optional
是一個enumeration
其實,有了associated value
之后就不難想象,Swift中的Optional,是基于enum實現(xiàn)的了。可以把一個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
}
其中,我們已經(jīng)在struct
和class
中使用了自定義類型的屬性。實際上,除了像定義變量一樣的使用屬性,Swift
為自定義類型的屬性提供了更多功能
Stored properties
顧名思義,這種屬性是用來真正存儲值的,就像之前我們?yōu)?code>Location和PointRef
定義的x
和y
一樣,它們有以下特點:
可以分別使用let
或var
定義成常量或變量;
init
方法要確保每一個stored property
被初始化;
可以在定義property
的時候指定初始值;
實際占用內(nèi)存空間;
使用obj.property
的形式讀取;
使用obj.property = val
的形式賦值;
只有struct
和class
可以定義stored property
;
每一個stored property
都表示了某一個自定義類型對象的某種特點,但是有些屬性是需要訪問的時候被計算出來的,而不是定義之后一成不變的。這種屬性叫做computed properties
。
Computed properties
顧名思義,作為properties
,它用來表示對象的某種屬性。但是,它的值在每次被訪問的時候,要被計算出來,而不是內(nèi)存中讀取出來。我們來看一個例子:
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)
}
}
這樣,我們就能“動態(tài)”讀取到一個矩形的中心了:
rect1.center
rect1.height = 200
rect1.center
```
從`Playground`結(jié)果里,我們可以看到`center`會隨著`height`的改變而改變。在我們的這個例子里我們只是讀取了一個`computed property`,我們可以對`computed property`賦值么?
答案是可以的,但是由于`computed property`并不實際占用內(nèi)存,因此我們要把傳入的值“拆”給`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關(guān)鍵字分別表示一個`computed propterty`的讀取和賦值操作;
當我們對一個`computed property`賦值的時候,由于它被拆分成多個`stored property`,因此我們在拆分的過程中總要做一些假設(shè)。在我們的例子里,我們假設(shè)當`center`變化時,矩形的寬高不變,移動了原點。
然后,我們可以通過下面的代碼來測試結(jié)果:
```
var center = rect1.center
rect1.origin
center.x += 100
rect1.center = center
rect1.origin
```
從`Playground`的結(jié)果可以看到,`rect1`的`orgin`向右移動了`100`。以上就是`computed property`的用法,接下來,我們回到`stored property`,如果我們想在`stored property`賦值的時候自動過濾掉“非法值”,或者在`stored property`賦值后自動更新相關(guān)的`property`怎么辦呢?在`Swift`里,我們可以給`stored property`添加`observer`。
###Property observer
`Swift`給`stored property`提供了兩個`observer`:`willSet`和`didSet`,它們分別在`stored property`被賦值前和后被調(diào)用。定義`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
```
從結(jié)果我們就可以看到,只有在`width大于0`的時候,`width`和`height`才會被更新。在上面的這兩個例子里,有以下幾點是我們要特別強調(diào)的:
在`didSet``里,我們可以直接使用width讀取MyRect的width屬性`,但是我們必須使用`self`讀取其它的屬性;
`willSet`和`didSet`不在對象的`init`過程中生效,僅針對一個已經(jīng)完整初始化的對象在對屬性賦值的時候生效;
如果我們不指定`willSet`和`didSet`的參數(shù),`Swift`默認使用`newValue`和`oldValue`作為它們的參數(shù);
###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`關(guān)鍵字為`named type`定義`type property`,定義好之后,我們不能通過具體的對象訪問`type property`,而是要直接使用類型的名字。例如:
```
// WRONG!!! rect1.shape
let shape = MyRect.shape
```
這就是`type property`的用法,它可以幫助我們很方便的描述一個類型所有對象的屬性。