Swift语法基础(五)-- (类和结构体、属性、方法)
本章将会介绍
类和结构体对比
结构体和枚举是值类型
类是引用类型
类和结构体的选择
字符串、数组和字典类型的赋值和复制行为
存储属性
计算属性
属性观察器
全局变量和局部变量
类型属性
实例方法
类型方法
类和结构体
类和结构体是人们构建代码所用的一种通用且灵活的构造体。我们可以使用完全相同的语法规则来为类和结构体定义属性(常量、变量)和添加方法,从而扩展类和结构体的功能。
与其他编程语言所不同的是,Swift 并不要求你为自定义类和结构体去创建独立的接口和实现文件。你所要做的是在一个单一文件中定义一个类或者结构体,系统将会自动生成面向其它代码的外部接口。
注意
通常一个类的实例被称为对象。然而在 Swift 中,类和结构体的关系要比在其他语言中更加的密切,大部分功能都可以用在类和结构体上。因此,我们会主要使用实例。
1.类和结构体对比
Swift 中类和结构体有很多共同点。共同处在于:
- 定义属性用于存储值
- 定义方法用于提供功能
- 定义下标操作使得可以通过下标语法来访问实例所包含的值
- 定义构造器用于生成初始化值
- 通过扩展以增加默认实现的功能
- 实现协议以提供某种标准功能
与结构体相比,类还有如下的附加功能:
- 继承允许一个类继承另一个类的特征
- 类型转换允许在运行时检查和解释一个类实例的类型
- 析构器允许一个类实例释放任何其所被分配的资源
- 引用计数允许对一个类的多次引用
定义语法
类和结构体有着类似的定义方式。我们通过关键字class和struct来分别表示类和结构体,并在一对大括号中定义它们的具体内容:
class SomeClass {
// 在这里定义类
}
struct SomeStructure {
// 在这里定义结构体
}
注意
在你每次定义一个新类或者结构体的时候,实际上你是定义了一个新的 Swift 类型。因此请使用UpperCamelCase这种方式来命名(如SomeClass和SomeStructure等),以便符合标准 Swift 类型的大写命名风格(如String,Int和Bool)。相反的,请使用lowerCamelCase这种方式为属性和方法命名(如frameRate和incrementCount),以便和类型名区分。
以下是定义结构体和定义类的示例:
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
}
在上面的示例中我们定义了一个名为Resolution的结构体,用来描述一个显示器的像素分辨率。这个结构体包含了两个名为width和height的存储属性。存储属性是被捆绑和存储在类或结构体中的常量或变量。当这两个属性被初始化为整数0的时候,它们会被推断为Int类型。
在上面的示例中我们还定义了一个名为VideoMode的类,用来描述一个视频显示器的特定模式。这个类包含了四个变量存储属性。第一个是分辨率,它被初始化为一个新的Resolution结构体的实例,属性类型被推断为Resolution。新VideoMode实例同时还会初始化其它三个属性,它们分别是,初始值为false的interlaced,初始值为0.0的frameRate,以及值为可选String的name。name属性会被自动赋予一个默认值nil,意为“没有name值”,因为它是一个可选类型。
类和结构体实例
Resolution结构体和VideoMode类的定义仅描述了什么是Resolution和VideoMode。它们并没有描述一个特定的分辨率(resolution)或者视频模式(video mode)。为了描述一个特定的分辨率或者视频模式,我们需要生成一个它们的实例。
生成结构体和类实例的语法非常相似:
let someResolution = Resolution()
let someVideoMode = VideoMode()
结构体和类都使用构造器语法来生成新的实例。构造器语法的最简单形式是在结构体或者类的类型名称后跟随一对空括号,如Resolution()或VideoMode()。通过这种方式所创建的类或者结构体实例,其属性均会被初始化为默认值。
属性访问
通过使用点语法,你可以访问实例的属性。其语法规则是,实例名后面紧跟属性名,两者通过点号(.)连接:
print("The width of someResolution is \(someResolution.width)")
// 打印 "The width of someResolution is 0"
在上面的例子中,someResolution.width引用someResolution的width属性,返回width的初始值0。
你也可以访问子属性,如VideoMode中Resolution属性的width属性:
print("The width of someVideoMode is \(someVideoMode.resolution.width)")
// 打印 "The width of someVideoMode is 0"
你也可以使用点语法为变量属性赋值:
someVideoMode.resolution.width = 1280
print("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// 打印 "The width of someVideoMode is now 1280"
注意
与 Objective-C 语言不同的是,Swift 允许直接设置结构体属性的子属性。上面的最后一个例子,就是直接设置了someVideoMode中resolution属性的width这个子属性,以上操作并不需要重新为整个resolution属性设置新值。
结构体类型的成员逐一构造器
所有结构体都有一个自动生成的成员逐一构造器,用于初始化新结构体实例中成员的属性。新实例中各个属性的初始值可以通过属性的名称传递到成员逐一构造器之中:
let vga = Resolution(width:640, height: 480)
与结构体不同,类实例没有默认的成员逐一构造器。
2.结构体和枚举是值类型
值类型被赋予给一个变量、常量或者被传递给一个函数的时候,其值会被拷贝。
在之前的章节中,我们已经大量使用了值类型。实际上,在 Swift 中,所有的基本类型:整数(Integer)、浮点数(floating-point)、布尔值(Boolean)、字符串(string)、数组(array)和字典(dictionary),都是值类型,并且在底层都是以结构体的形式所实现。
在 Swift 中,所有的结构体和枚举类型都是值类型。这意味着它们的实例,以及实例中所包含的任何值类型属性,在代码中传递的时候都会被复制。
请看下面这个示例,其使用了前一个示例中的Resolution结构体:
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd
在以上示例中,声明了一个名为hd的常量,其值为一个初始化为全高清视频分辨率(1920 像素宽,1080 像素高)的Resolution实例。
然后示例中又声明了一个名为cinema的变量,并将hd赋值给它。因为Resolution是一个结构体,所以cinema的值其实是hd的一个拷贝副本,而不是hd本身。尽管hd和cinema有着相同的宽(width)和高(height),但是在幕后它们是两个完全不同的实例。
下面,为了符合数码影院放映的需求(2048 像素宽,1080 像素高),cinema的width属性需要作如下修改:
cinema.width = 2048
这里,将会显示cinema的width属性确已改为了2048:
print("cinema is now \(cinema.width) pixels wide")
// 打印 "cinema is now 2048 pixels wide"
然而,初始的hd实例中width属性还是1920:
print("hd is still \(hd.width) pixels wide")
// 打印 "hd is still 1920 pixels wide"
在将hd赋予给cinema的时候,实际上是将hd中所存储的值进行拷贝,然后将拷贝的数据存储到新的cinema实例中。结果就是两个完全独立的实例碰巧包含有相同的数值。由于两者相互独立,因此将cinema的width修改为2048并不会影响hd中的width的值。
枚举也遵循相同的行为准则:
enum CompassPoint {
case North, South, East, West
}
var currentDirection = CompassPoint.West
let rememberedDirection = currentDirection
currentDirection = .East
if rememberedDirection == .West {
print("The remembered direction is still .West")
}
// 打印 "The remembered direction is still .West"
上例中rememberedDirection被赋予了currentDirection的值,实际上它被赋予的是值的一个拷贝。赋值过程结束后再修改currentDirection的值并不影响rememberedDirection所储存的原始值的拷贝。
3.类是引用类型
与值类型不同,引用类型在被赋予到一个变量、常量或者被传递到一个函数时,其值不会被拷贝。因此,引用的是已存在的实例本身而不是其拷贝。
请看下面这个示例,其使用了之前定义的VideoMode类:
let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0
以上示例中,声明了一个名为tenEighty的常量,其引用了一个VideoMode类的新实例。在之前的示例中,这个视频模式(video mode)被赋予了HD分辨率(1920*1080)的一个拷贝(即hd实例)。同时设置为interlaced,命名为“1080i”。最后,其帧率是25.0帧每秒。
然后,tenEighty被赋予名为alsoTenEighty的新常量,同时对alsoTenEighty的帧率进行修改:
let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0
因为类是引用类型,所以tenEight和alsoTenEight实际上引用的是相同的VideoMode实例。换句话说,它们是同一个实例的两种叫法。
下面,通过查看tenEighty的frameRate属性,我们会发现它正确的显示了所引用的VideoMode实例的新帧率,其值为30.0:
print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// 打印 "The frameRate property of theEighty is now 30.0"
需要注意的是tenEighty和alsoTenEighty被声明为常量而不是变量。然而你依然可以改变tenEighty.frameRate和alsoTenEighty.frameRate,因为tenEighty和alsoTenEighty这两个常量的值并未改变。它们并不“存储”这个VideoMode实例,而仅仅是对VideoMode实例的引用。所以,改变的是被引用的VideoMode的frameRate属性,而不是引用VideoMode的常量的值。
- 恒等运算符
因为类是引用类型,有可能有多个常量和变量在幕后同时引用同一个类实例。(对于结构体和枚举来说,这并不成立。因为它们作为值类型,在被赋予到常量、变量或者传递到函数时,其值总是会被拷贝。)
如果能够判定两个常量或者变量是否引用同一个类实例将会很有帮助。为了达到这个目的,Swift 内建了两个恒等运算符:
等价于(===)
不等价于(!==)
运用这两个运算符检测两个常量或者变量是否引用同一个实例:
if tenEighty === alsoTenEighty {
print("tenEighty and alsoTenEighty refer to the same Resolution instance.")
}
//打印 "tenEighty and alsoTenEighty refer to the same Resolution instance."
请注意,“等价于”(用三个等号表示,===
)与“等于”(用两个等号表示,==
)的不同:
“等价于”表示两个类类型(class type)的常量或者变量引用同一个类实例。
“等于”表示两个实例的值“相等”或“相同”,判定时要遵照设计者定义的评判标准,因此相对于“相等”来说,这是一种更加合适的叫法。
当你在定义你的自定义类和结构体的时候,你有义务来决定判定两个实例“相等”的标准。
- 指针
如果你有 C,C++ 或者 Objective-C 语言的经验,那么你也许会知道这些语言使用指针来引用内存中的地址。一个引用某个引用类型实例的 Swift 常量或者变量,与 C 语言中的指针类似,但是并不直接指向某个内存地址,也不要求你使用星号(*)来表明你在创建一个引用。Swift 中的这些引用与其它的常量或变量的定义方式相同。
4.类和结构体的选择
在你的代码中,你可以使用类和结构体来定义你的自定义数据类型。
然而,结构体实例总是通过值传递,类实例总是通过引用传递。这意味两者适用不同的任务。当你在考虑一个工程项目的数据结构和功能的时候,你需要决定每个数据结构是定义成类还是结构体。
按照通用的准则,当符合一条或多条以下条件时,请考虑构建结构体:
- 该数据结构的主要目的是用来封装少量相关简单数据值。
- 有理由预计该数据结构的实例在被赋值或传递时,封装的数据将会被拷贝而不是被引用。
- 该数据结构中储存的值类型属性,也应该被拷贝,而不是被引用。
- 该数据结构不需要去继承另一个既有类型的属性或者行为。
举例来说,以下情境中适合使用结构体:
- 几何形状的大小,封装一个width属性和height属性,两者均为Double类型。
- 一定范围内的路径,封装一个start属性和length属性,两者均为Int类型。
- 三维坐标系内一点,封装x,y和z属性,三者均为Double类型。
在所有其它案例中,定义一个类,生成一个它的实例,并通过引用来管理和传递。实际中,这意味着绝大部分的自定义数据构造都应该是类,而非结构体。
5.字符串、数组、和字典类型的赋值与复制行为
Swift 中,许多基本类型,诸如String,Array和Dictionary类型均以结构体的形式实现。这意味着被赋值给新的常量或变量,或者被传入函数或方法中时,它们的值会被拷贝。
Objective-C 中NSString,NSArray和NSDictionary类型均以类的形式实现,而并非结构体。它们在被赋值或者被传入函数或方法时,不会发生值拷贝,而是传递现有实例的引用。
注意
以上是对字符串、数组、字典的“拷贝”行为的描述。在你的代码中,拷贝行为看起来似乎总会发生。然而,Swift 在幕后只在绝对必要时才执行实际的拷贝。Swift 管理所有的值拷贝以确保性能最优化,所以你没必要去回避赋值来保证性能最优化。
6.类和结构体总结
// 类和结构体
// 定义
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
}
// 实例
let someResolution = Resolution()
let someVideoMode = VideoMode()
// 属性访问
someResolution.width
// 访问子属性
someVideoMode.resolution.width
// 为变量属性赋值
someVideoMode.resolution.width = 1280
someVideoMode.resolution.width
let vga = Resolution(width: 640, height: 480)
vga.width
// 结构体是值类型
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd
cinema.width = 2048
hd.width
// 类是引用类型
let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0
let alsoTenEight = tenEighty
alsoTenEight.frameRate = 30.0
tenEighty.frameRate
// 恒等运算符 用来判断两个常量或者变量是否引用同一个类实例
if alsoTenEight === tenEighty {
print("恒等的")
}
属性 (Properties)
属性将值跟特定的类、结构或枚举关联。存储属性存储常量或变量作为实例的一部分,而计算属性计算(不是存储)一个值。计算属性可以用于类、结构体和枚举,存储属性只能用于类和结构体。
存储属性和计算属性通常与特定类型的实例关联。但是,属性也可以直接作用于类型本身,这种属性称为类型属性。
另外,还可以定义属性观察器来监控属性值的变化,以此来触发一个自定义的操作。属性观察器可以添加到自己定义的存储属性上,也可以添加到从父类继承的属性上。
1.存储属性
简单来说,一个存储属性就是存储在特定类或结构体实例里的一个常量或变量。存储属性可以是变量存储属性(用关键字 var 定义),也可以是常量存储属性(用关键字 let 定义)。
可以在定义存储属性的时候指定默认值。也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值。
下面的例子定义了一个名为 FixedLengthRange 的结构体,该结构体用于描述整数的范围,且这个范围值在被创建后不能被修改.
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// 该区间表示整数0,1,2
rangeOfThreeItems.firstValue = 6
// 该区间现在表示整数6,7,8
FixedLengthRange 的实例包含一个名为 firstValue 的变量存储属性和一个名为 length 的常量存储属性。在上面的例子中,length 在创建实例的时候被初始化,因为它是一个常量存储属性,所以之后无法修改它的值。
- 常量结构体的存储属性
如果创建了一个结构体的实例并将其赋值给一个常量,则无法修改该实例的任何属性,即使有属性被声明为变量也不行:
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 该区间表示整数0,1,2,3
rangeOfFourItems.firstValue = 6
// 尽管 firstValue 是个变量属性,这里还是会报错
因为 rangeOfFourItems 被声明成了常量(用 let 关键字),即使 firstValue 是一个变量属性,也无法再修改它了。
这种行为是由于结构体(struct)属于值类型。当值类型的实例被声明为常量的时候,它的所有属性也就成了常量。
属于引用类型的类(class)则不一样。把一个引用类型的实例赋给一个常量后,仍然可以修改该实例的变量属性。
- 延迟存储属性
延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用 lazy 来标示一个延迟存储属性。
注意
必须将延迟存储属性声明成变量(使用 var 关键字),因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延迟属性。
延迟属性很有用,当属性的值依赖于在实例的构造过程结束后才会知道影响值的外部因素时,或者当获得属性的初始值需要复杂或大量计算时,可以只在需要的时候计算它。
下面的例子使用了延迟存储属性来避免复杂类中不必要的初始化。例子中定义了 DataImporter 和 DataManager 两个类,下面是部分代码:
class DataImporter {
/*
DataImporter 是一个负责将外部文件中的数据导入的类。
这个类的初始化会消耗不少时间。
*/
var fileName = "data.txt"
// 这里会提供数据导入功能
}
class DataManager {
lazy var importer = DataImporter()
var data = [String]()
// 这里会提供数据管理功能
}
let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// DataImporter 实例的 importer 属性还没有被创建
DataManager 类包含一个名为 data 的存储属性,初始值是一个空的字符串(String)数组。这里没有给出全部代码,只需知道 DataManager 类的目的是管理和提供对这个字符串数组的访问即可。
DataManager 的一个功能是从文件导入数据。该功能由 DataImporter 类提供,DataImporter 完成初始化需要消耗不少时间:因为它的实例在初始化时可能要打开文件,还要读取文件内容到内存。
DataManager 管理数据时也可能不从文件中导入数据。所以当 DataManager 的实例被创建时,没必要创建一个 DataImporter 的实例,更明智的做法是第一次用到 DataImporter 的时候才去创建它。
由于使用了 lazy ,importer 属性只有在第一次被访问的时候才被创建。比如访问它的属性 fileName 时:
print(manager.importer.fileName)
// DataImporter 实例的 importer 属性现在被创建了
// 输出 "data.txt”
注意
如果一个被标记为 lazy 的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次。
- 存储属性和实例变量
如果您有过 Objective-C 经验,应该知道 Objective-C 为类实例存储值和引用提供两种方法。除了属性之外,还可以使用实例变量作为属性值的后端存储。
Swift 编程语言中把这些理论统一用属性来实现。Swift 中的属性没有对应的实例变量,属性的后端存储也无法直接访问。这就避免了不同场景下访问方式的困扰,同时也将属性的定义简化成一个语句。属性的全部信息——包括命名、类型和内存管理特征——都在唯一一个地方(类型定义中)定义。
2.计算属性
除存储属性外,类、结构体和枚举可以定义计算属性。计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。
struct Point {
var x = 0.0, y = 0.0
}
struct Size {
var width = 0.0, height = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// 打印 "square.origin is now at (10.0, 10.0)”
这个例子定义了 3 个结构体来描述几何形状:
Point 封装了一个 (x, y) 的坐标
Size 封装了一个 width 和一个 height
Rect 表示一个有原点和尺寸的矩形
Rect也提供了一个名为center 的计算属性。一个矩形的中心点可以从原点(origin)和大小(size)算出,所以不需要将它以显式声明的 Point 来保存。Rect 的计算属性 center 提供了自定义的 getter 和 setter 来获取和设置矩形的中心点,就像它有一个存储属性一样。
上述例子中创建了一个名为 square 的 Rect 实例,初始值原点是 (0, 0),宽度高度都是 10。如下图中蓝色正方形所示。
square 的 center 属性可以通过点运算符(square.center)来访问,这会调用该属性的 getter 来获取它的值。跟直接返回已经存在的值不同,getter 实际上通过计算然后返回一个新的 Point 来表示 square 的中心点。如代码所示,它正确返回了中心点 (5, 5)。
center 属性之后被设置了一个新的值 (15, 15),表示向右上方移动正方形到如下图橙色正方形所示的位置。设置属性center的值会调用它的 setter 来修改属性 origin 的 x 和 y 的值,从而实现移动正方形到新的位置。
- 简化 setter 声明
如果计算属性的 setter 没有定义表示新值的参数名,则可以使用默认名称 newValue。下面是使用了简化 setter 声明的 Rect 结构体代码:
struct AlternativeRect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}
- 只读计算属性
只有 getter 没有 setter 的计算属性就是只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。
注意
必须使用 var 关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let 关键字只用来声明常量属性,表示初始化后再也无法修改的值。
只读计算属性的声明可以去掉 get 关键字和花括号:
struct Cuboid {
var width = 0.0, height = 0.0, depth = 0.0
var volume: Double {
return width * height * depth
}
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// 打印 "the volume of fourByFiveByTwo is 40.0"
这个例子定义了一个名为 Cuboid 的结构体,表示三维空间的立方体,包含 width、height 和 depth 属性。结构体还有一个名为 volume 的只读计算属性用来返回立方体的体积。为 volume 提供 setter 毫无意义,因为无法确定如何修改 width、height 和 depth 三者的值来匹配新的 volume。然而,Cuboid 提供一个只读计算属性来让外部用户直接获取体积是很有用的。
3.属性观察器
属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,即使新值和当前值相同的时候也不例外。
可以为除了延迟存储属性之外的其他存储属性添加属性观察器,也可以通过重写属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。你不必为非重写的计算属性添加属性观察器,因为可以通过它的 setter 直接监控和响应值的变化。
可以为属性添加如下的一个或全部观察器:
- willSet 在新的值被设置之前调用
- didSet 在新的值被设置之后立即调用
willSet 观察器会将新的属性值作为常量参数传入,在 willSet 的实现代码中可以为这个参数指定一个名称,如果不指定则参数仍然可用,这时使用默认名称 newValue 表示。
同样,didSet 观察器会将旧的属性值作为参数传入,可以为该参数命名或者使用默认参数名 oldValue。如果在 didSet 方法中再次对该属性赋值,那么新值会覆盖旧的值。
注意
父类的属性在子类的构造器中被赋值时,它在父类中的 willSet 和 didSet 观察器会被调用,随后才会调用子类的观察器。在父类初始化方法调用之前,子类给属性赋值时,观察器不会被调用。
下面是一个 willSet 和 didSet 实际运用的例子,其中定义了一个名为 StepCounter 的类,用来统计一个人步行时的总步数。这个类可以跟计步器或其他日常锻炼的统计装置的输入数据配合使用。
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("About to set totalSteps to \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("Added \(totalSteps - oldValue) steps")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps
StepCounter 类定义了一个 Int 类型的属性 totalSteps,它是一个存储属性,包含 willSet 和 didSet 观察器。
当 totalSteps 被设置新值的时候,它的 willSet 和 didSet 观察器都会被调用,即使新值和当前值完全相同时也会被调用。
例子中的 willSet 观察器将表示新值的参数自定义为 newTotalSteps,这个观察器只是简单的将新的值输出。
didSet 观察器在 totalSteps 的值改变后被调用,它把新值和旧值进行对比,如果总步数增加了,就输出一个消息表示增加了多少步。didSet 没有为旧值提供自定义名称,所以默认值 oldValue 表示旧值的参数名。
注意
如果将属性通过 in-out 方式传入函数,willSet 和 didSet 也会调用。这是因为 in-out 参数采用了拷入拷出模式:即在函数内部使用的是参数的 copy,函数结束后,又对参数重新赋值。
4.全局变量和局部变量
计算属性和属性观察器所描述的功能也可以用于全局变量和局部变量。全局变量是在函数、方法、闭包或任何类型之外定义的变量。局部变量是在函数、方法或闭包内部定义的变量。
前面章节提到的全局或局部变量都属于存储型变量,跟存储属性类似,它为特定类型的值提供存储空间,并允许读取和写入。
另外,在全局或局部范围都可以定义计算型变量和为存储型变量定义观察器。计算型变量跟计算属性一样,返回一个计算结果而不是存储值,声明格式也完全一样。
注意
全局的常量或变量都是延迟计算的,跟延迟存储属性相似,不同的地方在于,全局的常量或变量不需要标记lazy修饰符。
局部范围的常量或变量从不延迟计算。
5.类型属性
实例属性属于一个特定类型的实例,每创建一个实例,实例都拥有属于自己的一套属性值,实例之间的属性相互独立。
也可以为类型本身定义属性,无论创建了多少个该类型的实例,这些属性都只有唯一一份。这种属性就是类型属性。
类型属性用于定义某个类型所有实例共享的数据,比如所有实例都能用的一个常量(就像 C 语言中的静态常量),或者所有实例都能访问的一个变量(就像 C 语言中的静态变量)。
存储型类型属性可以是变量或常量,计算型类型属性跟实例的计算型属性一样只能定义成变量属性。
注意
跟实例的存储型属性不同,必须给存储型类型属性指定默认值,因为类型本身没有构造器,也就无法在初始化过程中使用构造器给类型属性赋值。
存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行一次初始化,并且不需要对其使用 lazy 修饰符。
- 类型属性的语法
在 C 或 Objective-C 中,与某个类型关联的静态常量和静态变量,是作为全局(global)静态变量定义的。但是在 Swift 中,类型属性是作为类型定义的一部分写在类型最外层的花括号内,因此它的作用范围也就在类型支持的范围内。
使用关键字 static 来定义类型属性。在为类定义计算型类型属性时,可以改用关键字 class 来支持子类对父类的实现进行重写。下面的例子演示了存储型和计算型类型属性的语法:
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 1
}
}
enum SomeEnumeration {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 6
}
}
class SomeClass {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 27
}
class var overrideableComputedTypeProperty: Int {
return 107
}
}
- 获取和设置类型属性的值
跟实例属性一样,类型属性也是通过点运算符来访问。但是,类型属性是通过类型本身来访问,而不是通过实例。比如:
print(SomeStructure.storedTypeProperty)
// 打印 "Some value."
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// 打印 "Another value.”
print(SomeEnumeration.computedTypeProperty)
// 打印 "6"
print(SomeClass.computedTypeProperty)
// 打印 "27"
下面的例子定义了一个结构体,使用两个存储型类型属性来表示两个声道的音量,每个声道具有 0 到 10 之间的整数音量。
下图展示了如何把两个声道结合来模拟立体声的音量。当声道的音量是 0,没有一个灯会亮;当声道的音量是 10,所有灯点亮。本图中,左声道的音量是 9,右声道的音量是 7:
上面所描述的声道模型使用 AudioChannel 结构体的实例来表示:
struct AudioChannel {
static let thresholdLevel = 10
static var maxInputLevelForAllChannels = 0
var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
// 将当前音量限制在阈值之内
currentLevel = AudioChannel.thresholdLevel
}
if currentLevel > AudioChannel.maxInputLevelForAllChannels {
// 存储当前音量作为新的最大输入音量
AudioChannel.maxInputLevelForAllChannels = currentLevel
}
}
}
}
结构 AudioChannel 定义了 2 个存储型类型属性来实现上述功能。第一个是 thresholdLevel,表示音量的最大上限阈值,它是一个值为 10 的常量,对所有实例都可见,如果音量高于 10,则取最大上限值 10(见后面描述)。
第二个类型属性是变量存储型属性 maxInputLevelForAllChannels,它用来表示所有 AudioChannel 实例的最大音量,初始值是0。
AudioChannel 也定义了一个名为 currentLevel 的存储型实例属性,表示当前声道现在的音量,取值为 0 到 10。
属性 currentLevel 包含 didSet 属性观察器来检查每次设置后的属性值,它做如下两个检查:
- 如果 currentLevel 的新值大于允许的阈值 thresholdLevel,属性观察器将 currentLevel 的值限定为阈值 thresholdLevel。
- 如果修正后的 currentLevel 值大于静态类型属性 maxInputLevelForAllChannels 的值,属性观察器就将新值保存在 maxInputLevelForAllChannels 中。
注意
在第一个检查过程中,didSet 属性观察器将 currentLevel 设置成了不同的值,但这不会造成属性观察器被再次调用。
可以使用结构体 AudioChannel 创建两个声道 leftChannel 和 rightChannel,用以表示立体声系统的音量:
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()
如果将左声道的 currentLevel 设置成 7,类型属性 maxInputLevelForAllChannels 也会更新成 7:
leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// 输出 "7"
print(AudioChannel.maxInputLevelForAllChannels)
// 输出 "7"
如果试图将右声道的 currentLevel 设置成 11,它会被修正到最大值 10,同时 maxInputLevelForAllChannels 的值也会更新到 10:
rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// 输出 "10"
print(AudioChannel.maxInputLevelForAllChannels)
// 输出 "10"
6.属性总结
属性
// 存储属性
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// firstValue是变量存储属性 因此可以更改
rangeOfThreeItems.firstValue = 6
// length是常量存储属性因此不可以再更改
//rangeOfThreeItems.length = 7
// 常量结构体存储属性,无法修改实例的任何属性,即使有属性被声明为变量也不行
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 不可以修改实例的任何属性 因为定义的是常量结构体
//rangeOfFourItems.firstValue = 3
// 上边也从灵位一个角度证明了结构体是值类型
// 延迟存储属性 lazy
// 延迟属性很有用,当属性的值依赖于在实例的构造过程结束后才会知道影响值的外部因素时,或者当获得属性的初始值需要复杂或大量计算时,可以只在需要的时候计算它。
class DataImporter {
// 导入数据,存入内存,这一系列动作需要不少时间
var fileName = "data.txt"
}
class DataManager {
lazy var importer = DataImporter()
var data = [String]()
}
let manager = DataManager()
manager.data.append("Some Data")
manager.data.append("Some More Data")
// 由于使用了 lazy ,importer 属性只有在第一次被访问的时候才被创建。比如访问它的属性 fileName 时:
print(manager.importer.fileName)
// 计算属性
struct Point {
var x = 0.0, y = 0.0
}
struct Size {
var width = 0.0, height = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}
var square = Rect(origin: Point(x: 0.0, y: 0.0), size: Size(width: 10.0, height: 10.0))
// center属性计算属性,在调用center属性的时候,会调用计算属性的get方法通过计算得出中心点的位置
let initialSquareCenter = square.center
print("square.center is now at (\(initialSquareCenter.x), \(initialSquareCenter.y))")
// 现在修改中心点,center属性通过set方法,重新计算原点的位置
square.center = Point(x: 15.0, y: 15.0)
print("origin is now at (\(square.origin.x), \(square.origin.y))")
// 只读计算属性 就是只有getter 没有 setter 这种计算属性不能设置新的值,只读计算属性的声明可以去掉get
struct Cuboid {
var width = 0.0, height = 0.0, depth = 0.0
var volume: Double {
return width * height * depth
}
}
let fourBtFiveByTwo = Cuboid(width: 4, height: 5, depth: 2)
fourBtFiveByTwo.volume
// 属性观察器
class StepCounter {
var totalSteps: Int = 0 {
willSet {
print("About to set totalSteps to \(newValue)")
}
didSet {
if totalSteps > oldValue {
print("Added \(totalSteps - oldValue) steps")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
stepCounter.totalSteps = 360
// 类型属性 多个实例公用一套属性值,势力之间的属性相互独立 使用static关键字
struct SomeStructure {
// 存储型类型属性
static var storedTypeProperty = "Some Value."
// 计算型类型属性
static var computedTypeProperty: Int {
return 1
}
}
class SomeClass {
// 存储型类型属性
static var storedTypeProperty = "Some Value."
// 计算型类型属性
static var computedTypeProperty: Int {
return 27
}
// 计算型类型属性 支持子类对父类的实现进行重写 改用class
class var overrideableComputedTypeProperty: Int {
return 107
}
}
// 获取和设置类型属性的值
print(SomeStructure.storedTypeProperty)
SomeStructure.storedTypeProperty = "Another Value"
print(SomeStructure.storedTypeProperty)
print(SomeClass.computedTypeProperty)
print(SomeClass.overrideableComputedTypeProperty)
// 示例
struct AudioChannel {
static let thresholdLevel = 10
static var maxInputLevelForAllChannels = 0
var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
// 将当前音量限制在阈值之内
currentLevel = AudioChannel.thresholdLevel
}
if currentLevel > AudioChannel.maxInputLevelForAllChannels {
// 存储当前音量作为新的最大输入音量
AudioChannel.maxInputLevelForAllChannels = currentLevel
}
}
}
}
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()
leftChannel.currentLevel = 7
print(leftChannel.currentLevel) // 7
print(AudioChannel.maxInputLevelForAllChannels) // 7
rightChannel.currentLevel = 11
print(rightChannel.currentLevel) // 10
print(AudioChannel.maxInputLevelForAllChannels) // 10
方法(Method)
方法是与某些特定类型相关联的函数。类、结构体、枚举都可以定义实例方法;实例方法为给定类型的实例封装了具体的任务与功能。类、结构体、枚举也可以定义类型方法;类型方法与类型本身相关联。类型方法与 Objective-C 中的类方法(class methods)相似。
结构体和枚举能够定义方法是 Swift 与 C/Objective-C 的主要区别之一。在 Objective-C 中,类是唯一能定义方法的类型。但在 Swift 中,你不仅能选择是否要定义一个类/结构体/枚举,还能灵活地在你创建的类型(类/结构体/枚举)上定义方法。
1.实例方法(Instance Methods)
实例方法是属于某个特定类、结构体或者枚举类型实例的方法。实例方法提供访问和修改实例属性的方法或提供与实例目的相关的功能,并以此来支撑实例的功能。实例方法的语法与函数完全一致。
实例方法要写在它所属的类型的前后大括号之间。实例方法能够隐式访问它所属类型的所有的其他实例方法和属性。实例方法只能被它所属的类的某个特定实例调用。实例方法不能脱离于现存的实例而被调用。
下面的例子,定义一个很简单的Counter类,Counter能被用来对一个动作发生的次数进行计数:
class Counter {
var count = 0
func increment() {
count += 1
}
func increment(by amount: Int) {
count += amount
}
func reset() {
count = 0
}
}
Counter类定义了三个实例方法:
- increment让计数器按一递增;
- increment(by: Int)让计数器按一个指定的整数值递增;
- reset将计数器重置为0。
Counter这个类还声明了一个可变属性count,用它来保持对当前计数器值的追踪。和调用属性一样,用点语法(dot syntax)调用实例方法:
let counter = Counter()
// 初始计数值是0
counter.increment()
// 计数值现在是1
counter.increment(by: 5)
// 计数值现在是6
counter.reset()
// 计数值现在是0
函数参数可以同时有一个局部名称(在函数体内部使用)和一个外部名称(在调用函数时使用)。方法参数也一样,因为方法就是函数,只是这个函数与某个类型相关联了。
- self属性
类型的每一个实例都有一个隐含属性叫做self,self完全等同于该实例本身。你可以在一个实例的实例方法中使用这个隐含的self属性来引用当前实例。
上面例子中的increment方法还可以这样写:
func increment() {
self.count += 1
}
实际上,你不必在你的代码里面经常写self。不论何时,只要在一个方法中使用一个已知的属性或者方法名称,如果你没有明确地写self,Swift 假定你是指当前实例的属性或者方法。这种假定在上面的Counter中已经示范了:Counter中的三个实例方法中都使用的是count(而不是self.count)。
使用这条规则的主要场景是实例方法的某个参数名称与实例的某个属性名称相同的时候。在这种情况下,参数名称享有优先权,并且在引用属性时必须使用一种更严格的方式。这时你可以使用self属性来区分参数名称和属性名称。
下面的例子中,self消除方法参数x和实例属性x之间的歧义:
struct Point {
var x = 0.0, y = 0.0
func isToTheRightOfX(x: Double) -> Bool {
return self.x > x
}
}
let somePoint = Point(x: 4.0, y: 5.0)
if somePoint.isToTheRightOfX(1.0) {
print("This point is to the right of the line where x == 1.0")
}
// 打印 "This point is to the right of the line where x == 1.0"
如果不使用self前缀,Swift 就认为两次使用的x都指的是名称为x的函数参数。
- 在实例方法中修改值类型
结构体和枚举是值类型。默认情况下,值类型的属性不能在它的实例方法中被修改。
但是,如果你确实需要在某个特定的方法中修改结构体或者枚举的属性,你可以为这个方法选择可变(mutating)行为,然后就可以从其方法内部改变它的属性;并且这个方法做的任何改变都会在方法执行结束时写回到原始结构中。方法还可以给它隐含的self属性赋予一个全新的实例,这个新实例在方法结束时会替换现存实例。
要使用可变方法,将关键字mutating 放到方法的func关键字之前就可以了:
struct Point {
var x = 0.0, y = 0.0
mutating func moveByX(deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveByX(2.0, y: 3.0)
print("The point is now at (\(somePoint.x), \(somePoint.y))")
// 打印 "The point is now at (3.0, 4.0)"
上面的Point结构体定义了一个可变方法 moveByX(_:y:) 来移动Point实例到给定的位置。该方法被调用时修改了这个点,而不是返回一个新的点。方法定义时加上了mutating关键字,从而允许修改属性。
注意,不能在结构体类型的常量(a constant of structure type)上调用可变方法,因为其属性不能被改变,即使属性是变量属性。
- 在可变方法中给 self 赋值
可变方法能够赋给隐含属性self一个全新的实例。上面Point的例子可以用下面的方式改写:
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
self = Point(x: x + deltaX, y: y + deltaY)
}
}
枚举的可变方法可以把self设置为同一枚举类型中不同的成员:
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 现在等于 .High
ovenLight.next()
// ovenLight 现在等于 .Off
2.类型方法
实例方法是被某个类型的实例调用的方法。你也可以定义在类型本身上调用的方法,这种方法就叫做类型方法。在方法的func关键字之前加上关键字static,来指定类型方法。类还可以用关键字class来允许子类重写父类的方法实现。
注意
在 Objective-C 中,你只能为 Objective-C 的类类型(classes)定义类型方法(type-level methods)。在 Swift 中,你可以为所有的类、结构体和枚举定义类型方法。每一个类型方法都被它所支持的类型显式包含。
类型方法和实例方法一样用点语法调用。但是,你是在类型上调用这个方法,而不是在实例上调用。下面是如何在SomeClass类上调用类型方法的例子:
class SomeClass {
class func someTypeMethod() {
// 在这里实现类型方法
}
}
SomeClass.someTypeMethod()
在类型方法的方法体(body)中,self指向这个类型本身,而不是类型的某个实例。这意味着你可以用self来消除类型属性和类型方法参数之间的歧义(类似于我们在前面处理实例属性和实例方法参数时做的那样)。
一般来说,在类型方法的方法体中,任何未限定的方法和属性名称,可以被本类中其他的类型方法和类型属性引用。一个类型方法可以直接通过类型方法的名称调用本类中的其它类型方法,而无需在方法名称前面加上类型名称。类似地,在结构体和枚举中,也能够直接通过类型属性的名称访问本类中的类型属性,而不需要前面加上类型名称。
下面的例子定义了一个名为LevelTracker结构体。它监测玩家的游戏发展情况(游戏的不同层次或阶段)。这是一个单人游戏,但也可以存储多个玩家在同一设备上的游戏信息。
游戏初始时,所有的游戏等级(除了等级 1)都被锁定。每次有玩家完成一个等级,这个等级就对这个设备上的所有玩家解锁。LevelTracker结构体用类型属性和方法监测游戏的哪个等级已经被解锁。它还监测每个玩家的当前等级。
struct LevelTracker {
static var highestUnlockedLevel = 1
var currentLevel = 1
static func unlock(_ level: Int) {
if level > highestUnlockedLevel { highestUnlockedLevel = level }
}
static func isUnlocked(_ level: Int) -> Bool {
return level <= highestUnlockedLevel
}
@discardableResult
mutating func advance(to level: Int) -> Bool {
if LevelTracker.isUnlocked(level) {
currentLevel = level
return true
} else {
return false
}
}
}
LevelTracker监测玩家已解锁的最高等级。这个值被存储在类型属性highestUnlockedLevel中。
LevelTracker还定义了两个类型方法与highestUnlockedLevel配合工作。第一个类型方法是unlock(_:)
,一旦新等级被解锁,它会更新highestUnlockedLevel的值。第二个类型方法是isUnlocked(_:)
,如果某个给定的等级已经被解锁,它将返回true。(注意,尽管我们没有使用类似LevelTracker.highestUnlockedLevel的写法,这个类型方法还是能够访问类型属性highestUnlockedLevel)
除了类型属性和类型方法,LevelTracker还监测每个玩家的进度。它用实例属性currentLevel来监测每个玩家当前的等级。
为了便于管理currentLevel属性,LevelTracker定义了实例方法advance(to:)
。这个方法会在更新currentLevel之前检查所请求的新等级是否已经解锁。advance(to:)
方法返回布尔值以指示是否能够设置currentLevel。因为允许在调用advance(to:)
时候忽略返回值,不会产生编译警告,所以函数被标注为@ discardableResult属性。
下面,Player类使用LevelTracker来监测和更新每个玩家的发展进度:
class Player {
var tracker = LevelTracker()
let playerName: String
func complete(level: Int) {
LevelTracker.unlock(level + 1)
tracker.advance(to: level + 1)
}
init(name: String) {
playerName = name
}
}
Player类创建一个新的LevelTracker实例来监测这个用户的进度。它提供了complete(level:)方法,一旦玩家完成某个指定等级就调用它。这个方法为所有玩家解锁下一等级,并且将当前玩家的进度更新为下一等级。(我们忽略了advance(to:)返回的布尔值,因为之前调用LevelTracker.unlock(_:)时就知道了这个等级已经被解锁了)。
你还可以为一个新的玩家创建一个Player的实例,然后看这个玩家完成等级一时发生了什么:
var player = Player(name: "Argyrios")
player.complete(level: 1)
print("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")
// 打印 "highest unlocked level is now 2"
如果你创建了第二个玩家,并尝试让他开始一个没有被任何玩家解锁的等级,那么试图设置玩家当前等级将会失败:
player = Player(name: "Beto")
if player.tracker.advance(to: 6) {
print("player is now on level 6")
} else {
print("level 6 has not yet been unlocked")
}
// 打印 "level 6 has not yet been unlocked"
3.方法(Methods)总结
方法
// 实例方法
class Counter {
var count = 0
func increment() {
count += 1
}
func increment(by amount: Int) {
// by是外部名称(在调用函数时调用) amount是局部名称(在函数体内部使用)
count += amount
}
func reset() {
count = 0
}
}
let counter = Counter()
print(counter.count)
counter.increment()
print(counter.count)
counter.increment(by: 10)
print(counter.count)
counter.reset()
print(counter.count)
// self 属性 类型的每一个实例都有一个隐含属性叫做self self完全等同于该实例本身 当出现实例方法的某个参数与实例的某个属性名称相同时使用,避免混淆
struct Point {
var x = 0.0, y = 0.0
func isToTheRightOfX(x: Double) -> Bool {
return self.x > x
}
// 在实例方法中修改值类型 使用mutating修饰符
mutating func moveByX(deltaX: Double, deltaY: Double) {
x += deltaX
y += deltaY
}
// 在可变方法中给self赋值 与上边的方法效果一样
mutating func moveByX2(deltaX: Double, deltaY: Double) {
self = Point(x: x + deltaX, y: y + deltaY)
}
}
var somePoint = Point(x: 4.0, y: 5.0)
if somePoint.isToTheRightOfX(x: 1.0) {
print("在x==1右边")
}
somePoint.moveByX(deltaX: 1, deltaY: 2)
print("The point is now at (\(somePoint.x), \(somePoint.y))")
somePoint.moveByX2(deltaX: 2, deltaY: 4)
print("The point is now at (\(somePoint.x), \(somePoint.y))")
// 枚举类型也可以在可变方法中给 self 赋值
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()
print(ovenLight)
ovenLight.next()
print(ovenLight)
ovenLight.next()
print(ovenLight)
// 类型方法
struct LevelTracker {
static var highestUnlockedLevel = 1
var currentLevel = 1
static func unlock(_ level: Int) {
if level > highestUnlockedLevel { highestUnlockedLevel = level }
}
static func isUnlocked(_ level: Int) -> Bool {
// LevelTracker.highestUnlockedLevel
return level <= LevelTracker.highestUnlockedLevel
}
@discardableResult
mutating func advance(to level: Int) -> Bool {
if LevelTracker.isUnlocked(level) {
currentLevel = level
return true
} else {
return false
}
}
}
class Player {
var tracker = LevelTracker()
let playerName: String
func complete(level: Int) {
LevelTracker.unlock(level + 1)
tracker.advance(to: level + 1)
}
init(name: String) {
playerName = name
}
}
var player = Player(name: "XiaoMing")
player.complete(level: 1)
print("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")
player = Player(name: "XiaoLi")
if player.tracker.advance(to: 6) {
print("player is now on level 6")
} else {
print("level 6 has not yet been unlocked")
}