第25章:访问控制
访问控制限制从其他源文件和模块中访问代码的能力。此功能使你可以隐藏代码的实现细节,并指定一个首选接口,通过该接口可以访问和使用该代码。
你可以为各个类型(类,结构和枚举)以及属于这些类型的属性,方法,初始化器和下标分配特定的访问级别。协议可以限制在某个上下文中,全局常量,变量和函数中。
除了提供各种级别的访问控制外,Swift还通过为典型方案提供默认访问级别来减少指定显式访问控制级别的需求。实际上,如果您正在编写单target应用程序,则可能根本不需要指定显式访问控制级别。
注意
为简便起见,代码中可以应用访问控制的各个方面(属性,类型,函数等)在下面的部分中称为“实体”。
25.1 模块和源文件
Swift的访问控制模型基于模块和源文件的概念。
模块是一个独立分发的单元——一个framework或应用程序(被编译以及包装成一个独立单元),并可以通过import关键字引入到另一个模块中。
Xcode中的每个构建目标(例如应用程序包或框架)都被视为Swift中的单独模块。如果你将应用程序代码的各个方面组合在一起作为一个独立的框架 - 也许是为了跨多个应用程序封装和重用该代码 - 那么您在该框架中定义的所有内容都将可以导入到其他模块或者target中。
源文件是一个模块内的单个Swift源代码文件(实际上,是一个应用程序或框架内的一个单独的文件)。虽然在单独的源文件中定义单个类型很常见,但单个源文件可以包含多个类型,函数等的定义。
25.2 访问级别
Swift为代码中的实体提供了五种不同的访问级别。这些访问级别与定义实体的源文件相关,也与源文件所属的模块相关。
* 开放访问和公共访问使实体可以在其定义模块的任何源文件中使用,也可以在来自导入定义模块的另一个模块的源文件中使用。在指定框架的公共接口时,通常使用开放或公共访问。开放和公共访问之间的区别如下所述。
* 内部访问使实体可以在其定义模块的任何源文件中使用,但不能在该模块之外的任何源文件中使用。在定义应用程序或框架的内部结构时,通常使用内部访问。
* 文件专用访问将实体的使用限制在其自己的定义源文件中。当在整个文件中使用这些详细信息时,使用文件专用访问来隐藏特定功能的实现细节。
* 私有访问将实体的使用限制为封闭声明,以及同一文件中该声明的扩展。当这些详细信息仅在单个声明中使用时,使用私有访问来隐藏特定功能的实现细节。
* 开放访问是最高(限制性最小)的访问级别,私有访问是最低(限制性最强)的访问级别。
开放访问仅适用于类和类成员,它与公共访问不同,如下所示:
* 具有公共访问权限或任何更严格的访问级别的类只能在定义它们的模块中进行子类化。
* 具有公共访问权限或任何更具限制性的访问级别的类成员只能在定义它们的模块中被子类覆盖。
* 开放类可以在定义它们的模块中进行子类化,也可以在导入模块的任何模块中进行子类化。
* 开放类成员可以由定义它们的模块中的子类覆盖,也可以在导入定义它们的模块的任何模块中覆盖。
将类标记为开放明确表示你已考虑使用该类作为超类的其他模块的代码的影响,并且你已相应地设计了类的代码。
25.2.1 访问级别的指导原则
Swift中的访问级别遵循一个总体指导原则:没有实体可以根据具有较低(更严格)访问级别的实体另一个实体来定义。
例如:
* 公共变量不能定义为具有内部,文件私有或私有类型,因为在使用公共变量的任何地方都可能无法使用该类型。
* 函数不能具有比其参数类型和返回类型更高的访问级别,因为该函数可用于其组成类型对周围代码不可用的情况。
下文详细介绍了该指导原则对该语言不同方面的具体影响。
25.2.2 默认访问级别
如果你没有自己指定显式访问级别,则代码中的所有实体(具有一些特定的例外情况,如本章后面所述)都具有内部的默认访问级别。因此,在许多情况下,你无需在代码中指定显式访问级别。
25.2.3 单目标应用的访问级别
当你编写一个简单的单目标应用程序时,应用程序中的代码通常是自包含在应用程序中的,并且不需要在应用程序模块外部提供。内部的默认访问级别已匹配此要求。因此,你无需指定自定义访问级别。但是,你可能希望将代码的某些部分标记为私有或私有文件,以便从应用程序模块中的其他代码中隐藏其实现细节。
25.2.4 框架的访问级别
在开发框架时,将该框架的面向公众的界面标记为开放或公共,以便其他模块(例如导入框架的应用程序)可以查看和访问该框架。这个面向公众的接口是框架的应用程序编程接口(或API)。
注意
框架的任何内部实现细节仍然可以使用内部的默认访问级别,或者如果要将它们隐藏在框架内部代码的其他部分中,则可以将其标记为私有或文件私有。如果您希望实体成为框架API的一部分,则需要将实体标记为开放或公开。
25.2.4 单元测试目标的访问级别
当你使用单元测试目标编写应用程序时,应用程序中的代码需要可供该模块使用才能进行测试。默认情况下,只有标记为open或public的实体才可供其他模块访问。但是,如果你使用该@testable属性标记产品模块的导入声明并且在启用测试的情况下编译该产品模块,则单元测试目标可以访问任何内部实体。
25.3 访问控制语法
通过放置的一个定义实体的访问级别open
,public
,internal
,fileprivate
,或private
的前导入修饰语来指定实体的访问控制级别:
1. public class SomePublicClass {}
2. internal class SomeInternalClass {}
3. fileprivate class SomeFilePrivateClass {}
4. private class SomePrivateClass {}
6. public var somePublicVariable = 0
7. internal let someInternalConstant = 0
8. fileprivate func someFilePrivateFunction() {}
9. private func somePrivateFunction() {}
除非另行指定,否则默认访问级别为内部,如默认访问级别中所述。这意味着,SomeInternalClass
和someInternalConstant
能够在没有明确的访问级别的修改写入,仍会有内部的访问级别:
1. class SomeInternalClass {} // implicitly internal
2. let someInternalConstant = 0 // implicitly internal
25.4 自定义类型
如果要为自定义类型指定显式访问级别,请在定义类型时执行此操作。然后可以在其访问级别允许的任何地方使用新类型。例如,如果定义文件专用类,则该类只能用作定义文件专用类的源文件中的属性类型,或者作为函数参数或返回类型。
类型的访问控制级别还会影响该类型成员的默认访问级别(其属性,方法,初始化器和下标)。如果将类型的访问级别定义为私有或文件专用,则其成员的默认访问级别也将为私有或文件专用。如果将类型的访问级别定义为内部或公共(或使用内部的默认访问级别而未明确指定访问级别),则类型成员的默认访问级别将是内部的。
重要
公共类型默认具有内部成员,而不是公共成员。如果你希望类型成员是公共的,则必须明确标记它。此要求可确保某个类型的面向公众的API是您选择发布的内容,并避免错误地将类型的内部工作方式显示为公共API。
public class SomePublicClass { // explicitly public class
public var somePublicProperty = 0 // explicitly public class member
var someInternalProperty = 0 // implicitly internal class member
fileprivate func someFilePrivateMethod() {} // explicitly file-private class member
private func somePrivateMethod() {} // explicitly private class member
}
class SomeInternalClass { // implicitly internal class
var someInternalProperty = 0 // implicitly internal class member
fileprivate func someFilePrivateMethod() {} // explicitly file-private class member
private func somePrivateMethod() {} // explicitly private class member
}
fileprivate class SomeFilePrivateClass { // explicitly file-private class
func someFilePrivateMethod() {} // implicitly file-private class member
private func somePrivateMethod() {} // explicitly private class member
}
private class SomePrivateClass { // explicitly private class
func somePrivateMethod() {} // implicitly private class member
}
25.4.1 元组类型
元组类型的访问级别是该元组中使用的所有类型的最严格的访问级别。例如,如果你从两种不同类型组成一个元组,一个具有内部访问权限,另一个具有私有访问权限,则该复合元组类型的访问级别将为私有。
注意
元组类型没有类,结构,枚举和函数的独立定义。使用元组类型时会自动推导出元组类型的访问级别,并且无法明确指定。
25.4.2 函数类型
函数类型的访问级别为函数参数类型和返回类型之中最严格的访问级别。如果函数的计算访问级别与上下文默认值不匹配,则必须明确指定访问级别作为函数定义的一部分。
下面的示例定义了一个名为someFunction()
的全局函数,但没有为函数本身提供特定的访问级别修饰符。你可能希望此函数具有默认的“内部”访问级别,但事实并非如此。实际上,someFunction()
不会通过编译:
1. func someFunction() -> (SomeInternalClass, SomePrivateClass) {
2. // function implementation goes here
3. }
函数的返回类型是一个元组类型,由两个自定义类型中定义的自定义类组成。其中一个类定义为内部,另一个定义为私有。因此,复合元组类型的整体访问级别是私有的(元组的组成类型的最小访问级别)。
因为函数的返回类型是私有的,所以必须使用private
函数声明的修饰符来标记函数的整体访问级别:
1. private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
2. // function implementation goes here
3. }
使用public
或使用默认的访问控制级别internal
修饰符标记someFunction()
是无效的,因为该函数的公共或内部用户可能没有对函数返回类型中使用的私有类的适当访问权限。
25.4.3 枚举类型
枚举的各个案例自动获得与其所属枚举相同的访问级别,无法为单个枚举案例指定不同的访问级别。
在下面的示例中,CompassPoint枚举具有显式的公共访问级别。枚举的情况下north,south,east,和west因此也有公共的访问级别:
public enum CompassPoint {
case north
case south
case east
case west
}
用于枚举定义中的任何原始值或关联值的类型必须具有至少与枚举的访问级别一样高的访问级别。例如,你不能将私有类型用作具有内部访问级别的枚举的原始值类型。
25.4.4 嵌套类型
私有类型中定义的嵌套类型具有私有的自动访问级别。在文件专用类型中定义的嵌套类型具有文件专用的自动访问级别。在公共类型或内部类型中定义的嵌套类型具有内部的自动访问级别。如果希望公共类型中的嵌套类型公开可用,则必须将嵌套类型显式声明为public。
25.5 子类
可以子类化当前访问上下文中可以访问的任何类。子类不能具有比其超类更高的访问级别 - 例如,不能编写内部超类的公共子类。
此外,可以覆盖在特定访问上下文中可见的任何类成员(方法,属性,初始化器或下标)。
重写可以使继承的类成员比其超类版本更易于访问。在下面的示例中,class A是一个名为file-private方法的公共类someMethod()。Class B是其子类A,具有降低的“内部”访问级别。尽管如此,class B提供了一个覆盖someMethod()访问级别为“internal” 的覆盖,它高于原始实现someMethod():
public class A {
fileprivate func someMethod() {}
}
internal class B: A {
override internal func someMethod() {}
}
它甚至对子类成员调用具有比子类成员更低访问权限的超类成员是有效的,只要对超类成员的调用发生在允许的访问级别上下文中(即,在与同一源文件相同的源文件中)用于文件私有成员调用的超类,或者与内部成员调用的超类在同一模块中):
public class A {
fileprivate func someMethod() {}
}
internal class B: A {
override internal func someMethod() {
//因为超类A和子类B是在同一个源文件中定义的,所以super.someMethod()调用是有效的。
super.someMethod()
}
}
25.6 常量,变量,属性和下标
常量,变量或属性不能比其类型更公开。例如,编写私有类型的公共属性是无效的。类似地,下标不能比其索引类型或返回类型更公开。
如果常量,变量,属性或下标使用私有类型,则常量,变量,属性或下标也必须标记为private:
private var privateInstance = SomePrivateClass()
常量,变量,属性和下标的getter和setter自动获得与它们所属的常量,变量,属性或下标相同的访问级别。
可以为setter提供比其对应的getter 更低的访问级别,以限制该变量,属性或下标的读写范围。通过编写分配较低访问级别fileprivate(set),private(set)或internal(set)前var或subscript引导。
注意
此规则适用于存储的属性以及计算的属性。即使您没有为存储的属性编写显式的getter和setter,Swift仍然会合成一个隐式的getter和setter,以便您提供对存储属性的后备存储的访问。使用fileprivate(set),, private(set)和internal(set)以与计算属性中的显式setter完全相同的方式更改此合成setter的访问级别。
下面的示例定义了一个名为的结构TrackedString,它跟踪字符串属性被修改的次数:
struct TrackedString {
private(set) var numberOfEdits = 0
var value: String = "" {
didSet {
numberOfEdits += 1
}
}
}
该TrackedString结构定义了一个名为value的存储字符串属性,其初始值为""(空字符串)。该结构还定义了一个名为的存储整数属性numberOfEdits,用于跟踪value修改的次数。此修改跟踪是通过didSet属性上的属性观察器实现的value,numberOfEdits每次将value属性设置为新值时,属性观察器都会递增。
在TrackedString结构和value性能不提供明确的访问级别的修正,所以他们都收到内部默认的访问级别。但是,numberOfEdits属性的访问级别标有一个private(set)修饰符,表示该属性的getter仍然具有内部的默认访问级别,但该属性只能从作为TrackedString结构一部分的代码中设置。这样可以TrackedString在numberOfEdits内部修改属性,但在属性在结构定义之外使用时,可以将属性显示为只读属性。
如果你创建一个TrackedString实例并多次修改其字符串值,则可以看到numberOfEdits属性值update以匹配修改次数:
var stringToEdit = TrackedString()
stringToEdit.value = "This string will be tracked."
stringToEdit.value += " This edit will increment numberOfEdits."
stringToEdit.value += " So will this one."
print("The number of edits is \(stringToEdit.numberOfEdits)")
// Prints "The number of edits is 3"
虽然你可以numberOfEdits从另一个源文件中查询属性的当前值,但您无法从其他源文件修改该属性。此限制可保护TrackedString编辑跟踪功能的实现细节,同时仍可方便地访问该功能的某个方面。
请注意,如果需要,您可以为getter和setter分配显式访问级别。下面的示例显示了TrackedString结构的一个版本,其中结构的显式访问级别为public。因此,结构的成员(包括numberOfEdits属性)默认具有内部访问级别。您可以numberOfEdits通过组合public和private(set)访问级别修饰符使结构的属性getter为public,其属性setter为private :
public struct TrackedString {
public private(set) var numberOfEdits = 0
public var value: String = "" {
didSet {
numberOfEdits += 1
}
}
public init() {}
}
25.7 初始化器
可以为自定义初始化器分配小于或等于它们初始化类型的访问级别。唯一的例外是必需的初始化器(在必需的初始化器中定义)。必需的初始化器必须具有与其所属类相同的访问级别。
与函数和方法参数一样,初始化程序参数的类型不能比初始化程序自己的访问级别更私密。
25.7.1 默认初始化程序
如在描述的默认初始值设定,Swift自动提供了一个默认初始值而无需任何结构或基类,对于其所有属性提供缺省值,并且不提供至少一个初始值设定本身的任何参数。
默认初始值设定项具有与其初始化类型相同的访问级别,除非该类型定义为public
。对于定义为的类型,public
默认初始值设定项被视为内部。如果希望在另一个模块中使用无参数初始化程序时可以初始化公共类型,则必须自己明确地提供公共无参数初始化程序作为类型定义的一部分。
25.7.2 结构类型的默认成员初始值设定项
如果结构的任何存储属性是私有的,则结构类型的默认成员初始值设定项将被视为私有。同样,如果任何结构的存储属性是文件专用的,则初始化程序是文件专用的。否则,初始化程序具有内部访问级别。
与上面的默认初始化程序一样,如果希望在另一个模块中使用成员初始化程序时可以初始化公共结构类型,则必须自己提供公共成员初始化程序作为类型定义的一部分。
25.8 协议
如果要为协议类型分配显式访问级别,请在定义协议时执行此操作。这使你可以创建只能在特定访问上下文中采用的协议。
协议定义中每个方法的访问级别自动设置为与协议相同的访问级别。你不能将协议方法设置为与其支持的协议不同的访问级别。这可确保在采用该协议的任何类型上都可以看到所有协议的要求。
注意
如果定义公共协议,则协议的要求在实施时需要公共访问级别。此行为与其他类型不同,其它公共类型定义意味着类型成员的内部访问级别。
25.8.1 协议继承
如果定义从现有协议继承的新协议,则新协议最多可以具有与其继承的协议相同的访问级别。例如,你无法编写从内部协议继承的公共协议。
25.8.2 协议一致性
类型可以符合访问级别低于类型本身的协议。例如,你可以定义可以在其他模块中使用的公共类型,但其与内部协议的一致性只能在内部协议的定义模块中使用。
类型符合特定协议的上下文是类型访问级别和协议访问级别的最小值。如果类型是公共类型,但它符合的协议是内部类型,则该类型与该协议的一致性也是内部的。
在编写或扩展类型以符合协议时,必须确保每个协议要求的类型实现至少具有与该协议类型一致的访问级别。例如,如果公共类型符合内部协议,则每个协议要求的类型实现必须至少是“内部”。
注意
在Swift中,与Objective-C一样,协议一致性是全局的 - 类型不可能在同一程序中以两种不同的方式符合协议。
25.9 扩展
可以在类,结构或枚举可用的任何访问上下文中扩展类,结构或枚举。扩展中添加的任何类型成员具有与要扩展的原始类型中声明的类型成员相同的默认访问级别。如果扩展公共或内部类型,则添加的任何新类型成员都具有内部的默认访问级别。如果扩展文件专用类型,则添加的任何新类型成员都具有文件专用的默认访问级别。如果扩展私有类型,则添加的任何新类型成员都具有私有的默认访问级别。
或者,可以使用显式访问级别修饰符标记扩展名(例如,),以便为扩展名中定义的所有成员设置新的默认访问级别。仍可以在单个类型成员的扩展中覆盖此新默认值。private extension
如果使用该扩展来添加协议一致性,则无法为扩展提供显式访问级别修饰符。相反,协议自身的访问级别用于为扩展中的每个协议要求实现提供默认访问级别。
25.9.1 扩展扩展中的私有
与它们扩展的类,结构或枚举位于同一文件中的扩展的行为就像扩展中的代码已写为原始类型声明的一部分一样。因此,你可以:
* 在原始声明中声明私有成员,并从同一文件中的扩展名访问该成员。
* 在一个扩展中声明一个私有成员,并从同一文件中的另一个扩展访问该成员。
* 在扩展中声明私有成员,并从同一文件中的原始声明中访问该成员。
此行为意味着你可以使用扩展以相同的方式组织代码,无论你的类型是否具有私有实体。例如,给出以下简单协议:
protocol SomeProtocol {
func doSomething()
}
你可以使用扩展来添加协议一致性,如下所示:
struct SomeStruct {
private var privateVariable = 12
}
extension SomeStruct: SomeProtocol {
func doSomething() {
print(privateVariable)
}
}
25.10 泛型
泛型类型或泛型函数的访问级别是泛型类型或函数本身的访问级别的最小值,以及对其类型参数的任何类型约束的访问级别。
25.11 别名
为了访问控制的目的,定义的任何类型别名都被视为不同类型。类型别名的访问级别可以小于或等于其别名类型的访问级别。例如,私有类型别名可以为私有,文件私有,内部,公共或开放类型设置别名,但公共类型别名不能为内部,文件私有或私有类别设置别名。