对象类型是通过一种对象类型风格(enum、struct与class)、对象类型的名字(应该以一个大写字母开头)和一对花括号进行声明的:
class Manny {}struct Moe {}enum Jack {}
对象类型声明可以出现在任何地方:在文件顶部、在另一个对象类型声明顶部,或在函数体中。对象类型相对于其他代码的可见性(作用域)与可用性取决于它声明的位置(参见第1章):
·在默认情况下,声明在文件顶部的对象类型对于项目(模块)中的所有文件都可见,对象类型通常都会声明在这个地方。
·有时需要在其他类型的声明中声明一个类型,从而赋予它一个命名空间,这叫作嵌套类型。
·声明在函数体中的对象类型只会在外围花括号的作用域内存活;这种声明是合法的,但并不常见。
任何对象类型声明的花括号中都可能包含如下内容:
初始化器
对象类型仅仅是一个对象的类型而已。声明对象类型的目的通常是(但不总是这样)创建该类型的实际对象,即实例。初始化器是个特殊的函数,它的声明和调用方式都与众不同,你可以通过它创建对象。
属性
声明在对象类型声明顶部的变量就是属性。在默认情况下,它是个实例属性。实例属性的作用域是实例:可以通过该类型的特定实例来访问它,该类型的每个实例的实例属性值可能都不同。
此外,属性还可以是静态/类属性。对于枚举或结构体来说,它是通过关键字static声明的;对于类来说,它是通过关键字class声明的。这种属性属于对象类型本身;可以通过类型来访问它,它只有一个值,与所属类型关联。
方法
声明在对象类型声明顶部的函数就是方法。在默认情况下,方法都是实例方法;可以通过向该类型的特定实例发送消息来调用它。在实例方法内部,self就是实例本身。
此外,函数还可以是静态/类方法。对于枚举或结构体来说,它是通过关键字static声明的;对于类来说,它是通过关键字class声明的。可以通过向类型发送消息来调用它。在静态/类方法内部,self就是类型。
下标
下标是一种特殊类型的实例方法,可以通过向实例引用附加方括号来调用它。
对象类型声明
对象类型声明还可以包含对象类型声明,即嵌套类型。从外部对象类型内部看,嵌套类型位于其作用域中;从外部对象类型外部看,嵌套类型必须要通过外部对象类型才能使用。这样,外部对象类型是嵌套类型的命名空间。
4.1.1 初始化器
初始化器是一个函数,用来生成对象类型的一个实例。严格来说,它是个静态/类方法,因为它是通过对象类型调用的。调用时通常会使用特殊的语法:类型名后面直接跟着一对圆括号,就好像类型本身是函数一样。当调用初始化器时,新的实例会被创建出来并作为结果返回。你通常会用到返回的实例,比如,将其赋给变量,从而将其保存起来并在后续代码中使用它。
比如,假设有一个Dog类:
class Dog {}
接下来可以创建一个Dog实例:
Dog
上述代码虽然合法,但却没什么用,甚至连编译器都会发出警告。我们创建了一个Dog实例,但却没有引用该实例。如果没有引用,那么Dog实例创建出来后立刻就会消亡。一般来说,我们会这么做:
let fido = Dog
现在,只要变量fido存在,Dog实例就会存在(参见第3章),变量fido引用了Dog实例,这样就可以使用它了。
注意,虽然Dog类没有声明任何初始化器,但Dog()还是调用了一个初始化器!原因在于对象类型会有隐式初始化器。这样你就不必费力编写自定义的初始化器了。不过,你还是可以编写自定义的初始化器,而且会经常这么做。
初始化器是一种函数,其声明语法与函数非常像。要想声明初始化器,你需要使用关键字init,后跟一个参数列表,然后是包含代码的花括号。一个对象类型可以有多个初始化器,由参数进行区分。在默认情况下,参数名(包括第一个参数)都是外化的(当然了,你可以在参数名前通过下划线阻止这一点)。参数常常用于设置实例属性的值。
比如,下面是拥有两个实例属性的Dog类:name(String)与license(Int)。我们为这些实例属性赋予了默认值,这些默认值起到了占位符的作用——一个空字符串和一个数字0。接下来声明了3个初始化器,这样调用者就可以通过3种不同方式来创建Dog实例了:提供一个名字、提供一个登记号,或提供这二者。在每个初始化器中,所提供的参数都用于设置相应属性的值:
class Dog { var name = "" var license = 0 init(name:String) { self.name = name } init(license:Int) { self.license = license } init(name:String, license:Int) { self.name = name self.license = license }}
注意,在上述代码的每个初始化器中,我为每个参数起了与其相应的属性相同的名字,这么做只是一种编程风格而已。在每个初始化器中,我可以通过self访问属性将参数与属性区分开。
上述声明的结果就是我可以通过3种不同方式来创建Dog:
let fido = Dog(name:"Fido")let rover = Dog(license:1234)let spot = Dog(name:"Spot", license:1357)
我无法做的是不使用初始化器参数创建Dog实例。我编写了初始化器,因此隐式初始化器就不复存在了。如下代码是不合法的:
let puff = Dog // compile error
当然,可以显式声明一个不带参数的初始化器,这样上述代码就合法了:
class Dog { var name = "" var license = 0 init { } init(name:String) { self.name = name } init(license:Int) { self.license = license } init(name:String, license:Int) { self.name = name self.license = license }}
其实不需要这么多初始化器,因为初始化器是个函数,函数的参数可以有默认值。这样,我可以将所有代码放到单个初始化器中,如下代码所示:
class Dog { var name = "" var license = 0 init(name:String = "", license:Int = 0) { self.name = name self.license = license }}
现在依然可以通过4种不同的方式创建一个Dog实例:
let fido = Dog(name:"Fido")let rover = Dog(license:1234)let spot = Dog(name:"Spot", license:1357)let puff = Dog
现在来看看有趣的地方。在属性声明中,我可以去掉默认初始值的赋值(只要显式声明每个属性的类型即可):
class Dog { var name : String // no default value! var license : Int // no default value! init(name:String = "", license:Int = 0) { self.name = name self.license = license }}
上述代码是合法的,也很常见,因为初始化器执行的确实是初始化工作!换言之,我无须在声明中为属性赋初值,而是在所有的初始化器中为它们赋初值。通过这种方式,我可以保证当实例创建出来后,所有实例属性都有值了,这正是重要之处。相反,当实例创建出来后,没有初值的实例属性是不合法的。属性要么在声明中初始化,要么被每个初始化器初始化,否则编译器会报错。
Swift编译器认为所有实例属性都要被恰当初始化是Swift的一个重要特性(这与Objective-C相反,它的实例属性可以没有初始化,这常常会导致后续一些奇怪的错误)。不要挑战编译器,请适应它。编译器会通过错误消息(“Return from initializer without initializing all stored properties'”)帮助你,直到初始化器初始化了所有实例属性。
class Dog { var name : String var license : Int init(name:String = "") { self.name = name // compile error }}
由于在初始化器中设置实例属性算是初始化,所以即便实例属性是通过let声明的常量也是合法的:
class Dog { let name : String let license : Int init(name:String = "", license:Int = 0) { self.name = name self.license = license }}
在这个示例中,我们没有对初始化器做任何限制:调用者可以在不提供name或license实参的情况下实例化Dog。但通常,初始化器的目的正好相反:我们会强制调用者在实例化时提供所有必要的信息。在实际情况下,Dog类更可能像是下面这样:
class Dog { let name : String let license : Int init(name:String, license:Int) { self.name = name self.license = license }}
在上述代码中,Dog有一个name和一个license,这两个变量的值是在实例化时提供的(它们没有默认值),并且之后这两个值就无法再改变了(这些属性是常量)。通过这种方式,我们强制要求每个Dog都必须要有一个有意义的名字与许可证号。现在,创建Dog只有一种方式:
let spot = Dog(name:"Spot", license:1357)
1.Optional属性
有时,在初始化时并没有可赋给实例属性的有意义的默认值。比如,也许直到实例创建出来一段时间后才能获取到属性的初始值。这种情况与所有实例属性要么在声明中,要么通过初始化器进行初始化的要求相冲突。当然,你可以通过给实例属性赋一个默认初始值来绕过这个问题,不过它并非“真正的”值。
正如我在第3章所提及的,这个问题合理且常见的解决方案是使用var将实例属性声明为Optional类型。值为nil的Optional表示没有提供“真正的”值;Optional var会被自动初始化为nil。这样,代码就可以比较该实例属性与nil,如果为nil,那就不使用该属性。稍后,属性会被赋予“真正的”值。当然,这个值现在被包装到了一个Optional中;但如果将其声明为隐式展开Optional,那么你还可以直接使用被包装的值,无须显式将其展开(就好像它根本就不是Optional一样),如果确定,那就可以这样做:
// this property will be set automatically when the nib loads@IBOutlet var myButton: UIButton!// this property will be set after time-consuming gathering of datavar albums : [MPMediaItemCollection]!
2.引用self
除了设置实例属性,初始化器不能引用self,无论显式还是隐式都不可以,除非所有实例属性都完成了初始化。这个原则可以确保实例在使用前已经完全构建完毕。比如,如下代码是不合法的:
struct Cat { var name : String var license : Int init(name:String, license:Int) { self.name = name meow // too soon - compile error self.license = license } func meow { print("meow") }}
对实例方法meow的调用隐式引用了self,它表示self.meow()。初始化器可以这么做,但需要在初始化完所有未初始化的属性后才可以。对实例方法meow的调用只需要下移一行即可,这样在完成了name与license的初始化后就可以调用它了。
3.委托初始化器
对象类型中的初始化器可以通过语法self.init(...)调用其他初始化器。调用其他初始化器的初始化器叫作委托初始化器。当一个初始化器委托另一个初始化器时,被委托的初始化器必须要先完成实例的初始化,接下来委托初始化器才能使用初始化完毕的实例,可以再次设置被委托初始化器已经设定的var属性。
委托初始化器看起来好像是之前介绍的关于self的规则的一个例外。但实际上并非如此,因为它要通过self才能委托,而且委托会导致所有实例属性都被初始化。事实上,关于委托初始化器使用self的规则要更加严格:委托初始化器不能引用self,也不能设置属性,直到对其他初始化器的调用完毕后才可以。比如:
struct Digit { var number : Int var meaningOfLife : Bool init(number:Int) { self.number = number self.meaningOfLife = false } init { // this is a delegating initializer self.init(number:42) self.meaningOfLife = true }}
此外,委托初始化器不能设置不可变属性(即let变量)。这是因为只有在调用了其他初始化器后它才可以引用属性,而这时实例已经构建完毕——初始化已经结束,通往不可变属性的初始化之门已经关闭。这样,如果meaningOfLife是通过let声明的,那么上述代码就不合法,因为第2个初始化器是委托初始化器,它无法设置不可变属性。
请注意,不要递归委托!如果让初始化器委托给自身,或是创建了循环委托初始化器,那么编译器不会报错(我认为这是个Bug),不过运行着的应用会挂起。比如,不要这么做:
struct Digit { // do not do this! var number : Int = 100 init(value:Int) { self.init(number:value) } init(number:Int) { self.init(value:number) }}
4.可失败初始化器
初始化器可以返回一个包装新实例的Optional。通过这种方式,可以返回nil来表示失败。具备这种行为的初始化器叫作可失败初始化器。在声明时要想将某个初始化器标记为可失败的,请在关键字init后面放置一个问号(对于隐式展开Optional,放置一个感叹号)。如果可失败初始化器需要返回nil,请显式写明return nil。判断返回的Optional与nil是否相等是调用者的事,请展开它,然后比较,与其他Optional的做法一样。
下面这个版本的Dog有一个返回隐式展开Optional的初始化器,如果name:或是license:实参无效,那么它会返回nil:
class Dog { let name : String let license : Int init!(name:String, license:Int) { self.name = name self.license = license if name.isEmpty { return nil } if license <= 0 { return nil } }}
返回值的类型是Dog,Optional会隐式展开,因此以这种方式实例化Dog的调用者可以直接使用该结果,就好像它是个Dog实例一样。不过如果返回的是nil,那么调用者访问Dog实例的成员就会导致程序在运行时崩溃:
let fido = Dog(name:"", license:0)let name = fido.name // crash
按照惯例,Cocoa与Objective-C会从初始化器中返回nil来表示失败;如果初始化真的可能失败,那么这种初始化器API已经被转换为了Swift可失败初始化器。比如,UIImage初始化器init?(named:)就是个可失败初始化器,因为给定的名字可能并不表示一张图片。它不会隐式展开,因此结果值是一个UIImage?,并且在使用前需要展开(不过,大多数Objective-C初始化器都没有被桥接为可失败初始化器,即便从理论上说,任何Objective-C初始化器都可能返回nil)。
4.1.2 属性
属性是个变量,它声明在对象类型声明的顶部。这意味着第3章所介绍的关于变量的一切都适用于属性。属性拥有确定的类型;可以通过var或let声明属性,它可以是存储变量,也可以是计算变量;它也可以拥有Setter观察者。实例属性也可以声明为lazy。
存储实例属性必须要赋予一个初始值。不过,正如我之前说过的,这不一定非得通过声明中的赋值来实现;也可以通过初始化器。Setter观察者在属性的初始化过程中是不会被调用的。
初始化属性的代码不能获取实例属性,也不能调用实例方法。这种行为需要一个对self的显式或隐式引用;在初始化过程中还不存在self,self是在初始化过程中所创建的。这个错误所导致的Swift编译错误消息令人感到很费解。比如,如下代码是不合法的(删除对self的显式引用也不行):
class Moi { let first = "Matt" let last = "Neuburg" let whole = self.first + " " + self.last // compile error}
一种解决办法就是将whole作为一个计算属性:
class Moi { let first = "Matt" let last = "Neuburg" var whole : String { return self.first + " " + self.last }}
这是合法的,因为计算直到self存在后才会执行。另一个解决办法是将whole声明为lazy:
class Moi { let first = "Matt" let last = "Neuburg" lazy var whole : String = self.first + " " + self.last}
这也是合法的,因为直到self存在后对它的引用才会执行。与之类似,属性初始化器是无法调用实例方法的,不过,计算属性却可以,lazy属性也可以。
正如第3章所述,变量的初始化器可以包含多行代码,前提是将其写成定义与调用匿名函数。如果变量是实例属性,并且代码引用了其他的实例属性或实例方法,那么变量就可以声明为lazy:
class Moi { let first = "Matt" let last = "Neuburg" lazy var whole : String = { var s = self.first s.appendContentsOf(" ") s.appendContentsOf(self.last) return s }}
如果属性是实例属性(默认情况),那么只能通过实例来访问它,并且对于每个实例来说,其值都是独立的。比如,再来看看这个Dog类:
class Dog { let name : String let license : Int init(name:String, license:Int) { self.name = name self.license = license }}
这个Dog类有一个name实例属性,接下来可以通过两个不同的name值创建两个不同的Dog实例,并通过实例访问每个Dog的name属性:
let fido = Dog(name:"Fido", license:1234)let spot = Dog(name:"Spot", license:1357)let aName = fido.name // "Fido"let anotherName = spot.name // "Spot"
另一方面,静态/类属性是通过类型访问的,其作用域是类型,这意味着它是全局且唯一的,这里使用一个结构体作为示例:
struct Greeting { static let friendly = "hello there" static let hostile = "go away"}
现在,其他地方的代码可以获取到Greeting.friendly与Greeting.hostile的值。该示例非常有代表性;不变的静态/类属性可以作为一种非常便捷且有效的方式为代码提供命名空间下的常量。
与实例属性不同,静态属性可以通过对其他静态属性的引用进行实例化,这是因为静态属性初始化器是延迟的(参见第3章):
struct Greeting { static let friendly = "hello there" static let hostile = "go away" static let ambivalent = friendly + " but " + hostile}
注意到上述代码中没有使用self。在静态/类代码中,self表示类型本身。即便在self会被隐式使用的场景下,我也倾向于显式使用它,不过这里却无法使用self,虽然编译器不会报错(我认为这是个Bug)。为了表示friendly与hostile的状态,我可以使用类型名字,就像其他代码一样:
struct Greeting { static let friendly = "hello there" static let hostile = "go away" static let ambivalent = Greeting.friendly + " but " + Greeting.hostile}
另外,如果将ambivalent作为计算属性,那就可以使用self了:
struct Greeting { static let friendly = "hello there" static let hostile = "go away" static var ambivalent : String { return self.friendly + " but " + self.hostile }}
此外,如果初始值是通过定义与调用匿名函数所设置的,那就无法使用self(我认为这也是个Bug):
struct Greeting { static let friendly = "hello there" static let hostile = "go away" static var ambivalent : String = { return self.friendly + " but " + self.hostile // compile error }}
4.1.3 方法
方法就是函数,只是声明在对象类型声明顶部的函数,这意味着第2章介绍的关于函数的一切也都适用于方法。
在默认情况下,方法是实例方法,这意味着只能通过实例来进入它。在实例方法体中,self指的就是实例。为了说明这一点,我们继续在Dog类中添加一些内容:
class Dog { let name : String let license : Int let whatDogsSay = "Woof" init(name:String, license:Int) { self.name = name self.license = license } func bark { print(self.whatDogsSay) } func speak { self.bark print("I'm /(self.name)") }}
现在可以创建Dog实例并调用它的speak方法:
let fido = Dog(name:"Fido", license:1234)fido.speak // Woof I'm Fido
在Dog类中,speak方法通过self调用了实例方法bark,然后又通过self获取到实例属性name的值;而bark实例方法则通过self获取到实例属性whatDogsSay的值。这是因为实例代码可以通过self引用到该实例;如果引用没有歧义,那么代码就可以省略self;比如,代码可以写成这样:
func speak { bark print("I'm /(name)")}
不过,我从来都不会这么写(仅仅是偶尔为之)。我认为,省略self会导致代码的可读性与可维护性变差;仅仅使用bark与name看起来会令人费解且困惑。此外,有时self是不可以省略的。比如,在init(name:license:)实现中,我必须得使用self消除参数name与属性self.name之间的差别。
静态/类属性是通过类型访问的,self表示的是类型。参见如下Greeting结构体示例:
struct Greeting { static let friendly = "hello there" static let hostile = "go away" static var ambivalent : String { return self.friendly + " but " + self.hostile } static func beFriendly { print(self.friendly) }}
下面展示了如何调用静态方法beFriendly:
Greeting.beFriendly // hello there
虽然声明在相同的对象类型中,但静态/类成员与实例成员之间在概念上还是存在一些差别,它们位于不同的世界中。静态/类方法不能引用“实例”,因为根本就没有实例存在;静态/类方法不能直接引用任何实例属性,也不能调用任何实例方法。另外,实例方法却可以通过名字引用类型,也可以访问静态/类属性,调用静态/类方法(本章后面将会介绍实例方法引用类型的另一种方式)。
比如,回到Dog类上来,解决一下狗会叫的问题。假设所有狗叫的都一样。因此,我们倾向于在类级别而非实例级别表示whatDogsSay。这正是静态属性的用武之地,下面是一个用于说明问题的简化的Dog类:
实例方法揭秘
有这样一个秘密:实例方法实际上可以访问静态/类方法。比如,如下代码是合法的(但看起来很奇怪):
class MyClass { var s = "" func store(s:String) { self.s = s }}let m = MyClasslet f = MyClass.store(m) // what just happened!?
虽然store是个实例方法,但我们能以类方法的形式调用它,即通过将类实例作为参数!原因在于实例方法实际上是由两个函数构成的调制静态/类方法:一个函数接收一个实例,另一个函数接收实例方法的参数。这样,在上述代码执行后,f就成为第2个函数,调用它就相当于调用实例m的store方法一样:
f("howdy")print(m.s) // howdyclass Dog { static var whatDogsSay = "Woof" func bark { print(Dog.whatDogsSay) }}
接下来创建一个Dog实例并调用其bark方法:
let fido = Dogfido.bark // Woof
4.1.4 下标
下标是一种实例方法,不过调用方式比较特殊:在实例引用后面使用方括号,方括号可以包含传递给下标方法的参数。你可以通过该特性做任何想做的事情,不过它特别适合于通过键或索引号访问对象类型中的元素的场景。第3章曾介绍过该语法搭配字符串的使用方式,字典与数组也经常见到这种使用方式;你可以对字符串、字典与数组使用方括号,因为Swift的String、Dictionary与Array类型都声明了下标方法。
声明下标方法的语法类似于函数声明和计算属性声明,这并非巧合!下标类似于函数,因为它可以接收参数:当调用下标方法时,实参位于方括号中。下标类似于计算属性,因为调用就好像是对属性的引用:你可以获取其值,也可以对其赋值。
为了说明问题,我声明一个结构体,它对待整型的方式就像是字符串,通过在方括号中使用索引数的方式返回一个数字;出于简化的目的,我有意省略了错误检查代码:
struct Digit { var number : Int init(_ n:Int) { self.number = n } subscript(ix:Int) -> Int { ①② get { ③ let s = String(self.number) return Int(String(s[s.startIndex.advancedBy(ix)]))! } }}
①关键字subscript后面有一个参数列表,指定什么参数可以出现在方括号中;在默认情况下,其名字不是外化的。
②接下来,在箭头运算符后面指定了传出(调用getter时)或传入(调用setter时)的值类型;这与计算属性的类型声明是类似的,不过箭头运算符的语法类似于函数声明中的返回值。
③最后,花括号中的内容就像是计算属性的内容。你可以为getter提供get与花括号,为setter提供set与花括号。如果只有getter没有setter,那么单词get及后面的花括号就可以省略。setter会将新值作为newValue,不过你可以在圆括号中单词set后面提供不同的名字来改变它。
下面是调用getter的一个示例;实例名后面跟着方括号,里面是实参值,调用时相当于获取一个属性值一样:
var d = Digit(1234)let aDigit = d[1] // 2
现在来扩展Digit结构体,使其下标方法包含setter(再次省略错误检查代码):
struct Digit { var number : Int init(_ n:Int) { self.number = n } subscript(ix:Int) -> Int { get { let s = String(self.number) return Int(String(s[s.startIndex.advancedBy(ix)]))! } set { var s = String(self.number) let i = s.startIndex.advancedBy(ix) s.replaceRange(i...i, with: String(newValue)) self.number = Int(s)! } }}
下面是调用setter的一个示例;实例名后面跟着方括号,里面是实参值,调用时相当于设置一个属性值一样:
var d = Digit(1234)d[0] = 2 // now d.number is 2234
一个对象类型可以声明多个下标方法,前提是其签名不同。
4.1.5 嵌套对象类型
一个对象类型可以声明在另一个对象类型声明中,从而形成嵌套类型:
class Dog { struct Noise { static var noise = "Woof" } func bark { print (Dog.Noise.noise) }}
嵌套对象类型与一般的对象类型没有区别,不过从外部引用它的规则发生了变化;外部对象类型成为一个命名空间,必须要显式通过它才能访问到嵌套对象类型:
Dog.Noise.noise = "Arf"
Noise结构体位于Dog类命名空间下面,该命名空间增强了清晰性:名字Noise不能随意使用,必须要显式关联到所属的Dog类。借助命名空间,我们可以创建多个Noise结构体,而不会造成名字冲突。Swift内建对象类型通常都会利用命名空间;比如,有一些结构体包含了Index结构体,而String结构体就是其中之一,它们之间不会造成名字冲突。
(借助于Swift的隐私原则,我们还可以隐藏嵌套对象类型,这样就无法在外部引用它了。这样,如果一个对象类型需要另一个对象类型作为辅助,而其他对象类型无须了解这个辅助对象类型,那么通过这种方式就可以很好地起到组织和封装的目的。第5章将会介绍隐私。)
4.1.6 实例引用
总的来说,对象类型的名字是全局的,只需通过其名字就可以引用它们,不过实例则不同。实例必须要显式地逐一创建,这正是实例化的目的之所在。创建好实例后,你可以将它存储到具有足够长生命周期的变量中以保证实例一直存在;将该变量作为引用,你可以向实例发送实例消息,访问实例属性并调用实例方法。
对对象类型的实例化是直接创建该类型全新实例的一种方式,这需要调用初始化器。不过在很多情况下,其他对象会创建对象并将其提供给你。
一个简单的例子就是像下面这样操纵一个String时会发生什么:
let s = "Hello, world"let s2 = s.uppercaseString
上述代码执行完毕后会生成两个String实例。第1个s是通过字符串字面值创建的;第2个s2是通过访问第1个字符串的uppercaseString属性创建的。因此,我们会得到两个实例,只要对它们的引用存在,这两个实例就会存在而且相互独立;不过,在创建它们时并未调用初始化器。
有时,你所需要的实例已经以某种持久化形式存在了;接下来的问题就在于如何获得对该实例的引用。
比如,有一个实际的iOS应用。你当然会有一个根视图控制器,它是某种UIViewController的实例。假设它是ViewController类的实例。当应用启动并运行后,该实例就已经存在了。接下来,通过实例化ViewController类来与根视图控制器进行通信显然与我们的想法是背道而驰的:
let theVC = ViewController
上述代码会创建另一个完全不同的ViewController类实例,向该实例发送的消息都是毫无意义的,因为它并非你想要与之通信的那个特定实例。这是初学者常犯的一个错误,请注意。
获取对已经存在的实例的引用是个很有意思的话题。显然,实例化并不是解决之道,那该怎么做呢?要具体问题具体分析。在这个特定的情况下,我们的目标是从代码中获取到对应用根视图控制器实例的引用。下面来介绍一下该怎么做。
获取引用总是从你已经具有引用的对象开始,通常这是个类。在iOS编程中,应用本身就是个实例,有一个类会持有一个对该实例的引用,它会在你需要时将其传递给你。这个类就是UIApplication,我们可以通过调用其sharedApplication类方法来获得对应用实例的引用:
let app = UIApplication.sharedApplication
现在,我们拥有了对应用实例的引用,该应用实例有一个keyWindow属性:
let window = app.keyWindow
现在,我们有了对应用主窗口的引用。该窗口拥有根视图控制器,并且会将对其的引用给我们,即其rootViewController属性;应用的keyWindow是个Optional,因此需要将其展开才能得到rootViewController:
let vc = window?.rootViewController
现在,我们有了对应用根视图控制器的引用。为了获得对该持久化实例的引用,我们实际上创建了一个方法调用与属性链,从已知到未知,从全局类到特定实例:
let app = UIApplication.sharedApplicationlet window = app.keyWindowlet vc = window?.rootViewController
显然,可以通过一个链来表示上述代码,使用重复的点符号即可:
let vc = UIApplication.sharedApplication.keyWindow?.rootViewController
无需将实例消息链接为单独一行:使用多个let赋值会更具效率、更加清晰、也更易于调试。不过这么做会更加便捷,也是Swift这种使用点符号的面向对象语言的一个特性。
获取对已经存在的实例的引用是个很有趣的话题,应用也非常广泛,第13章将会对其进行深入介绍。