属性
属性将值与特定的类、结构或枚举关联。存储属性将常量和变量值存储为实例的一部分,而computed属性计算(而不是存储)值。计算属性由类、结构和枚举提供。存储的属性仅由类和结构提供。
存储和计算属性通常与特定类型的实例相关联。但是,属性也可以与类型本身关联。这些属性称为类型属性。
此外,您可以定义属性观察者来监视属性值的更改,您可以使用自定义操作对其进行响应。属性观察者可以添加到您定义的存储属性中,也可以添加到子类从父类继承的属性中。
存储属性
在最简单的形式中,存储属性是作为特定类或结构的实例的一部分存储的常量或变量。存储属性可以是变量存储属性(由var关键字引入)或常量存储属性(由let关键字引入)。
可以为存储属性提供默认值作为其定义的一部分,如默认属性值中所述。还可以在初始化期间设置和修改存储属性的初始值。甚至对于常量存储属性也是如此,正如在初始化期间分配常量属性所描述的那样。
下面的示例定义了一个名为FixedLengthRange的结构,该结构描述了一个整数范围,该范围的长度在创建后不能更改:
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// the range represents integer values 0, 1, and 2
rangeOfThreeItems.firstValue = 6
// the range now represents integer values 6, 7, and 8
FixedLengthRange实例有一个名为firstValue的变量存储属性和一个名为length的常量存储属性。在上面的示例中,长度在创建新范围时初始化,之后不能更改,因为它是一个常量属性。
常量结构实例的存储属性
如果您创建一个结构的实例并将该实例分配给一个常量,您就不能修改实例的属性,即使它们被声明为变量属性:
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// this range represents integer values 0, 1, 2, and 3
rangeOfFourItems.firstValue = 6
// this will report an error, even though firstValue is a variable property
因为rangeOfFourItems被声明为一个常量(使用let关键字),所以不可能更改其firstValue属性,即使firstValue是一个变量属性。
这种行为是由于结构是值类型。当值类型的实例被标记为常量时,它的所有属性都被标记为常量。
对于引用类型的类则不是这样。如果将引用类型的实例赋给常量,则仍然可以更改该实例的变量属性。
懒惰的存储属性
惰性存储属性是在首次使用时才计算其初始值的属性。通过在其声明前编写lazy修饰符,可以指示惰性存储属性。
必须始终将惰性属性声明为变量(使用var关键字),因为在实例初始化完成之前,可能无法检索其初始值。常量属性必须总是在初始化完成之前有一个值,因此不能声明为惰性。
当属性的初始值依赖于外部因素时,惰性属性非常有用,这些外部因素的值在实例初始化完成后才知道。当属性的初始值需要复杂或计算代价昂贵的设置时,惰性属性也很有用,除非需要,否则不应该执行这些设置。
下面的示例使用惰性存储属性来避免对复杂类进行不必要的初始化。这个例子定义了两个类DataImporter和DataManager,这两个类都没有完整显示:
class DataImporter {
/*
DataImporter is a class to import data from an external file.
The class is assumed to take a nontrivial amount of time to initialize.
*/
var filename = "data.txt"
// the DataImporter class would provide data importing functionality here
}
class DataManager {
lazy var importer = DataImporter()
var data = [String]()
// the DataManager class would provide data management functionality here
}
let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// the DataImporter instance for the importer property has not yet been created
DataManager类有一个名为data的存储属性,它是用一个新的空字符串值数组初始化的。虽然没有显示它的其他功能,但这个DataManager类的目的是管理和提供对字符串数据数组的访问。
DataManager类的部分功能是能够从文件中导入数据。这个功能由DataImporter类提供,它被认为需要花费大量的时间来初始化。这可能是因为DataImporter实例在初始化DataImporter实例时需要打开文件并将其内容读入内存。
DataManager实例可以在不从文件中导入数据的情况下管理其数据,因此在创建DataManager本身时不需要创建新的DataImporter实例。相反,在首次使用DataImporter实例时创建它更有意义。
由于它被标记为惰性修饰符,导入器属性的DataImporter实例仅在首次访问导入器属性时才会创建,比如在查询其filename属性时:
print(manager.importer.filename)
// the DataImporter instance for the importer property has now been created
// Prints "data.txt"
如果多个线程同时访问了一个标记为lazy modifier的属性,并且该属性还没有初始化,那么不能保证该属性只初始化一次。
存储属性和实例变量
如果您有Objective-C的经验,您可能知道它提供了两种方法来存储值和引用作为类实例的一部分。除了属性之外,您还可以使用实例变量作为存储在属性中的值的备份存储。
Swift将这些概念统一为单个属性声明。Swift属性没有对应的实例变量,而且不会直接访问属性的备份存储。这种方法避免了在不同上下文中如何访问值的混淆,并将属性的声明简化为一个单独的、确定的语句。关于属性的所有信息,包括它的名称、类型和内存管理特性,都作为类型定义的一部分在一个位置中定义。
计算属性
除了存储属性之外,类、结构和枚举还可以定义计算属性,这些计算属性实际上并不存储值。相反,它们提供了一个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))")
// Prints "square.origin is now at (10.0, 10.0)"
这个例子定义了三个用于处理几何形状的结构:
点封装了一个点的x坐标和y坐标。
大小封装了宽度和高度。
Rect通过原点和大小定义矩形。
Rect结构还提供了一个名为center的计算属性。Rect的当前中心位置总是可以由其原点和大小确定,因此您不需要将中心点存储为显式的点值。相反,Rect为一个名为center的计算变量定义了一个定制的getter和setter,使您能够像处理实际存储的属性一样处理矩形的中心。
上面的示例创建一个名为square的新的Rect变量。平方变量的初始化起点为(0,0),宽度和高度为10。这个正方形用下图中的蓝色正方形表示。
速记Setter宣言
如果computed属性的setter没有为要设置的新值定义名称,则使用默认名称newValue。下面是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的computed属性被称为只读计算属性。只读的computed属性总是返回一个值,并且可以通过点语法访问,但是不能将其设置为不同的值。
必须使用var关键字将computed property(包括只读computed property)声明为变量属性,因为它们的值不是固定的。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)")
// Prints "the volume of fourByFiveByTwo is 40.0"
这个示例定义了一个名为长方体的新结构,它表示一个具有宽度、高度和深度属性的3D矩形框。这个结构还有一个只读的计算属性volume,它计算并返回长方体的当前卷。对于可设置的卷来说,这是没有意义的,因为对于一个特定的卷值应该使用哪些宽度、高度和深度的值,这将是不明确的。尽管如此,长方体提供一个只读的computed属性很有用,以便让外部用户发现它当前计算的卷。
属性观察者
属性观察者观察和响应属性价值的变化。属性观察者在每次设置属性值时都会被调用,即使新值与属性的当前值相同。
除了惰性存储属性之外,您可以向您定义的任何存储属性添加属性观察者。还可以通过重写子类中的属性,将属性观察者添加到任何继承的属性(无论是存储的还是计算的)。您不需要为不被覆盖的computed properties定义属性观察者,因为您可以在computed property的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属性。
只要给totalSteps的属性赋一个新值,就会调用willSet和didSet观察者。即使新值与当前值相同,也是如此。
这个示例的willSet观察者使用newTotalSteps的自定义参数名来表示即将到来的新值。在本例中,它只是输出将要设置的值。
在totalSteps的值更新后调用didSet观察者。它将totalSteps的新值与旧值进行比较。如果总的步骤数增加了,就会打印一条消息,表明已经采取了多少新步骤。didSet observer不为旧值提供自定义参数名,而是使用旧值的默认名称。
如果您将具有观察者的属性传递给作为in-out参数的函数,则总是调用willSet和didSet观察者。这是因为in-out参数的copy-in -out内存模型:值总是写回到函数末尾的属性
全局变量和局部变量
上面描述的用于计算和观察属性的功能也适用于全局变量和局部变量。全局变量是在任何函数、方法、闭包或类型上下文之外定义的变量。局部变量是在函数、方法或闭包上下文中定义的变量。
您在前几章中遇到的全局变量和局部变量都是存储变量。存储变量,比如存储属性,为特定类型的值提供存储,并允许设置和检索该值。
但是,您也可以定义计算变量,并为存储变量定义观察者,可以是全局的,也可以是局部的。计算的变量计算它们的值,而不是存储它们,它们的写法与计算的属性相同。
全局常量和变量总是以类似于延迟存储属性的方式惰性地计算。与惰性存储属性不同,全局常量和变量不需要使用惰性修饰符进行标记。
局部常量和变量的计算从来都不是惰性的。
类型属性
实例属性属于特定类型的实例。每次创建该类型的新实例时,它都有自己的一组属性值,与任何其他实例分开。
您还可以定义属于类型本身的属性,而不是属于该类型的任何一个实例。无论您创建了多少实例,这些属性只会有一个副本。这些类型的属性称为类型属性。
属性用于定义值类型是普遍的某一特定类型的所有实例,如一个常数所有实例的属性可以使用(如静态常数C),或一个变量属性存储一个值,该类型的所有实例都是全球性的(像一个静态变量C)。
存储类型属性可以是变量或常量。计算类型属性总是被声明为变量属性,就像计算实例属性一样。
与存储实例属性不同,必须始终为存储类型属性提供默认值。这是因为类型本身没有一个初始化器,可以在初始化时为存储类型属性赋值。
存储类型属性在第一次访问时惰性初始化。它们被保证只初始化一次,即使是在多个线程同时访问时,它们也不需要用惰性修饰符来标记。
类型属性的语法
在C和Objective-C中,您将静态常量和与类型关联的变量定义为全局静态变量。然而,在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
}
}
上面的computed type属性示例适用于只读的computed type属性,但是您也可以用与computed实例属性相同的语法定义read-write computed type属性。
查询和设置类型属性
使用点语法查询和设置类型属性,就像实例属性一样。但是,类型属性是在类型上查询和设置的,而不是在该类型的实例上。例如:
print(SomeStructure.storedTypeProperty)
// Prints "Some value."
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// Prints "Another value."
print(SomeEnumeration.computedTypeProperty)
// Prints "6"
print(SomeClass.computedTypeProperty)
// Prints "27"
方法
方法是与特定类型相关联的函数。类、结构和枚举都可以定义实例方法,这些方法封装用于处理给定类型的实例的特定任务和功能。类、结构和枚举也可以定义与类型本身相关联的类型方法。类型方法类似于Objective-C中的类方法。
结构和枚举可以用Swift定义方法这一事实与C和Objective-C有很大区别。在Objective-C中,类是唯一可以定义方法的类型。在Swift中,您可以选择是定义类、结构还是枚举,并且仍然可以灵活地定义对所创建类型的方法。
实例方法
实例方法属于特定类、结构或枚举的实例。它们通过提供访问和修改实例属性的方法或提供与实例用途相关的功能来支持这些实例的功能。实例方法的语法与函数完全相同,正如函数中所描述的那样。
您可以在其所属类型的启闭大括号内编写实例方法。实例方法具有对该类型的所有其他实例方法和属性的隐式访问。实例方法只能在其所属类型的特定实例上调用。在没有现有实例的情况下,不能单独调用它。
这里有一个例子,它定义了一个简单的计数器类,可以用来计算一个动作发生的次数:
class Counter {
var count = 0
func increment() {
count += 1
}
func increment(by amount: Int) {
count += amount
}
func reset() {
count = 0
}
}
您调用实例方法,其点语法与属性相同:
let counter = Counter()
// the initial counter value is 0
counter.increment()
// the counter's value is now 1
counter.increment(by: 5)
// the counter's value is now 6
counter.reset()
// the counter's value is now 0
函数参数可以有一个名称(用于在函数体中使用)和一个参数标签(用于调用函数时使用),如函数参数标签和参数名中所述。方法参数也是如此,因为方法只是与类型相关联的函数。
self------属性
类型的每个实例都有一个隐式属性self,它与实例本身完全等价。您可以使用self属性引用其自身的当前实例
上面示例中的increment()方法可以写成这样:
func increment() {
self.count += 1
}
实际上,您不需要经常在代码中编写self。如果您没有显式地编写self,那么Swift假设您在使用方法中的已知属性或方法名时引用当前实例的属性或方法。通过在三个实例方法中使用count(而不是self.count)来演示这个假设。
当实例方法的参数名与该实例的属性名相同时,会出现此规则的主要异常。在这种情况下,参数名优先,并且需要以更限定的方式引用属性。您可以使用self属性来区分参数名和属性名。
在这里,自消歧方法参数x和实例属性(也称为x)之间的歧义
struct Point {
var x = 0.0, y = 0.0
func isToTheRightOf(x: Double) -> Bool {
return self.x > x
}
}
let somePoint = Point(x: 4.0, y: 5.0)
if somePoint.isToTheRightOf(x: 1.0) {
print("This point is to the right of the line where x == 1.0")
}
// Prints "This point is to the right of the line where x == 1.0"
如果没有self前缀,Swift将假设x的两种用法都引用了名为x的方法参数。
从实例方法中修改值类型
结构和枚举是值类型。默认情况下,值类型的属性不能从其实例方法中修改。
然而,如果您需要在特定方法中修改结构或枚举的属性,您可以选择对该方法进行突变行为。然后,该方法可以从方法内部更改其属性(即更改),并且在方法结束时,它所做的任何更改都会被写回原始结构。该方法还可以为它的隐式self属性分配一个全新的实例,这个新实例将在方法结束时替换现有的实例。
您可以通过将变异关键字放在func关键字之前来选择这种行为:
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveBy(x: 2.0, y: 3.0)
print("The point is now at (\(somePoint.x), \(somePoint.y))")
// Prints "The point is now at (3.0, 4.0)"
上面的点结构定义了一个突变的moveBy(x:y:)方法,它将一个点实例移动一定数量。这个方法不是返回一个新点,而是实际修改调用它的点。将突变关键字添加到其定义中,以使其能够修改其属性。
注意,不能对结构类型常量调用突变方法,因为它的属性不能更改,即使它们是可变属性,如常量结构实例的存储属性所述:
let fixedPoint = Point(x: 3.0, y: 3.0)
fixedPoint.moveBy(x: 2.0, y: 3.0)
// this will report an error
在变异的方法中给自己赋值
可变方法可以为隐式self属性分配一个全新的实例。上面所示的要点示例可以用以下方式编写:
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)
}
}
这个版本的突变moveBy(x:y:)方法创建了一个新的结构,其x和y值被设置为目标位置。调用该方法的替代版本的最终结果将与调用早期版本完全相同。
枚举的突变方法可以将隐式自参数设置为与相同枚举不同的情况:
num 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 is now equal to .high
ovenLight.next()
// ovenLight is now equal to .off
这个例子为一个三态开关定义了一个枚举。每次调用其next()方法时,开关在三种不同的电源状态(off、low和high)之间循环。
类型的方法
如上所述,实例方法是对特定类型的实例调用的方法。还可以定义类型本身上调用的方法。这些类型的方法称为类型方法。通过在方法的func关键字之前写入static关键字来指定类型方法。类还可以使用class关键字来允许子类重写该方法的超类实现。
在Objective-C中,只能为Objective-C类定义类型级别的方法。在Swift中,您可以为所有类、结构和枚举定义类型级别的方法。每个类型方法的作用域都明确地限定于它支持的类型。
类型方法是用点语法调用的,比如实例方法。但是,在类型上调用类型方法,而不是在该类型的实例上。下面是如何在一个名为SomeClass的类上调用类型方法:
class SomeClass {
class func someTypeMethod() {
// type method implementation goes here
}
}
SomeClass.someTypeMethod()
在类型方法的主体中,隐式self属性引用类型本身,而不是该类型的实例。这意味着您可以使用self来消除类型属性和类型方法参数之间的歧义,就像您对实例属性和实例方法参数所做的那样。
更一般地说,在类型方法的主体中使用的任何不合格的方法和属性名称都将引用其他类型级别的方法和属性。类型方法可以使用另一个方法的名称调用另一个类型方法,而不需要在其前面加上类型名称。类似地,结构和枚举上的类型方法可以通过使用类型属性的名称来访问类型属性,而不使用类型名称前缀。
下面的例子定义了一种叫做LevelTracker的结构,它可以追踪玩家在游戏不同关卡或阶段的进展。这是一款单人游戏,但可以在一台设备上存储多名玩家的信息。
游戏的所有关卡(除了一级)在游戏开始时都是锁定的。每次玩家完成关卡时,设备上的所有玩家都可以解锁该关卡。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结构可以追踪任何玩家已经解锁的最高等级。这个值存储在一个名为highestunlock - level的类型属性中。
LevelTracker还定义了两个类型函数来处理highestunlock - level属性。第一个是类型函数unlock(_:),它在新级别解锁时更新highestunlock - dlevel的值。第二个是一个方便的函数isunlock(_:),如果一个特定的level number已经解锁了,它将返回true。(注意,这些类型方法可以访问highestUnlockedLevel类型属性,而不需要将其编写为leveltrack . highestunlock - level。)
除了类型属性和类型方法之外,LevelTracker还可以追踪玩家在游戏中的进程。它使用一个名为currentLevel的实例属性来跟踪玩家当前正在玩的关卡。
为了帮助管理currentLevel属性,LevelTracker定义了一个名为advance(To:)的实例方法。在更新currentLevel之前,这个方法检查请求的新级别是否已经解锁。advance(to:)方法返回一个布尔值,以指示它是否能够实际设置currentLevel。因为调用advance(to:)方法来忽略返回值的代码不一定是错误的,所以这个函数被标记为@discardableResult属性。有关此属性的更多信息,请参阅属性。
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)")
// Prints "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")
}
// Prints "level 6 has not yet been unlocked"
下标
类、结构和枚举可以定义下标,这些下标是访问集合、列表或序列的成员元素的快捷方式。您可以使用下标按索引设置和检索值,而不需要单独的方法进行设置和检索。例如,您可以访问数组实例中的元素作为someArray[index],访问字典实例中的元素作为someDictionary[key]。
您可以为单个类型定义多个下标,并且根据传递给下标的索引值的类型选择适当的下标重载。下标不限于单个维度,您可以使用多个输入参数定义下标,以满足定制类型的需要。
下标的语法
下标允许您通过在实例名后面的方括号中写入一个或多个值来查询类型的实例。它们的语法类似于实例方法语法和计算属性语法。使用subscript关键字编写下标定义,并以与实例方法相同的方式指定一个或多个输入参数和返回类型。与实例方法不同,下标可以是读写的,也可以是只读的。这个行为由getter和setter以与计算属性相同的方式进行通信:
subscript(index: Int) -> Int {
get {
// return an appropriate subscript value here
}
set(newValue) {
// perform a suitable setting action here
}
}
newValue的类型与下标的返回值相同。与computed properties一样,您可以选择不指定setter的(newValue)参数。如果您自己不提供一个默认参数newValue,那么它将提供给您的setter。
与只读计算属性一样,通过删除get关键字及其大括号,可以简化只读下标的声明:
subscript(index: Int) -> Int {
// return an appropriate subscript value here
}
下面是一个只读下标实现的例子,它定义了一个时间稳定的结构来表示一个n次整数表:
struct TimesTable {
let multiplier: Int
subscript(index: Int) -> Int {
return multiplier * index
}
}
let threeTimesTable = TimesTable(multiplier: 3)
print("six times three is \(threeTimesTable[6])")
// Prints "six times three is 18"
在本例中,创建了一个TimesTable的新实例来表示三次表。这是通过将值3传递给结构的初始化器作为实例的乘法器参数使用的值来表示的。
您可以通过调用其下标查询threeTimesTable实例,如调用threeTimesTable[6]所示。它请求三次表中的第六项,返回值18,或3乘以6。
n时间表基于固定的数学规则。将threeTimesTable[someIndex]设置为新值是不合适的,因此TimesTable的下标被定义为只读下标。
下标的使用
“下标”的确切含义取决于使用它的上下文。下标通常用作访问集合、列表或序列中的成员元素的快捷方式。您可以自由地以最适合您的特定类或结构功能的方式实现下标。
例如,Swift的Dictionary类型实现了一个下标来设置和检索存储在Dictionary实例中的值。通过在下标括号中提供字典键类型的键,并将字典值类型的值赋给下标,可以在字典中设置值:
var numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
numberOfLegs["bird"] = 2
上面的示例定义了一个名为numberOfLegs的变量,并使用包含三个键-值对的字典文本对它进行初始化。numberOfLegs字典的类型被推断为[String: Int]。在创建字典之后,这个示例使用下标赋值向字典添加一个字符串键“bird”和一个Int值2。
Swift的Dictionary类型将其键值下标实现为接受并返回可选类型的下标。对于上面的numberOfLegs字典,键-值下标获取并返回Int类型的值?,或“可选int”。字典类型使用一个可选的下标类型来建模不是每个键都有一个值的事实,并通过为该键分配一个nil值来给出删除一个键值的方法。
下标选项
下标可以接受任意数量的输入参数,这些输入参数可以是任何类型的。下标也可以返回任何类型。下标可以使用可变参数,但不能使用in-out参数或提供默认参数值。
类或结构可以根据需要提供尽可能多的下标实现,并且根据使用下标时下标括号中包含的值的类型推断出适当的下标。多下标的定义称为下标重载。
虽然下标接受单个参数是最常见的,但如果适合您的类型,也可以定义具有多个参数的下标。下面的示例定义了一个矩阵结构,它表示一个二维双值矩阵。矩阵结构的下标取两个整数参数:
struct Matrix {
let rows: Int, columns: Int
var grid: [Double]
init(rows: Int, columns: Int) {
self.rows = rows
self.columns = columns
grid = Array(repeating: 0.0, count: rows * columns)
}
func indexIsValid(row: Int, column: Int) -> Bool {
return row >= 0 && row < rows && column >= 0 && column < columns
}
subscript(row: Int, column: Int) -> Double {
get {
assert(indexIsValid(row: row, column: column), "Index out of range")
return grid[(row * columns) + column]
}
set {
assert(indexIsValid(row: row, column: column), "Index out of range")
grid[(row * columns) + column] = newValue
}
}
}
Matrix提供了一个初始化器,它接受两个称为行和列的参数,并创建一个足够大的数组,以存储类型为Double的行*列值。矩阵中每个位置的初始值都是0.0。为此,将数组的大小和初始单元格值0.0传递给创建并初始化正确大小的新数组的数组初始化器。在创建具有默认值的数组时,将更详细地描述此初始化器。
您可以通过向其初始化器传递适当的行和列计数来构造一个新的矩阵实例:
var matrix = Matrix(rows: 2, columns: 2)
继承
一个类可以从另一个类继承方法、属性和其他特征。当一个类从另一个类继承时,所继承的类称为子类,它所继承的类称为超类。继承是Swift中区分类和其他类型的基本行为。
Swift中的类可以调用和访问属于其超类的方法、属性和下标,并可以提供它们自己的覆盖版本的这些方法、属性和下标,以改进或修改它们的行为。Swift通过检查覆盖定义是否具有匹配的超类定义来帮助确保覆盖是正确的。
类还可以向继承的属性添加属性观察者,以便在属性值发生更改时得到通知。属性观察者可以添加到任何属性,而不管它最初是定义为存储属性还是计算属性。
定义基类
任何不从另一个类继承的类都称为基类。
Swift类不会从通用基类继承。您在没有指定超类的情况下定义的类自动成为您要构建的基类。
下面的示例定义了一个名为Vehicle的基类。这个基类定义了一个名为currentSpeed的存储属性,默认值为0.0(推断属性类型为Double)。currentSpeed属性的值被称为description的只读计算字符串属性用于创建车辆的描述。
Vehicle基类还定义了一个名为makeNoise的方法。这个方法实际上并不为基本的车辆实例做任何事情,但是稍后将由车辆的子类定制:
class Vehicle {
var currentSpeed = 0.0
var description: String {
return "traveling at \(currentSpeed) miles per hour"
}
func makeNoise() {
// do nothing - an arbitrary vehicle doesn't necessarily make a noise
}
}
你用初始化器语法创建了一个新的Vehicle实例,它被写成一个类型名后面跟着空括号:
let someVehicle = Vehicle()
创建了一个新的车辆实例后,您可以访问其description属性来打印一个人类可读的车辆当前速度描述:
print("Vehicle: \(someVehicle.description)")
// Vehicle: traveling at 0.0 miles per hour
Vehicle类为任意车辆定义了公共特征,但本身并没有多大用处。为了使它更有用,您需要改进它来描述更具体的车辆类型。
子类化
子类化是在现有类的基础上建立新类的行为。子类继承现有类的特征,然后可以对其进行细化。您还可以向子类添加新的特征。
为了表明一个子类有一个超类,在超类名之前写上子类名,用冒号隔开:
class SomeSubclass: SomeSuperclass {
// subclass definition goes here
}
下面的示例定义了一个名为Bicycle的子类,带有Vehicle的超类:
class Bicycle: Vehicle {
var hasBasket = false
}
新的自行车类可以自动获得车辆的所有特性,例如当前速度和描述属性以及makeNoise()方法。
除了继承的特性之外,Bicycle类还定义了一个新的存储属性hasBasket,默认值为false(推断属性的Bool类型)。
默认情况下,您创建的任何新自行车实例都没有篮子。在创建了特定的自行车实例之后,您可以将hasBasket属性设置为true:
let bicycle = Bicycle()
bicycle.hasBasket = true
您还可以修改自行车实例的继承currentSpeed属性,并查询实例的继承描述属性:
bicycle.currentSpeed = 15.0
print("Bicycle: \(bicycle.description)")
// Bicycle: traveling at 15.0 miles per hour
子类本身可以被子类化。下一个示例为双座自行车创建一个子类,称为“串联”:
class Tandem: Bicycle {
var currentNumberOfPassengers = 0
}
Tandem继承了Bicycle的所有属性和方法,反过来继承了Vehicle的所有属性和方法。串联子类还添加了一个新的存储属性currentNumberOfPassengers,默认值是0。
如果您创建了一个串联的实例,您可以使用它的任何新属性和继承属性,并查询它从Vehicle继承的只读描述属性:
let tandem = Tandem()
tandem.hasBasket = true
tandem.currentNumberOfPassengers = 2
tandem.currentSpeed = 22.0
print("Tandem: \(tandem.description)")
// Tandem: traveling at 22.0 miles per hour
压倒一切的
子类可以提供它自己的实例方法、类型方法、实例属性、类型属性或从超类继承的下标的自定义实现。这就是所谓的压倒一切。
要覆盖一个否则将被继承的特征,可以在覆盖定义前面加上override关键字。这样做说明您打算提供一个覆盖,并且没有错误地提供匹配定义。意外重写可能会导致意外行为,在编译代码时,任何没有override关键字的重写都会被诊断为错误。
override关键字还会提示Swift编译器检查覆盖类的超类(或其父类之一)是否具有与您为覆盖提供的声明匹配的声明。此检查确保您的覆盖定义是正确的。
访问超类方法、属性和下标
当您为子类提供方法、属性或下标覆盖时,有时使用现有的超类实现作为覆盖的一部分是很有用的。例如,您可以细化现有实现的行为,或者将修改后的值存储在现有的继承变量中。
在适当的情况下,您可以使用超级前缀访问方法、属性或下标的超类版本:
个名为someMethod()的重写方法可以通过在重写方法实现中调用super.someMethod()来调用someMethod()的超类版本。
一个被重写的属性someProperty可以访问someProperty的超类版本。重写getter或setter实现中的someProperty。
覆盖someIndex的下标可以从覆盖的下标实现中访问与super[someIndex]相同的下标的超类版本。
重写方法
您可以覆盖继承的实例或类型方法,以在子类中提供定制的或可选的方法实现。
下面的示例定义了一个名为Train的新车辆子类,它覆盖了Train继承自Vehicle的makeNoise()方法:
class Train: Vehicle {
override func makeNoise() {
print("Choo Choo")
}
}
如果您创建一个新的Train实例并调用其makeNoise()方法,您可以看到该方法的Train子类版本被调用:
let train = Train()
train.makeNoise()
// Prints "Choo Choo"
最重要的属性
您可以覆盖继承的实例或类型属性,以为该属性提供您自己的自定义getter和setter,或者添加属性观察者,使覆盖属性在基础属性值发生变化时能够观察到。
覆盖属性getter和setter
您可以提供一个定制的getter(如果合适的话,还有setter)来覆盖任何继承的属性,而不管继承的属性是作为存储属性实现的还是作为计算属性实现的。子类不知道继承属性的存储或计算性质——只知道继承属性有特定的名称和类型。您必须始终声明要覆盖的属性的名称和类型,以便编译器检查您的覆盖是否匹配具有相同名称和类型的超类属性。
通过在子类属性覆盖中同时提供getter和setter,可以将继承的只读属性表示为读写属性。但是,您不能将继承的读写属性表示为只读属性。
如果您将setter作为属性覆盖的一部分提供,则还必须为该覆盖提供getter。如果您不想在重写getter中修改继承的属性的值,您可以通过返回super简单地传递继承的值。getter中的someProperty,其中someProperty是要重写的属性的名称。
下面的示例定义了一个名为Car的新类,它是Vehicle的子类。Car类引入了一个新的存储属性gear,默认的整数值为1。Car类还覆盖了它从Vehicle继承的description属性,以提供包含当前齿轮的自定义描述:
class Car: Vehicle {
var gear = 1
override var description: String {
return super.description + " in gear \(gear)"
}
}
description属性的覆盖首先调用super.description,它返回Vehicle类的description属性。Car类的描述版本然后在描述的末尾添加一些额外的文本,以提供关于当前齿轮的信息。
如果你创建一个Car类的实例并设置它的gear和currentSpeed属性,你会看到它的description属性返回在Car类中定义的定制描述:
let car = Car()
car.currentSpeed = 25.0
car.gear = 3
print("Car: \(car.description)")
// Car: traveling at 25.0 miles per hour in gear 3
压倒一切的属性观察员
您可以使用property override向继承的属性添加property observer。这使您能够在继承属性的值发生更改时得到通知,而不管该属性最初是如何实现的。
不能向继承的常量存储属性或继承的只读计算属性添加属性观察者。无法设置这些属性的值,因此不适合将willSet或didSet实现作为覆盖的一部分提供。
还要注意,不能同时为同一属性提供覆盖setter和覆盖属性观察者。如果您想要观察对属性值的更改,并且您已经为该属性提供了自定义setter,那么您只需在自定义setter中观察任何值更改。
下面的示例定义了一个名为AutomaticCar的新类,它是Car的子类。AutomaticCar类表示带有自动变速箱的汽车,根据当前速度自动选择合适的齿轮:
class AutomaticCar: Car {
override var currentSpeed: Double {
didSet {
gear = Int(currentSpeed / 10.0) + 1
}
}
}
无论何时设置AutomaticCar实例的currentSpeed属性,该属性的didSet观察者都会将实例的gear属性设置为新速度的合适齿轮选择。具体来说,属性观察者选择的齿轮是新的currentSpeed值除以10,四舍五入到最近的整数,加1。35.0的速度产生4个齿轮:
let automatic = AutomaticCar()
automatic.currentSpeed = 35.0
print("AutomaticCar: \(automatic.description)")
// AutomaticCar: traveling at 35.0 miles per hour in gear 4
防止覆盖
通过将方法、属性或下标标记为final,可以防止其被重写。通过在方法、属性或下标的介绍人关键字(如final var、final func、final class func和final subscript)之前编写final修饰符来实现这一点。
在子类中重写最终方法、属性或下标的任何尝试都被报告为编译时错误。在扩展中添加到类的方法、属性或下标也可以在扩展的定义中标记为final。
您可以通过在类定义(final类)中的类关键字前面写入final修饰符来将整个类标记为final。任何子类化最终类的尝试都被报告为编译时错误。