2.28 访问控制

原文链接: https://docs.swift.org/latest/documentation/the-swift-programming-language/accesscontrol/

2.28 访问控制

按声明、文件和模块管理代码的可见性。

访问控制限制其他源文件和模块中的代码对代码某些部分的访问。这一特性让你能够隐藏代码的实现细节,并指定访问和使用这些代码所经由的首选接口。

你可以为单个类型(类、结构体和枚举)指定特定的访问级别,也可以为这些类型的属性、方法、构造器和下标指定访问级别。协议可以被限制在特定上下文中使用,全局常量、变量和函数也是如此。

除了提供多种访问控制级别,Swift 还为典型场景提供默认访问级别,从而减少显式指定访问控制级别的需要。事实上,如果你写的是单目标应用,可能根本不需要显式指定访问控制级别。

注意:下文为了简洁,把你代码中各种可以施加访问控制的对象 (属性、类型、函数等) 统称为"实体"。

模块、源文件与包

Swift 的访问控制模型基于模块、源文件和包这几个概念。

模块是代码分发的单一单元——一个作为单一单元构建和发布的框架或应用,可以被另一个模块用 Swift 的 import 关键字导入。

Xcode 中的每个构建目标(例如应用包或框架)在 Swift 中都被视为一个独立的模块。如果你把应用代码的某些部分组织成一个独立框架——也许是为了封装并在多个应用中复用这些代码——那么在该框架中定义的一切,在被导入到应用或其他框架中使用时,都属于一个独立的模块。

源文件是模块中的单个 Swift 源代码文件(实际上就是应用或框架中的单个文件)。虽然通常把各个类型定义在不同的源文件中,但单个源文件也可以包含多个类型、函数等的定义。

包是作为一个单元来开发的一组模块。构成包的模块是在配置所用构建系统时定义的,而不是在 Swift 源代码中定义的。例如,如果你用 Swift Package Manager 构建代码,就在 Package.swift 文件中使用 PackageDescription 模块的 API 定义包;如果你使用 Xcode,就在 Package Access Identifier 构建设置中指定包名。

访问级别

Swift 为代码中的实体提供了六种不同的访问级别。这些访问级别是相对于定义实体的源文件、该源文件所属的模块,以及该模块所属的包而言的。

  • open 访问和public 访问让实体可以在其定义模块的任意源文件中使用,也可以在导入该定义模块的其他模块的源文件中使用。为框架指定公开接口时,通常使用 open 或 public 访问。open 与 public 访问的区别见下文。
  • package 访问让实体可以在其定义包内的任意源文件中使用,但不能在该包之外的任何源文件中使用。包访问通常用在由多个模块构成的应用或框架中。
  • internal 访问让实体可以在其定义模块的任意源文件中使用,但不能在该模块之外的任何源文件中使用。internal 访问通常用于定义应用或框架的内部结构。
  • file-private 访问把实体的使用限制在它自己所在的源文件内。当某项功能的实现细节会在整个文件内被使用时,用 file-private 访问把这些细节隐藏起来。
  • private 访问把实体的使用限制在包含它的那个声明,以及同一文件中该声明的扩展内。当某项功能的实现细节只在单个声明内使用时,用 private 访问把这些细节隐藏起来。

open 访问是最高(限制最少)的访问级别,private 访问是最低(限制最多)的访问级别。

open 访问只适用于类和类的成员,它与 public 访问的区别在于允许模块之外的代码继承和覆盖,详见下文的子类化。把类标记为 open,明确表示你已经考虑过其他模块把该类用作父类会产生的影响,并且据此设计了类的代码。

访问级别的指导原则

Swift 中的访问级别遵循一条总体指导原则:任何实体都不能用访问级别更低(限制更多)的另一个实体来定义。

例如:

  • public 变量不能被定义为具有 internal、file-private 或 private 的类型,因为该类型未必在使用这个 public 变量的所有地方都可用。
  • 函数的访问级别不能高于它的参数类型和返回类型的访问级别,因为函数可能在其组成部分类型对周围代码不可用的场景中被使用。

这条指导原则对语言各个方面的具体影响,下文会有详细介绍。

默认访问级别

如果你不自己指定显式访问级别,代码中的所有实体(本章稍后会说明少数特定例外)默认的访问级别都是 internal。因此在很多情况下,你不需要在代码中指定显式访问级别。

单目标应用的访问级别

当你编写简单的单目标应用时,应用中的代码通常自包含在应用内部,不需要在应用的模块之外提供。默认的 internal 访问级别已经符合这一要求,因此你不需要指定自定义访问级别。不过,你可能想把代码的某些部分标记为 file private 或 private,以便对应用模块中的其他代码隐藏它们的实现细节。

框架的访问级别

开发框架时,把该框架面向公众的接口标记为 open 或 public,从而让它能被其他模块(例如导入该框架的应用)查看和访问。这个面向公众的接口就是框架的应用编程接口(API)。

注意:框架内部的实现细节仍然可以使用默认的 internal 访问级别, 或者如果你想对框架内部的其他代码隐藏它们, 也可以标记为 private 或 file private。 只有当你希望某个实体成为框架 API 的一部分时, 才需要把它标记为 open 或 public。

单元测试目标的访问级别

当你编写带单元测试目标的应用时,应用中的代码需要提供给该测试模块才能被测试。默认情况下,只有标记为 open 或 public 的实体才能被其他模块访问。不过,只要用 @testable 特性标记产品模块的 import 声明,并在启用测试的情况下编译该产品模块,单元测试目标就可以访问任何 internal 实体。

访问控制语法

要定义实体的访问级别,就在实体声明开头放置访问级别中列出的某个修饰符,例如 public 或 private。例如:

1
2
3
public class SomePublicClass {}
internal struct SomeInternalStruct() {}
private func somePrivateFunction() {}

上面的代码把 SomePublicClass 声明为 public,把 SomeInternalStruct 声明为 internal,把 somePrivateFunction() 声明为 private。如果你不写显式访问级别,默认的访问级别修饰符就是 internal,详见默认访问级别。例如在下面的代码中,SomeInternalStruct 隐式为 internal:

1
struct SomeInternalStruct() {}

自定义类型

如果你想为自定义类型指定显式访问级别,就在定义该类型时指定。此后该新类型就可以在它的访问级别允许的任何地方使用。例如,如果你定义一个 file-private 类,那么该类只能在其定义所在的源文件中用作属性的类型、函数的参数类型或返回类型。

类型的访问控制级别还会影响该类型成员(属性、方法、构造器和下标)的默认访问级别。如果你把类型的访问级别定义为 private 或 file private,其成员的默认访问级别也会是 private 或 file private;如果你把类型的访问级别定义为 internal 或 public(或者不显式指定、使用默认的 internal),其成员的默认访问级别就是 internal。

重要:public 类型默认拥有 internal 成员,而不是 public 成员。 如果你希望某个类型成员是 public,必须显式把它标记为 public。 这一要求确保类型的公开 API 是你主动选择发布的, 也避免误把类型的内部运作当作公开 API 呈现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class SomePublicClass {                   // 显式 public 类
    public var somePublicProperty = 0            // 显式 public 类成员
    var someInternalProperty = 0                 // 隐式 internal 类成员
    fileprivate func someFilePrivateMethod() {}  // 显式 file-private 类成员
    private func somePrivateMethod() {}          // 显式 private 类成员
}

class SomeInternalClass {                        // 隐式 internal 类
    var someInternalProperty = 0                 // 隐式 internal 类成员
    fileprivate func someFilePrivateMethod() {}  // 显式 file-private 类成员
    private func somePrivateMethod() {}          // 显式 private 类成员
}

fileprivate class SomeFilePrivateClass {         // 显式 file-private 类
    func someFilePrivateMethod() {}              // 隐式 file-private 类成员
    private func somePrivateMethod() {}          // 显式 private 类成员
}

private class SomePrivateClass {                 // 显式 private 类
    func somePrivateMethod() {}                  // 隐式 private 类成员
}

元组类型

元组类型的访问级别是该元组中所有类型访问级别中最严格的那个。例如,如果你的元组由两个不同类型组成,一个是 internal 访问,另一个是 private 访问,那么这个复合元组类型的访问级别就是 private。

注意:元组类型不像类、结构体、枚举和函数那样有独立的定义。 元组类型的访问级别由构成该元组类型的各个类型自动决定, 不能显式指定。

函数类型

函数类型的访问级别按该函数参数类型和返回类型中最严格的访问级别计算。如果函数计算出的访问级别与上下文默认值不一致,你必须在函数定义中显式指定访问级别。

下面的例子定义了一个名为 someFunction() 的全局函数,但没有为该函数本身提供具体的访问级别修饰符。你可能会以为这个函数具有默认的 internal 访问级别,但事实并非如此——实际上,下面这样写的 someFunction() 无法通过编译:

1
2
3
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // 函数实现写在这里
}

这个函数的返回类型是由上面自定义类型中定义的两个自定义类构成的元组类型,其中一个是 internal,另一个是 private。因此这个复合元组类型的整体访问级别是 private(也就是元组各组成部分类型中最低的访问级别)。

因为函数的返回类型是 private,你必须用 private 修饰符标记该函数的整体访问级别,函数声明才有效:

1
2
3
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // 函数实现写在这里
}

用 public 或 internal 修饰符标记 someFunction() 的定义,或者使用默认的 internal 设置,都是无效的,因为该函数的 public 或 internal 使用者可能没有适当权限访问函数返回类型中使用的 private 类。

枚举类型

枚举的各个成员自动获得与它们所属枚举相同的访问级别,你不能为单个枚举成员指定不同的访问级别。

在下面的例子里,CompassPoint 枚举的显式访问级别是 public,因此枚举成员 north、south、east 和 west 的访问级别也是 public:

1
2
3
4
5
6
public enum CompassPoint {
    case north
    case south
    case east
    case west
}

原始值与关联值

枚举定义中用作原始值或关联值的类型,其访问级别必须至少与枚举自身的访问级别一样高。例如,你不能把一个 private 类型用作 internal 访问级别枚举的原始值类型。

嵌套类型

嵌套类型的访问级别与包含它的类型相同,除非包含它的类型是 public。定义在 public 类型内部的嵌套类型,自动拥有 internal 访问级别。如果你希望 public 类型中的嵌套类型可以公开使用,必须显式把该嵌套类型声明为 public。

子类化

任何在当前访问上下文中可访问、且与子类定义在同一模块中的类,都可以被继承;定义在其他模块中的任何 open 类也可以被继承。子类的访问级别不能高于父类——例如,你不能给 internal 的父类写一个 public 的子类。

此外,对于定义在同一模块中的类,你可以覆盖在某个访问上下文中可见的任何类成员(方法、属性、构造器或下标);对于定义在另一个模块中的类,你可以覆盖任何 open 类成员。

覆盖可以让继承来的类成员比父类版本更易于访问。在下面的例子里,类 A 是 public 类,带有一个名为 someMethod() 的 file-private 方法;类 B 是 A 的子类,访问级别降为 internal。尽管如此,类 B 为 someMethod() 提供的覆盖具有 internal 访问级别,高于 someMethod() 的原始实现:

1
2
3
4
5
6
7
public class A {
    fileprivate func someMethod() {}
}

internal class B: A {
    override internal func someMethod() {}
}

子类成员甚至可以调用访问权限比它更低的父类成员,只要对父类成员的调用发生在允许的访问级别上下文中(也就是说,调用 file-private 成员时要在与父类相同的源文件内,调用 internal 成员时要在与父类相同的模块内):

1
2
3
4
5
6
7
8
9
public class A {
    fileprivate func someMethod() {}
}

internal class B: A {
    override internal func someMethod() {
        super.someMethod()
    }
}

由于父类 A 和子类 B 定义在同一个源文件中,B 中对 someMethod() 的实现调用 super.someMethod() 是有效的。

常量、变量、属性和下标

常量、变量或属性的公开程度不能高于它的类型。例如,写一个类型为 private 的 public 属性是无效的。同样,下标的公开程度也不能高于它的索引类型或返回类型。

如果常量、变量、属性或下标使用了 private 类型,那么该常量、变量、属性或下标也必须标记为 private:

1
private var privateInstance = SomePrivateClass()

取值器与设值器

常量、变量、属性和下标的取值器与设值器自动获得与它们所属的常量、变量、属性或下标相同的访问级别。

你可以让设值器拥有比对应取值器更低的访问级别,从而限制该变量、属性或下标的读写范围。要指定更低的访问级别,就在 var 或 subscript 引入关键字之前写 fileprivate(set)、private(set)、internal(set) 或 package(set)。

注意:这条规则对存储属性和计算属性都适用。 即使你没有为存储属性显式写出取值器和设值器, Swift 仍会为你合成隐式的取值器和设值器, 以提供对该存储属性底层存储的访问。 用 fileprivate(set)、private(set)、internal(set) 和 package(set) 修改这个合成设值器的访问级别, 其方式与计算属性中显式写出的设值器完全相同。

下面的例子定义了一个名为 TrackedString 的结构体,用来统计某个字符串属性被修改了多少次:

1
2
3
4
5
6
7
8
struct TrackedString {
    private(set) var numberOfEdits = 0
    var value: String = "" {
        didSet {
            numberOfEdits += 1
        }
    }
}

TrackedString 结构体定义了一个名为 value 的存储字符串属性,初始值为 ""(空字符串);还定义了一个名为 numberOfEdits 的存储整数属性,用来跟踪 value 被修改的次数。这一跟踪功能通过 value 属性上的 didSet 属性观察器实现,每当 value 属性被设为新值时就把 numberOfEdits 加一。

TrackedString 结构体和 value 属性都没有提供显式访问级别修饰符,因此它们都获得默认的 internal 访问级别。不过 numberOfEdits 属性用 private(set) 修饰符标记了访问级别,表示该属性的取值器仍然具有默认的 internal 访问级别,但该属性只能在属于 TrackedString 结构体的代码内部被设置。这让 TrackedString 能够在内部修改 numberOfEdits 属性,而在结构体定义之外使用时把它呈现为只读属性。

如果你创建一个 TrackedString 实例并修改它的字符串值几次,就能看到 numberOfEdits 属性的值更新为修改次数:

1
2
3
4
5
6
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)")
// 输出 "The number of edits is 3"。

虽然你可以在另一个源文件中查询 numberOfEdits 属性的当前值,但不能在另一个源文件中修改这个属性。这一限制保护了 TrackedString 编辑跟踪功能的实现细节,同时仍然方便地公开了该功能的一个方面。

注意,如果需要,你可以同时为取值器和设值器指定显式访问级别。下面的例子展示了 TrackedString 结构体的另一个版本,其中结构体被定义为显式 public 访问级别,因此它的成员(包括 numberOfEdits 属性)默认拥有 internal 访问级别。你可以把该结构体 numberOfEdits 属性的取值器设为 public、设值器设为 private,只需把 public 和 private(set) 两个访问级别修饰符组合起来:

1
2
3
4
5
6
7
8
9
public struct TrackedString {
    public private(set) var numberOfEdits = 0
    public var value: String = "" {
        didSet {
            numberOfEdits += 1
        }
    }
    public init() {}
}

构造器

自定义构造器可以被指定的访问级别只能小于或等于它所初始化的类型。唯一的例外是必需构造器(定义见必需构造器),必需构造器必须与其所属类的访问级别相同。

与函数和方法的参数一样,构造器参数的类型不能比构造器自身的访问级别更私有。

默认构造器

如默认构造器所述,对于为所有属性都提供了默认值、且自身没有提供至少一个构造器的任何结构体或基类,Swift 会自动提供一个不带任何参数的默认构造器。

默认构造器的访问级别与它所初始化的类型相同,除非该类型定义为 public。对于定义为 public 的类型,默认构造器被视为 internal。如果你希望 public 类型在其他模块中使用时能用无参数构造器初始化,就必须在这个类型定义中自己提供一个 public 的无参数构造器。

结构体类型的默认逐成员构造器

如果结构体的任何存储属性是 private,那么该结构体类型的默认逐成员构造器就被视为 private;同样,如果任何存储属性是 file private,该构造器就是 file private;其他情况下,该构造器的访问级别是 internal。

与上面的默认构造器一样,如果你希望 public 结构体类型在其他模块中使用时能用逐成员构造器初始化,就必须在这个类型定义中自己提供一个 public 的逐成员构造器。

协议

如果你想为协议类型指定显式访问级别,就在定义该协议时指定。这让你能够创建只能在特定访问上下文中被采纳的协议。

协议定义中每条要求的访问级别都自动设为与协议相同的访问级别。你不能把协议要求的访问级别设为与它所支持的协议不同,这保证了采纳该协议的任何类型上都能看到该协议的所有要求。

注意:如果你定义一个 public 协议, 该协议的各项要求在实现时需要具有 public 访问级别。 这一行为与其他类型不同: 对其他类型来说,public 的类型定义 意味着其成员的访问级别是 internal。

协议继承

如果你定义的新协议继承自已有协议,新协议的访问级别最多只能与它所继承的协议相同。例如,你不能写一个继承自 internal 协议的 public 协议。

协议遵循性

类型可以遵循一个访问级别比它自身更低的协议。例如,你可以定义一个可在其他模块中使用的 public 类型,但它对某个 internal 协议的遵循只能在该 internal 协议的定义模块内使用。

类型遵循某个特定协议的上下文,取该类型访问级别与该协议访问级别中的最小值。例如,如果某个类型是 public,而它遵循的某个协议是 internal,那么该类型对该协议的遵循也是 internal。

当你编写或扩展某个类型使其遵循某个协议时,必须确保该类型对每条协议要求的实现,至少具有与"该类型对该协议的遵循"相同的访问级别。例如,如果 public 类型遵循 internal 协议,那么该类型对每条协议要求的实现至少必须是 internal。

注意:和在 Objective-C 中一样,Swift 中的协议遵循是全局的—— 同一个程序中,类型不可能以两种不同的方式遵循同一个协议。

扩展

你可以在该类、结构体或枚举可用的任何访问上下文中扩展它。扩展中添加的任何类型成员,其默认访问级别与被扩展类型原始声明中声明的类型成员相同:如果你扩展 public 或 internal 类型,添加的新类型成员默认访问级别是 internal;如果你扩展 file-private 类型,添加的新类型成员默认访问级别是 file private;如果你扩展 private 类型,添加的新类型成员默认访问级别是 private。

另一种做法是给扩展加上显式的访问级别修饰符(例如 private),为扩展内定义的所有成员设置新的默认访问级别。这个新默认值仍然可以在扩展内为单个类型成员覆盖。

如果你用扩展来添加协议遵循性,就不能为该扩展提供显式的访问级别修饰符;此时使用协议自身的访问级别,为扩展内每条协议要求的实现提供默认访问级别。

扩展中的私有成员

与被扩展的类、结构体或枚举位于同一文件中的扩展,其行为就像扩展中的代码是写在原类型声明中一样。因此,你可以:

  • 在原声明中声明 private 成员,并在同一文件的扩展中访问该成员。
  • 在一个扩展中声明 private 成员,并在同一文件的另一个扩展中访问该成员。
  • 在扩展中声明 private 成员,并在同一文件的原声明中访问该成员。

这一行为意味着,无论你的类型是否有私有实体,你都可以用同样的方式用扩展来组织代码。例如,给定下面这个简单协议:

1
2
3
protocol SomeProtocol {
    func doSomething()
}

你可以用扩展添加协议遵循性,如下所示:

1
2
3
4
5
6
7
8
9
struct SomeStruct {
    private var privateVariable = 12
}

extension SomeStruct: SomeProtocol {
    func doSomething() {
        print(privateVariable)
    }
}

泛型

泛型类型或泛型函数的访问级别,取该泛型类型或函数自身的访问级别与其类型参数上任何类型约束的访问级别中的最小值。

类型别名

就访问控制而言,你定义的任何类型别名都被视为独立的类型。类型别名的访问级别可以小于或等于它所别名类型的访问级别:例如,private 的类型别名可以别名 private、file-private、internal、public 或 open 类型,但 public 的类型别名不能别名 internal、file-private 或 private 类型。

注意:这条规则同样适用于为满足协议遵循性而使用的关联类型别名。