Skip to content

访问控制限制了从其他源文件和模块中的代码访问代码中部分内容。此功能使您可以隐藏代码的实现细节,并指定一个首选接口,通过该接口可以访问和使用该代码。

您可以为个体类型(类、结构体和枚举)以及这些类型的属性、方法、初始化器和下标分配特定的访问级别。协议可以限制在特定上下文中,就像全局常量、变量和函数一样。

除了提供各种级别的访问控制之外,Swift 还通过为典型场景提供默认访问级别来减少显式指定访问控制级别的需求。实际上,如果您正在编写单目标应用,可能根本不需要显式指定访问控制级别。

注意

可以应用访问控制的代码的各种方面(属性、类型、函数等)在下面的段落中简称为“实体”。

模块、源文件和包

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

  • 模块是一个单一的代码分发单元——一个框架或应用程序,作为单一单元构建和发布,并且可以通过 Swift 的关键字导入到另一个模块中。

    在 Xcode 中,每个构建目标(如应用程序包或框架)都被视为 Swift 中的一个单独模块。如果你将应用程序代码的某些方面组合成一个独立的框架——也许是为了封装和在多个应用程序中重用该代码——那么当你将该框架导入并用于应用程序,或用于另一个框架时,该框架中定义的一切都将属于一个单独的模块。

  • 源文件是模块内的单个 Swift 源代码文件(实际上,是应用程序或框架内的单个文件)。虽然通常会在单独的源文件中定义个别类型,但一个源文件可以包含多个类型的定义,例如函数等。

  • 是一组作为一个单元开发的模块。您在配置使用的构建系统时定义这些模块,而不是在 Swift 源代码中定义。例如,如果您使用 Swift 包管理器构建代码,您可以在文件中使用 PackageDescription 模块的 API 定义一个包;如果您使用 Xcode,则可以在包访问标识符构建设置中指定包名称。

访问级别

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

  • 开放访问公共访问允许实体在其定义模块内的任何源文件中使用,也可以在另一个模块的源文件中使用,该模块导入了定义模块。通常在指定框架的公共接口时会使用开放或公共访问。开放访问和公共访问之间的区别将在下面描述。

  • 包访问允许实体在其定义包内的任何源文件中使用,但不允许在包外的任何源文件中使用。通常在结构化为多个模块的应用或框架中使用包访问。

  • 内部访问允许实体在其定义模块内的任何源文件中使用,但不允许在模块外的任何源文件中使用。通常在定义应用或框架的内部结构时使用内部访问。

  • 文件私有访问限制实体仅在其自己的定义源文件中使用。使用文件私有访问来隐藏特定功能的实现细节,当这些细节在整个文件中使用时。

  • 私有访问限制实体仅在包含声明中使用,并且仅限于在同一文件中的该声明的扩展使用。使用私有访问来隐藏特定功能的实现细节,当这些细节仅在单个声明内部使用时。

开放访问是最高(最不限制)的访问级别,私有访问是最低(最限制)的访问级别。

开放访问仅适用于类和类成员,并且与公共访问不同,允许外部模块的代码对该类进行子类化和覆盖,如下面的子类化部分所述。将类标记为开放明确表示您已考虑了其他模块中的代码使用该类作为超类的影响,并且已相应地设计了该类的代码。

访问级别指导原则

Swift 中的访问级别遵循一个总体指导原则:不能用具有较低(更严格)访问级别的实体来定义另一个实体。

例如:

  • 一个公开的变量不能定义为具有内部、文件私有或私有类型,因为该类型可能在公开变量使用的所有地方不可用。
  • 函数不能比其参数类型和返回类型具有更高的访问级别,因为该函数可能在其组成部分类型对周围代码不可用的情况下被使用。

这一指导原则对语言不同方面的具体含义将在下面详细阐述。

默认访问级别

你的代码中的所有实体(少数特定例外情况除外,这些情况将在本章后面部分描述)如果没有你自己指定明确的访问级别,将默认为内部访问级别。因此,在许多情况下,你不需要在代码中明确指定访问级别。

单目标应用的访问级别

当你编写一个简单的单目标应用时,应用中的代码通常在应用内部自包含,不需要对外公开。默认的内部访问级别已经满足了这一需求。因此,你不需要指定自定义的访问级别。然而,你可能希望将代码的某些部分标记为文件私有或私有,以隐藏其实现细节,使其不被应用模块内的其他代码访问。

框架的访问级别

当你开发一个框架时,将框架面向外部的接口标记为公开或公共,以便其他模块,如导入该框架的应用程序,可以查看和访问它。这个面向外部的接口是框架的应用程序编程接口(或 API)。

注意

你的框架的内部实现细节仍然可以使用默认的内部访问级别,或者如果你想隐藏它们,可以标记为私有或文件私有。只有当你希望某个实体成为框架 API 的一部分时,才需要将其标记为公开或公共。

单元测试目标的访问级别

当您编写包含单元测试目标的应用程序时,需要使应用程序中的代码可供该模块访问以便进行测试。默认情况下,只有标记为公开或开放的实体可以被其他模块访问。但是,如果将产品模块的导入声明标记为具有该属性,并且以启用测试的方式编译该产品模块,则单元测试目标可以访问任何内部实体。 @testable

访问控制语法

定义实体的访问级别,请在实体声明的开头放置其中一个 open, public, internal, fileprivate, 或 private 修饰符。

swift
open class SomeOpenClass {}
public class SomePublicClass {}
internal class SomeInternalClass {}
fileprivate class SomeFilePrivateClass {}
private class SomePrivateClass {}


open var someOpenVariable = 0
public var somePublicVariable = 0
internal let someInternalConstant = 0
fileprivate func someFilePrivateFunction() {}
private func somePrivateFunction() {}

除非另有说明,默认访问级别为内部,如默认访问级别中所述。这意味着 SomeInternalClasssomeInternalConstant 可以不带显式的访问级别修饰符编写,并且仍然具有内部的访问级别:

swift
class SomeInternalClass {}              // implicitly internal
let someInternalConstant = 0            // implicitly internal

自定义类型

如果您想为自定义类型指定明确的访问级别,请在定义该类型时进行指定。新类型随后可以在其访问级别允许的任何地方使用。例如,如果您定义了一个文件私有类,那么该类只能用作属性类型、函数参数或返回类型的类型,在定义该文件私有类的源文件中。

类的访问控制级别还会影响该类成员(其属性、方法、初始化器和下标)的默认访问级别。如果您将类的访问级别定义为私有或文件私有,那么该类成员的默认访问级别也将是私有或文件私有。如果您将类的访问级别定义为内部或公开(或使用默认的内部访问级别而未显式指定访问级别),那么该类成员的默认访问级别将是内部。

重要

公开类型默认具有内部成员,而不是公开成员。如果你想让类型成员是公开的,必须明确地将其标记为公开。这一要求确保了类型面向公众的 API 是你主动选择发布的,并避免了无意中将类型内部的工作方式作为公开 API 呈现。

swift
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
}

元组类型

元组类型的访问级别是构成该元组的所有类型中最严格的访问级别。例如,如果你从两种不同类型的组合中构建一个元组,一种类型具有内部访问级别,另一种类型具有私有访问级别,那么该复合元组类型的访问级别将是私有。

注意

元组类型不像类、结构、枚举和函数那样具有独立的定义。元组类型的访问级别是根据构成元组类型的类型自动确定的,不能明确指定。

函数类型

函数类型的访问级别是根据函数参数类型和返回类型中最严格的访问级别计算得出的。如果函数计算出的访问级别与上下文默认值不符,您必须在函数定义中显式指定访问级别。

以下示例定义了一个全局函数,但没有为该函数提供具体的访问级别修饰符。您可能会期望该函数具有默认的“内部”访问级别,但这并不是实际情况。实际上,如下面所示的代码将无法编译: someFunction() someFunction()

swift
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // function implementation goes here
}

函数的返回类型是由上面在自定义类型中定义的两个自定义类组成的元组类型。其中一个类被定义为内部,另一个被定义为私有。因此,该复合元组类型的总体访问级别为私有(元组组成部分类型的最小访问级别)。

由于函数的返回类型为私有,您必须在函数声明中使用相应的修饰符来标记函数的总体访问级别,以使函数声明有效:

swift
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // function implementation goes here
}

不能使用 publicinternal 修饰符标记 someFunction() 的定义,也不能使用 internal 的默认设置,因为函数的 publicinternal 用户可能没有适当访问用于函数返回类型中的私有类。

枚举类型

枚举中的各个情况会自动继承所属枚举的访问级别。您不能为各个枚举情况指定不同的访问级别。

在下面的示例中,枚举具有显式的 public 访问级别。因此,枚举情况 CompassPoint north south east west 也具有 public 的访问级别:

swift
public enum CompassPoint {
    case north
    case south
    case east
    case west
}

原始值和关联值

任何枚举定义中的原始值或关联值所使用的类型,其访问级别必须至少与枚举的访问级别相同。例如,您不能使用私有类型作为具有内部访问级别的枚举的原始值类型。

嵌套类型

嵌套类型的访问级别与其包含类型的访问级别相同,除非包含类型的访问级别为公共。在公共类型内部定义的嵌套类型具有自动的内部访问级别。如果您希望公共类型内的嵌套类型对外公开,必须显式声明该嵌套类型为公共。

子类继承

您可以继承当前访问上下文中可以访问且定义在同一模块中的任何类的子类。您还可以继承其他模块中定义的任何开放类。子类的访问级别不能高于其父类的访问级别——例如,您不能编写一个公共的子类来继承一个内部的父类。

此外,在同一模块中定义的类中,可以覆盖在特定访问上下文中可见的任何类成员(方法、属性、初始化器或下标)。对于在另一个模块中定义的类,可以覆盖任何公开的类成员。

覆盖可以使其继承的类成员比其超类版本更易于访问。在下面的示例中,类 A 是一个公共类,包含一个文件私有方法 someMethod()。类 B 是类 A 的子类,具有较低的访问级别“内部”。然而,类 B 提供了对具有“内部”访问级别的 someMethod() 的覆盖,这高于原始实现:

swift
public class A {
    fileprivate func someMethod() {}
}


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

甚至在子类成员可以调用具有比子类成员更低访问权限的超类成员,只要调用超类成员的上下文在允许的访问级别范围内(即,对于文件私有成员调用,位于超类的同一源文件中;对于内部成员调用,位于超类的同一模块中):

swift
public class A {
    fileprivate func someMethod() {}
}


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

由于超类 A 和子类 B 定义在同一源文件中,因此 BsomeMethod() 实现可以对 super.someMethod() 进行调用。

访问控制

常量、变量、属性和下标

常量、变量或属性不能比其类型更公开。例如,写一个公开的属性但其类型是私有的是无效的。同样,下标也不能比其索引类型或返回类型更公开。

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

swift
private var privateInstance = SomePrivateClass()

获取和设置

常量、变量、属性和下标的 getter 和 setter 自动具有与它们所属的常量、变量、属性或下标相同的访问级别。

您可以给一个 setter 设置比其对应的 getter 更低的访问级别,以限制该变量、属性或下标的读写范围。您可以通过在 varsubscript 之前写入 fileprivate(set)private(set)internal(set)package(set) 来设置更低的访问级别。

注意

这个规则同样适用于存储属性和计算属性。尽管您不需要为存储属性显式编写 getter 和 setter,Swift 仍然会为您合成一个隐式的 getter 和 setter,以便访问存储属性的底层存储。您可以使用 fileprivate(set)private(set)internal(set)package(set) 以与计算属性中的显式 setter 相同的方式更改合成的 setter 的访问级别。

以下示例定义了一个名为 TrackedString 的结构,用于跟踪字符串属性被修改的次数:

swift
struct TrackedString {
    private(set) var numberOfEdits = 0
    var value: String = "" {
        didSet {
            numberOfEdits += 1
        }
    }
}

结构 TrackedString 定义了一个名为 value 的存储字符串属性,初始值为空字符串 ""。结构还定义了一个名为 numberOfEdits 的存储整数属性,用于跟踪该属性被修改的次数。该修改跟踪是通过属性上的 didSet 属性观察者实现的,每当 value 属性被设置为新值时,观察者会递增 numberOfEdits

TrackedString 结构和 value 属性没有显式访问级别修饰符,因此它们都接收默认的内部访问级别。然而,numberOfEdits 属性的访问级别带有 private(set) 修饰符,表示属性的获取器仍然具有默认的内部访问级别,但属性只能从结构代码的一部分中设置。这使得可以在结构内部修改 numberOfEdits 属性,但在结构定义之外使用时将其呈现为只读属性。

如果创建一个 TrackedString 实例并多次修改其字符串值,可以看到 numberOfEdits 属性值会更新以匹配修改次数:

swift
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 编辑跟踪功能的实现细节,同时仍然提供了对该功能方面方便的访问。

请注意,如果需要,您可以为获取器和设置器分配显式访问级别。下面的示例显示了一个 TrackedString 结构的版本,在该版本中,结构被定义为具有公共的显式访问级别。因此,结构的成员(包括 numberOfEdits 属性)默认具有内部访问级别。您可以使用访问级别修饰符组合将结构的 numberOfEdits 属性获取器设置为公共,将属性设置器设置为私有:

swift
public struct TrackedString {
    public private(set) var numberOfEdits = 0
    public var value: String = "" {
        didSet {
            numberOfEdits += 1
        }
    }
    public init() {}
}

初始化器

自定义初始化器的访问级别可以小于或等于它们初始化的类型。唯一的例外是必需的初始化器(如必需的初始化器中所定义的)。必需的初始化器必须具有与所属类相同的访问级别。

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

默认初始化器

如默认初始化器一节所述,Swift 会为所有提供所有属性默认值且不提供任何初始化器的结构或基类自动提供一个不带参数的默认初始化器。

默认初始化器的访问级别与其初始化的类型相同,除非该类型定义为 public。对于定义为 public 的类型,默认初始化器被视为内部。如果您希望在其他模块中使用时,一个公共类型可以使用无参数的初始化器进行初始化,您必须在该类型的定义中显式地提供一个公共的无参数初始化器。

结构类型默认成员初始化器

如果结构体中的任何存储属性是私有的,那么该结构体的默认成员初始化器将被视为私有的。同样地,如果结构体中的任何存储属性是文件私有的,初始化器也将是文件私有的。否则,初始化器的访问级别为内部。

就像上面的默认初始化器一样,如果你想让另一个模块中的公共结构体类型能够使用成员初始化器进行初始化,你必须在类型定义中提供一个公共的成员初始化器。

协议

如果你想为协议类型指定显式的访问级别,那么你必须在定义协议时进行指定。这使得你可以创建只能在特定访问上下文中采用的协议。

协议定义中的每个要求的访问级别将自动设置为与协议相同的访问级别。你不能将协议要求设置为与支持它的协议不同的访问级别。这确保了协议的所有要求在采用该协议的任何类型中都是可见的。

注意

如果您定义了一个公开协议,那么在实现这些要求时,协议的要求需要具有公开的访问级别。这种行为与其他类型不同,在其他类型中,公开类型定义意味着该类型的成员具有内部访问级别。

协议继承

如果您定义了一个新的协议并继承了一个现有的协议,新的协议的访问级别最多与它继承的协议相同。例如,您不能编写一个公开协议并使其继承一个内部协议。

协议符合性

一种类型可以遵循比该类型本身具有更低访问级别的协议。例如,您可以定义一个公开类型,该类型可以在其他模块中使用,但该类型对内部协议的遵循只能在内部协议定义的模块中使用。

类型符合特定协议的上下文是该类型的访问级别和协议的访问级别的最小值。例如,如果一个类型是公开的,但其符合的协议是内部的,那么该类型对该协议的符合性也是内部的。

当你为类型编写或扩展使其符合协议时,必须确保该类型对每个协议要求的实现至少具有与该类型对该协议的符合性相同的访问级别。例如,如果一个公开的类型符合一个内部的协议,那么该类型对每个协议要求的实现至少必须是内部的。

注意

在 Swift 中,就像在 Objective-C 中一样,协议符合性是全局的——在同一个程序中,一个类型不能以两种不同的方式符合一个协议。

扩展

您可以在任何可以访问类、结构或枚举的访问上下文中扩展类、结构或枚举。在扩展中添加的任何类型成员都具有与原始扩展类型中声明的类型成员相同的默认访问级别。如果扩展的是公共或内部类型,您添加的新类型成员的默认访问级别为内部。如果扩展的是文件私有类型,您添加的新类型成员的默认访问级别为文件私有。如果扩展的是私有类型,您添加的新类型成员的默认访问级别为私有。

或者,您可以使用显式的访问级别修饰符(例如 private)标记扩展,以设置扩展中所有成员的新默认访问级别。扩展中对个别类型成员的访问级别仍然可以被覆盖。

如果您使用扩展来添加协议遵从性,则不能为该扩展显式指定访问级别修饰符。相反,扩展中每个协议要求实现的默认访问级别将使用协议本身的访问级别来提供。

扩展中的私有成员

如果扩展与它们所扩展的类、结构或枚举位于同一个文件中,则扩展中的代码行为就像扩展中的代码是原始类型声明的一部分一样。因此,您可以:

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

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

swift
protocol SomeProtocol {
    func doSomething()
}

你可以使用一个扩展来添加协议遵循,就像这样:

swift
struct SomeStruct {
    private var privateVariable = 12
}

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

通配符

通用类型或通用函数的访问级别是该通用类型或函数本身和其类型参数的任何类型约束的访问级别的最小值。

类型别名

你定义的任何类型别名在访问控制方面被视为不同的类型。类型别名的访问级别可以小于或等于它所别名的类型。例如,一个私有类型别名可以别名一个私有、文件私有、内部、公开或开放类型,但一个公开类型别名不能别名一个内部、文件私有或私有类型。

注意

这个规则也适用于用于满足协议遵从性的关联类型别名。