27.访问控制
可以明确地给单个类型(类、结构体、枚举)设置访问级别,也可以给这些类型的属性、方法、构造器、下标等设置访问级别。协议也可以被限定在一定访问级别的范围内使用,包括协议里的全局常量、变量和函数。
模块指的是独立的代码单元,框架或应用程序会作为一个独立的模块来构建和发布。在 Swift 中,一个模块可以使用
import
关键字导入另外一个模块。源文件 就是 Swift 模块中的源代码文件(实际上,源文件属于一个应用程序或框架)。尽管我们一般会将不同的类型分别定义在不同的源文件中,但是同一个源文件也可以包含多个类型、函数等的定义。
访问级别:open、public、internal、fileprivate、private
-
open 和 public 级别可以让实体被同一模块源文件中的所有实体访问,在模块外也可以通过导入该模块来访问源文件里的所有实体。通常情况下,你会使用 open 或 public 级别来指定框架的外部接口。open 和 public 的区别在后面会提到。
open 只能作用于类和类的成员,它和 public 的区别主要在于 open 限定的类和成员能够在模块外能被继承和重写。将类的访问级别显式指定为
open
表明你已经设计好了类的代码,并且充分考虑过这个类在其他模块中用作父类时的影响。 -
internal 级别让实体被同一模块源文件中的任何实体访问,但是不能被模块外的实体访问。通常情况下,如果某个接口只在应用程序或框架内部使用,就可以将其设置为 internal 级别。也是默认访问级别。
-
fileprivate 限制实体只能在其定义的文件内部访问。如果功能的部分实现细节只需要在文件内使用时,可以使用 fileprivate 来将其隐藏。
-
private 限制实体只能在其定义的作用域,以及同一文件内的 extension 访问。如果功能的部分细节只需要在当前作用域内使用时,可以使用 private 来将其隐藏。
设置访问级别的基本原则:
-
实体不能定义在访问级别更低的实体中,比如public属性不能定义在访问级别是internal的类型内
-
函数的访问级别不能高于返回值类型、参数类型的访问级别,导致函数在各个地方可以调用,但参数和返回值不行
单元测试target的访问级别
当你的应用程序包含单元测试 target 时,为了测试,测试模块需要访问应用程序模块中的代码。默认情况下只有
open
或public
级别的实体才可以被其他模块访问。然而,如果在导入应用程序模块的语句前使用@testable
特性,然后在允许测试的编译设置(Build Options -> Enable Testability
)下编译这个应用程序模块,单元测试目标就可以访问应用程序模块中所有内部级别的实体。
自定义类型访问级别对类型成员的影响
-
一个
public
类型的所有成员的访问级别默认为internal
级别,而不是public
级别。如果你想将某个成员指定为public
级别,那么你必须显式指定。这样做的好处是,在你定义公共接口的时候,可以明确地选择哪些接口是需要公开的,哪些是内部使用的,避免不小心将内部使用的接口公开。 -
如果你将类型指定为
private
或者fileprivate
级别,那么该类型的所有成员的默认访问级别也会变成private
或者fileprivate
级别
元组类型
元组不同于类、结构体、枚举、函数那样有单独的定义。元组的访问级别将由元组中访问级别最严格的类型来决定。例如,如果你构建了一个包含两种不同类型的元组,其中一个类型为 internal
,另一个类型为 private
,那么这个元组的访问级别为 private
。
函数类型
// 此处定义一个错误的全局的函数定义
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// 返回值为元组类型,包含的访问权限分别是 ,internal、private,这种情况需要明确函数的访问权限,否则报错
}
// 正确写法,函数访问权限不高于返回值类型、参数类型权限
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {}
枚举类型
枚举成员的访问级别和该枚举类型相同,你不能为枚举成员单独指定不同的访问级别。
-
原始值和关联值
- 枚举定义中的任何原始值或关联值的类型的访问级别至少不能低于枚举类型的访问级别。例如,你不能在一个
internal
的枚举中定义private
的原始值类型。
- 枚举定义中的任何原始值或关联值的类型的访问级别至少不能低于枚举类型的访问级别。例如,你不能在一个
嵌套类型
与自定义类型与自定义类型成员间的访问关系相同
子类
public class A {
fileprivate func someMethod() {}
}
internal class B: A {
// 通过重写,可以将父类中低访问权限的方法改为更高的权限,但要符合2大原则
override internal func someMethod() {}
}
public class A {
fileprivate func someMethod() {}
}
internal class B: A {
override internal func someMethod() {
super.someMethod() // 必须与A在同一个源文件内
}
}
Getter 和 Setter
常量、变量、属性、下标的 Getters
和 Setters
的访问级别和它们所属类型的访问级别相同。
struct TrackedString {
// fileprivate(set),private(set) 和 internal(set)
// 这意味着 numberOfEdits 属性只能在结构体的定义中进行赋值。numberOfEdits 属性的 Getter 依然是默认的访问级别 internal,但是 Setter 的访问级别是 private,这表示该属性只能在内部修改,而在结构体的外部则表现为一个只读属性。
// 你不能对其进行赋值。这一限制保护了该记录功能的实现细节,同时还提供了方便的访问方式。
private(set) var numberOfEdits = 0
var value: String = "" {
didSet {
numberOfEdits += 1
}
}
}
public struct TrackedString {
// 这里将 numberOfEdits 属性的 Getter 的访问级别设置为 public,而 Setter 的访问级别设置为 private
public private(set) var numberOfEdits = 0
public var value: String = "" {
didSet {
numberOfEdits += 1
}
}
public init() {}
}
构造器
自定义构造器的访问级别可以低于或等于其所属类型的访问级别。唯一的例外是 必要构造器,它的访问级别必须和所属类型的访问级别相同。
默认构造器
public权限的结构体的默认构造器为internal权限,如果需要public权限的构造器,你需要自己提供