Swift之访问控制(Access Control)

2,222 阅读13分钟

前言

目前,Swift 语言已经更新到了 5.x 版本,记得 18 年 3 月份新产品立项,决定使用 Swift 开发项目时,还是 Swift 4.x。在此期间,一直使用 CocoaPods 作为第三方库管理工具,尽管 Swift 和 Xcode 已然经过几多版本更迭,笔者 pod update 的次数却屈指可数。很长一段时间里,Xcode 的 Issue navigator 充斥着各种警告⚠️,其中 Swift Compiler Warning 尤为瞩目。编译器警告数量众多,以下几种是其主要组成部分:

  • '***' was never used, consider replacing with '_' or removing it
  • Variable '***' was never mutated, consider changing to 'let' constant
  • 'public' modifier is redundant for property declared in a public extension

本着“不要忽视编译器警告”的准则,最近更新了仓库,警告也随之消失。而最后一条引发的对访问控制使用规范的疑惑,便是撰写本文的诱因。

一个例子

KingfisherKingfisherCompatible 协议为例,4.10.1 版本中相关代码如图:

警告表明,为一个声明在以 public 修饰的 extension 中的属性使用 public 修饰符是多余的。读起来有点绕,换句话说:一个以 public 修饰的 extension,它的属性无需再用 public 修饰。更新之后 5.15.8 版本相关代码如图:

可以看到,新版本中去掉了 extension KingfisherCompatiblepublic 修饰符,保留了 kf 的。而 4.10.1 版本的代码建议是移除 kf 的修饰符,两者有何区别?带着疑问,我们来看官方文档中关于 protocolextension 访问控制使用规范的描述。

Description

For Protocols:

在定义阶段为协议显式声明访问级别,可使协议只能在特定访问上下文中使用。协议定义时,其内部成员的访问级别默认与协议相同,如定义一个 public protocol,其内部成员默认访问级别也是 public

使用 public 修饰的其他类型,隐式为其内部成员提供了默认访问级别 internal

For Extensions:

定义的 classstructureenumeration 类型,可以在任何能访问的地方,使用 extension 对其扩展。扩展内新增成员拥有和原始类型中声明的成员相同的访问级别。例如,扩展一个 publicinternal 修饰的类型,扩展内新增成员的默认访问级别是 internalfileprivatefileprivateprivateprivate

此时,modifier 访问级别默认与 extension Access 相同,即为 internal,只能在 Module AccessControl 中访问。

或者,为一个 extension 显式声明修饰符,其成员就默认拥有了新的访问级别,扩展内成员仍可以显式声明访问,来覆盖默认访问级别

modifier 显式声明 public,覆盖了默认的 internal,报错消失。

不能为一个遵循某协议的扩展显式声明访问级别,因为扩展内的协议实现,已由该协议本身提供了默认访问级别。

读完文档,我们试着解答上面示例的问题,移除 extension KingfisherCompatiblepublic 修饰符和移除 kf 的,两种方式有何区别。

5.15.8 版本。

图中 extension KingfisherCompatible 未显式声明访问级别,默认为 internal,所以 kf 默认访问级别也为 internal,此时 kf 只能在当前模块(Module)中使用,外部不可访问。但代码中为 kf 显式声明 public,覆盖了原有的 internal,这样一来,开发者就能访问 kf,使用 KingfisherCompatibleInstance.kf.setImage 来展示图片了。

4.10.1 版本。

代码建议,移除 kfpublic 修饰符。由于 extension KingfisherCompatible 显式声明了 public,所以 kf 默认访问级别也是 public,此时已是对外可访问状态,再为 kf 显式声明 public 确实 redundant

测试:

可以看到,两种方式的最终效果一致。既然如此,我们开发中应选择哪一种作为标准?

我认为应该选择,为 extension 内成员显式声明访问级别的方式。这一点在系统库 API 中有很多体现,比如 UIView

这种方式有一个好处,清晰明了。 当查阅 API 时,它的适用版本、调用对象和访问级别等一并声明在头部,使我们可以毫不费力就了解它有哪些使用条件。

试想一下,如果某个 API 没有显式声明访问级别,它的注释长度以屏幕为单位,我们甚至不知道它是声明在定义(definition)中还是扩展(extension)中,而它的访问级别又特别重要,我们就不得不伸出食指按在鼠标滚轮上反复滑动,急促而均匀的哒~哒声,时刻在提醒“前途未卜”。当然,如果你是触控板用户,两指滑动的确能省些力气,但我敢肯定,这种 API 对于那些左手指按下触控板的同时,右手指谨慎拖拽滚动条的开发者来说,是极不友好的。

我相信,无论是有何种操作习惯的开发者,都希望查阅 API 的方式简便而快捷。

以上是仅针对 KingfisherCompatible 协议访问控制使用情况的解析,下面我们了解一下访问控制使用规范的更多细节。

访问控制

访问控制,通过限制你的代码和其他源文件(Source files)及模块(Modules)之间的访问,以达到隐藏代码实现细节,和指定首选可用接口的目的。

Swift 会为多种场景提供默认访问级别,如果你的 appsingle-target,就没有显式声明访问级别的必要。

访问控制模型是基于模块(Modules)和源文件(Source files)的概念实现的。

模块

模块是一个独立的代码分发单元,可以由另一个模块使用 import 关键字导入。一个 framework或一个作为独立单元交付的应用程序,都称为模块。

源文件

源文件是一个模块中的源代码文件。

简洁起见,代码中所有可使用访问控制的部分,如属性、类型及函数等,以下统称为 实体

访问级别

五种级别

Swift 提供了五种不同的访问级别,分别是:openpublicinternalfileprivateprivate,访问权限依次由高到低。

  • open open 修饰的实体,在本模块的所有源文件和被引入到的其他模块的任意源文件中,都是可访问的。open 只能修饰类和类成员,表示可被子类化和重写。

  • public public 的作用和 open 相似,修饰的实体,在本模块的所有源文件和被引入到的其他模块的任意源文件中,都是可访问的。唯一的不同是,public 修饰的实体不可被子类化或重写。如:

  • internal internal 修饰的实体,仅能在本模块中访问。若无显式声明,代码中所有实体的默认访问级别即为 internal。所以,当你在开发一款 single-target app,若没有对外可见的必要,就无需为实体显式声明访问级别。除非在特定场景下,你有意对本模块中其他代码隐藏代码细节,可使用 fileprivateprivate 实现功能。

比如多人协作开发,你要在自己写的文件中定义了一个或多个类型,又担心可能会与其他人定义的类型重名,就可以这样:

当然,我们日常开发过程中,应尽量避免重名的情况发生,可一旦有必要,也不妨用一用。

  • fileprivate fileprivate 修饰的实体,仅在当前文件内可访问。如:

  • private private 修饰的实体,仅在当前封闭声明中和与该声明同一文件的扩展中可访问。如:

使用总则

不能用一个更低访问级别的实体来定义当前实体。如:

  • internalfileprivateprivate 修饰的类型,不能定义一个 public 变量,因为在 public 变量使用的地方,这个类型均不可用。

  • 函数的访问级别不能高于其参数类型和返回值类型,因为这种函数的使用场景,其组成部分的类型不可用。如:

默认访问级别

若无显式声明,代码中所有实体的访问级别默认为 internal。如果定义一个 publicinternal 类型,其成员的访问级别默认为 internal;如果定义一个 fileprivateprivate 类型,其成员的访问级别默认为 fileprivateprivate

元组类型

元组类型的访问级别,与其所有组成类型中访问级别最低者相同。

枚举类型

枚举类型的所有 case 拥有和该类型相同的访问级别,无法为单个 case 显式指定访问级别。另外,有原始值或关联值的枚举类型,其原始值或关联值类型的访问级别应不低于枚举类型。

嵌套类型

嵌套类型的访问级别与其包含类型相同,包含类型是 public 的情况除外。 在 public 类型中定义的嵌套类型自动拥有 internal 访问级别,可以为嵌套类型显式声明 public,使其对外可用,。

子类化

在当前访问上下文中的类,当前模块中的其他类,以及不同模块中以 open 修饰的类,都可以被子类化。子类的访问级别不高于父类。

如果和被子类化的类位于同一模块,则可以重写任何在当前访问上下文中可见的类成员(方法、属性、初始化器及下标等);如果不同模块,则可以重写任何以 open 修饰的类成员。

重写可赋予父类成员更高的访问级别,甚至,在父类成员可访问的范围内,即使父类成员的访问级别比子类成员低,子类也可以调用父类成员。

常量、变量、属性及下标

常量、变量和属性的访问级别不高于所属类型,下标的访问级别不高于其下标类型或返回值类型。

getter 和 setter

常量、变量、属性及下标的 gettersetter 的访问级别自动与其同步。你可以赋予 setter 一个比 getter 更低的访问级别来限制可读写范围,如 internal(set)fileprivate(set)private(set)。在某些特定场景下,你甚至需要使用两个访问级别来分别控制 settergetter 的使用范围。

我们可以从第三方库源码中找到一些使用范例。如 Kingfisher

public 保证 getter 对外可见,private(set) 保证 setter 对外不可见。所以当外部模块使用 KFCrossPlatformImageViewInstance.kf.taskIdentifier = someValue 时,会发现 setter 是不可访问的。

初始化器

自定义初始化器的访问级别,可以高于其所属类型。而 required 初始化器的访问级别,必须与所属类型相同。与函数参数和方法参数类似,初始化器参数类型的访问级别,不高于初始化器本身。

默认初始化器

Swift 为那些其所有属性已有默认值但自身没有初始化器的结构体或基类,自动提供一个无参的初始化器,该初始化器的访问级别,默认与所属类型相同。如果该类型以 public 修饰,则默认初始化器的访问级别为 internal,可以为默认初始化器显式声明 public,使其对外可见。

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

如果一个结构体类型的任一存储属性,其访问级别为 fileprivateprivate,则默认初始化器的访问级别,即被视为 fileprivateprivate

两图对比,ageprivate 修饰之后,Person 类型的成员初始化器转为不可见。

协议

继承

子协议的访问级别不高于父协议。

遵守

一个类型可以遵守比自己访问级别低的协议。一个类型遵守特定协议的上下文环境,是该类型和协议两者的访问级别中较低者。比如,一个 public 类型遵守一个 internal 协议,遵守的访问级别也是 internal

一个类型遵守协议时,要保证协议需求的类型实现,访问级别不低于协议遵守。

Internal 遵守 Access 协议的访问级别为 internal,所以 modifier 的访问级别应不低于 internal

扩展

扩展中的私有成员

同一文件中,对类、结构体或枚举的扩展,其内容可被视作原始类型声明的一部分。于是:

  • 在原始类型中声明的 private 成员,在同一文件的扩展中可使用;
  • 在扩展中声明的 private 成员,在同一文件的另一扩展中可使用;
  • 在扩展中声明的 private 成员,在同一文件的原始类型中可使用。

这个特性使 extension 具有了组织代码结构的功能。

我们知道,在系统库或者第三方开源库中,开发者都利用了这一特性为代码分区,让具有相同或相似功能的代码聚合,使代码结构更清晰。日常开发工作时,尤其多人协作开发,为了更快速地创建文件,和保证代码风格统一,我们会自定义一些常用的文件模板,模板可能是这样的:

随着业务逻辑增加,文件会有逐渐 Massive 的倾向,查找业务逻辑变得艰难。如果能尽早利用 extension 为代码分区并做适当的注释,我们就可以使用 Jump Bar 轻松定位目标代码。

泛型

泛型类型或泛型函数的访问级别,是其自身访问级别与其参数类型的访问级别的最小值。

类型别名

为实现访问控制,你定义的任何类型别名,都被视为与原类型不同的类型。类型别名的访问级别不高于原类型。规则同样适用于用于满足协议遵守的关联类型的类型别名。

上图中,Phone 类型别名的访问级别 public 高于 Object 类型的 internal,报错;Wine 类型别名的访问级别 private 低于 Object 类型的 internal,正常;Winery 遵守 Factory 协议,关联类型别名 Product 的访问级别 internal 高于 Wine 类型别名的 private,报错。

最后

以上内容的介绍顺序,基本与 The Swift Programming Language Guide(以下简称 Guide)Access Control 部分一致,内容多以 翻译 + 示例 的形式呈现。

电脑升级 Big Sur 以后,Safari 自带了翻译功能。笔者一开始想节省时间,就试用了 Safari 翻译,但仅就 Guide 翻译结果而言,笔者并没有感受到 Safari 翻译带来的便利,译文中偶然但不意外出现的生硬翻译,反而加大了理解的难度。相同一段内容,翻译造成的阅读障碍,远比英文原版严重。鉴于如此不良好的使用体验,笔者最终选择自己翻译,有疑惑的概念则更多地参考了 Google TranslateGoogle Search

示例部分,笔者力求使用简短的代码完整表达文档内容。整体 review 的过程中,又发现了报错信息未显示完全、命名拼写有误或缺少重要注释等诸多问题,经过多次修改之后,整篇文章才算完成。

本文撰写主要基于笔者对 GuideAccess Control 部分的个人理解,示例代码片段节选自 Kingfisher 及笔者测试所写。笔者深知自己翻译和编码水平有限,如文中有理解偏颇或表述不精准之处,还请直言解惑。