Swift学习笔记(十二)--泛型与访问控制

这一章应该算是最后一章了, 官方文档上有一个高级操作符的, 主要是将一些位运算或者左移右移等其它的运算符, 这个东西没什么太多的意思, 记住就好了, 而且一般不涉及到算法的话, 比较少会用到, 不过值得注意的是, 整数溢出的情况在Swift里面会有runtime error, 也就是如果你的整数溢出了, 那么会crash掉, 为了避免这种情况, 就大概有如下两种做法:

  1. 加大整型范围, 确保不会溢出
  2. 用&+替代+来执行加法运算, 乘法和减法也是一样的, 确保溢出不会crash
    一般来说, 除非有特殊的目的, 否则是要保证1的. 苹果这么做的目的大概是因为整型溢出应该算是最难debug的问题之一了.
    同时最后一章还讲了些操作符重载的, 或者说是操作符函数, 也挺有意思的, 看看也就明白了.

进入正题:

泛型(Generics)

泛型整个看起来和C++里面的模板差不多, 其本质就是与类型无关, 一般情况下, 示例都会用交换两个变量的值开始:

func swapTwoInts(inout a: Int, inout _ b: Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

这么写的话就只能交换2个Int, 而不能是String或者Double, 如果需要对其它类型值进行交换, 那么就必须再写几个函数, 作为偷懒党来说, 这绝对是不能容忍的, 所以就出现了泛型或者模板这种东西.

泛型函数

先看一下结果:

func swapTwoValues<T>(inout a: T, inout _ b: T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

来看一下两个函数的声明:

func swapTwoInts(inout a: Int, inout _ b: Int)
func swapTwoValues<T>(inout a: T, inout _ b: T)

另外提一句, 泛型之所以能成功, 是因为编译器在遇到泛型的时候, 会自动生成一个指定类型的函数代码, 所以, 其实本质上还是我们手动写的那些函数, 只不过是编译器替我们代劳了.

类型中的泛型

除了在函数中使用泛型外, 我们还可以在类型中使用泛型, 如:

struct Stack<Element> {
    var items = [Element]()
    mutating func push(item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

Element是类型占位符, Swift建议如果不是普通意义的类型, 如之前说的swapTwoValues, 里面的参数就是任意类型的, 没有特别的指代意义, 就用T即可, 否则的话, 就要用一些稍微能够代表类型本身意义的词语来描述这个类型, 如这里的Element, 也就是数组的元素.

拓展类型中的泛型

extension Stack {
    var topItem: Element? {  // <-- 这里必须和前面保持一致
        return items.isEmpty ? nil : items[items.count - 1]
    }
}

类型约束

有些时候, 我们需要对泛型加一些限制, 比如必须继承自某个类, 必须实现某个协议等等.
基本语法如下:

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // function body goes here
}

这里要求类型T必须继承自SomeClass, 类型U必须实现SomeProtocol.
以例子来引出其作用, 我们写一个在字符串数组中找到传入字符串下标的函数:

func findStringIndex(array: [String], _ valueToFind: String) -> Int? {
    for (index, value) in array.enumerate() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

如果我们写成泛型的话, 可能是这样的:

func findIndex<T>(array: [T], _ valueToFind: T) -> Int? {
    for (index, value) in array.enumerate() {
        if value == valueToFind {  // <---这里会报错
            return index
        }
    }
    return nil
}

出现了编译错误, 原因是, 类型T没有加Equatable的限制, 所以不能用==来比较, 因此, 我们需要加上它, 函数声明就是这样的:

func findIndex<T: Equatable>(array: [T], _ valueToFind: T) -> Int? {
//...
}

关联类型

在定义类型的时候, 是不允许使用泛型的, 但是可以使用关联类型, 例如:

protocol Container<Element> {  // <-- 编译报错
    mutating func append(item: Element)
    var count: Int { get }
    subscript(i: Int) -> Element { get }
}
// 下面才对
protocol Container {
    typealias ItemType  // <-- 关联类型
    mutating func append(item: ItemType)
    var count: Int { get }
    subscript(i: Int) -> ItemType { get }
}

泛型和关联类型看起来是一个维度的东西, 其实是两个维度的东西, 协议比类型更高一级, 所以关联类型是对实现协议的类型进行限制, 而泛型是对类型内部变量, 方法参数进行限制, 虽然它们可能都指向了同一个具体的类型, 如下面的代码:

struct Stack<Element>: Container {
    // 泛型
    var items = [Element]() 
    mutating func push(item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
    // 关联类型
    mutating func append(item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

此外, 我们还可以像之前协议说的那样, 为现有的类型增加protocol一样, 如果这个protocol有关联类型, 且目标类型也已经实现, 我们也可以为它增加一个拓展, 声明其实现了这个协议:

extension Array: Container {}

where语句

where语句是前面说的类型约束的进阶, 比如, 要比较两个两个Container是不是一样的, 就需要对类型加约束, 还要对类型内部的元素加限制, 如下面的代码:

func allItemsMatch<
    C1: Container, C2: Container  // <-- 类型约束, 必须实现Container
    where C1.ItemType == C2.ItemType, C1.ItemType: Equatable> // <-- 协议内部关联类型必须相等, 关联类型必须实现Equatable协议
    (someContainer: C1, _ anotherContainer: C2) -> Bool {
        
        // check that both containers contain the same number of items
        if someContainer.count != anotherContainer.count {
            return false
        }
        // check each pair of items to see if they are equivalent
        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }
        // all items match, so return true
        return true
        
}

使用起来就是这样的:

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
 
var arrayOfStrings = ["uno", "dos", "tres"]
 
if allItemsMatch(stackOfStrings, arrayOfStrings) {
    print("All items match.")
} else {
    print("Not all items match.")
}
// prints "All items match."

泛型在这里就差不多了, 另外提一句, 以前在写ObjC代码的时候, 没有泛型可以用, 所以进场用id类型, 在Swift里也找到了和id相对应的AnyObject(Any也算, 但是比AnyObject范围更大), 所以在写代码的时候容易惯性想到传入AnyObject, 在这里建议写Swift代码, 如果遇到需要写AnyObject的地方, 多想想用泛型会不会更好, 毕竟AnyObject的用法并没有像id那样灵活.

访问控制

Swift提供访问控制来约束其它模块和源文件来访问你的的代码. 这么做可以隐藏一些实现细节, 更能显式指出你期望被使用的接口.
访问控制可以加给独立的类型(类, 结构体和枚举), 也可以加给属性, 方法, 构造器, 下标. 协议和全局常量,变量,函数一样, 在某些情况下可以限制.
如果你的app是单目标的, 那么可能不太需要显式的访问控制.

模块和源文件

Swift的访问控制是基于模块和源文件的.
所谓模块, 最直接的描述是可以用import来导入的代码单元, 例如framework和application. 每个构建目标都会被当做一个模块.
源文件则是模块内定义的一个文件, 一个类型可以被定义在多个源文件中, 一个源文件也可以定义多个类型, 函数等等.

访问级别

Swift里有Public, Internal和Private三个访问级别, 分别有以下作用范围:
1). Public访问级别允许此代码被模块内任意源文件使用, 也允许import了定义的模块的源文件使用. 在framework的公开的接口中使用.
2). Internal访问级别允许此代码被模块内任意源文件调用, 但是不允许模块外的源文件使用. 在定义app或者framework内部结构的时候使用.
3). Private访问级别严格要求只能在定义的源文件中使用. 在隐藏特定功能的实现细节时使用.

可以看到, 这里的3个等级是基于源文件和模块的, 而不是其它语言中基于类.

访问级别的指导守则

// 实体: 属性, 类型, 方法, 函数等
Swift的访问级别有个总纲性指导原则: 不能定义一个实体比引入的实体更高的访问权限. 听起来很拗口, 举两个个例子:
1). 一个public的变量不能被定义为含有internal或者private类型, 因为它引入的类型并不能在任意地方都被使用.
2). 一个函数不能有比它的参数类型或者返回值类型更高的访问权限, 因为这个函数可以在一些参数类型或返回值类型不能被访问的地方使用.

默认访问级别

所有的实体(除了后面会说的特定例外)都有默认的访问权限--internal.

单目标app的访问权限

在编写单目标的app的时候, 因为不需要暴露接口给别的模块使用, 所以默认的internal访问权限已经足够了. 当然, 某些情况下, 可能会加一些private的权限来隐藏实现细节.

框架的访问权限

开发框架的时候, 把那些需要被其它模块访问的接口标注为public即可, 正如你的app引入其它的框架一样.

单元测试目标的访问权限

为app写单元测试的时候, app的代码需要调整来测试. 默认情况是, 只有public的实体才能被其它模块访问, 但是, 如果用@testable属性标注import声明之后开启这个模块的测试, 单元测试目标可以访问模块内任何internal的实体.

访问控制语法

直接看例子吧:

public class SomePublicClass {}
internal class SomeInternalClass {}
private class SomePrivateClass {}
 
public var somePublicVariable = 0
internal let someInternalConstant = 0
private func somePrivateFunction() {}

public var someInternalVariable = SomeInternalClass() // <-- 报错, 变量本身不能大于类型的访问权限

自定义类型

上面的访问控制的主体一直在说实体, 类型也是一个实体, 所以自然也能加上访问控制. 需要提一下的是, 默认的访问级别是internal, 如果类型显式指定了一个访问级别, 那么, 类型的成员访问级别将会受到影响. 举个例子, 如果类型的访问级别是private, 那么默认的成员访问级别就是private(如果显式指定为public或者internal, 只会警告----目前我的Xcode版本是7.2, 很疑惑为什么不直接报错?这么做明显违背了上面所说的总纲原则), 然而public和internal默认还是internal.
看看官方的例子:

public class SomePublicClass {          // 显式指定为public类
    public var somePublicProperty = 0    // 显式指定为public成员
    var someInternalProperty = 0         // 隐式指定为internal成员
    private func somePrivateMethod() {}  // 显式指定为private成员
}
 
class SomeInternalClass {               // 隐式指定为internal类
    var someInternalProperty = 0         // 隐式指定为internal成员
    private func somePrivateMethod() {}  // 显式指定为private成员
}
 
private class SomePrivateClass {        // 显式指定为private成员
    var somePrivateProperty = 0          // 隐式指定为private成员
    func somePrivateMethod() {}          // 隐式指定为private成员
}

元组类型

元组的访问级别取决于其所有使用类型中最严格的那个级别. 这点是符合上面所说的总纲原则的. 举个例子, 一个元组含有2个不同的类型, 分别是internal和private访问级别, 那么这个元组就会是private访问级别
值得一提的是, 元组类型并不想类型, 函数这样可以单独定义出来的, 而是在使用的时候自动推断出来的.

函数类型

同理, 函数类型的访问权限也是取决于其使用的参数类型和返回值类型中最严格的那个. 同时, 如果函数的访问级别低于上下文环境中的默认访问级别, 那么就要显式加上访问级别, 例如下面这个全局函数:

// 根据返回值推断下面的函数是private
// 但是上下文环境中默认的级别是internal
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // function implementation goes here
}
// 需要显式加上访问级别, 否则报错:
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // function implementation goes here
}
枚举类型

枚举类型里, 每个独立的case的访问级别只取决于其类型, 而不能单独指定.
枚举类型的raw value和associated value的访问级别, 必须不低于枚举本身的访问级别, 否则就违背了上面的总纲原则.

聚合类型

private访问级别的类型内部定义的类型默认访问是private, public和internal访问级别的类型内部定义的类型默认访问级别是internal.

子类

我们只可以继承当前可以访问的类, 一个子类的访问权限不能高于其父类.
此外, 我们也可以重载任意的父类成员, 只要在子类中, 这个父类成员是可见的. 并且子类的重载成员访问级别可以高于父类.
上面这句话的前半句比较难理解, 举个例子:

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

如果A和B都在A.Swift文件中定义, 那么, B可以重载someMethod, 因为B定义的地方, someMethod是可见的(private对同一个源文件可见). 然而, 如果B定义在B.Swift里, 那么B将无法重载someMethod.

至于后半句则比较好理解, 如果在A.Swift文件中定义上面A,B两个类, 在B.Swift文件中, A类型的实例将无法调用someMethod, 而B类型的实例可以. 而且B中的someMethod可以调用super.someMethod()

常量,变量,属性和下标

常量,变量和属性的访问级别都不能大于其类型. 把一个private类型的变量写成public就会报错. 同理, 下标的访问级别不能高于返回类型的级别.
如果常量,变量,属性或者下标的类型是private, 那么它需要被显式标注为private. 与上面的函数保持一致.

Getter和Setter

Getter和Setter的访问级别将与上面所说的常量, 变量,属性和下标保持一致.
我们也可以给setter一个比getter更低的访问级别来控制其写权限, 在var和subscript前面加上private(set)或者internal(set)即可.
上面的规则适用于存储属性和运算属性.
例子如下:

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

构造器

说到构造器的权限就不得不提一个设计模式(或者是反设计模式)--单例模式, 大多数语言里面, 单例模式都会要求把init设置为private, 以避免外部调用者新增实例出来, 从而使用自己提供的单例.
Swift也一样, 而且, Swift的单例模式相比其它语言而言, 更加简洁, 只需要一行核心代码即可:

class SingletonClass {
    static let sharedInstance = SingletonClass() 
    private init() {}  
}

言归正传, 说回构造器的访问级别.

自定义的构造器访问级别同样不能高于其构造的类型访问级别. 唯一的例外就是required构造器. 一个required构造器必须和它所属的类拥有一样的访问级别.(前面说的是类型, 后面说的是类, 因为required构造器只有类才有)

同样, 为了避免违背总纲原则, 构造的参数类型访问级别不能低于构造器本身的访问级别.

默认构造器

默认的构造器与其类型有一样的访问级别, 除非这个类型被定义为public, 那么它的默认访问级别就是internal(这也可以理解, Swift在public访问级别上比较严格, 在这一章里也一直在强调, 只有你需要暴露给外部调用的接口, 才需要用到public, 反复强调只是为了确定我们写上的每个public都是再三思虑过的)

结构体类型的默认逐位构造器

如果结构体内部有属性类型为private, 那么其默认逐位构造器就为private, 否则其访问级别为internal.
和上面默认构造器一样, 如果需要一个public结构体类型的逐位构造器访问级别为public, 就需要显式写上.

协议

如果想要给协议加上访问级别, 就在定义它的地方加. 这么干可以让你在特定的上下文中实现这个协议.
值得一提的是, 协议内部的每个要求的访问级别都会与协议本身保持一致, 而不能单独定义, 这和类型的访问级别是不一样的. 这样才能保证所有能实现协议的地方, 协议的需求都能被满足.

协议继承

如果新协议继承自某个协议, 那么新协议的访问级别将与其继承的协议保持一致, 我们不能在父协议为internal的情况下把新协议定义为public.

协议实现

一个类型可以实现一个访问级别低于自己的协议. 例如, 我们可以定义一个public类型, 让它来实现一个internal的协议(当然, 这个协议必须要在类型定义的上下文中可见).

文档中接下来的两段和我本身所做的实验是有冲突的, 文档是这么说的:
1). 在类型实现特定协议的上下文的访问级别是类型的访问级别和协议的访问级别的最小值. 如果类型是public, 而它实现的协议是internal, 那么类型实现协议的部分就是internal.
2). 当我们拓展类型来实现协议, 必须保证类型对协议的每一个实现至少和协议的访问级别是一样的. 例如, 一个public类型实现一个internal协议, 这个类型实现的每一个协议要求都至少为internal.
先说一下我写的代码吧:

// Protocol.Swift文件中:
private protocol PrivateProtocol {
    var name:String
}
public class PriProClass: PrivateProtocol {
    var name: String = ""
}
// ViewController.Swift文件中
var ppc = PriProClass()
let name = ppc.name

上面的代码并没有报错, 编译可以通过, 也能正常存取值, 因此和1)冲突
接下来我写了如下代码:

// Protocol.Swift文件中:
internal protocol InternalProtocol {
    var age: Int { get }
}
// ViewController.Swift文件中:
extension PriProClass: InternalProtocol{
    public var age: Int {
        return 10
    }
}

这样写也没有报错, 这也看来又和2)冲突了, 因此对这两点表示存疑, 将会继续关注. 如果有同学知道是怎么回事, 还望相告.

拓展

拓展中所添加的任何成员, 其默认访问级别都与原始类型本身一致(public还是例外).
我们也可以为extension显式标注一个访问级别--例如private extension--来设置其访问级别. 这个显式设置的级别可以被内部成员所重载掉. 同样的, 显式设定的级别还是不能高于原始级别.

拓展协议的默认实现

我们不能为协议默认实现的拓展显式加上访问级别(实际上, 拒我所实验的代码, 是不能高于源协议的访问级别, 而不是不能显式加). 相反地, 协议本身的访问级别会是拓展实现的默认级别.

泛型

泛型或泛型函数的访问权限同样也是泛型类型或函数本身的访问级别 与 类型参数的类型约束的访问级别 的最小值. 只需要时刻牢记总纲原则即可.

类型别名

我们定义的任何类型别名都可以被当做不同的类型, 所以可以用不同的类型控制. 当然, 只能是等于或者小于原类型. 例如, 一个internal的类型, 可以被别名为internal或者private, 一个public的类型则3个级别都可以. (原文档的例子太拗口, 这里稍作调整)

上面的这条规则同样适用于关联类型.

访问控制就讲到这里了, 可以看到, 全篇都是在围绕那个总纲原则在讲, 这里再次强调一次--不能定义一个实体比引入的实体更高的访问权限. 不同的小节只是针对某些特定类型有细微的区别.
同时, 这一章里面也有一些和文档所冲突的地方, 到时候如果能得到解答将会马上更新到这里.

这一章的细节参考官方文档

Swift基础就到这里了, 官方文档后面有一些语言参考, 是一些大集合, 看个人兴趣看不看吧. 对于这十二篇笔记, 我到时候也会不定期回顾, 有不恰当的地方就会修正, 希望不会引入歧途.
至于文档, 网上有开源小组在维护中文文档, 不过我一般比较习惯看英文文档, 因为更加接近原版, 在面对一些疑问的时候不需要再回过头来看是翻译出错了还是自己理解的问题.

之后会持续关注一些Swift更加深入的知识, 这里先预报一下, 这几天打算掌握一下Factor(函子)和Monad(单子).

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

推荐阅读更多精彩内容