第5章

枚举与结构体






在Swift语言中,数据类型包括基本类型、组合类型和自定义类型三种,其中,基本类型包括整型、字符型、字符串型、单精度浮点型、双精度浮点型、布尔型等; 组合类型包括元组、集合、数组和字典等; 自定义类型包括枚举、结构体和类等。在自定义类型中,枚举和结构体属于值类型,而类属于引用类型。此外,在Swift语言中所有基本类型和集类型均属于值类型,这种类型的量在复制或传递给函数的参数时,将其值复制一个副本给新的量,原来的量和新量间不再有关系。然而,引用类型的量在赋值给另一个量时,原来的量和新量均指向同一个实例。本章将介绍枚举类型和结构体的概念与用法,第6章介绍类与实例的概念与用法。

5.1枚举

枚举是一种自定义类型,也是一种值类型。枚举用于定义一组相互关联且可列举的量,以增加程序的可读性。枚举类型的基本语法为

enum枚举类型名

{

case 枚举值1, 枚举值2, …, 枚举值n

}

这里,enum为定义枚举类型的关键字; 枚举类型名一般使用“大骆驼命名法”,即首字母大小、名称中包含的每个英文单词的首字母均大写; 各个枚举值的名称不能相同。

例如,下面的枚举类型Week定义了一周的情况: 

enum Week

{

case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday

}

注意: 枚举值一般使用“小骆驼命名法”,即首字母小写、名称中包含的每个英文单词的首字母大写。但是,这里表示一星期中的每天的英文单词都是首字母大写,为了和真实的英语单词表示统一。


给定枚举类型后,定义枚举类型的变量或常量的方法与使用基本类型定义变量或常量的方法相同,即分别借助var和let关键字。

程序段51展示了枚举的基本用法。




视频讲解

程序段51枚举基本用法实例

1import Foundation

2

3enum Week

4{

5case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday

6}

7var day = Week.Thursday

8switch day

9{

10case .Monday, .Tuesday, .Wednesday, .Thursday, .Friday:

11print("We work at \(day).")

12case .Saturday, .Sunday:

13print("We have a rest at \(day).")

14}

在程序段51中,第3~6行定义了枚举类型Week。第7行等价于“var day:Week=Week.Thursday”或“var day:Week=.Thursday”,如果可从上下文中知道枚举类型名,可省略枚举类型名,这里表示定义变量day,赋初值为“Week.Thursday”。

第8~14行为一个switch结构,根据day的值选择相应的分支执行。由于day为“Week.Thursday”,故匹配了第10行的case,从而第11行“print("We work at \(day).")”被执行,输出“We work at Thursday.”。每个枚举类型的量,在默认情况下为它本身表示的字符串,这里的day为“Week.Thursday”,故day的值为“Thursday”。

5.1.1枚举量原始值

在Swift语言中,枚举类型中的枚举量可具有同一类型的值,称为原始值,原始值可为字符串型、整型、字符型、单精度或双精度浮点型等,有以下几种情况。

(1) 字符串类型的枚举类型,原始值为各枚举量的文本,例如: 

enum Polarity : String

{

case negative, positive

}

此时,Polarity.negative具有默认的原始值negative,Polarity.positive具有默认的原始值positive。在程序段51中,枚举类型Week的各个枚举量也属于这类情况。

(2) 整数类型的枚举类型,第一个枚举量的原始值为0,后续的枚举量的原始值依次加1,例如: 

enum DNACode : Int

{

case A, G, C, T

}

此时,DNACode.A具有默认的原始值0,DNACode.G的原始值为1,DNACode.C的原始值为2,DNACode.T的原始值为3。

(3) 整数类型的枚举类型,可以为每个枚举量指定整数值,此时指定的值即为这些枚举量的原始值; 也可以只为第一个枚举量指定整数值作为其原始值,后续的各个枚举量的原始值依次加1,例如: 

enum Season : Int

{

case spring = 1

case summer

case autumn

case winter

}

这里,枚举类型的各个枚举量可以使用多个case,上述Season.spring的原始值被指定为1,则Season.summer的原始值为2,Season.autumn的原始值为3,Season.winter的原始值为4。

(4) 除上述三种情况,当指定枚举类型的变量类型时,必须指定相同的类型,可单独为每个枚举量设定原始值,例如: 

enum Constant : Double

{

case pi = 3.14159

case e = 2.71828

}

这里枚举类型Constant的每个枚举量都被指定了原始值。

当为枚举类型的枚举量指定了原始值后,枚举类型将隐式包含一个“初始化器”,所谓的“初始化器”是指由枚举类型名作为函数名、rawValue(即原始值)作为参数的函数,用于生成一个枚举类型的变量或常量。例如,对于上述Constant枚举类型,可以使用它的初始化器定义一个枚举变量c,即“var c=Constant(rawValue:3.14159)”。注意,初始化器返回的类型为可选类型,对于Constant枚举类型,返回的类型为可选双精度浮点类型。

程序段52展示了带有原始值的枚举类型的用法。




视频讲解

程序段52带原始值的枚举类型用法实例

1import Foundation

2

3enum Polarity : String

4{

5case negative, positive

6}

7var p = Polarity(rawValue: "positive")

8print("\(p!), \(p!.rawValue)")

9enum DNACode : Int

10{

11case A, G, C, T

12}

13var dna = DNACode(rawValue: 3)

14print("\(dna!),\(dna!.rawValue)")

15enum Season : Int

16{

17case spring = 1

18case summer

19case autumn

20case winter

21}

22var s = Season.summer

23print("\(s),\(s.rawValue)")

24enum Constant : Double

25{

26case pi = 3.14159

27case e = 2.71828

28}

29var c1 = Constant(rawValue: 3.14159)

30print("\(c1!),\(c1!.rawValue)")

31var c2 = Constant.pi

32print("\(c2),\(c2.rawValue)")

33if let c3 = Constant(rawValue: 2.71828)

34{

35print("\(c3.rawValue)")

36}

在程序段52中,第3~6行定义了枚举类型Polarity,为字符串类型。第7行“var p=Polarity(rawValue:"positive")”使用枚举类型Polarity的初始化器定义枚举变量p,此时的p为可选枚举类型。第8行“print("\(p!), \(p!.rawValue)")”输出p表示的枚举量和p表示的枚举量的原始值,得到“positive, positive”。

第9~12行定义枚举类型DNACode,为整数类型。第13行“var dna=DNACode(rawValue:3)”调用枚举类型DNACode的初始化器定义枚举变量dna,此时的dna为可选枚举类型。第14行“print("\(dna!),\(dna!.rawValue)")”输出枚举变量dna的枚举量和它的原始值,得到“T, 3”。

第15~21行定义了枚举类型Season。第22行“var s=Season.summer”定义枚举变量s,赋值为Season.summer。第23行“print("\(s),\(s.rawValue)")”输出s的枚举量和它的原始值,得到“summer, 2”。

第24~28行定义了枚举类型Constant。第29行“var c1=Constant(rawValue:3.14159)”借助Constant的初始化器为变量c1赋值; 第30行“print("\(c1!),\(c1!.rawValue)")”输出c1的枚举量和它的原始值,得到“pi, 3.14159”。第31行“var c2=Constant.pi”定义变量c2,赋值为枚举量Constant.pi; 第32行“print("\(c2),\(c2.rawValue)")”输出c2的枚举量和它的原始值,得到“pi, 3.14159”。第33~36行为一个if结构,第33行“if let c3=Constant(rawValue:2.71828)”使用可选绑定方法将Constant(rawValue:2.71828)的值赋给常量c3,如果c3不为nil,则执行第35行“print("\(c3.rawValue)")”,输出c3的原始值,得到“2.71828”。

5.1.2枚举量关联值

枚举类型的枚举量可以关联值,其语法为

case枚举量(值的类型)

当有多个值时,需要为每个值指定类型,例如: 

enumComputer

{

case CPU(Int, Int)

case memory(String)

case display(Int, String)

}

上述代码为一个枚举类型Computer,其中,枚举量CPU关联了两个整型值; memory关联了一个字符串; display关联了一个整型值和一个字符串。

此外,枚举类型中可以包括“方法”,所谓的方法是指包含在枚举类型中的函数。需要注意的是,枚举类型为值类型,枚举类型中的方法要改变枚举本身的枚举量时,需要使用关键字mutating修改方法。在枚举类型的方法中,使用self指代枚举类型定义的变量或常量(一般称为实例)本身。

枚举类型定义的变量仅能保存一个枚举值,借助枚举类型的这一特点和枚举量关联值可以实现联合体,即多种类型的量共用同一个存储空间,但是任一时刻只能存储一种类型。

程序段53介绍了枚举类型用作联合体的方法,同时,介绍了枚举类型中的方法。




视频讲解

程序段53枚举类型实现联合体和枚举类型方法实例

1import Foundation

2

3enum ASCIICode

4{

5case ascii(Int)

6case code(Character)

7mutating func change()

8{

9switch self

10{

11case .ascii(let t):

12self = .code(Character(UnicodeScalar(t)!))

13case .code(let c):

14self = .ascii(Int(c.asciiValue!))

15}

16}

17}

18

19var a1 = ASCIICode.ascii(65)

20switch a1

21{

22case .ascii(let t):

23print("\(t)")

24case .code(let c):

25print("\(c)")

26}

27

28var a2 = ASCIICode.ascii(65)

29a2.change()

30switch a2

31{

32case .ascii(let t):

33print("\(t)")

34case .code(let c):

35print("\(c)")

36}

37

38var a3 = ASCIICode.code("C")

39a3.change()

40switch a3

41{

42case .ascii(let t):

43print("\(t)")

44case .code(let c):

45print("\(c)")

46}

在程序段53中,第3~17行定义了枚举类型ASCIICode,包含两个枚举量,即第5行的“case ascii(Int)”,关联一个整型值; 第6行的“case code(Character)”,关联一个字符值。还包含一个方法,即第7~16行的change方法“mutating func change()”。在方法change内部,第9~15行为一个switch结构,第9行“switch self”根据枚举类型定义的实例做出选择,如果匹配第11行“case .ascii(let t):”(这里根据上下文可省略枚举类型名,完整的形式为“ASCIICode.ascii(let t)”),则执行第12行“self=.code(Character(UnicodeScalar(t)!))”,将整数值作为ASCII值转换为字符; 如果匹配第13行“case .code(let c):”,则执行第14行
“self=.ascii(Int(c.asciiValue!))”,将字符转换为对应的ASCII整数值。

第19行“var a1=ASCIICode.ascii(65)”定义枚举变量a1,赋值为枚举量ascii,关联值为65。第20~26行为一个switch结构,第20行“switch a1”根据a1的值选择执行后续操作,如果a1匹配第22行“case .ascii(let t):”,则执行第23行“print("\(t)")”。由第19行可知,a1匹配第22行,这里第23行执行得到“65”。

第28行“var a2=ASCIICode.ascii(65)”定义枚举变量a2,赋值为枚举量ascii,关联值为65。第29行“a2.change()”调用change方法将变量a2的关联整型值转换为字符。第30~36行为一个switch结构,这里第30行“switch a2”根据a2的值进行匹配,将与第34行“case .code(let c):”匹配成功,执行第35行“print("\(c)")”输出“A”。

第38行“var a3=ASCIICode.code("C")”定义枚举变量a3,赋值为枚举量code,关联值为字符C。第39行“a3.change()”调用change方法将变量a3的关联字符值转换为其ASCII码整型值。第40~46行为一个switch结构,第40行“switch a3”根据a3的值选择支路执行,这里a3与第42行“case .ascii(let t):”相匹配,第43行“print("\(t)")”输出“67”。

5.1.3遍历枚举量

将枚举类型定义为CaseIterable,使用枚举类型的属性allCases可将枚举类型转换为数组,此时可以遍历枚举类型中的各个枚举量。

程序段54介绍了遍历枚举类型中枚举量的方法。




视频讲解

程序段54遍历枚举量实例

1import Foundation

2

3enum Vehicle : CaseIterable

4{

5case plane, train, bus, ship

6}

7for e in Vehicle.allCases

8{

9print(e)

10}

在程序段54中,第3~6行定义枚举类型Vehicle,使用CaseIterable类型将其转换可数特性。第7~10行为一个forin结构,第7行“for e in Vehicle.allCases”中“Vehicle.allCases”为由枚举类型Vehicle中各枚举量组成的数组,这里遍历这个数组,输出各个枚举量,执行程序将依次输出plane、train、bus、ship。

枚举类型定义了包含原始值的情况下可以输出每个枚举量的原始值,如程序段55所示。




视频讲解

程序段55遍历枚举量的原始值实例

1import Foundation

2

3enum Vehicle : Int, CaseIterable

4{

5case plane = 1, train, bus, ship

6}

7for e in Vehicle.allCases

8{

9print(e.rawValue)

10}

对比程序段54和程序段55可知,在程序段55中第3行定义枚举变量时指定了类型为“Int, CaseIterable”,表示为枚举量指定整型原始值。在第5行中指定plane的原始值为1,则train、bus和ship的原始值依次为2、3和4。在第7~10行的forin结构中,第9行“print(e.rawValue)”输出每个枚举量的原始值,将依次输出1、2、3、4。

5.1.4递归枚举

Swift语言支持递归枚举结构,即在一个枚举类型中可以使用其定义了的实例作为关联值,此时需要用indirect关键字修饰该枚举类型,或者用indirect关键字修饰使用了自身实例作为关联值的枚举情况,例如: 

indirect enum Fibonacci

{

case number(Int)

case next(Fibonacci, Fibonacci)

}

或

enum Fibonacci

{

case number(Int)

indirect case next(Fibonacci, Fibonacci)

}

上述定义了一个枚举类型Fibonacci,其中包含了将枚举类型Fibonacci的实例作为关联值的枚举量,这类枚举类型称为递归枚举。

程序段56展示了递归枚举的用法。




视频讲解

程序56递归枚举类型用法实例

1import Foundation

2

3enum Fibonacci

4{

5case number(Int)

6indirect case next(Fibonacci, Fibonacci)

7}

8var fab : [Int] = [1,1]

9for i in 2...10

10{

11var k1=Fibonacci.number(fab[i-2])

12var k2=Fibonacci.number(fab[i-1])

13var k3=Fibonacci.next(k1, k2)

14fab.append(calc(next: k3))

15}

16print(fab)

17func calc(next: Fibonacci)->Int

18{

19switch next

20{

21case.number(let v):

22return v

23case let.next(f1,f2):

24return calc(next:f1) + calc(next:f2)

25}

26}

在程序段56中,第3~7行定义了递归枚举类型Fibonacci,具有两个枚举量,其中,number关联了一个整型值; next关联了两个Fibonacci类型值。

第8行“var fab:[Int]=[1,1]”定义整型数组fab,赋初始数组为“[1, 1]”。第9~15行为一个forin结构,循环变量i从2递增到10,对于每个i,循环执行第11~14行,第11行“var k1=Fibonacci.number(fab[i2])”定义枚举变量k1,赋值为枚举量number,关联值为fab[i2]; 第12行“var k2=Fibonacci.number(fab[i1])”定义枚举变量k2,赋值为枚举量number,关联值为fab[i1]; 第13行“var k3=Fibonacci.next(k1, k2)”定义枚举变量k3,赋值为枚举量next,关联值为k1和k2。第14行“fab.append(calc(next:k3))”先调用calc函数,根据其参数next计算下一个Fibonacci数,然后,调用append方法将该Fibonacci数添加到数组fab中。

第16行“print(fab)”输出数组fab,得到“[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]”。

第17~26行为calc函数,具有一个Fibonacci枚举类型的参数,返回整型值。第19~25行为一个switch结构,第19行switch next根据next的值选择执行后续程序,当next匹配了枚举量number时,执行第22行return v返回number关联的整数值; 当next匹配了枚举量next时,执行第24行“return calc(next:f1)+calc(next:f2)”递归执行calc函数返回f1和f2的和。

在第21行和第23行使用枚举量number和next时,均无须指出其所在的枚举类型Fibonacci,因为从上下文环境中(即从第19行的next上)可推断其枚举类型。

程序段56中的calc函数可以移到枚举类型Fibonacci中,作为它的一个方法,这样程序更加简洁易读。在程序段57中作了这种调整。




视频讲解

程序段57带有内部方法的递归枚举类型用法实例

1import Foundation

2

3enum Fibonacci

4{

5case number(Int)

6indirect case next(Fibonacci, Fibonacci)

7func calc(next: Fibonacci)->Int

8{

9switch next

10{

11case.number(let v):

12return v

13case let.next(f1,f2):

14return calc(next:f1) + calc(next:f2)

15}

16}

17}

18var fab : [Int] = [1,1]

19for i in 2...10

20{

21var k1=Fibonacci.number(fab[i-2])

22var k2=Fibonacci.number(fab[i-1])

23var k3=Fibonacci.next(k1, k2)

24fab.append(k3.calc(next: k3))

25}

26print(fab)

对比程序56和程序段57可知,在程序段57中,函数calc移至枚举类型Fibonacci内部,作为它的一个方法,注意,该方法没有修改Fibonacci类型的内部枚举量,所以不使用mutating关键字修改该方法。

注意,在第24行“fab.append(k3.calc(next:k3))”调用calc函数时需要使用Fibonacci类型定义的实例,这里使用了实例k3。

5.1.5枚举初始化器

枚举类型属于值类型,在定义一个枚举类型的变量(称为实例)时,枚举类型具有一个默认的初始化器。典型情况为带有原始值的枚举类型,其默认的初始化器为“枚举类型名(rawValue: 枚举值)”。除此之外,还有两种情况: ①枚举类型可自定义初始化器,初始化器方法名为init,可以带有参数,但没有返回值; ②枚举类型可自定义带容错能力的初始化器,这类初始化器的方法名为“init?”,如果初始化器执行不成功,将把空值nil赋给实例。

注意: ①一旦自定义了初始化器,枚举类型默认的初始化器将不能再使用,因为自定义的初始化器“覆盖”了默认的初始化器; ②初始化器方法init或“init?”不能被直接调用。


下面借助程序段58介绍枚举类型的初始化器。




视频讲解

程序段58枚举初始化器实例

1import Foundation

2

3enum Score : Int

4{

5case perfect = 100, good = 80, bad = 60

6}

7enum Weather

8{

9case sunny, cloudy, rainy, snowy, windy

10init(state v : Character)

11{

12switch v

13{

14case "A":

15self = .sunny

16case "B":

17self = .cloudy

18case "C":

19self = .rainy

20case "D":

21self = .snowy

22case "E":

23self = .windy

24case _:

25self = .sunny

26}

27}

28init?(condition w : Character)

29{

30switch w

31{

32case "A":

33self = .sunny

34case "B":

35self = .cloudy

36case "C":

37self = .rainy

38case "D":

39self = .snowy

40case "E":

41self = .windy

42case _:

43return nil

44}

45}

46}

47if let s = Score(rawValue: 60)

48{

49print(s)

50}

51let w1 = Weather(state: "B")

52print(w1)

53if let w2 = Weather(condition: "C")

54{

55print(w2)

56}

在程序段58中,第3~6行定义了一个带原始值的枚举类型Score,其原始值类型为整型。在第5行“case perfect=100, good=80, bad=60”中为各个枚举量指定了原始值。

第7~46行定义了枚举类型Weather,第9行定义了其枚举量“case sunny, cloudy, rainy, snowy, windy”。第10~27行定义了其初始化器“init(state v:Character)”,具有一个字符型的参数。第12~26行为一个switch结构,其中,self表示枚举类型创建的实例本身,如果第12行“switch v”的v为字符A,则第14行“case "A":”匹配成功,第15行“self=.sunny”表示将枚举量sunny赋给新创建的实例。

第28~45行为带有容错能力的初始化器“init?(condition w:Character)”,带有一字符型的参数。与第10~27行的初始化器不同的是,这里带容错能力的初始化当输入字符不合法时,可以返回空值nil,如第42~43行所示。

第47~50行为一个if结构,第47行“if let s=Score(rawValue:60)”调用Score类型的默认初始化器,将原始值60对应的枚举量赋给常量s,由于默认初始化器返回可选类型,故这里使用了可选绑定技术,如果s不为空值nil,则执行第49行“print(s)”,输出s,得到“bad”。

第51行“let w1=Weather(state:"B")”将自动调用Weather枚举类型的初始化器init,将参数“B”对应的枚举量cloudy赋给w1。第52行“print(w1)”将输出“cloudy”。

第53~56行为一个if结构,使用可选绑定技术判断第53行“if let w2=Weather(condition:"C")”中w2是否为空值nil,这里自动调用Weather枚举类型的带容错能力的初始化器“init?”,如果w2不为空值nil,则执行第55行“print(w2)”,输出w2的值。此处,第53行调用初始化器“init?”将字符C对应的枚举量赋给w2,因此,第55行执行得到“rainy”。

5.2结构体

Swift语言开发者建议程序设计者多用结构体开发应用程序。在Swift语言中,结构体具有了很多类的特性(除类的与继承相关的特性外),具有属性和方法,且为值类型。所谓的属性是指结构体中的变量或常量,所谓的方法是指结构体中的函数。在结构体中使用属性和方法是因为: ①匹别于结构体外部定义的变量和常量; ②从面向对象程序设计的角度,结构体对应着现实世界的一个客观物体,描述这个物体的性质需要用到它的属性和方法; ③结构体定义的常量或变量称为实例,实例内部的变量或常量成员称为属性,实例内部的函数成员称为方法,这种称谓比常规含义的变量、常量和函数更具有意义。

定义结构体需使用关键字struct,结构体名建议使用“大骆驼命名法”,即首字母大写且其中的完整英文单词的首字母也大写。结构体定义的实例名建议使用“小骆驼命名法”,即首字母小写且其中的完整英文单词的首字母大写。结构体是一种类型,其中定义的属性和方法的位置可任意放置,可以先定义属性,再定义方法; 也可以先定义方法,再定义属性。典型的结构体定义形式如下: 

1struct Circle

2{

3var radius = 1.0

4func area() -> Double

5{

6return 3.14*radius*radius

7}

8}

上述代码定义了一个结构体类型,名称为Circle,具有一个属性radius和一个方法area。一般地,在结构体中定义属性时需要为属性赋初始值,或者使用初始化器为结构体的各个属性赋初始值。属性分为两种,一种为存储类型的属性,如上述的radius属性; 另一种为计算类型的属性,这种属性的行为类似于方法,但是形式是属性,将在5.2.2节讨论。结构体为值类型,如果结构体中定义的方法修改了其中的属性,需要使用关键字mutating修饰该方法,这种方法将为属性创建临时存储空间,待方法执行完后,将临时存储空间的值赋回给需要修改的属性。

结构体定义的变量或常量称为实例,实例调用属性和方法时使用“实例.属性”或“实例.方法(实际参数列表)”的形式。除了属于实例的属性和方法外,还可以定义属于结构体的属性和方法,借助static关键字定义,使用形式“结构体名.属性”或“结构体名.方法(实际参数列表)”访问这类属性和方法。

5.2.1结构体用法

结构体的基本用法包括以下几点: 

(1) 借助关键字struct定义结构体,结构体名建议首字母大写。

(2) 使用与定义变量和常量相同的方法在结构体内部定义属性,这类属性称为存储属性,也称实例属性,即通过结构体定义的实例才能访问的属性。

(3) 使用与定义函数相同的方法在结构体中定义方法,这种方法称为实例方法,表示通过结构体定义的实例访问的方法。如果方法中需要修改结构体的属性,则在定义方法时要使用关键字mutating。

(4) 使用关键字static定义的属性,称为结构体属性,或称静态属性,表示通过结构体名才能访问的属性。

(5) 使用关键字static定义的方法,称为结构体方法,或称静态方法,只能通过结构体名才能访问结构体方法。结构体方法只能访问其他的结构体方法和结构体属性,而不能访问实例方法,因为实例方法只有创建了实例后才存在。

(6) 结构体具有默认的初始化器,称为面向属性的初始化器,借助该初始化器,可初始化结构体中的全部存储属性。

程序段59展示了结构体的基本用法。




视频讲解

程序段59结构体基本用法实例

1import Foundation

2

3struct Point

4{

5var x,y : Double

6}

7struct Circle

8{

9static var name = ""

10static func getName()->String

11{

12return name

13}

14var radius = 1.0

15var center = Point(x:0,y:0)

16func area()->Double

17{

18return 3.14 * radius * radius

19}

20mutating func moveTo(point: Point)

21{

22center.x = point.x

23center.y = point.y

24}

25}

26Circle.name = "A Circle."

27var str = Circle.getName()

28print(str)

29var cir = Circle(radius: 3.0,center: Point(x: 4.0, y: 5.0))

30print("Circle at (\(cir.center.x),\(cir.center.y)) with ",terminator: "")

31print("area = " + String(format: "%5.2f", cir.area()))

32cir.radius = 5.0

33cir.center = Point(x: 12.0, y: 8.0)

34print("Circle at (\(cir.center.x),\(cir.center.y)) with ",terminator: "")

35print("area = " + String(format: "%5.2f", cir.area()))

36let p = Point(x: 2.0, y: 2.0)

37cir.moveTo(point: p)

38print("Circle at (\(cir.center.x),\(cir.center.y)) with ",terminator: "")

39print("area = " + String(format: "%5.2f", cir.area()))

程序段59的执行结果如图51所示。





图51程序段59的执行结果


结合图51分析程序段59的代码执行过程。在程序段59中,第3~6行定义了结构体Point,其代码再次罗列如下: 

3struct Point

4{

5var x,y : Double

6}

可见,结构体Point只具有两个存储属性x和y,因此Point具有默认的面向属性的初始化器,其调用格式为“Point(x: 传递给x的值, y: 传递给y的值)”。例如,在第15行“var center=Point(x:0,y:0)”中定义变量center,使用Point的初始化器将center设为Point结构体类型的实例。同样地,在第36行“let p=Point(x:2.0, y:2.0)”定义常量p,使用Point的初始化器将p设为Point结构体类型的实例。在面向属性的默认初始化器中,属性名自动被当作参数。

第7~25行定义了结构体Circle。第9行“static var name=""”定义了结构体属性或静态属性name,该属性借助结构体名访问。第10~13行定义了结构体方法或静态方法getName,该方法只能借助结构体名访问。注意,结构体的静态方法只能访问其静态属性。

第14行“var radius=1.0”定义了实例属性radius,该属性也可称为动态属性,这里的radius为存储属性。第15行“var center=Point(x:0,y:0)”定义了实例属性center,赋值为“Point(x:0,y:0)”,将调用Point结构体默认的面向属性的初始化器将center设为坐标点(0,0)。

第16~19行定义了实例方法area,返回圆的面积。第20~24行定义了实例方法moveTo,由于在moveTo方法中修改了结构体Circle的存储属性center,故使用了mutating关键字。moveTo方法将圆心移动到参数point指示的坐标点处。

第26行“Circle.name="A Circle."”将结构体Circle的静态属性name赋为“A Circle.”。第27行“var str=Circle.getName()”定义变量str,调用结构体Circle的静态方法getName将获取的结构体静态属性name的值赋给str。第28行“print(str)”输出“ACircle.”。

第29行“var cir=Circle(radius:3.0,center:Point(x:4.0, y:5.0))”定义实例cir,使用了结构体Circle的面向属性的默认初始化器以及结构体Point的面向属性的默认初始化器,这里表示定义了一个圆心在(4.0, 5.0)半径为3.0的圆。第30行“print("Circle at (\(cir.center.x),\(cir.center.y)) with ",terminator:"")”和第31行“print("area=" + String(format:"%5.2f", cir.area()))”输出“Circle at (4.0,5.0) with area=28.26”,这里使用“cir.center.x”访问圆心的x坐标,使用cir.center.y访问圆心的y坐标,使用cir.area()获取圆的面积。

第32行“cir.radius=5.0”将圆的半径设为5.0,这里采用“实例.存储属性”的方式向存储属性赋值。第33行“cir.center=Point(x:12.0, y:8.0)”将圆心设为坐标点“(12.0, 8.0)”。第34~35行输出“Circle at (12.0,8.0) with area=78.50”。

第36行“let p=Point(x:2.0, y:2.0)”定义结构体Point的常量实例p。第37行“cir.moveTo(point:p)”调用实例cir的moveTo方法将圆心移动到p处。第38~39行输出“Circle at (2.0,2.0) with area=78.50”。

5.2.2存储属性与计算属性

在结构体中,属性有以下三种类型: 

(1) 使用static定义的静态属性,称为结构体属性,这类属性采用“结构体名.结构体属性名”的方式访问,如程序段59的第26行所示; 

(2) 存储属性,又可称为动态属性,在结构体中定义的常量和变量均属于存储属性,存储属性为实例属性,采用“实例名.存储属性名”的方式访问; 

(3) 计算属性,也属于实例属性,采用“实例名.计算属性名”的方式访问,但是计算属性本身不保存属性值,与存储属性不同之处在于在定义计算属性后添加一对“{ }”,在其中添加get方法和set方法(至少要添加get方法),依次用于获取存储属性值或设置存储属性值。

对于存储属性: ①当使用private修饰时将变成私有存储属性,这类私有存储属性只能借助计算属性间接访问,不能借助实例访问,从而实现了数据“封装”特性; ②当使用lazy修饰时将变成惰性存储属性,这类惰性存储属性必须为变量类型,当它被首次使用时才初始化,即如果惰性存储属性只定义了但没有使用,则该属性不会占用存储空间。

程序段510详细介绍了存储属性、计算属性和私有存储属性的用法。




视频讲解

程序段510结构体的属性用法实例

1import Foundation

2

3struct Point

4{

5var x,y : Double

6}

7struct Circle

8{

9var radius = 1.0

10var r : Double

11{

12get

13{

14return radius

15}

16set

17{

18radius = newValue

19}

20}

21private var center = Point(x: 0, y: 0)

22var c : Point

23{

24return center

25}

26mutating func moveTo(point: Point)

27{

28center = point

29}

30func area()->Double

31{

32return 3.14*r*r

33}

34}

35

36var circle = Circle()

37circle.r = 5.0

38print("radius = \(circle.r)")

39circle.moveTo(point: Point(x: 3.0, y: 4.0))

40print("Circle at (\(circle.c.x),\(circle.c.y))",terminator: " ")

41print("with area: \(String(format:"%5.2f",circle.area()))")

程序段510的执行结果如图52所示。





图52程序段510的执行结果


下面结合图52介绍程序段510。在程序段510中,第3~6行定义了结构体Point,具有两个存储属性x和y。

第7~34行定义结构体Circle。其中第9行“var radius=1.0”定义存储属性radius,并赋初值为1.0。一般地,在结构体内定义存储属性时需给它们赋初值。第10~20行定义了计算属性r,将其代码再次罗列如下: 

10var r : Double

11{

12get

13{

14return radius

15}

16set

17{

18radius = newValue

19}

20}

计算属性r更像是一个方法,具有“{ }”括起来的可执行代码。其中,第12~15行为get方法,这是一种程序员约定的名称,因为这种方法形如“get{ }”,get方法用于返回结构体中某个设定的存储属性的值,这里第14行返回存储属性radius的值,即读取r相当于读取radius。第16~19行为set方法,这也是一种程序员约定的名称,因为这种方法形式“set{ }”,set方法用于向某个选定的存储属性赋值,这里第18行向radius赋值,其中,newValue为关键字,自动保存赋给r的值,因此,向r赋值(即写r)就是向radius赋值(即写radius)。注意: ①如果计算属性只有get方法,则去掉set部分,同时,get和“{ }”也可省略,如第22~25行的计算属性c,此时的计算属性为只读属性; ②计算属性可以只有get方法,称为只读属性; 可以同时有get方法和set方法,称为可读可写属性; 但是不能只有set方法,即没有只写属性; ③在set方法中可以使用newValue关键字,用于表示赋给计算属性的值,也可以自定义该量,例如第16~19行的代码可以替换为下述代码: 

16set(val)

17{

18radius = val

19}

这里使用了自定义的val变量,此处无须指定数据类型,而是根据计算属性r的类型自动推断数据类型。

第21行“private var center=Point(x:0, y:0)”定义私有存储属性center,这类属性只能借助计算属性访问,同时这类属性可被结构内的其余属性和方法使用,但不能被结构体外部的方法调用,结构体定义的实例也不能访问这类属性。

第22~25行定义一个计算属性c,其代码再次罗列如下: 

22var c : Point

23{

24return center

25}

这里计算属性c为一个只读属性,省略了get和“{ }”,返回center的值,即读取c相当于读取center。

第26~29行定义了函数moveTo,使用了mutating关键字,因为该函数将修改私有存储属性center的值。第28行“center=point”将参数point赋给私有存储属性center。

第30~33行定义了函数area,返回值为Double类型,函数返回圆的面积。第32行“return 3.14*r*r”使用了计算属性r,因为计算属性r为可读可写属性,这里读取r相当于读取radius。

第36行“var circle=Circle()”定义结构体实例circle。第37行“circle.r=5.0”将5.0赋给实例circle的计算属性r,计算属性r并不保存值,其内部将5.0赋给存储属性radius。第38行“print("radius=\(circle.r)")”输出存储属性radius的值,得到“radius=5.0”。第39行“circle.moveTo(point:Point(x:3.0, y:4.0))”调用moveTo方法将圆心移动到点(3.0, 4.0)处。第40行“print("Circle at (\(circle.c.x),\(circle.c.y))",terminator:" ")”和第41行“print("with area:\(String(format:"%5.2f",circle.area()))")”联合输出“Circle at (3.0,4.0) with area:78.50”,其中,第40行通过只读的计算属性c,获取圆心坐标。

5.2.3结构体初始化器

结构体的初始化器包括两类。

(1) 普通初始化器。

普通初始化器由init()方法实现,在定义结构体实例时自动调用初始化器。初始化器可以重载,当定义了初始化器后,结构体默认的面向属性的初始化器将被“覆盖”而不能再使用。

(2) 带容错能力的初始化器。

普通初始化器可以带参数,也可编写程序代码处理参数的有效性,但是普通初始化器无返回值。带容错能力的初始化器,当参数无效时,返回空值nil,使用“init?()”方法实现,借助带容错能力的初始化器生成的结构体实例为可选类型。

程序段511展示了上述两种结构体初始化器的用法。




视频讲解

程序511结构体初始化器用法实例

1import Foundation

2

3struct Point

4{

5var x,y : Double

6init()

7{

8x=0

9y=0

10}

第6~10行定义了一个无参数初始化器,将属性x和y均赋为0。

11init(x:Double,y:Double)

12{

13self.x = x

14self.y = y

15}

第11~15行定义了一个带参数的初始化器,第13行“self.x”中的self表示结构体定义实例本身,这里“self.x=x”表示将参数x赋给结构体的属性x,第14行表示将参数y赋给结构体的属性y。

16}

17struct Circle

18{

19var radius = 1.0

20var center = Point()

21init()

22{

23radius = 1.0

24center = Point()

25}

第21~25行定义了一个无参数的初始化器,将属性radius赋为1.0,将属性center赋为Point(),即调用结构体Point的无参数初始化器,将center赋为点(0,0)。

26init(r:Double)

27{

28radius = r

29center = Point(x:1.0,y:1.0)

30}

第26~30行定义了带有一个参数的初始化器,将参数r赋给属性radius,调用Point的带参数的初始化器将(1.0,1.0)赋给center。

31init(r:Double, p:Point)

32{

33radius = r

34center = p

35}

第31~35行为带有两个参数的初始化器,将参数r赋给属性radius,将参数p赋给属性center。

36init?(radius r:Double,point p:Point)

37{

38if r<0

39{

40return nil

41}

42radius = r

43center = p

44}

第36~44行为带有容错能力的初始化器,如果参数r小于0,则执行第40行返回空值nil; 否则,将参数r赋给属性radius,将参数p赋给属性center。

45mutating func moveTo(point: Point)

46{

47center = point

48}

49func area()->Double

50{

51return 3.14*radius*radius

52}

53}

54

55var circle = Circle(r:3.0,p:Point(x:2.0,y:2.0))

56print("radius = \(circle.radius)")

57circle.moveTo(point: Point(x: 5.0, y: 4.0))

58print("Circle at (\(circle.center.x),\(circle.center.y))",terminator: " ")

59print("with area: \(String(format:"%5.2f",circle.area()))")

60

61if let c = Circle(radius: 2.5, point: Point(x: 8.0, y: 6.5))

62{

63print("Circle at (\(c.center.x),\(c.center.y))",terminator: " ")

64print("with area: \(String(format:"%5.2f",c.area()))")

65}

在程序段511中,第3~16行定义了结构体Point,在结构体Point中定义了两个普通初始化器。第17~53行定义了结构体Circle,在结构体Circle中定义了三个普通初始化器和一个带容错能力的初始化器。

第55行调用Circle的普通初始化器(第31~35行的初始化器)创建实例circle。第56行输出radius的值,得到“radius=3.0”。第57行将圆心移至点(5.0, 4.0)。第58~59行输出“Circle at (5.0,4.0) with area:28.26”。

第61~65行为一个if结构,第61行使用可选绑定技术,调用Circle结构体的带容错能力的初始化器,如果c不为空值nil(此处,c不为空值),则执行第63~64行,输出“Circle at(8.0,6.5) with area:19.62”。

5.2.4实例方法与静态方法

结构体中的方法分为以下两种。

(1) 实例方法。

结构体中的方法与普通的函数类似(以func关键字开头),主要的区别在于结构体中的方法一般不具有参数,而是直接使用结构体中的属性,而普通的函数往往带有很多参数。结构体中的方法一般不具有参数或带有少量必要参数,是因为结构体中的方法主要为结构体中的属性服务。那些具有通用功能的方法不应作为某个结构体中的方法,而应作为全局意义上的函数。结构体中的方法通过结构体定义的实例访问,故称为实例方法。

(2) 静态方法,也称结构体方法。

定义在结构体中的以static开头的方法,称为静态方法或结构体方法。这类方法通过结构体名调用。静态方法只能访问结构体中的静态属性。某些情况下,使用静态方法比实例方法更方便,例如,对于一些通用算法实现的功能,如数学函数运算等,可以作为静态方法集中在一个结构体中,使用结构体名调用。静态方法一般具有参数。

注意: 结构体中的实例方法可以调用静态方法,在调用静态方法时必须使用“结构体名.静态方法名”的形式,即使调用同一个结构体内部的静态方法,在调用时也要为静态方法指定结构体名。然而,静态方法不能调用动态方法,因为动态方法只有在创建了结构体实例后才能使用,而静态方法在定义结构体后就可以使用了。类似地,实例方法可以使用静态属性(或结构体属性),但需要为静态属性指定结构体名; 然而,静态方法不能使用动态的存储属性和计算属性。


下面的工程MyCh0512中的程序文件展示了上述两种方法的用法。工程MyCh0512包括两个程序文件,即mystruct.swift和main.swift,分别如程序段512和程序段513所示。




视频讲解

程序段512程序文件mystruct.swift

1import Foundation

2

3struct Math

4{

5static let pi = 3.14159

6static func gcd(first op1:Int,second op2:Int)->Int

7{

8var a,b :Int

9a=max(op1,op2)

10b=min(op1,op2)

11while b>0

12{

13(a,b) = (b,a % b)

14}

15return a

16}

17static func lcm(first op1:Int, second op2:Int)->Int

18{

19return op1*op2/gcd(first:op1,second:op2)

20}

21}

22

23struct Circle

24{

25static var count : Int = 0

26var r = 1.0

27init(radius r:Double)

28{

29Circle.count += 1

30self.r = r

31}

32func area()->Double

33{

34return Math.pi*r*r

35}

36}




视频讲解

程序段513程序文件main.swift

1import Foundation

2

3print("GCD(120,90) = ",Math.gcd(first: 120, second: 90))

4print("LCM(120,90) = ",Math.lcm(first: 120, second: 90))

5

6var c1 = Circle(radius: 3.5)

7print("We have created \(Circle.count) instance(s).")

8print("Circle's area = "+String(format:"%5.2f",c1.area()))

9var c2 = Circle(radius: 3.5)

10print("We have created \(Circle.count) instance(s).")

11print("Circle's area = "+String(format:"%5.2f",c2.area()))



图53工程MyCh0512的执行结果


工程MyCh0512的执行结果如图53所示。


下面结合图53介绍程序段512和程序段513。

在程序段512中,第3~21行定义了结构体Math,其中,第5行“static let pi=3.14159”定义了静态属性pi,表示圆周率。第6~16行定义了静态方法gcd,用于计算两个整数的最大公约数,使用欧几里得算法; 第17~20行定义了静态方法lcm,用于计算两个整数的最小公倍数,其中调用了静态方法gcd。注意,在同一个结构体内部,一个静态方法可以直接调用另一个静态方法,无须指定结构体名。

第23~36行定义了结构体Circle,其中,第25行“static var count:Int=0”定义了静态属性count,用于统计结构体Circle创建实例的次数。第26行“var r=1.0”定义了存储属性r,赋初值为1.0。第27~31行为初始化器init,其中,第29行“Circle.count += 1”表示每调用一次初始化器,count的值累加1,由于count为静态属性,故其值在工程的生命期内一直有效; 第30行“self.r=r”将参数r的值赋给存储属性r。第32~35行定义函数area,返回圆的面积,其中使用静态属性“Math.pi”。

在程序段513中,第3行“print("GCD(120,90)=",Math.gcd(first:120, second:90))”调用静态方法“Math.gcd”计算120和90的最大公约数,输出“GCD(120,90)=30”。第4行“print("LCM(120,90)=",Math.lcm(first:120, second:90))”调用静态方法“Math.lcm”计算120和90的最小公倍数,输出“LCM(120,90)=360”。第6行“var c1=Circle(radius:3.5)”定义实例c1。第7行“print("We have created \(Circle.count) instance(s).")”输出“We have created 1 instance(s).”表示已创建了一个结构体Circle类型的实例。第8行“print("Circles area="+String(format:"%5.2f",c1.area()))”调用实例方法area输出圆的面积,得到“Circles area=38.48”。第9行“var c2=Circle(radius:3.5)”创建了结构体Circle的另一个实例c2。第10行“print("We have created \(Circle.count) instance(s).")”输出“We have created 2 instance(s).”,表示已创建了2个实例。第11行“print("Circles area="+String(format:"%5.2f",c2.area()))”调用实例方法area输出新实例c2的面积,得到“Circles area=38.48”。

5.2.5结构体索引器

结构体支持自定义索引器方法,索引器也称下标,例如,在访问数组元素时,使用的“数组名[元素下标]”为一种索引器的形式; 在访问字典的元素值时,使用的“字典名[键名]”也是一种索引器的形式。在结构体中,自定义索引器使用关键字subscript,其语法为

subscript(参数列表) ->返回值类型

{

get

{

语句组

return 语句

}

set(参数列表)

{

语句组

}

}

在上述索引器语法中,可只有get方法,称为只读索引器; 或同时具有get方法和set方法,称为可读可写索引器; 但不能只有set方法,即没有只写索引器。如果一个索引器为只读索引器,get和其相关的“{ }”可以省略。对于set方法,如果其参数省略,则使用默认参数newValue,也可以在set的“参数列表”中指定参数,无须指定类型。索引器的“参数列表”可以为空,但是必须有返回值。

程序段514演示了索引器的用法。




视频讲解

程序段514索引器用法实例

1import Foundation

2

3struct Matrix

4{

5var row : Int

6var col : Int

7private var val : [Int]

8init(row:Int, col:Int)

9{

10self.row = row

11self.col = col

12val = Array(repeating: 0, count: row*col)

13}

14subscript(row:Int,col:Int)->Int

15{

16get

17{

18return val[row*self.col+col]

19}

20set(v)

21{

22val[row*self.col+col] = v

23}

24}

25}

26var m = Matrix(row: 3, col: 4)

27for i in 0...3-1

28{

29for j in 0...4-1

30{

31m[i,j] = i*4+j+1

32}

33}

34for i in 0...3-1

35{

36for j in 0...4-1

37{

38print(String(format: "%6d", m[i,j]), terminator: "")

39}

40print()

41}

在程序段514中,第3~25行定义了一个结构体Matrix。第5行“var row:Int”定义存储属性row,用于保存矩阵的行数; 第6行“var col:Int”定义存储属性col,用于保存矩阵的列数; 第7行“private var val:[Int]”定义私有属性val,为一个整型数组。第8~13行为初始化器init,具有两个参数row和col,表示矩阵的行数和列数; 在初始化器中,将参数row赋给属性row,将参数col赋给属性col,生成一个长度为“row*col”的全0数组赋给私有属性val。

第14~24行为索引器,具有两个参数row和col,指定矩阵的行和列,返回整型值。在第16~19行的get方法中,第18行“return val[row*self.col+col]”返回数组的第“row*self.col+col”号元素,相当于返回矩阵的第row行第col列的元素。在第20~23行的set方法中,将赋给索引器的值v赋给“val[row*self.col+col]”,相当于赋给矩阵的第row行第col列的元素。

第26行“var m=Matrix(row:3, col:4)”定义了一个Matrix实例m,初始值为一个长度为12的全0数组。尽管实例m本身只包含了一个长度为12的数组,但是通过它的索引器,可使它的元素操作表现为读写一个3行4列的矩阵。

第27~33行为嵌套的forin结构,执行第31行“m[i,j]=i*4+j+1”,向矩阵m的第i行第j列写入值“i*4+j+1”。

第34~41行为嵌套的forin结构,执行第38行“print(String(format:"%6d", m[i,j]), terminator:"")”输出m[i,j]的值,对于每个i还将执行第40行“print()”输出一个回车换行符,最终的输出结果如图54所示。



图54程序段514的输出结果


除了程序段514所示的使用整数值的索引器外,索引器的参数可以为任意基本类型和集类型(例如数组和字典等),甚至可为空参数。程序段515中使用了双精度浮点型和空类型作为索引器的参数,进一步说明索引器的用法。




视频讲解

程序段515参数为双精度浮点型和空类型的索引器用法实例

1import Foundation

2

3struct Circle

4{

5var r : Double = 1.0

6subscript(radius : Double) -> Double

7{

8get

9{

10return 3.14*radius*radius

11}

12}

13subscript()->Double

14{

15get

16{

17return 2*3.14*r

18}

19set

20{

21r = newValue

22}

23}

24}

25var c = Circle(r: 3.0)

26print("Area =",String(format: "%6.2f", c[10.0]))

27c[]=20

28print("Perimeter =",String(format: "%6.2f", c[]))

在程序段515中,第3~24行定义了结构体Circle。第5行“var r:Double=1.0”定义存储属性r,赋初始值为1.0,表示圆的半径。

第6~12行定义了一个只读索引器,具有一个双精度浮点型参数radius,其中,第8、9、11行的内容可以省略,第10行“return 3.14*radius*radius”返回以索引器的参数为半径的圆面积。

第13~23行定义了一个空参数的索引器,在第15~18行的get方法中返回以存储属性r为半径的圆周长; 在第19~22行的set方法中,将赋给索引器的值(这里用newValue关键字表示)赋给属性r。

第25行“var c=Circle(r:3.0)”定义结构体Circle的实例c,将其属性r设为3.0。第26行“print("Area =",String(format:"%6.2f", c[10.0]))”调用索引器c[10.0]输出半径为10.0的圆面积,得到“Area=314.00”。第27行“c[]=20”向参数为空的索引器赋值,即将实例c的属性r赋为20。第28行“print("Perimeter =",String(format:"%6.2f", c[]))”调用参数为空的索引器c[]得到圆的周长,输出“Perimeter=125.60”。

5.3本章小结

在Swift语言中,枚举类型和结构体类型都是值类型,并且结构体类型是程序设计常用的自定义类型。在类和结构体之间选择时,Swift语言设计者鼓励使用结构体类型。本章详细介绍了枚举类型和结构体类型的定义与用法,讨论了这两种类型的初始化器,详细阐述了结构体的属性和方法,介绍了结构体的索引器用法。在Swift语言中,面向结构体的程序设计,已经可以体现面向对象编程的特性。结构体类型除了为值类型外,它的其他特性,如属性、方法、初始化器、索引器等,也体现在类类型上,但是类为引用类型。第6章将深入介绍类与其实例。


习题

1. 简述Swift语言枚举类型中枚举量的表示方法。

2. 简述Swift语言中结构体的定义方法。

3. 给定一个日期,例如2023年12月28日,计算该日期对应的星期,并输出该星期。

4. 给定两个日期,例如2011年5月12日和2023年10月10日,计算这两个日期间的天数。

5. (实践题型)创建一个表示学生信息的结构体,具有属性: 姓名、学号、出生日期、性别等,具有输出学生信息的方法display。编写一个应用,定义上述结构体数组,统计班上同学的出生日期的分布率(按月份统计,即每月出生人数除以总人数的比例),输出生日相同的同学。

6. 在第5题基础上,定义一个班级结构体,将全部学生作为班级结构体的属性,将学号作为索引器,通过学号可访问班级中对应的学生,并输出该学生的信息。