Swift基础篇 --- 26访问控制

访问控制可以限定其它源文件或模块对你的代码的访问。

你可以明确地给单个类型(类、结构体、枚举)设置访问级别,也可以给这些类型的属性、函数、初始化方法、基本类型、下标索引等设置访问级别。

协议也可以被限定在一定的范围内使用,包括协议里的全局常量、变量和函数。

1.模块和源文件

Swift 中的访问控制模型基于模块和源文件:

  • 模块指的是以独立单元构建和发布的 FrameworkApplication。在 Swift 中的一个模块可以使用 import 关键字引入另外一个模块。
  • 源文件是单个源码文件,它通常属于一个模块,源文件可以包含多个类和函数的定义。

2.访问级别

Swift 为代码中的实体提供了四种不同的访问级别:publicopeninternalfileprivateprivate

访问级别 定义 用法
public/open 可以访问自己模块中源文件里的任何实体,别人也可以通过引入该模块来访问源文件里的所有实体 指定框架的外部接口
internal 可以访问自己模块中源文件里的任何实体,但是别人不能访问该模块中源文件里的实体 只在应用程序或框架内部使用
fileprivate 文件内私有,只能在当前源文件中使用 功能的部分实现细节只需要在文件内使用
private 只能在类中访问,离开了这个类或者结构体的作用域外面就无法访问 功能的部分细节只需要在当前作用域内使用

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

open 只能作用于类和类的成员,它和 public 的区别主要在于 open 限定的类和成员能够在模块外能被继承和重写。

2.1 访问级别基本原则

Swift 中的访问级别遵循一个基本原则:实体不能定义在具有更低访问级别(更严格)的实体中。例如:

  • 一个 public 的变量,其类型的访问级别不能是 internalfileprivate 或是 private。因为无法保证变量的类型在使用变量的地方也具有访问权限。
  • 函数的访问级别不能高于它的参数类型和返回类型的访问级别。因为这样就会出现函数可以在任何地方被访问,但是它的参数类型和返回类型却不可以的情况。

2.2 默认访问级别

代码中所有的实体,如果不显式的指定它们的访问级别,那么它们将都有一个 internal 的默认访问级别。因此,多数情况下不需要显示指定实体的访问级别。

3.访问控制语法

通过修饰符 openpublicinternalfileprivateprivate 来声明实体的访问级别:

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:

class SomeInternalClass {}   // 隐式 internal
var someInternalConstant = 0 // 隐式 internal

4.不同类型的访问级别

4.1 单 target 应用程序的访问级别

对于单 target 应用程序,应用的所有功能都是为该应用服务,而不需要提供给其他应用或者模块使用,所以不需要明确设置访问级别,使用默认的访问级别 internal 即可。但是,也可以使用 fileprivateprivate 访问级别,用于隐藏一些功能的实现细节。

4.2 框架的访问级别

当开发框架时,就需要把一些对外的接口定义为 openpublic 访问级别,以便使用者导入该框架后可以正常使用其功能。这些被定义为对外的接口,就是这个框架的 API。

框架的内部实现仍然可以使用默认的访问级别 internal,当需要对框架内部其它部分隐藏细节时可以使用 privatefileprivate

4.3 单元测试 target 的访问级别

当应用程序包含单元测试 target 时,为了测试,测试模块需要访问应用程序模块中的代码。默认情况下只有 openpublic 级别的实体才可以被其他模块访问。

然而,如果在导入应用程序模块的语句前使用 @testable 特性,然后在允许测试的编译设置(Build Options -> Enable Testability)下编译这个应用程序模块,单元测试目标就可以访问应用程序模块中所有内部级别的实体。

4.4 自定义类型

如果想为一个自定义类型指定访问级别,在定义类型时进行指定即可。

一个类型的访问级别也会影响到类型成员(属性、方法、构造器、下标)的默认访问级别:

  • 如果你将类型指定为 private 或者 fileprivate 级别,那么该类型的所有成员的默认访问级别也会变成 private 或者 fileprivate 级别。
  • 如果你将类型指定为 internalpublic(或者不明确指定访问级别,而使用默认的 internal ),那么该类型的所有成员的默认访问级别将是 internal

一个 public 类型的所有成员的访问级别默认为 internal 级别,而不是 public 级别。如果你想将某个成员指定为 public 级别,那么你必须显式指定。这样做的好处是,在你定义公共接口的时候,可以明确地选择哪些接口是需要公开的,哪些是内部使用的,避免不小心将内部使用的接口公开。

public class SomePublicClass {                  // 显式 public 类
    public var somePublicProperty = 0            // 显式 public 类成员
    var someInternalProperty = 0                 // 隐式 internal 类成员
    fileprivate func someFilePrivateMethod() {}  // 显式 fileprivate 类成员
    private func somePrivateMethod() {}          // 显式 private 类成员
}

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

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

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

4.4.1 元组类型

元组的访问级别将由元组中访问级别最严格的类型来决定。例如,如果你构建了一个包含两种不同类型的元组,其中一个类型为 internal,另一个类型为 private,那么这个元组的访问级别为 private

元组不同于类、结构体、枚举、函数那样有单独的定义。一个元组的访问级别由元组中元素的访问级别来决定的,不能被显示指定。

4.4.2 函数类型

函数的访问级别根据访问级别最严格的参数类型或返回类型的访问级别来决定。但是,如果这种访问级别不符合函数定义所在环境的默认访问级别,那么就需要明确地指定该函数的访问级别。

func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // 此处是函数实现部分
}

这个函数的返回类型是一个元组,该元组中包含两个自定义的类。其中一个类的访问级别是 internal,另一个的访问级别是 private,所以根据元组访问级别的原则,该元组的访问级别是 private(元组的访问级别与元组中访问级别最低的类型一致)。

因为该函数返回类型的访问级别是 private,所以你必须使用 private 修饰符来明确指定该函数的访问级别:

private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // 此处是函数实现部分
}

将函数指定为 publicinternal,或者使用默认的访问级别 internal 都是错误的,因为如果把该函数当做 publicinternal 级别来使用的话,可能会无法访问 private 级别的返回值。

4.4.3 枚举类型

枚举成员的访问级别和该枚举类型相同,不能为枚举成员单独指定不同的访问级别。

public enum CompassPoint {
    case north
    case south
    case east
    case west
}
// CompassPoint 被指定为 public,则所有成员的访问级别都是 public

枚举定义中的任何原始值或关联值的类型的访问级别至少不能低于枚举类型的访问级别。例如,你不能在一个 internal 的枚举中定义 private 的原始值类型。

4.4.4 嵌套类型

嵌套类型的访问级别和包含它的类型的访问级别相同,嵌套类型是 public 的情况除外。在一个 public 的类型中定义嵌套类型,那么嵌套类型自动拥有 internal 的访问级别。如果你想让嵌套类型拥有 public 访问级别,那么必须显式指定该嵌套类型的访问级别为 public

4.5 子类

可以继承同一模块中的所有有访问权限的类,也可以继承不同模块中被 open 修饰的类。一个子类的访问级别不得高于父类的访问级别。例如,父类的访问级别是 internal,子类的访问级别就不能是 public

此外,在同一模块中,你可以在符合当前访问级别的条件下重写任意类成员(方法、属性、构造器、下标等)。在不同模块中,你可以重写类中被 open 修饰的成员。

可以通过重写给所继承类的成员提供更高的访问级别:

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

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

在子类中,用子类成员去访问访问级别更低的父类成员,只要这一操作在相应访问级别的限制范围内(也就是说,在同一源文件中访问父类 fileprivate 级别的成员,在同一模块内访问父类 internal 级别的成员):

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

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

4.6 常量、变量、属性、下标

  • 常量、变量、属性: 不能拥有比它们的类型更高的访问级别(不能定义一个 public 级别的属性,但是它的类型却是 private 级别的)
  • 下标: 不能拥有比索引类型或返回类型更高的访问级别

如果常量、变量、属性、下标的类型是 private 级别的,那么它们必须明确指定访问级别为 private

private var privateInstance = SomePrivateClass()

常量、变量、属性、下标的 GettersSetters 的访问级别和它们所属类型的访问级别相同。

Setter 的访问级别可以低于对应的 Getter 的访问级别,这样就可以控制变量、属性或下标的读写权限。在 varsubscript 关键字之前,你可以通过 fileprivate(set)private(set)internal(set) 为它们的写入权限指定更低的访问级别。

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

结构体 TrackedString 和它的属性 value 都没有显式地指定访问级别,所以它们都是用默认的访问级别 internal。但是该结构体的 numberOfEdits 属性使用了 private(set) 修饰符,这意味着 numberOfEdits 属性只能在结构体的定义中进行赋值。numberOfEdits 属性的 Getter 依然是默认的访问级别 internal,但是 Setter 的访问级别是 private,这表示该属性只能在内部修改,而在结构体的外部则表现为一个只读属性。

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 属性的值,但是你不能对其进行赋值。这一限制保护了该记录功能的实现细节,同时还提供了方便的访问方式。

可以在必要时为 GetterSetter 显式指定访问级别

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

上面的例子将 TrackedString 结构体明确指定为了 public 访问级别。结构体的成员(包括 numberOfEdits 属性)拥有默认的访问级别 internal。结合 publicprivate(set) 修饰符把结构体中的 numberOfEdits 属性的 Getter 的访问级别设置为 public,而 Setter 的访问级别设置为 private

4.7 构造器

自定义构造器的访问级别可以低于或等于其所属类型的访问级别。唯一的例外是 必要构造器,它的访问级别必须和所属类型的访问级别相同。

如同函数或方法的参数,构造器参数的访问级别也不能低于构造器本身的访问级别。

4.7.1 默认构造器

默认构造器的访问级别与所属类型的访问级别相同,除非类型的访问级别是 public:

  • 如果一个类型被指定为 public 级别,那么默认构造器的访问级别将为 internal
  • 如果希望一个 public 级别的类型也能在其他模块中使用这种无参数的默认构造器,只能自己提供一个 public 访问级别的无参数构造器。

4.7.2 结构体默认的成员逐一构造器

如果结构体中任意存储型属性的访问级别为 private,那么该结构体默认的成员逐一构造器的访问级别就是 private。否则,这种构造器的访问级别依然是 internal

如同前面提到的默认构造器,如果你希望一个 public 级别的结构体也能在其他模块中使用其默认的成员逐一构造器,你依然只能自己提供一个 public 访问级别的成员逐一构造器。

4.8 协议

如果想为一个协议类型明确地指定访问级别,在声明协议时指定即可。这将限制该协议只能在适当的访问级别范围内被遵循。

协议中的每个方法或属性都必须具有和该协议相同的访问级别。你不能将协议中的方法或属性设置为其他访问级别。这样才能确保该协议的所有方法或属性对于任意遵循者都可用。

如果你定义了一个 public 访问级别的协议,那么该协议的所有实现也会是 public 访问级别。这一点不同于其他类型,例如,类型是 public 访问级别时,其成员的访问级别却只是 internal

4.8.1 协议继承

如果定义了一个继承自其他协议的新协议,那么新协议拥有的访问级别最高也只能和被继承协议的访问级别相同。例如,你不能将继承自 internal 协议的新协议访问级别指定为 public 协议。

4.8.2 协议遵循

  • 一个类型可以遵循比它级别更低的协议,例如,你可以定义一个 public 级别类型,它能在别的模块中使用,但是如果它遵循一个 internal 协议,这个遵循的部分就只能在这个 internal 协议所在的模块中使用。
  • 遵循协议时的上下文级别是类型和协议中级别最小的那个。如果一个类型是 public 级别,但它要遵循的协议是 internal 级别,那么这个类型对该协议的遵循上下文就是 internal 级别。
  • 当你编写或扩展一个类型让它遵循一个协议时,你必须确保该类型对协议的每一个要求的实现,至少与遵循协议的上下文级别一致。例如,一个 public 类型遵循一个 internal 协议,这个类型对协议的所有实现至少都应是 internal 级别的。

4.9 扩展

  • Extension 可以在访问级别允许的情况下对类、结构体、枚举进行扩展。Extension 的新增成员具有和原始类型成员一致的访问级别
  • 可以通过修饰语重新指定 extension 的默认访问级别(例如,private),从而给该 extension 中的所有成员指定一个新的默认访问级别。这个新的默认访问级别仍然可以被单独成员指定的访问级别所覆盖。
  • 如果使用 extension 来遵循协议的话,就不能显式地声明 extension 的访问级别。extension 每个 protocol 要求的实现都默认使用 protocol 的访问级别。

4.9.1 Extension 的私有成员

扩展同一文件内的类,结构体或者枚举,extension 里的代码会表现得跟声明在原类型里的一模一样。也就是说你可以这样:

  • 在类型的声明里声明一个私有成员,在同一文件的 extension 里访问。
  • extension 里声明一个私有成员,在同一文件的另一个 extension 里访问。
  • extension 里声明一个私有成员,在同一文件的类型声明里访问。
protocol SomeProtocol {
    func doSomething()
}
struct SomeStruct {
    private var privateVariable = 12
}

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

4.10 泛型

泛型类型或泛型函数的访问级别取决于泛型类型或泛型函数本身的访问级别,还需结合类型参数的类型约束的访问级别,根据这些访问级别中的最低访问级别来确定。

4.11 类型别名

定义的任何类型别名都会被当作不同的类型,以便于进行访问控制。类型别名的访问级别不可高于其表示的类型的访问级别。例如,private 级别的类型别名可以作为 privatefileprivateinternalpublic 或者 open 类型的别名,但是 public 级别的类型别名只能作为 public 类型的别名,不能作为 privatefileprivateinternal 类型的别名。

本文作者:Rui
发布时间:2021-03-30, 22:39:53
原始链接:https://wrf8.github.io/posts/4250655424.html
版权声明:转载请保留原文链接及作者。

×

喜欢就点赞,疼爱就打赏