25 Access Control 访问控制

访问控制限制从其他源文件和模块中的代码访问代码的部分。该特性使您能够隐藏代码的实现细节,并指定可以访问和使用代码的首选接口。

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

除了提供不同级别的访问控制,Swift还通过为典型场景提供默认访问级别,减少了指定显式访问控制级别的需要。实际上,如果您正在编写一个单个 target 应用程序,您可能根本不需要指定显式的访问控制级别。

为了简单起见,下面几节将代码的各个方面(属性、类型、函数等等)称为“实体entities”。

Modules and Source Files 模块和源文件

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

Module是一个代码分发的单一单元——a framework or application作为一个单元构建和发布,并且可以由另一个模块使用Swift的import关键字导入。

Xcode中的每个构建目标(如 app bundle 和 framework)在Swift中被视为一个单独的模块。如果您将应用程序代码的各个方面作为一个独立的框架组合在一起——可能是为了在多个应用程序之间封装和重用这些代码——那么当您在该框架中定义的所有内容在应用程序中导入和使用时,或者在另一个框架中使用时,都将是一个单独模块的一部分。

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

Access Levels 访问等级

Swift为代码中的实体提供了五种不同的访问级别。
这些访问级别相对于定义实体的源文件,也相对于源文件所属的模块。

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

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

  • File-private 文件私有访问将实体的使用限制为它自己定义的源文件。当在整个文件中使用特定功能的实现细节时,使用file-private访问隐藏这些细节。

  • Private 私有访问将实体的使用限制为所包含的声明,以及对同一文件中声明的扩展名的使用。当仅在单个声明中使用特定功能块的实现细节时,使用私有访问隐藏这些细节。

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

open 访问只适用于类和类成员,它与 public 访问的区别如下:

  • 具有 public 访问或任何更严格访问级别的类只能在定义它们的模块中子类化。
  • 具有 public 访问权限的类成员,或任何具有更严格访问级别的类成员,只能由定义它们的模块中的子类覆盖。
  • open 类可以在定义它们的模块中子类化,也可以在导入它们定义的模块的任何模块中子类化。
  • open 类成员可以被定义它们的模块中的子类覆盖,也可以被导入定义它们的模块的任何模块覆盖。

将一个类显式地标记为open表明您已经考虑了使用该类作为超类的其他模块的代码的影响,并且您已经相应地设计了类的代码。

Guiding Principle of Access Levels 访问级别指导原则

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

例如:

  • 不能将 public 变量定义为具有internal、file-private或private 类型,因为该类型可能在使用 public 变量的所有地方都不可用。
  • 函数的访问级别不能高于其参数类型和返回类型,因为函数可以用于其组成类型对周围代码不可用的情况。

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

Default Access Levels 默认的访问级别

如果您自己没有指定显式的访问级别,那么代码中的所有实体(除了少数特定的例外情况,如本章后面所述)都有一个默认的internal 访问级别。因此,在许多情况下,您不需要在代码中指定显式的访问级别。

Access Levels for Single-Target Apps 单个target的App访问级别

当您编写一个简单的单目标应用程序时,应用程序中的代码通常是自包含在应用程序内的,并且不需要在应用程序的模块之外可用。默认访问级别( internal )已经匹配此要求。因此,您不需要指定自定义访问级别。但是,您可能希望将代码的某些部分标记为file private或private,以便对应用程序模块中的其他代码隐藏它们的实现细节。

Access Levels for Frameworks 框架访问级别

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

注意:框架的任何内部实现细节仍然可以使用默认的内部访问级别,或者如果您想将它们隐藏在框架内部代码的其他部分中,可以将它们标记为私有或文件私有。只有在希望实体成为框架API的一部分时,才需要将其标记为open或public。

Access Levels for Unit Test Targets 单元测试target的访问级别

当您编写带有单元测试目标的应用程序时,应用程序中的代码需要对该模块可用,以便进行测试。默认情况下,其他模块只能访问标记为open或public的实体。但是,如果您使用@属性标记产品模块的导入声明,并在启用测试的情况下编译该产品模块,则单元测试目标可以访问任何内部实体。

Access Control Syntax 访问控制语法

定义实体的访问级别,方法是在实体的引入者之前放置一个open, public, internal, fileprivate, or private 修饰符:

public class SomePublicClass {}
internal class SomeInternalClass {}
fileprivate class SomeFilePrivateClass {}
private class SomePrivateClass {}

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

除非另外指定,否则默认访问级别是 internal,这意味着SomeInternalClass和someInternalConstant可以在没有显式访问级别修饰符的情况下编写,并且仍然具有internal的访问级别:

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

Custom Types 自定义类型

如果希望为自定义类型指定显式访问级别,请在定义该类型时指定。然后,只要访问级别允许,就可以使用新类型。例如,如果定义了文件私有类,该类只能用作定义文件私有类的源文件中的属性类型、函数参数或返回类型。

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

重要的:public 类型默认拥有 internal 成员,而不是 public 成员。如果希望类型成员为公共,则必须显式地将其标记为公共。这一要求确保类型的面向公共的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
}

Tuple Types 元组类型

元组类型的访问级别是该元组中使用的所有类型中最受限制的访问级别。例如,如果您使用两种不同的类型(一种具有内部访问,另一种具有私有访问)组合一个元组,那么该复合元组类型的访问级别将是私有的。

元组类型不像类、结构、枚举和函数那样具有独立的定义。当使用元组类型时,将自动推断出该元组类型的访问级别,并且不能显式指定。

Function Types 函数类型

函数类型的访问级别计算为函数参数类型和返回类型中最受限制的访问级别。如果函数计算的访问级别与上下文默认值不匹配,则必须将访问级别显式指定为函数定义的一部分。

下面的示例定义了一个名为someFunction()的全局函数,但没有为函数本身提供特定的访问级别修饰符。您可能期望这个函数的默认访问级别为“internal”,但事实并非如此。事实上,someFunction()不会像下面这样编译:

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

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

因为函数的返回类型是私有的,所以必须用私有修饰符标记函数的整体访问级别,这样函数声明才有效:

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

使用 public 或者 iternal 修饰符标记someFunction()的定义,或者使用默认访问,都是无效的,因为函数的公共或内部用户可能没有适当的访问函数返回类型中使用的私有类的权限。

Enumeration Types 枚举类型

枚举的各个用例自动接收与其所属枚举相同的访问级别。不能为个别枚举用例指定不同的访问级别。

在下面的示例中,CompassPoint枚举具有显式的public访问级别。因此,北、南、东和西的枚举案例也具有公共访问级别:

public enum CompassPoint {
    case north
    case south
    case east
    case west
}
原始值和关联值

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

Nested Types 嵌套类型

在private 类型中定义的嵌套类型具有私有的自动访问级别。
在file-private类型中定义的嵌套类型具有文件私有的自动访问级别。
在公共类型或内部类型中的嵌套类型具有内部的自动访问级别。如果希望公共类型中的嵌套类型公开可用,则必须显式地将嵌套类型声明为public。

Subclassing 子类化

您可以将当前访问上下文中可访问的任何类子类化。
子类不能具有比超类更高的访问级别——例如,不能给 internal 超类编写 public 子类。

此外,您可以覆盖在特定访问上下文中可见的任何类成员(方法、属性、初始化器或下标)。

override 可以使继承的类成员比其超类版本更容易访问。在下面的示例中,类A是一个公共类,带有一个名为someMethod()的文件私有方法。类B是a的子类,具有较低的“内部”访问级别。尽管如此,B类提供了一个覆盖someMethod()的访问级别为“internal”,这比someMethod()的原始实现要高:

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

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

甚至一个子类成员调用访问级别更低的超类成员也是有效的,
只要调用超类的成员发生在一个允许访问级别上下文(也就是说,在同一个源文件作为file-private成员调用超类,或在同一个模块中调用)作为一个内部的超类成员:

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

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

因为超类A和子类B定义在同一个源文件中,所以someMethod()的B实现可以调用super.someMethod()。

Constants, Variables, Properties, and Subscripts 常量,变量,属性,下标

常量、变量或属性不能比其类型更公开。例如,用私有类型编写公共属性是无效的。类似地,下标不能比其索引类型或返回类型更公开。

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

private var privateInstance = SomePrivateClass()

Getters and Setters

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

您可以为setter提供比其相应的getter更低的访问级别,以限制该变量、属性或下标的读写范围。通过在var或下标导入器之前编写fileprivate(set)、private(set)或internal(set)来分配较低的访问级别。

此规则适用于存储属性和计算属性。即使您没有为存储属性编写显式的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的存储字符串属性,其初始值为“”(一个空字符串)。该结构还定义了一个名为numberofe的存储整数属性,用于跟踪该值被修改的次数。这种修改跟踪是由value属性上的didSet属性观察者实现的,每当value属性被设置为一个新值时,didSet属性观察者就会增加看板的数量。

TrackedString结构和value属性不提供显式的访问级别修饰符,因此它们都接收internal的默认访问级别。然而,numberofe属性的访问级别用私有(set)修饰符标记,以表明属性的getter仍然具有内部的默认访问级别,但是属性只能从TrackedString结构的一部分代码中设置。这使得TrackedString能够在内部修改numberofe属性,但是当它在结构定义之外被使用时,它将以只读的形式呈现。

如果你创建一个TrackedString实例,并修改它的字符串值几次,你可以看到numberofe属性值的更新,以匹配修改的数量:

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"

虽然您可以从另一个源文件中查询numberofe属性的当前值,但是您不能从另一个源文件中修改该属性。这个限制保护了TrackedString编辑跟踪功能的实现细节,同时仍然提供了对该功能某个方面的方便访问。

注意,如果需要,可以为getter和setter分配显式访问级别。下面的示例显示了TrackedString结构的一个版本,其中该结构定义了一个显式的public访问级别。结构的成员(包括numberofe属性)因此默认有一个内部访问级别。你可以让结构的numberofe属性getter是公共的,而它的属性setter是私有的,通过结合公共和私有(set)访问级修饰符:

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

Initializers 初始化

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

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

Default Initializers 默认初始化器

正如在缺省初始化器中所描述的,Swift自动提供一个缺省初始化器,而不为任何结构或基类提供任何参数,这些结构或基类为其所有属性提供缺省值,并且本身不提供至少一个初始化器。

默认初始化器具有与其初始化的类型相同的访问级别,除非该类型定义为public。对于定义为public的类型,默认初始化器被认为是内部的。如果希望在另一个模块中使用公共类型时使用无参数初始化器使其可初始化,则必须自己显式地提供一个公共无参数初始化器作为类型定义的一部分。

Default Memberwise Initializers for Structure Types 结构类型的默认成员初始化器

如果结构的任何存储属性是私有的,则结构类型的默认成员初始化器被认为是私有的。同样,如果结构的任何存储属性是file private,那么初始化器就是file private。否则,初始化器的访问级别为internal。

与上面的缺省初始化器一样,如果希望在另一个模块中使用成员明智的初始化器可初始化公共结构类型,则必须自己提供一个公共成员明智的初始化器作为类型定义的一部分。

Protocols 协议

如果希望为协议类型分配显式访问级别,请在定义协议时指定访问级别。这使您能够创建只能在特定访问上下文中采用的协议。

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

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

协议继承访问级别

如果定义从现有协议继承的新协议,则新协议最多可以具有与其继承的协议相同的访问级别。例如,您不能编写继承自内部协议的公共协议。

协议组合访问级别

类型可以遵循比类型本身访问级别更低的协议。例如,您可以定义一个可以在其他模块中使用的公共类型,但是它与内部协议的一致性只能在内部协议的定义模块中使用。

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

当您编写或扩展一个类型以符合协议时,您必须确保该类型对每个协议需求的实现至少具有与该类型对该协议的一致性相同的访问级别。例如,如果一个公共类型符合一个内部协议,那么该类型对每个协议需求的实现必须至少是“内部的”。

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

Extensions 扩展访问级别

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

或者,可以使用显式访问级别修饰符(例如,私有扩展)标记扩展,为扩展中定义的所有成员设置新的默认访问级别。这个新的默认值仍然可以在扩展中为单个类型成员重写。

如果要使用扩展来添加协议一致性,则不能为扩展提供显式访问级修饰符。相反,协议自己的访问级别用于为扩展内的每个协议需求实现提供默认的访问级别。

Private Members in Extensions 扩展中的私有成员

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

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

这种行为意味着您可以以相同的方式使用扩展来组织代码,无论您的类型是否具有私有实体。例如,给定以下简单协议:

protocol SomeProtocol {
    func doSomething()
}

您可以使用扩展来添加协议一致性,如下所示:

struct SomeStruct {
    private var privateVariable = 12
}

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

Generics 泛型

泛型类型或泛型函数的访问级别是泛型类型或函数本身的访问级别及其类型参数上的任何类型约束的访问级别的最小值。

Type Aliases 类型别名

出于访问控制的目的,您定义的任何类型别名都被视为不同的类型。类型别名的访问级别可以小于或等于其别名类型的访问级别。例如,私有类型别名可以别名私有、文件私有、内部、公共或开放类型,但是公共类型别名不能别名内部、文件私有或私有类型。

注意:此规则也适用于用于满足协议一致性的关联类型的类型别名。

<<返回目录

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容