这是我参与更文挑战的第7天,活动详情查看: 更文挑战
文章来自对 objccn.io/ 相关书籍的整理笔记。“感谢 objc.io 及其撰稿作者们无私地将他们的知识分享给全世界”。
21. 多类型和容器
Swift 中常用的原生容器类型有三种: Array 、 Dictionay 和 Set :
struct Array<Element> :
RandomAccessCollection, MutableCollection {
//...
}
struct Dictionary<Key : Hashable, Value> :
Collection, ExpressibleByDictionaryLiteral {
//...
}
public struct Set<Element : Hashable> :
SetAlgebra, Hashable, Collection, ExpressibleByArrayLiteral {
//...
}
复制代码
它们都是泛型的,一个集合中只能放同一个类型的元素。
let numbers = [1,2,3,4,5] // numbers 的类型是 [Int]
let strings = ["hello", "world"] // strings 的类型是 [String]
复制代码
如果要把不相关的类型放到同一个容器类型中的话,需要做一些转换的工作:
// Any 类型可以隐式转换
let mixed: [Any] = [1, "two", 3]
// 转换为 [NSObject]
let objectArray = [1 as NSObject, "two" as NSObject, 3 as NSObject]
复制代码
这样的转换会造成部分信息的损失,从容器中取值时只能得到信息完全丢失后的结果,在使用时还需要进行一次类型转换。这是在无其他可选方案后的最差选择,使用这样的转换编译器就不能提供警告信息。可以随意地将任意对象添加进容器,也可以将容器中取出的值转换为任意类型,这是一件危险的事情:
let any = mixed[0] // Any 类型
let nsObject = objectArray[0] // NSObject 类型
复制代码
Any 其实不是具体的某个类型。在容器类型泛型的帮助下,不仅可以在容器中添加同一具体类型的对象,也可以添加实现了同一协议的类型的对象。绝大多数情况下,放入一个容器中的元素会有某些共同点,使得用协议来规定容器类型会很有用。比如上面的例子如果希望的是打印出容器内的元素的 description ,更倾向于将数组声明为 [CustomStringConvertible] 的:
let mixed: [CustomStringConvertible] = [1, "two", 3]
for obj in mixed {
print(obj.description)
}
复制代码
这种方法虽然也损失了一部分类型信息,但是相对于 Any 或者 AnyObject 还是改善的,对于对象存在某种共同特性的情况下是最方便的。另一种做法是使用 enum 可以带有值的特点,将类型信息封装到特定的 enum 中。下面的代码封装了 Int 或者 String 类型:
enum IntOrString {
case IntValue(Int)
case StringValue(String)
}
let mixed = [IntOrString.IntValue(1),
IntOrString.StringValue("two"),
IntOrString.IntValue(3)]
for value in mixed {
switch value {
case let .IntValue(i):
print(i * 2)
case let .StringValue(s):
print(s.capitalized)
}
}
// 2 Two 6
复制代码
通过这种方法完整地在编译时保留了不同类型的信息。可以进一步为 IntOrString 使用字面量转换的方法编写简单的获取方式。
22. default 参数
Swift 的方法是支持默认参数的,在声明方法时,可以给某个参数指定一个默认使用的值。在调用该方法时要是传入了这个参数,则使用传入的值,如果缺少这个输入参数,那么直接使用设定的默认值进行调用。
func sayHello1(str1: String = "Hello", str2: String, str3: String) {
print(str1 + str2 + str3)
}
func sayHello2(str1: String, str2: String, str3: String = "World") {
print(str1 + str2 + str3)
}
// 其他不少语言只能使用后面一种写法,将默认参数作为方法的最后一个参数
// 在调用的时候,如果想要使用默认值的话,只要不传入相应的值就可以
// 下面这样的调用将得到同样的结果
sayHello1(str2: " ", str3: "World")
sayHello2(str1: "Hello", str2: " ")
//输出都是 Hello World
复制代码
这两个调用都省略了带有默认值的参数,sayHello1 中 str1 是默认的 “Hello”,而 sayHello2 中的 str3 是默认的 “World”。
NSLocalizedString 和 assert 方法的签名是:
func NSLocalizedString(key: String,
tableName: String? = default,
bundle: NSBundle = default,
value: String = default,
comment: String) -> String
func assert(@autoclosure condition: () -> Bool,
@autoclosure _ message: () -> String = default,
file: StaticString = default,
line: UWord = default)
复制代码
默认参数写的是 default ,这是含有默认参数的方法所生成的 Swift 的调用接口。指定一个编译时就能确定的常量来作为默认参数的取值时,这个取值是隐藏在方法实现内部,而不应该暴露给其他部分。
23. 正则表达式
Swift 至今为止并没有在语言层面上支持正则表达式。
最容易想到也是最容易实现的当然是自定义 =~ 运算符。在 Cocoa 中可以使用 NSRegularExpression 来做正则匹配,为它写一个包装。因为做的是字符串正则匹配,所以 =~ 左右两边都是字符串。可以先写一个接受正则表达式的字符串,以此生成 NSRegularExpression 对象。然后使用该对象来匹配输入字符串,并返回结果告诉调用者匹配是否成功。一个最简单的实现可能是下面这样的:
struct RegexHelper {
let regex: NSRegularExpression
init(_ pattern: String) throws {
try regex = NSRegularExpression(pattern: pattern,
options: .caseInsensitive)
}
func match(_ input: String) -> Bool {
let matches = regex.matches(in: input,
options: [],
range: NSMakeRange(0, input.utf16.count))
return matches.count > 0
}
}
// 想要匹配一个邮箱地址
let mailPattern =
"^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
let matcher: RegexHelper
do {
matcher = try RegexHelper(mailPattern)
}
let maybeMailAddress = "123@456.com"
if matcher.match(maybeMailAddress) {
print("有效的邮箱地址")
}
// 实现 =~
precedencegroup MatchPrecedence {
associativity: none
higherThan: DefaultPrecedence
}
infix operator =~: MatchPrecedence
func =~(lhs: String, rhs: String) -> Bool {
do {
return try RegexHelper(rhs).match(lhs)
} catch _ {
return false
}
}
if "123@456.com" =~ "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$" {
print("有效的邮箱地址")
}
复制代码
24. 模式匹配
一个和正则匹配有些相似的特性内置于 Swift 中,模式匹配。
从概念上来说正则匹配只是模式匹配的一个子集,但是在 Swift 里现在的模式匹配还很初级,也很简单,只能支持最简单的相等匹配和范围匹配。在 Swift 中,使用 ~= 来表示模式匹配的操作符。这个操作符有下面几种版本:
func ~=<T : Equatable>(a: T, b: T) -> Bool
func ~=<T>(lhs: _OptionalNilComparisonType, rhs: T?) -> Bool
func ~=<I : IntervalType>(pattern: I, value: I.Bound) -> Bool
复制代码
从上至下在操作符左右两边分别接收可以判等的类型,可以与 nil 比较的类型,以及一个范围输入和某个特定值,返回值都是是否匹配成功的 Bool 值。就是 Swift 中的 switch:
// 1. 可以判等的类型的判断
let password = "akfuv(3"
switch password {
case "akfuv(3": print("密码通过")
default: print("验证失败")
}
// 2. 对 Optional 的判断
let num: Int? = nil
switch num {
case nil: print("没值")
default: print("\(num!)")
}
// 3. 对范围的判断
let x = 0.5
switch x {
case -1.0...1.0: print("区间内")
default: print("区间外")
}
复制代码
switch 就是使用了 ~= 操作符进行模式匹配, case 指定的模式作为左参数输入,而等待匹配的被 switch 的元素作为操作符的右侧参数。这个调用是由 Swift 隐式地完成的。
在 switch 中做 case 判断的 时候,完全可以使用我们自定义的模式匹配方法来进行判断,有时候这会让代码变得非常简洁,具有条理。需要按照需求重载 ~= 操作符,一个使用正则表达式做匹配的例子:
首先重载 ~= 操作符,接受一个 NSRegularExpression 作为模式,去匹配输入的 String :
func ~=(pattern: NSRegularExpression, input: String) -> Bool {
return pattern.numberOfMatches(in: input,
options: [],
range: NSRange(location: 0, length: input.count)) > 0
}
复制代码
然后为了简便起⻅,我们再添加一个将字符串转换为 NSRegularExpression 的操作符
prefix operator ~/
prefix func ~/(pattern: String) -> NSRegularExpression {
return NSRegularExpression(pattern: pattern, options: nil, error: nil)
}
复制代码
在 case 语句里使用正则表达式的话,可以去匹配被 switch 的字符串:
let contact = ("http://onevcat.com", "onev@onevcat.com")
let mailRegex: NSRegularExpression
let siteRegex: NSRegularExpression
mailRegex =
try ~/"^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
siteRegex =
try ~/"^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$"
switch contact {
case (siteRegex, mailRegex): print("同时拥有有效的网站和邮箱") case (_, mailRegex): print("只拥有有效的邮箱")
case (siteRegex, _): print("只拥有有效的网站")
default: print("没有")
}
复制代码
25. … 和 ..<
在很多脚本语言中都有类似 0..3 或者 0…3 这样的 Range 操作符,用来简单地指定一个从 X 开始连续计数到 Y 的范围。
最基础的用法是在两边指定数字, 0…3 就表示从 0 开始到 3 为止并包含 3 这个数字的范围,称为全闭合的范围操作;而在某些时候更常用的是不包括最后一个数字的范围,Swift 中写作 0..<3。
对于这样得到的数字的范围,可以进行 for…in 的访问:
for i in 0...3 {
print(i, terminator: "")
}
复制代码
Swift 中对着两个操作符的定义:
/// Forms a closed range that contains both `minimum` and `maximum`.
func ...<Pos : ForwardIndexType>(minimum: Pos, maximum: Pos)
-> Range<Pos>
/// Forms a closed range that contains both `start` and `end`.
/// Requres: `start <= end`
func ...<Pos : ForwardIndexType where Pos : Comparable>(start: Pos, end: Pos)
-> Range<Pos>
/// Forms a half-open range that contains `minimum`, but not
/// `maximum`.
func ..<<Pos : ForwardIndexType>(minimum: Pos, maximum: Pos)
-> Range<Pos>
/// Forms a half-open range that contains `start`, but not
/// `end`. Requires: `start <= end`
func ..<<Pos : ForwardIndexType where Pos : Comparable>(start: Pos, end: Pos)
-> Range<Pos>
/// Returns a closed interval from `start` through `end`
func ...<T : Comparable>(start: T, end: T) -> ClosedInterval<T>
/// Returns a half-open interval from `start` to `end`
func ..<<T : Comparable>(start: T, end: T) -> HalfOpenInterval<T>
复制代码
这几个方法都是支持泛型的。除了常用的输入 Int 或者 Double ,返回一个 Range 以外,这个操作符还有一个接受 Comparable 的输入,并返回 ClosedInterval 或 HalfOpenInterval 的重载。
在 Swift 中,除了数字以外另一个实现了 Comparable 的基本类型就是 String 。也就是说可以通过 … 或者 ..< 来连接两个字符串。一个常⻅的使用场景就是检查某个字符是否是合法的字符。比如想确认一个单词里的全部字符都是小写英文字母的话,可以这么做:
let test = "helLo"
let interval = "a"..."z"
for c in test {
if !interval.contains(String(c)) { print("\(c) 不是小写字母")
}
}
复制代码
在日常开发中,可能会需要确定某个字符是不是有效的 ASCII 字符,可以使用 \0…~ 这样的 ClosedInterval 来进行 ( \0 和 ~ 分别是 ASCII 的第一个和最后一个字符)。
26. AnyClass,元类型和 .self
在 Swift 中能够表示 “任意” 这个概念的除了 Any 和 AnyObject 以外,还有一个 AnyClass 。 AnyClass 在 Swift 中被一个 typealias 所定义:
typealias AnyClass = AnyObject.Type
复制代码
通过 AnyObject.Type 这种方式所得到是一个元类型 (Meta)。在声明时在类型的名称后面加上 .Type ,比如 A.Type 代表的是 A 这个类型的类型。也就是说,可以声明一个元类型来存储 A 这个类型本身,而在从 A 中取出其类型时,需要使用到 .self :
class A {}
let typeA: A.Type = A.self
复制代码
在 Swift 中, .self 可以用在类型后面取得类型本身,也可以用在某个实例后面取得这个实例本身。前一种方法可以用来获得一个表示该类型的值,这在某些时候会很有用; 而后者因为拿到的实例本身,所以暂时似乎没有太多需要这么使用的案例。
白 AnyObject.Type ( AnyClass )所表达的东⻄其实就是任意类型本身。上面对于 A 的类型的取值,也可以强制让它是一个 AnyClass :
class A {}
let typeA: AnyClass = A.self
// A 中有一个类方法时,可以通过 typeA 来对其进行调用
class A {
class func method() {
print("Hello")
}
}
let typeA: A.Type = A.self
typeA.method()
// 或者
let anyClass: AnyClass = A.self (anyClass as! A.Type).method()
复制代码
元类型或者元编程的概念可以变得非常灵活和强大,在编写某些框架性的代码时会非常方便。比如想要传递一些类型的时候,就不需要不断地去改动代码了。
在下面的这个例子中用代码声明的方式获取 了 MusicViewController 和 AlbumViewController 的元类型,可以通过读入配置文件之类的方式来完成的。而在将这些元类型存入数组并且传递给别的方法来进行配置这一点上,元类型编程就很难被替代了:
class MusicViewController: UIViewController {
}
class AlbumViewController: UIViewController {
}
let usingVCTypes: [AnyClass] = [MusicViewController.self, AlbumViewController.self]
func setupViewControllers(_ vcTypes: [AnyClass]) {
for vcType in vcTypes {
if vcType is UIViewController.Type {
let vc = (vcType as! UIViewController.Type).init()
print(vc)
}
setupViewControllers(usingVCTypes)
复制代码
可以搭好框架,然后用 DSL 的方式进行配置,不触及 Swift 编码的情况下,简单地完成复杂操作。
另外,在 Cocoa API 中常遇到需要一个 AnyClass 的输入,这时候也应该使用 .self 的方式来获取所需要的元类型,例如在注册 tableView 的 cell 的类型的时候:
self.tableView.registerClass(
UITableViewCell.self, forCellReuseIdentifier: "myCell")
复制代码
.Type 表示的是某个类型的元类型,在 Swift 中,除了 class , struct 和 enum 这三个类型外,还可以定义 protocol 。可以在某个 protocol 的名字后面使用 .Protocol 来获取,使用的方法和 .Type 是类似的。
27. 协议和类方法中的 Self
一些协议的定义,可能会注意到出现了首字母大写的 Self 出现在类型的位置上:
protocol IntervalType {
//...
/// Return `rhs` clamped to `self`. The bounds of the result, even
/// if it is empty, are always within the bounds of `self`
func clamp(intervalToClamp: Self) -> Self
//...
}
复制代码
IntervalType 的协议定义了一个方法,接受实现该协议的自身的类型,并返回一个同样的类型。
协议其实本身是没有自己的上下文类型信息的,在声明协议的时候,并不知道最后究竟会是什么样的类型来实现这个协议,Swift 中也不能在协议中定义泛型进行限制。而在声明协议时,希望在协议中使用的类型就是实现这个协议本身的类型的话,就需要使用 Self 进行指代。
但是在这种情况下,Self 不仅指代的是实现该协议的类型本身,也包括了这个类型的子类。
从概念上来说,Self 十分简单,但是实际实现一个这样的方法却稍微要转个弯。假设要实现一个 Copyable 的协议,满足这个协议的类型需要返回一个和接受方法调用的实例相同的拷⻉。一开始可能考虑的协议是这样的:
protocol Copyable {
func copy() -> Self
}
复制代码
应该做的是创建一个和接受这个方法的对象同样的东⻄,然后将其返回,返回的类型不应该发生改变,所以写为 Self 。然后开始尝试实现一个 MyClass 来满足这个协议:
class MyClass: Copyable {
var num = 1
func copy() -> Self {
// TODO: 返回什么?
// return
}
}
// 可能会写类似这样的代码
// 这是错误代码
func copy() -> Self {
let result = MyClass()
result.num = num
return result
}
复制代码
显然类型是有问题的,因为该方法要求返回一个抽象的、表示当前类型的 Self ,但是返回了它的真实类型 MyClass ,这导致了无法编译。尝试把方法声明中的 Self 改为 MyClass ,这样声明就和实际返回一致了,但是实现的方法又和协议中的定义不一样了,依然不能编译。
为了解决这个问题,需要的是通过一个和当前上下文 (也就是和 MyClass ) 无关的,又能够指代当前类型的方式进行初始化。可以使用 type(of:) 来获取对象类型,通过它也可以进一步进行初始化,以保证方法与当前类型上下文无关,这样不论是 MyClass 还是它的子类,都可以正确地返回合适的类型满足 Self 的要求:
func copy() -> Self {
let result = type(of: self).init()
result.num = num
return result
}
复制代码
但还是无法通过编译,编译器提示如果想要构建一个 Self 类型的对象的话,需要有 required 关键字修饰的初始化方法,这是因为 Swift 必须保证当前类和其子类都能响应这个 init 方法。另一个解决的方案是在当前类类的声明前添加 final 关键字,告诉编译器不再会有子类来继承这个类型。在这个例子中选择添加上 required 的 init 方法。 最后,MyClass 类型是这样的:
class MyClass: Copyable {
var num = 1
func copy() -> Self {
let result = type(of: self).init()
result.num = num
return result
}
required init() {
}
}
let object = MyClass()
object.num = 100
let newObject = object.copy()
object.num = 1
print(object.num) // 1
print(newObject.num) // 100
复制代码
而对于 MyClass 的子类, copy() 方法也能正确地返回子类的经过拷⻉的对象了。另一个可以使用 Self 的地方是在类方法中,使用起来也十分相似,核心就在于保证子类也能返回恰当的类型。
28. 动态类型和多方法
Swift 中虽然可以通过 dynamicType 来获取一个对象的动态类型 (也就是运行时的实际类型,而非代码指定或编译器看到的类型)。但是在使用中,Swift 现在却是不支持多方法的,也就是说,不能根据对象在动态时的类型进行合适的重载方法调用。
在 Swift 里可以重载同样名字的方法,而只需要保证参数类型不同:
class Pet {}
class Cat: Pet {}
class Dog: Pet {}
func printPet(_ pet: Pet) {
print("Pet")
}
func printPet(_ cat: Cat) {
print("Meow")
}
func printPet(_ dog: Dog) {
print("Bark")
}
printPet(Cat()) // Meow
printPet(Dog()) // Bark
printPet(Pet()) // Pet
复制代码
对于 Cat 或者 Dog 的实例,总是会寻找最合适的方法,而不会去调用一个通用的父类 Pet 的方法。这一切的行为都是发生在编译时的,如果下面这样的代码:
func printThem(_ pet: Pet, _ cat: Cat) {
printPet(pet)
printPet(cat)
}
printThem(Dog(), Cat())
// Pet
// Meow
复制代码
打印时的 Dog() 的类型信息并没有被用来在运行时选择合适的 printPet(dog: Dog) 版本的方法,而是被忽略掉,并采用了编译期间决定的 Pet 版本的方法。因为 Swift 默认情况下是不采用动态派发的,因此方法的调用只能在编译时决定。
要想绕过这个限制需要进行通过对输入类型做判断和转换:
func printThem(_ pet: Pet, _ cat: Cat) {
if let aCat = pet as? Cat {
printPet(aCat)
} else if let aDog = pet as? Dog {
printPet(aDog)
}
printPet(cat)
}
// Bark
// Meow
复制代码
29. 属性观察
属性观察 (Property Observers) 是 Swift 中一个很特殊的特性,利用属性观察可以在当前类型内监视对于属性的设定,并作出一些响应。Swift 提供了两个属性观察的方法,它们分别 是 willSet 和 didSet。
使用这两个方法只要在属性声明的时候添加相应的代码块,就可以对将要设定的值和已经设置的值进行监听了:
class MyClass {
var date: NSDate {
willSet {
let d = date
print("即将将日期从 \(d) 设定至 \(newValue)") }
didSet {
print("已经将日期从 \(oldValue) 设定至 \(date)")
}
}
init() {
date = NSDate()
}
}
let foo = MyClass()
foo.date = foo.date.addingTimeInterval(10000)
复制代码
在 willSet 和 didSet 中分别可以使用 newValue 和 oldValue 来获取将要设定的和已经设定的值。属性观察的一个重要用处是作为设置值的验证,比如上面的例子中不希望 date 超过 当前时间的一年以上的话,可以将 didSet 修改一下:
class MyClass {
let oneYearInSecond: TimeInterval = 365 * 24 * 60 * 60
var date: NSDate {
//...
didSet {
if (date.timeIntervalSinceNow > oneYearInSecond) {
print("设定的时间太晚了!")
date = NSDate().addingTimeInterval(oneYearInSecond)
}
print("已经将日期从 \(oldValue) 设定至 \(date)") }
}
//...
}
复制代码
初始化方法对属性的设定,以及在 willSet 和 didSet 中对属性的再次设定都不会再次触发属性观察的调用。
在 Swift 中所声明的属性包括存储属性和计算属性两种。其中存储属性将会在内存中实际分配地址对属性进行存储,而计算属性则不包括背后的存储,只是提供 set 和 get 两种方法。在同一个类型中,属性观察和计算属性是不能同时共存的。也就是说,想在一个属性定义中同时出现 set 和 willSet 或 didSet 是一件办不到的事情。计算属性中可以通过改写 set 中的内容来达到和 willSet 及 didSet 同样的属性观察的目的。如果我们无法改动这个类,又想要通过属性观察做一些事情的话,可能就需要子类化这个类,并且重写它的属性了。重写的属性并不知道父类属性的具体实现情况,而只从父类属性中继承名字和类型,因此在子类的重载属性中是可以对父类的属性任意地添加属性观察的,而不用在意父类中到底是存储属性还是计算属性:
class A {
var number :Int {
get {
print("get")
return 1 }
set {print("set")}
}
}
class B: A {
override var number: Int {
willSet {print("willSet")}
didSet {print("didSet")}
}
}
let b = B()
b.number = 0
// 输出
// get
// willSet
// set
// didSet
复制代码
这里要注意的是 get 首先被调用了一次。 这是因为实现了 didSet , didSet 中会用到 oldValue ,而这个值需要在整个 set 动作之前进行获取并存储待用,否则将无法确保正确性。如果不实现 didSet 的话,这次 get 操作也将不存在。
30. final
final 关键字可以用在 class ,func 或者 var 前面进行修饰,表示不允许对该内容进行继承或者重写操作。有一派认为,类似这样的禁止继承和重写的做法是非常有益的,它可以更好地对代码进行版本控制,得到更佳的性能,以及使代码更安全。甚至认为语言应当是默认不允许继承的,只有在显式地指明可以继承的时候才能子类化。但是另一个事实是不论是 Apple 或者微软,以及世界上很多其他语言都没有作出默认不让继承和重写的决定。
权限控制
给一段代码加上 final 就意味着编译器作出保证,这段代码不会再被修改;同时认为这段代码已经完备并且没有再被进行继承或重写的必要。在 Cocoa 开发中 app 开发是一块很大的内容,其实不太会提供给别人使用,这种情况下即使是将所有自己写的代码标记为 final 都是一件无可厚非的事情,但在需要的任何时候你都可以将这个关键字去掉以恢复其可继承性。而在开发给其他开发者使用的库时,就必须更深入地考虑各种使用场景和需求了。
一般来说,不希望被继承和重写会有这几种情况:
- 类或者方法的功能确实已经完备了
对于很多的辅助性质的工具类或者方法,可能会加上 final 。这样的类有一个比较大的特点,是很可能只包含类方法而没有实例方法。比如我们很难想到一种情况需要继承或重写一个负责计算一段字符串的 MD5 或者 AES 加密解密的工具类。这种工具类和方法的算法是经过完备验证和固定的,使用者只需要调用,而相对来说不可能有继承和重写的需求。
- 子类继承和修改是一件危险的事情
在子类继承或重写某些方法后可能做一些破坏性的事情,导致子类或者父类部分也无法正常工作的情况。在这类情况下,将编号方法标记为 final 以确保稳定,可能是一种更好的做法。
- 为了父类中某些代码一定会被执行
有时候父类中有一些关键代码是在被继承重写后必须执行的 (比如状态配置,认证等等),否则将导致运行时候的错误。而在一般的方法中,如果子类重写了父类方法,是没有办法强制子类方法一定去调用相同的父类方法的。可以使用一个 final 的方法,在其中进行一些必要的配置,然后再调用某个需要子类实现的方法,以确保正常运行:
class Parent {
final func method() {
print("开始配置")
// ..必要的代码
methodImpl()
// ..必要的代码
print("结束配置")
}
func methodImpl() {
fatalError("子类必须实现这个方法") // 或者也可以给出默认实现
}
}
class Child: Parent {
override func methodImpl() {
//..子类的业务逻辑
}
}
复制代码
这样,无论如何如何使用 method ,都可以保证需要的代码一定被运行过,而同时又给了子类继承和重写自定义具体实现的机会。
性能考虑
使用 final 的另一个重要理由是可能带来的性能改善。因为编译器能够从 final 中获取额外的信息,因此可以对类或者方法调用进行额外的优化处理。但是这个优势在实际表现中可能带来的好处十分有限,因此在项目还有其他方面可以优化的情况下,并不建议使用将类或者方法转为final 的方式来追求性能的提升。