十 属性
1、存储属性
最简单的情形,作为特定类或结构实例的一部分,存储属性存储着常量或者变量的值。存储属性可分为变量存储属性(关键字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
常量结构实例的存储属性
如果你创建一个结构实例,并将其赋给一个常量,这个实例中的属性将不可以被改变,即使他们被声明为变量属性
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
this range represents integer values 0, 1, 2, and 3
rangeOfFourItems.firstValue = 6 这里报错,属性不可变
因为rangeOfFourItems是一个常量(let),即便firstValue是一个变量属性,它的值也是不可以被改变的
这样的特性是因为结构是值类型。当一个值类型实例作为常量而存在,它的所有属性也作为常量而存在。
而这个特性对类并不适用,因为类是引用类型。如果你将引用类型的实例赋值给常量,依然能够改变实例的变量属性。
Lazy Stored Properties(懒惰存储属性)
懒惰存储属性是当它第一次被使用时才进行初值计算。通过在属性声明前加上lazy来标识一个懒惰存储属性。
注意
必须声明懒惰存储属性为变量属性(通过var),因为它的初始值直到实例初始化完成之后才被检索。常量属性在实例初始化完成之前就应该被赋值,因此常量属性不能够被声明为懒惰存储属性。
当属性初始值因为外部原因,在实例初始化完成之前不能够确定时,就要定义成懒惰存储属性。当属性初始值需要复杂或高代价的设置,在它需要时才被赋值时,懒惰存储属性就派上用场了。
下面的例子使用懒惰存储属性来防止类中不必要的初始化操作。它定义了类DataImporter和类DataManager:
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")
类DataManager有一个称为data的存储属性,它被初始化为一个空的String数组。虽然DataManager定义的其它部分并没有写出来,但可以看出DataManager的目的是管理String数据并为其提供访问接口。
DataManager类的部分功能是从文件中引用数据。这个功能是由DataImporter类提供的,这个类需要一定的时间来初始化,因为它的实例需要打开文件并见内容读到内存中。
因为DataManager实例可能并不需要立即管理从文件中引用的数据,所以在DataManager实例被创建时,并不需要马上就创建一个新的DataImporter实例。这就使得当DataImporter实例在需要时才被创建起来。
因为被声明为lazy属性,DataImporter的实例importer只有在当它在第一次被访问时才被创建。例如它的fileName属性需要被访问时:
print(manager.importer.fileName)
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( 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))")
这个例子定义了三个处理几何图形的结构:
Point包含一个(x,y)坐标
Size包含宽度width和高度height
Rect定义了一个长方形,包含原点和大小size
Rect结构包含一个称之为center的计算属性。Rect当前中心点的坐标可以通过origin和size属性得来,所以并不需要显式地存储中心点的值。取而代之的是,Rect定义一个称为center的计算属性,它包含一个get和一个set方法,通过它们来操作长方形的中心点,就像它是一个真正的存储属性一样。
setter声明的简略写法
如果计算属性的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方法,通过点操作符,可以放回属性值,但是不能修改它的值。
注意
应该使用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( 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is (fourByFiveByTwo.volume)")
这个例子定义了一个三维长方体结构Cuboid,包含了长宽高三个属性,和一个表示长方体容积的只读计算属性volume。volume值是不可被设置的,因为它直接由长宽高三个属性计算而来。通过提供这样一个只读计算属性,Cuboid使外部用户能够访问到其当前的容积值。
3、属性观察者
属性观察者观察属性值的改变并对此做出响应。当设置属性的值时,属性观察者就被调用,即使当新值同原值相同时也会被调用。
除了懒惰存储属性,你可以为任何存储属性加上属性观察者定义。另外,通过重写子类属性,也可以继承属性(存储或计算)加上属性观察者定义。属性重写在“重写”章节定义。
注意
不必为未重写的计算属性定义属性观察者,因为可以通过它的setter方法直接对值的改变做出响应
定义属性的观察者时,你可以单独或同时使用下面的方法:
willSet:设置值前被调用
didSet:设置值后立刻被调用
当实现willSet观察者时,新的属性值作为常量参数被传递。你可以为这个参数起一个名字,如果不的话,这个参数就默认地被命名成newValue。
在实现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类型的、含有willSet和didSet观察者的存储属性totalSteps。当这个属性被赋予新值时,willSet和didSet将会被调用,即使新值和旧值是相同的。
例子中的willSet观察者为参数起了个新的名字newTotalSteps,它简单地打印了即将被设置的值。
当totalSteps值被更新时,didSet观察者被调用,它比较totalSteps的新值和旧值,如果新值比旧值大,就打印所增加的步数。didSet并没有为旧值参数命名,在本例中,将会使用默认的名字oldValue来表示旧的值。
注意
如果通过didSet来设置属性的值,即使属性值刚刚被设置过,起作用的也将会是didSet,即新值是didSet设置的值
4、全局和局部变量
以上所写的关于计算与观察属性值的特性同样适用于全局和局部变量。全局变量是在任何函数、方法、闭包、类型上下文外部定义的变量,而局部变量是在函数、方法、闭包中定义的变量。
前面章节所遇到过的全局、局部变量都是存储变量。和存储属性一样,存储变量为特定类型提供存储空间并且可以被访问
但是,你可以在全局或局部范围定义计算变量和存储变量观察者。计算变量并不存储值,只用来计算特定值,它的定义方式与计算属性一样。
注意
全局常量和变量通常是延迟计算的,跟懒惰存储属性一样,但是不需要加上@lazy。而局部常量与变量不是延迟计算的。
5、类型属性
实例属性是特定类型实例的属性。当创建一个类型的实例时,这个实例有自己的属性值的集合,这将它与其它实例区分开来。
也可以定义属于类型本身的属性,即使创建再多的这个类的实例,这个属性也不属于任何一个,它只属于类型本身,这样的属性就称为类型属性。
类型属性适用于定义那些特定类型实例所通用的属性,例如一个可以被所有实例使用的常量属性(就像c中的静态常量),或者变量属性(c中的静态变量)。
可以为值类型(结构、枚举)定义存储类型属性和计算类型属性。对类而言,只能够定义计算类型属性。
值类型的存储类型属性可以是常量也可以是变量。而计算类型属性通常声明成变量属性,类似于计算实例属性
注意
不想存储实例属性,你需要给存储类型属性一个初始值。因为类型本身在初始化时不能为存储类型属性设置值
类型属性句法
在C和Objective-C中,定义静态常量、变量和全局静态变量一样。但是在swift中,类型属性的定义要放在类型定义中进行,在类型定义的大括号中,显示地声明它在类型中的作用域。
对值类型而言,定义类型属性使用static关键字,而定义类类型的类型属性使用class关键字。下面的例子展示了存储和计算类型属性的用法:
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 3
}
}
enum SomeEnumeration {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 4
}
}
class SomeClass {
class var computedTypeProperty: Int {
return 5
}
}
注意
上面的例子是针对只读计算类型属性而言的,不过你也可以像计算实例属性一样定义可读可写的计算类型属性
查询与设置类型属性
像实例属性一样,类型属性通过点操作符来查询与设置。但是类型属性的查询与设置是针对类型而言的,并不是针对类型的实例。例如:
print(SomeClass.computedTypeProperty)
print(SomeStructure.storedTypeProperty)
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
十一方法
方法是关联到一个特定类型的函数。类、结构、枚举所有可以定义实例方法,封装特定任务和功能处理给定类型的一个实例。类、结构、枚举类型还可以定义方法,相关的类型本身。类型方法类似于objective – c类方法。
结构和枚举可以定义方法swift与C和objective – C是一个重大的区别。在objective – c中,类是唯一类型可以定义方法。在swift,你可以选择是否要定义一个类,结构,或枚举,还有你定义方法类型的灵活性创造。
1、实例方法
实例方法是属于一个特定的类,结构或枚举实例的功能。他们支持这些实例的功能,无论是通过提供方法来访问和修改实例属性,或提供的功能与实例的目的。实例方法具有完全相同的语法功能,如功能描述
你所属的类型的打开和关闭括号内写一个实例方法。一个实例方法具有隐式访问所有其他实例方法和该类型的属性。一个实例方法只能在它所属的类的特定实例调用,它不能访问一个不存在的实例。
这里,定义了一个简单的计数器类,它可以用来计数一个动作发生的次数的示例:
class Counter {
var count = 0
func increment() {
count++
}
func incrementBy(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.incrementBy(5)
the counter's value is now 6
counter.reset()
the counter's value is now 0
本地和外部参数名称的方法
函数参数可以有一个本地名称(在函数体内使用)和外部名称(在调用函数时使用),所述外部参数名称。方法参数也是如此,因为方法与类型相关的函数。然而,本地名称和外部名称的默认行为是不同的函数和方法。
方法在Swift非常类似于objective – c的同行。在objective – c中,一个方法的名称在Swift通常是指使用preposition等方法的第一个参数,,或者,就像在incrementBy方法从前面的counter类的例子。使用可以被解读为一个判断的方法叫做preposition。Swift使这个方法建立命名约定易于编写通过使用一个不同的默认方法。
具体来说,Swift给第一个参数名称方法默认本地参数名称,并给出第二和后续的参数名称默认本地和外部参数名称。这个约定可以在熟悉的objective – c中调用到,并使得表达方法调用而不需要符合你的参数名称。
考虑这个替代版本的counter类,它定义了一个更复杂的形式的incrementBy方法:
class Counter1 {
var count: Int = 0
func incrementBy(amount: Int, numberOfTimes: Int) {
count += amount * numberOfTimes
}
}
let counter1 = Counter1()
counter1.incrementBy(3, numberOfTimes: 3)
print(counter1.count)
你不需要定义一个外部参数名称为第一个参数值,因为它是明确的函数名incrementBy。然而,第二个参数是由外部参数名称进行限定。
Self属性
一个类型的每个实例都有所谓的一个隐含self属性,它是完全等同于该实例本身。您可以使用这个隐含的self属性来引用当前实例中它自己的实例方法。
在实践中,你不需要写self,这在你的代码会非常频繁。如果你没有明确写self,Swift假设你是指当前实例的属性或方法,每当你使用一个方法中一个已知的属性或方法名。这个假设是证明了里边三个实例方法的计数器使用count(rather than self.count)的。
主要的例外发生在一个实例方法的参数名称相同的名称作为该实例的属性。在这种情况下,参数名称的优先,有必要参考属性更多合格的方式。您可以使用隐式的自我属性的参数名和属性名来区分。
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")
}
修改值类型的实例方法
结构和枚举值类型。默认情况下,一个值类型的属性不能修改它的实例方法
然而,如果您需要修改的属性结构或枚举在一个特定的方法,你可以选择该方法的变化行为。但任何更改都会使它得编写的方法结束时回到原来的结构。当该方法结束时还可以分配一个完全新的实例对其隐含的self属性,而这个新的实例将取代现有的。
你可以选择这个行为之前将变异的关键字mutating嵌入函数关键字的方法:
struct Point1 {
var x = 0.0, y = 0.0
mutating func moveByX(deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
var somePoint1 = Point1(x: 1.0, y: 1.0)
somePoint1.moveByX(2.0, y: 3.0)
print("The point is now at ((somePoint1.x), (somePoint1.y))")
Point结构上面定义了一个变异moveByX方法,它通过一定量移动一个Point实例。而不是返回一个新的起点,这种方法实际上会修改在其上调用点。该变异包含被添加到它的定义,使其能够修改其属性。
请注意,您不能调用变异方法结构类型的常数,因为它的属性不能改变
分配中的self变异方法
变异的方法可以分配一个全新的实例隐含的self属性。上面所示的点的例子也可以写成下面的方式来代替:
struct Point2 {
var x = 0.0, y = 0.0
mutating func moveByX(deltaX: Double, y deltaY: Double) {
self = Point2(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.next()
这个例子定义了一个三态开关枚举。三种不同的功率状态之间的切换周期(关,低,高)
2、类型方法
如上所述,实例方法的方法要求一个特定类型的实例。您还可以定义该类型自身的方法,这种方法被称为type方法,您显示的type方法直接在类结构体里面用class func开头 ,对于枚举和结构来说,类型方法是用static func开头。
请注意;
在objective – c中,您可以定义type-level方法仅为objective – c类。在Swift可以为所有类定义type-level方法,结构,和枚举。每种方法的显示局限于它所支持的类型。
在类型方法的主体,隐含的self属性是指类型本身,而不是该类型的一个实例。对于结构体和枚举,这意味着你可以使用自助静态属性和静态方法的参数消除歧义,就像你做的实例属性和实例方法的参数。
更普遍的是,你一个类型的方法体中使用任何不合格的方法和属性名称会参考其他 type-level方法和属性。 一种方法可以调用另一个类的方法与其他方法的名称,而不需要与类型名称前缀了。同样,结构和枚举类型的方法可以使用静态属性的名称,没有类型名称前缀访问静态属性。
下面的例子定义了一个名为LevelTracker结构,它通过游戏的不同层次或阶段跟踪球员的进步。这是一个单人游戏,但可以存储的信息为一个单一的设备上的多个玩家。
所有的游戏的水平(除了一级)当游戏第一次玩。每当玩家完成一个级别,该级别解锁设备上的所有玩家。LevelTracker结构使用静态属性和方法来跟踪哪些级别的比赛已经解锁。它还跟踪当前个别球员水平
struct LevelTracker {
static var highestUnlockedLevel = 1
static func unlockLevel(level: Int) {
if level > highestUnlockedLevel {
highestUnlockedLevel = level
}
}
static func levelIsUnlocked(level: Int) -> Bool {
return level <= highestUnlockedLevel
}
var currentLevel = 1
mutating func advanceToLevel(level: Int) -> Bool {
if LevelTracker.levelIsUnlocked(level) {
currentLevel = level
return true
} else {
return false
}
}
}
该LevelTracker结构跟踪任何玩家解锁的最高水平。这个值是存储在一个名为highestUnlockedLevel的静态属性。
LevelTracker还定义了两种类型的功能与highestUnlockedLevel,首先是一种叫做unlockLevel功能,每当一个新的水平解锁都会用来更新highestUnlockedLevel,第二个是levelIsUnlocked功能,如果一个特定的水平数已经解锁,就会返回ture。注意,这些类型的方法可以访问highestUnlockedLevel静态属性但是你需要把它写成LevelTracker.highestUnlockedLevel)。
除了它的静态属性和类型的方法,LevelTracker通过游戏追踪每个玩家的进度。它使用被称为currentLevel实例属性来跟踪玩家级别。
为了帮助管理urrentLevel属性,advanceToLevel LevelTracker定义一个实例方法。这种方法更新currentLevel之前,用来检查是否要求新的水平已经解除锁定。该advanceToLevel方法返回一个布尔值来指示它是否能够设置currentLevel。
该LevelTracker结构使用Player类,如下所示,跟踪和更新单个球员的进步:
class Player {
var tracker = LevelTracker()
let playerName: String
func completedLevel(level: Int) {
LevelTracker.unlockLevel(level + 1)
tracker.advanceToLevel(level + 1)
}
init(name: String) {
playerName = name
}
}
Player类创建LevelTracker的一个新实例来跟踪球员的进步。它也提供了一个名为completedLevel方法,每当玩家到达一个特定的级别,这种方法就会解锁一个新的级别和进度并把玩家移到下一个级别。(advanceToLevel返回的布尔值将被忽略,因为已知被调用LevelTracker.unlockLevel。)
您可以创建一个新球员Player 的实例,看看当玩家完成一个级别会发生什么:
var player = Player(name: "Argyrios")
player.completedLevel(1)
print("highest unlocked level is now (LevelTracker.highestUnlockedLevel)")
如果你创建第二个球员,你想尝试移动到尚未被游戏解锁的级别,就会出现当前级别失败
player = Player(name: "Beto")
if player.tracker.advanceToLevel(6) {
print("player is now on level 6")
} else {
print("level 6 has not yet been unlocked")
}