欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Swift编程二十七(访问控制)

程序员文章站 2024-03-19 13:00:28
...

案例代码下载

访问控制

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

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

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

注意

为简便起见,代码中可以应用访问控制的各个方面(属性,类型,函数等)在下面的部分中称为“实体”。

模块和源文件

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

模块是代码分布的单个单元——框架或应用程序被构建和包装为单个单元并可以通过另一个模块使用Swift的import关键字引入。

Xcode中的每个构建目标(例如应用程序包或框架)都被视为Swift中的单独模块。如果将应用程序代码的各个方面组合在一起作为一个独立的框架 - 也许是为了跨多个应用程序封装和重用该代码 - 当被导入和使用在应用程序中或使用在其他框架中那么在该框架中定义的所有内容将成为单独模块的一部分。

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

访问级别

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

  • 开放访问和公共访问使实体可以在其定义模块的任何源文件中使用,也可以在另一个导入定义模块的模块的源文件中使用。在指定框架的公共接口时,通常使用开放或公共访问。开放和公共访问之间的区别如下所述。
  • 内部访问使实体可以在其定义模块的任何源文件中使用,但不能在该模块之外的任何源文件中使用。在定义应用程序或框架的内部结构时,通常使用内部访问。
  • 文件私有访问将实体的使用限制在其自己的定义源文件中。当在整个文件中使用这些详细信息时,使用文件专用访问来隐藏特定功能的实现细节。
  • 私有访问将实体的使用限制为封闭声明,以及同一文件中该声明的扩展。当这些详细信息仅在单个声明中使用时,使用私有访问来隐藏特定功能的实现细节。

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

开放访问仅适用于类和类成员,它与公共访问不同,如下所示:

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

将类标记为开放访问明确表示已考虑使用该类作为其他模块的超类的代码的影响,并且已相应地设计了类的代码。

访问级别的指导原则

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

例如:

  • 公共变量不能定义为具有内部、文件私有或私有类型,因为在使用公共变量的任何地方都可能无法使用该类型。
  • 函数不能具有比其参数类型和返回类型更高的访问级别,因为该函数可用于其组成类型对周围代码不可用的情况。

下文详细介绍了该指导原则对该语言不同方面的具体影响。

默认访问级别

如果没有自己指定显式访问级别,则代码中的所有实体(具有一些特定的例外情况,如本章后面所述)都具有内部的默认访问级别。因此,在许多情况下,无需在代码中指定显式访问级别。

单目标应用的访问级别

当编写一个简单的单目标应用程序时,应用程序中的代码通常是自包含在应用程序中的,并且不需要在应用程序模块外部提供。内部的默认访问级别已匹配此要求。因此,无需指定自定义访问级别。但是,可能希望将代码的某些部分标记为私有或文件私有,以便从应用程序模块中的其他代码中隐藏其实现细节。

框架的访问级别

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

注意:

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

单元测试目标的访问级别

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

访问控制语法

定义实体的访问级别通过在实体的导入前放置一个open,public,internal,fileprivate,或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() {}

除非另行指定,否则默认访问级别为内部,如默认访问级别中所述。这意味着,SomeInternalClass和someInternalConstant能够在没有明确的访问级别的修改写入,仍会有内部的访问级别:

class SomeInternalClass {}              // 默认内部访问级别
let someInternalConstant = 0            // 默认内部访问级别

自定义类型

如果要为自定义类型指定显式访问级别,请在定义类型时执行此操作。然后可以在其访问级别允许的任何地方使用新类型。例如,如果定义文件专用类,则该类只能用作定义文件专用类的源文件中的属性类型,或者作为函数参数或返回类型。

类型的访问控制级别还会影响该类型成员的默认访问级别(其属性,方法,初始值设定项和下标)。如果将类型的访问级别定义为私有或文件专用,则其成员的默认访问级别也将为私有或文件专用。如果将类型的访问级别定义为内部或公共(或使用内部的默认访问级别而未明确指定访问级别),则类型成员的默认访问级别将是内部的。

重要

公共类型默认具有内部成员,而不是公共成员。如果希望类型成员是公共的,则必须明确标记它。此要求可确保某个类型的面向公众的API是选择发布的内容,并避免错误地将类型的内部工作方式显示为公共API。

public class SomePublicClass {
    public var somePublicProperty = 0
    var someInternalProperty = 0
    fileprivate func someFilePrivateMethod() {}
    private func somePrivateMethod() {}
}

class SomeInternalClass {
    var someInternalProperty = 0
    fileprivate func someFilePrivateMethod() {}
    private func somePrivateMethod() {}
}

fileprivate class SomeFilePrivateClass {
    func someFilePrivateMethod() {}
    private func somePrivateMethod() {}
}

private class SomePrivateClass {
    func somePrivateMethod() {}                  
}

元组类型

元组类型的访问级别是元组中使用的所有类型的最严格的访问级别。例如,如果从两种不同类型组成一个元组,一个具有内部访问权限,另一个具有私有访问权限,则该复合元组类型的访问级别将是私有的。

注意

元组类型没有类,结构,枚举和函数的独立定义。使用元组类型时会自动推导出元组类型的访问级别,并且无法明确指定。

函数类型

函数类型的访问级别计算为函数参数类型和返回类型的最严格的访问级别。如果函数的计算访问级别与上下文默认值不匹配,则必须明确指定访问级别作为函数定义的一部分。

下面的示例定义了一个名为someFunction()的全局函数,但没有为函数本身提供特定的访问级别修饰符。可能希望此函数具有默认的“内部”访问级别,但事实并非如此。实际上如下所写的someFunction()不会:

func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // 这是函数实现
}

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

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

private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // 这是函数实现
}

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

枚举类型

枚举的各个案例自动获得与其所属枚举相同的访问级别。无法为单个枚举案例指定不同的访问级别。

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

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

原始值和关联值

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

嵌套类型

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

子类

可以子类化当前访问上下文中可以访问的任何类。子类不能具有比其超类更高的访问级别 - 例如,不能编写内部超类的公共子类。

此外,可以重写在特定访问上下文中可见的任何类成员(方法,属性,初始化程序或下标)。

重写可以使继承的类成员比其超类版本更易于访问。在下面的示例中,类A是有一个名为someMethod()的file-private方法的公共类。类B是A子类,具有更低的“内部”访问级别。尽管如此,class B提供了一个访问级别为“internal” 的someMethod()覆盖,它高于原始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() {
        super.someMethod()
    }
}

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

常量,变量,属性和下标

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

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

private var privateInstance = SomePrivateClass()

Getters和Setters

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

可以为setter提供比其对应的getter 更低的访问级别,以限制该变量,属性或下标的读写范围。通过在var或subscript引导前编写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的字符串存储属性,其初始值为""(空字符串)。该结构还定义了一个名为numberOfEdits的整数存储属性,用于跟踪value修改的次数。此修改跟踪是通过value属性上的属性观察器didSet实现的,每次将value属性设置为新值时,属性观察器都会递增numberOfEdits。

在TrackedString结构和value属性不提供明确的访问级别的修改,所以他们都收到默认的内部访问级别。但是,numberOfEdits属性的访问级别标有一个private(set)修饰符,表示该属性的getter仍然具有内部的默认访问级别,但该属性只能从作为TrackedString结构一部分的代码中设置。这样TrackedString可以在内部修改numberOfEdits属性,但在属性在结构定义之外使用时,可以将属性显示为只读属性。

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

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编辑跟踪功能的实现细节,同时仍可方便地访问该功能的某个方面。

请注意,如果需要,可以为getter和setter分配显式访问级别。下面的示例显示了TrackedString结构的一个版本,其中结构的显式访问级别为public。因此,结构的成员(包括numberOfEdits属性)默认具有内部访问级别。可以通过组合public和private(set)访问级别修饰符使结构的numberOfEdits属性的getter为public,其属性setter为private :

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

初始化

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

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

默认initializers

如在默认initializers中描述,Swift为任何结构或所有属性提供缺省值并且本身不提供一个initializers的类自动提供了一个默认initializers而无需任何参数。

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

结构类型的默认initializers

如果结构的任何存储属性是私有的,则结构类型的默认initializers将被视为私有。同样,如果结构的任何存储属性是文件专用的,则initializers是文件专用的。否则,initializers具有内部访问级别。

与上面的默认任何一样,如果希望在另一个模块中使用initializers时可以初始化公共结构类型,则必须提供公共成员初始化程序作为类型定义的一部分。

协议

如果要为协议类型分配显式访问级别,请在定义协议时执行此操作。这使可以创建只能在特定访问上下文中遵守的协议。

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

注意

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

协议继承

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

遵守协议

类型可以遵守访问级别低于类型本身的协议。例如,可以定义在其他模块中可以使用的公共类型,但遵守内部协议的只能在内部协议定义的模块中使用。

遵守特定协议的类型上下文是类型访问级别和协议访问级别的最小值。如果类型是公共类型,但它符合的协议是内部类型,则遵守该协议的类型也是内部的。

在编写类型的扩展以遵守协议时,必须确保每个遵守协议要求的类型实现至少具有与该协议类型一致的访问级别。例如,如果公共类型遵守内部协议,则每个遵守协议要求的类型实现必须至少是“内部”的。

注意

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

扩展

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

或者,可以使用显式访问级别修饰符标记扩展名(例如,private extension),以便为扩展名中定义的所有成员设置新的默认访问级别。仍可以在单个类型成员的扩展中覆盖此新默认值。

如果使用扩展来添加遵守协议,则无法为扩展提供显式访问级别修饰符。相反,协议自身的访问级别用于为扩展中的每个协议要求实现提供默认访问级别。

扩展中的私有成员

与扩展的类,结构或枚举位于同一文件中的扩展的行为就像扩展中的代码已写为原始类型声明的一部分一样。因此,可以:

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

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

protocol SomeProtocol {
    func doSomething()
}

可以使用扩展来添加遵守的协议,如下所示:

struct SomeStruct {
    private var privateVariable = 12
}

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

泛型

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

类型别名

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

注意

此规则也适用于用于满足遵守协议的关联类型的类型别名。