Swift 访问控制(Access Control)

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

  1. open : 允许在定义实体的模块、其他模块中访问。允许其他模块进行继承、重写(open只能用在类、类成员上)

模块概念如下图所示:

模块概念示例图

TARGETS下的Demo就是程序编译后的可执行文件,是一个独立的模块。也就是说,在此模块中,不管是main.swift文件还是test.swift文件,在此模块下的任意文件用到其他任意文件中的类或者结构体等其他数据,都不用像OC中那样import头文件。

当然,这里需要注意权限访问级别open是允许所有模块都可任意访问的,不管是工程可执行模块还是系统库模块又或者是其他第三方库模块。

动态库的一些想要公开出去给别人调用的类,一般声明open 访问权限,如下:

open class Person {}

注意:open 只能用在类上,不能用在结构体、枚举


  1. public : 允许在定义实体的模块、其他模块中访问,不允许其他模块继承、重写
  1. internal : 只允许在定义实体的模块中访问,不允许在其他模块中访问
  1. fileprivate : 只允许在定义实体的源文件中访问
main.swift

test.swift

如上面两图所示,Person类定义在main.swift文件中,且用fileprivate访问级别修饰,就不能在test.swift等其他文件中调用,它只允许在自己的定义源文件main.swift中调用。


  1. private : 只允许在定义实体的封闭声明中访问

private 修饰变量

如图所示,变量nameprivate修饰,它的被访问权限仅限于封闭声明中,也就是大括号内。

通过上面这五个访问控制权限可以看出,Swift不同于其他语言,它是以模块、文件为单位控制访问级别权限的。


绝大部分实体默认都是internal级别

也就是说,不写访问控制级别的修饰关键字的话,默认都是添加了internal,整个项目都可访问。


访问级别的使用准则

  • 一个实体不可以被更低访问级别的实体定义,例如:
  1. 变量\常量类型 >= 变量\常量(变量类型的访问级别要大于等于变量的访问级别,同理,常量类型的访问级别要大于等于常量的访问级别)
    变量与类型不同访问级别

    如图所示,变量person的类型是Personinternal的访问级别是高于fileprivate,编译报错。
    也就是上面说的,一个实体不可以被更低访问级别的实体定义internal修饰的实体:变量person,不能被访问级别低于它的fileprivate修饰的类型Person定义。
    本质:其他地方访问类实例person,意味着也要能够访问类Person,就要保证Person的访问级别不能低于其实例person的访问级别,否则,访问实例的地方无法访问类信息,怎么用?很矛盾,无法正常使用
  1. 参数类型、返回值类型 >= 函数
    同理:别的地方调用函数,必然用到函数参数和返回值,这就意味着参数类型和返回值类型的访问级别必须不低于函数的访问级别。
    否则函数参数和返回值都无法访问,怎么能调用函数?
  1. 父类 >= 子类
  • 子类重写成员的访问级别必须 >= 子类的访问级别,或者>=父类被重写成员的访问级别(也就是说大于等于子类和父类被重写成员两者中的最低的那个访问级别)
  • 父类的成员不能被成员作用域外定义的子类重写(比如:private修饰的父类成员,不能被父类{}的子类重写,因为private作用域仅限于父类{}内,若是将子类放到父类{}内,则是没问题的)。
  1. 父协议 >= 子协议
  1. 原类型 >= typealias
class Person {}
public typealias MyPerson = Person

这样的是不行的,编译报错(Type alias cannot be declared public because its underlying type uses a indternal type)
Person定义MyPerson,也就是定义typealias,原类型Person的访问级别必须大于typealias的访问级别,显然这里的Person的默认访问级别是indternal,是低于public的。

  1. 原始值类型、关联值类型 >= 枚举类型
  2. 定义类型A时用到的其他类型 >= 类型A
  3. ......

元组类型

  • 元组类型的访问级别是所有成员类型最低的那个
internal class Person {}
fileprivate class Cat {}

fileprivate var aa: (Person, Cat)
internal    var bb: (Person, Cat) //编译报错Variable cannot be declared internal because its type uses a fileprivate type

如上代码示例,(Person, Cat)是元组类型,它的访问级别是所有成员中类型最低的那个,也就是此元组类型的访问级别是fileprivate
Swift 的访问控制级别规定,一个实体不可以被更低访问级别的实体定义
代码中,变量aa的访问的级别与定义它的元组类型的级别都是fileprivate,是附和访问级别的使用准则。
而变量bb的访问级别internal是高于定义它的元组类型(Person, Cat)的访问级别fileprivate,是不符合Swift访问级别语法规定的,所以编译报错。


泛型类型

  • 泛型类型的访问级别是类型的访问级别以及所有泛型类型参数的访问级别中最低的那个
internal class Cat {}
fileprivate class Dog {}
public class Person<T1, T2> {}

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

代码示例中,Person<Cat, Dog>的访问级别是由类型Person和泛型参数类型CatDog三者中最低的那个决定的,最终确定为Dog的访问级别fileprivate
Person<Cat, Dog>的访问级别fileprivate是大于等于变量p的访问级别,编译通过。


成员、嵌套类型

  • 类型的访问级别会影响成员(属性、方法、初始化器、下标)、嵌套类型的默认访问级别
  1. 一般情况下,类型为privatefileprivate,那么成员、嵌套类型默认也是privatefileprivate
  2. 一般情况下,类型为internalpublic,那么成员、嵌套类型默认是internal
class Test {
    private class Person {}
    fileprivate class Student : Person {}//编译报错Class cannot be declared fileprivate because its superclass is private
}

父类Person的访问级别private是低于子类Studentfileprivate,所以编译报错。
但是将Test中的类放到类外面,也就是源文件中,全局作用域内,如下:

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

是不报错的,编译能通过。
为什么呢?

因为private访问级别仅限于封闭声明中,此刻代码在全局作用域内,并没有在某个代码块中,所以,它的访问级别就是此源文件内,本质上与fileprivate的访问级别等同了,都是在实体源文件中。

private struct Dog {
    private var name: String = "Dog"
    private func eat() {};
}

fileprivate struct Cat {
    var dog: Dog = Dog()
    mutating func dosomethings() {
        dog.name = "张三" //编译报错'name' is inaccessible due to 'private' protection level
        dog.eat() //编译报错'eat' is inaccessible due to 'private' protection level
    }
}

这段代码为什么编译报错呢?

因为Dog成员访问级别是private,被限定在Dog作用域内了,外部是没有权限访问的。

private struct Dog {
    var name: String = "Dog"
    func eat() {};
}

fileprivate struct Cat {
    var dog: Dog = Dog()
    mutating func dosomethings() {
        dog.name = "张三"
        dog.eat()
    }
}

此段代码编译通过,为什么呢?前面不是说了类型为private或fileprivate,那么成员、嵌套类型默认也是private或fileprivate的吗?
既然Dog访问级别为private,那么它的成员nameeat()的访问级别也应该是private
为什么编译通过了呢?
因为全局作用域内,privatefileprivate的访问权限一样,都是此源文件内。
那么,private就是fileprivate,上面代码就等同于下面代码:

fileprivate struct Dog {
    fileprivate var name: String = "Dog"
    fileprivate func eat() {};
}

fileprivate struct Cat {
    var dog: Dog = Dog()
    mutating func dosomethings() {
        dog.name = "张三"
        dog.eat()
    }
}

注意:睁大眼睛,不要被private轻易迷惑,要看作用域的。
直接在全局作用域下定义的private等价于fileprivate

再看下边代码:

class Test {
    private struct Dog {
        var name: String = "Dog"
        func eat() {};
    }
    
    private struct Cat {
        var dog: Dog = Dog()
        mutating func dosomethings() {
            dog.name = "张三"
            dog.eat()
        }
    }
}

此刻,Dog不在全局作用域了,按说,它的属性name和方法eat()被默认修饰为private,不能被外部访问的,也就是Cat内无法调用dog.name 和 dog.eat()的。
为什么没有编译出错呢?

因为,Dog的访问级别private是限定在类Test内的,它的成员继承了它的private,这个private是限定在类Test内的private,不是其他private
也就是说,成员的privateDogprivate是一样的,都可以被Test作用域内访问。
若是手动明确添加写上访问限定符private var name: String = "Dog",那么这里的privateDogprivate是不一样的,它是明确告诉编译器,这里的属性name是不能被外部访问的。
PS:有点绕,只可意会不可言传,就当是基因遗传吧。


getter、setter

  • getter、setter默认自动接收它们所属环境的访问级别

可以给setter单独设置一个比getter更低的访问级别,用以限制写的权限

//其他文件可以获取全局变量num的值,但是不能修改其值,只能在此文件内可修改值
fileprivate(set) public var num = 10
class Person {
    //age的setter访问级别被修饰为private,Person内部可访问,Person外部无法访问
    private(set) var age = 0
    
    //weight的setter访问级别被修饰为fileprivate,main文件内部可访问,main文件外部无法访问
    fileprivate(set) public var weight: Int {
        set {}
        get { 10 }
    }
    
    //subscript的setter访问级别被修饰为internal,模块内可访问,模块外无法访问
    internal(set) public subscript(index: Int) -> Int {
        set {}
        get { index }
    }
}

var p: Person = Person()
p.age = 22  //编译报错Cannot assign to property: 'age' setter is inaccessible

如上面代码,p.age = 22编译报错,因为存储属性agesetter访问级别被修饰为private,所以,age的写操作仅限于Person内部,其他地方无法使用写操作,也就不能修改其值。
由于agegetter没有被明确修饰为某个访问级别,默认是Person的访问级别,也就是internal,所以,读操作,模块内任何地方都是可以访问调用的。

注意:getter访问级别是不能比setter低的,上面案例中如private(set)这样的操作,只能用于setter上,不能用此方法设置getter的访问级别。

上面代码中,若是将private(set) var age = 0改为public(get) var age = 0,为什么编译会报错(Expected 'set' as subject of 'public' modifier)
因为看报错原因,编译器告诉我们,这种单独设置读写的访问权限的方式,它只能用于set,且访问级别不能低于get的访问级别。


初始化器

  • 如果一个public类 想在另一个模块调用编译生成的默认无参初始化器,必须显式提供public的无参初始化器

因为public类 的默认初始化器是internal级别

public class Cat {
    var name: String?
}

Cat类的访问级别是public,其内部成员(属性、方法、下标)的访级别默认都是internal,也就是说默认的初始化器的访问级别也是internal
所以,其他另一个模块是无法访问此类默认初始化器的。

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

  • 如果结构体有private/fileprivate的存储实例属性,那么它的成员初始化器(带有成员的初始化器)也是private/fileprivate,否则默认就是internal

public struct Cat {
    private var name = "Cat"
    var age = 1
}
//编译报错,因为name的访问级别被限定在Cat{}内部,外部无法访问
var cat = Cat(name: "张三", age: 22)

还是要注意访问级别的问题,默认初始化器的访问级别是internal,但是存储属性的访问级别是private/fileprivate了,默认初始化器的访问级别也就是private/fileprivate了,private是在Cat类外无法调用默认初始化器的,fileprivate可以在源文件其他地方任意调用Cat类的默认初始化器,但是其他文件没有权限调用。


枚举类型的case

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

协议

  • 协议中定义的要求自动接收协议的访问级别,不能单独设置访问级别
  • public协议定义的要求(协议方法)也是public
  • 协议实现的访问级别必须 >= 类型的访问级别,或者 >= 协议的访问级别

就是说,协议实现的访问级别必须是 >= 类型访问级别和协议访问级别中的最低的那个访问级别

fileprivate protocol Say {
    func sayHello()
}
public struct Cat : Say {
    fileprivate func sayHello() {}
}

如上代码,在Cat类内,协议的实现sayHello() {}访问级别fileprivate
类型Cat的访问级别是public
协议Say的访问级别是fileprivate
协议实现的访问级别是大于等于类型Cat和协议Say的访问级别中的最低的那个,编译通过。

public protocol Say {
    func sayHello()
}
public struct Cat : Say {
    func sayHello() {}
}

上面这段代码是编译不通过的,为什么?

因为前面说了,类型的访问级别是public,其内部成员访问级别默认都是internal
也就是说,func sayHello() {}等同于internal func sayHello() {},协议实现的访问级别internal低于public,编译报错。


扩展

  • 如果有显式设置扩展的访问级别,扩展添加的成员自动接收扩展的访问级别
//main.swift 文件
class Cat {}

private extension Cat {
    func sayHello() {}
}
//test.swift 文件
var cat = Cat()
cat.sayHello()
//编译报错'sayHello' is inaccessible due to 'fileprivate' protection level
  • 如上面两段代码,在test.swift文件中调用,编译报错,因为访问级别不够。
  • main.swift文件中,Cat扩展的访问级别是private,所以编译器报错'sayHello' is inaccessible due to 'fileprivate' protection level
    此处编译器报错中提示的是fileprivate,因为全局作用域内,private等同于fileprivate
  • 所以,猜测,应该是编译器做了优化,凡是全局作用域的private,就按fileprivate处理了。
  • 如果没有显式设置扩展的访问级别,扩展添加的成员的默认访问级别跟直接在类型中定义的成员一样

  • 可以单独给扩展添加的成员设置访问级别

  • 不能给用于遵守协议的扩展显式设置扩展的访问级别

  • 在同一文件中的扩展,可以写成类似多个部分的类型声明

class Cat {
    private func say0() {}
    private func eat0() {
        say1()
    }
}
extension Cat {
    private func say1() {}
    private func eat1() {
        eat0()
    }
}
extension Cat {
    private func eat2() {
        say1()
    }
}

编译通过,为什么呢?私有private成员的访问级别不都是限定在当前定义的{}内吗?
其实,同一文件内的后面两个扩展相当于将类Cat拆分成不同部分,也就是说,上面的代码等同于下面的代码:

class Cat {
    private func say0() {}
    private func eat0() {
        say1()
    }
    private func say1() {}
    private func eat1() {
        eat0()
    }
    private func eat2() {
        say1()
    }
}
  • 在原本的声明中声明一个私有成员,可以在同一文件的扩展中访问它
  • 在扩展中声明的一个私有成员,可以在同一文件的其他扩展中、原本声明中访问它

将方法赋值给let、var

  • 方法也可以像函数那样赋值给一个let或者var
class Cat {
    func say(_ str: String) {
        print("Cat say:", str)
    }
    
    static func eat(_ food: String) {
        print("Cat eat:", food)
    }
}

let cat = Cat()
var fn = cat.say //fn类型为:(String) ->()
fn("呵呵")
//打印结果Cat say: 呵呵

var fn1 = Cat.say  //fn1类型为: (Cat) ->(String) ->()
var fn2 = fn1(Cat()) //fn2类型为:(String) ->()
fn2("哈哈")
//打印结果Cat say: 哈哈

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