20-访问控制

86 阅读12分钟

Swift 访问控制 (Access Control)

概述

在访问权限控制这块, Swift提供了5个不同的访问级别 (以下是从高到低排列, 实体指被访问级别修饰的内容)

  • open: 允许在定义实体的模块、其他模块中访问,允许其他模块进行继承、重写(open只能用在类、类成员上)
  • public: 允许在定义实体的模块、其他模块中访问,不允许其他模块进行继承、重写
  • internal: 只允许在定义实体的模块中访问,不允许在其他模块中访问
  • fileprivate: 只允许在定义实体的源文件中访问
  • private: 只允许在定义实体的封闭声明中访问

绝大部分实体默认都是 internal 级别 下面是自己的理解:

  1. 首先open是级别最高的,基本上相当于没有限制。可以访问,继承,重写
  2. public和open类似,也可以是不同的模块,和open差别是只能访问,不能继承和重写
  3. internal是只能在模块内重写和访问,不允许访问其他模块。还有一个关键词是默认。不写默认就是这个
  4. fileprivate 核心是文件,访问,重写都限定在文件内
  5. private 核心是大括号,只限制在大括号。前面说的声明其实就是大括号。访问和重写都限定在大括号里面

使用准则

一个实体不可以被更低访问级别的实体定义,比如

  • 变量类型\常量类型 ≥ 变量\常量
  • 参数类型、返回值类型 ≥ 函数
  • 父类 ≥ 子类
  • 父协议 ≥ 子协议
  • 原类型 ≥ typealias
  • 原始值类型、关联值类型 ≥ 枚举类型
  • 定义类型A时用到的其他类型 ≥ 类型A

准则实例讲解

1.变量类型和变量

fileprivate class Person { }

internal var person: Person
// 变量类型的访问级别(fileprivate) >= 变量的访问级别(internal)

上面的会报错,因为这里的变量是person,而他的访问级别是internal,但是变量类型Person却是fileprivate。显然是变量大于变量类型,而不是类型大于变量。可以想想如果是上面的例子,就会报下面的错Variable cannot be declared internal because its type uses a fileprivate.可以简单的想想,我在使用某个变量的时候,有可能会因为变量类型访问级别小于变量,导致变量类型,这样就有问题。

其实仔细想想,只要存在有A能访问到B,调用B,则B的级别一定要比A的访问级别大,要不就会出现这个问题 2. 参数类型、返回值类型 ≥ 函数 这个可以这么想,既然函数都能访问,那么返回值和参数,可能能访问。参数和返回值的访问级别只能比函数高才对

  1. 父类,子类,父协议,子协议也可以按照上面的理解
  2. 原类型 ≥ typealias
class Person {

}
fileprivate typealias MyPerson = Person

上面我们可以使用 MyPerson 来访问 Person,所以只要能访问到MyPerson,就应该能访问到MyPerson,所以原类型要大于等于typealias定义的类型 5.原始值类型、关联值类型 ≥ 枚举类型

public enum Score {
    case point(Int)
    case grade(String)
}

既然可以访问枚举值,那么就应该可以访问到Int,String这些类型,否则还是有问题

6.定义类型A时用到的其他类型 ≥ 类型A 这个也是和上面的差不多理解

其他特殊情况

1.元祖类型

元组类型的访问级别是所有成员类型最低的那个

internal struct Dog {}
fileprivate class Person {}

// (Dog, Person)的访问级别是fileprivate
fileprivate var data1: (Dog, Person)
private var data2: (Dog, Person)

元祖有小括号表示,那么元祖类型的类型级别是有(Dog, Person)这里面类型低的那个确定,即Person的级别fileprivate。那么声明data1,data2 的级别一定要比person相等或者低于

2.泛型类型 (Generic Types)

泛型类型的访问级别是类型的访问级别以及所有泛型类型参数的访问级别中最低的那个

internal class Car {}
fileprivate class Dog {}
public class Person<T1, T2> {}

// Person<Car,Dog>的访问级别是fileprivate
fileprivate var p = Person<Car, Dog>()

就像是木桶效应,同样能是有访问级别低的决定泛型类型的访问级别

3.成员、嵌套类型

类型的访问级别会影响成员(属性、方法、初始化器、下标)、嵌套类型的默认访问级别. 简单来说就是类型的访问级别用响应类型里面的属性,方法,初始化器,下标的访问级别。

  • 一般情况下,类型为 private 或 fileprivate,那么成员\嵌套类型默认也是 private 或 fileprivate
  • 一般情况下,类型为 internal 或 public,那么成员\嵌套类型默认是 internal
public class PublicClass {
    public var p1 = 0 // public
    var p2 = 0 // internal
    fileprivate func f1() {} // fileprivate
    private func f2() {} // private
}

class InternalClass { // internal
    var p = 0 // internal
    fileprivate func f1() {} // fileprivate
    private func f2() {} // private
}

fileprivate class FilePrivateClass { // fileprivate
    func f1() {} // fileprivate
    private func f2() {} // private
}

private class PrivateClass { // private
    func f() {} // private
}

上面的代码其实完全严格按照上面的两条规则执行的,并没有什么特殊的。下面讲解几个特殊情况

private class Person {}
fileprivate class Student : Person {}

上面的代码可能能编译通过,为啥呢,因为两行代码如果直接写在一个文件的最外层,也就是不在大括号里面,那么编译能通过,因为这时的private就是当前文件的作用域,和fileprivate是一样,而如果这两句你写在大括号里面,那么编译就不通过,因为private限定是在当前大括号里面,而fileprivate是当前文件,而且变量大于变量类型,肯定是编译不通过的

这里可以看到一个规律,是private是取决于写的位置,写在文件最外边其实就相当于fileprivate,而写在类,或者其他大括号里面就是当前的大括号里面

private struct Dog {
    var age: Int = 0
    func run() {}
}

fileprivate struct Person {
    var dog: Dog = Dog()
    mutating func walk() {
        dog.run()
        dog.age = 1
    }
}

上面的例子能编译通过,根据类型为 private,则嵌套类型默认也是private,那么就可以说 Dog里面的age,run 都是private.这个是没错的,但是外层的private写在文件的最外层,相当于filePrivate.所以里面的age,run 也相当于,所以访问不会有问题。

private struct Dog {
    private var age: Int = 0
    private func run() {}
}

fileprivate struct Person {
    var dog: Dog = Dog()
    mutating func walk() {
        dog.run()
        dog.age = 1
    }
}

上面的例子能编译不能通过,和上面的例子区别上,这里的age,run的private显示写出来的,就只能当做是当前大括号里面,就是Dog结构体里面的。所以在 Person访问dog 的run和age会报错

4.成员的重写

  • 子类重写成员的访问级别必须 ≥ 子类的访问级别,或者 ≥ 父类被重写成员的访问级别(用自己的话说其实是子类重写成员的访问级别必须大于等于子类,和父类被重写的访问界别的小值)
  • 父类的成员不能被成员作用域外定义的子类重写
public class Person {
        internal var age: Int = 0
    }
filePrivate class Student : Person {
   filePrivate override var age: Int {
        set {}
        get {10}
    }
}

上面的代码不会报错,因为上面的filePrivate必须大于等于 Student类的和var age的最小访问级别,filePrivate 大于等于filePrivate,所以可以编译通过。 上面的例子解释了第一条

class Person {
    private func run() {}
}

class Student : Person {
    override func run() { }
}

Person 中的 run() 是 private,Student 无法访问或重写它,因此编译报错。解决方案:将 Person 中的 run() 改为 fileprivate、internal 或更高访问级别,才能被 Student 重写,这个例子解释了第二条。

5.getter、setter

  • getter、setter默认自动接收它们所属环境的访问级别
  • 可以给setter单独设置一个比getter更低的访问级别,用以限制写的权限
 fileprivate(set) public var num = 10
    class Person {
        private(set) var age = 0
        fileprivate(set) public var weight: Int {
            set {}
            get { 10 }
        }
        internal(set) public subscript(index: Int) -> Int {
            set {}
            get { index }
        }
    }

上面两句话啥意思?

  1. 自动接收所所属环境的访问级别,是啥意思?对于var num = 10来说,他get set 方法的访问级别就是这个变量的。而类里面的age,weight,subscript的级别是根据Person类的访问级别按照前面的规则嵌套关系去判断的
  2. setter可以单独设置一个更低的一个优先级,因为可读的时候不一定有可写权限
  3. getter setter,其中全局变量,实例属性,计算属性,下标都有getter和setter方法

6.初始化器

  • 如果一个 public 类想在另一个模块调用编译生成的默认无参初始化器,必须显式提供 public 的无参初始化器,因为 根据嵌套关系的规则public 类的默认初始化器是 internal 级别,这样第三方用的时候只能访问这个类,而不能访问到这个初始化器,这个时间要想访问到就要显示的写一个public,将初始化器改为Public。

  • required 初始化器 ≥ 它的默认访问级别

class Person {
    private required init() {}
}

上面的代码会报错,是因为 required 初始化器 在上面的情况下默认是internal,而你现在用的private,private小于internal了,所以报错了。

  • 如果结构体有 private \ fileprivate 的存储实例属性,那么它的成员初始化器也是private \ fileprivate,这个是为啥呢?因为初始化器中用到的实例属性,这个肯定是由实例初始化器中最低的实例属性访问级别确定.所以有这个规则,而且会影响所有初始化器的访问级别,否则默认就是 internal

7.枚举类型的 case

  • 不能给 enum 的每个 case 单独设置访问级别
  • 每个 case 自动接收 enum 的访问级别
  • public enum 定义的 case 也是 public

枚举这里如果枚举外边定义是public,里面也是public,为什么呢?因为里面不允许单独给每个case设置访问权限,所以里面的权限和外边的权限是一样的,不存在说外层设置public,里面是internal,记住他这个原则,凡是遇到里面不能设置访问权限的,基本都是和外边保持一致

8.协议

协议中定义的要求自动接收协议的访问级别,不能单独设置访问级别

  • public 协议定义的要求也是 public
  • 协议实现的访问级别必须 ≥ 类型的访问级别,或者 ≥ 协议的访问级别
public protocol Runnable { 
    func run()
}

public class Person: Runnable {
    func run() {}
}

和上面枚举一样,因为协议内容不能单独设置,所以协议里面内容的访问级别和外层协议定义的地方是一样的,比方说上面的代码外层是public,那么里面的 func run() 也是public

image.png

下面我们讲讲第二点,首先看协议实现的访问级别指的是14号的实现,他现在的访问权限是internal,必须大于类型的访问级别,这里的类型的访问界别其实就是13号的fileprivate,大于等于协议的访问级别,其实就是第10行的访问级别,由于协议不能单独访问级别,其实就是第9行的访问级别。这句话总结一下就是说第14行访问级别必须大于等于第9行和第13行的小的访问级别

9.扩展 (Extensions)

  • 如果有显式设置扩展的访问级别,扩展添加的成员自动接收扩展的访问级别.如果没有显式设置扩展的访问级别,扩展添加的成员的默认访问级别,跟直接在类型中定义的成员一样.也可以单独给扩展添加的成员设置访问级别

先讲讲上面的这点讲的啥意思呢?

public class Person {}

fileprivate extension Person {
   private func run() {}
}

就是说如果 没有fileprivate的话,这个扩展的访问级别相当于这个方法直接写在类里面一样是internal,如果写的话就是里面的 func run()就是外层的fileprivate,当然你可以给 func run()设置其他访问级别。比方说设置 private.

  • 不能给用于遵守协议的扩展显式设置扩展的访问级别
  • 在同一文件中的扩展,可以写成类似多个部分的类型声明,在原本的声明中声明一个私有成员,可以在同一文件的扩展中访问它,在扩展中声明一个私有成员,可以在同一文件的其他扩展中、原本声明中访问它

代码示例:

public class Person {
    private func run0() {}
    private func eat0() {
        run1() // 编译错误,run1() 在另一个扩展中定义
    }
}

extension Person {
    private func run1() {}
    private func eat1() {
        run0() // 可以访问 run0()
    }
}

extension Person {
    private func eat2() {
        run1() // 可以访问 run1()
    }
}

我们平时做开发的时候,常常会需要在同一个文件对同一个类写多个扩展,那么在同一个文件中写的多个扩展里面其实相当于写在一个扩展里面,就算是在一个扩展里面写了private,其他扩展里面也能访问使用,原来是说private只能在大括号里面访问,但这里相当于多个扩展写在一个大括号里面。还有就是你在原来类的声明里面写了private run0,在扩展里面也可以访问

10.将方法赋值给 var\let

将方法赋值给 var\let方法也可以像函数那样,赋值给一个 let 或者 var

struct Person {
    var age: Int
    func run(_ v: Int) { print("func run", age, v) }
}

var fn1 = Person.run
// fn1类型其实是接收一个person类型,返回一个函数
var fn2 = fn1(Person(age: 10))
fn2(20)

上面是将方法传给var后面的正确调用方式

struct Person {
    var age: Int
    func run(_ v: Int) { print("func run", age, v) }
    static func run(_ v: Int) { print("static func run", v) }
}

let fn1 = Person.run
fn1(10) // static func run 10

let fn2: (Int) -> () = Person.run
fn2(20) // static func run 20

let fn3: (Person) -> ((Int) -> () ) = Person.run
fn3(Person(age: 18))(30) // func run 18 30