Swift中常用的数据结构(下)

在上一篇笔记中,我们介绍了集合类型和元组相关的基础知识,但是还留有一部分内容,接下来我们把它讲完。

三、可变性与不可变性

与Objective-C不同,Swift不要求你在定义的时候单独区分可变性与不可变性。比如说,以前在Objective-C中,如果要定义不可变数组,你需要用NSArray,定义可变数组要使用NSMutableArray,但是在Swift中,如果你想定义可变的集合,直接使用关键字var就可以了,如果你想定义一个常量,可以使用关键字let。常量集合,比如说Array、Dictionary和Set,一旦定义就不能再修改

1、可变的集合

关于集合类型与类,你需要了解Swift是如何处理它们之间的可变性的。有时候你可能会有这样的困惑,当你把一个结构体赋值给一个常量,你无法再修改它内部的属性。但是,当你把一个类实例赋值给一个常量之后,你却可以修改它内部的属性,这是为什么呢?主要是因为,集合类型是值类型,而类是引用类型。先来看具体的示例:

// 创建一个结构体
struct Person {
    var firstName: String
    var lastName: String
    init(firstName: String, lastName: String){
        self.firstName = firstName
        self.lastName = lastName
    }
}

// 创建一个类
class Address {
    var street: String = ""
    var city: String = ""
    init(street: String, city: String){
        self.street = street
        self.city = city
    }
}

// 创建一个结构体常量
let person = Person(firstName: "飞", lastName: "张")

// 结构体常量内部的属性是不能修改的
person.firstName = "大胆"  // 编译器会报错

// 创建一个类实例常量
let address = Address(street: "淮海中路755号", city: "上海市")

// 你可以对这个常量内部的属性进行修改,因为类是引用类型
address.city = "北京市"

// 你不能再将另外一个变量赋值给常量address,因为address的内存地址是一个常量,不能再让它指向另外一个不同的地址
address = Address(street: "金融街35号", city: "北京市") // 编译器会报错

如果集合类型不需要再进行修改,建议在创建它的时候使用关键字let,因为这样有利于编译器优化集合类型代码的性能。

四、Swift和Objective-C之间的桥接

Swift在设计的时候就已经考虑到了与Objective-C之间的兼容性,因此在Objective-C中你可以使用Swift的API。同样,在Swift中你也可以使用Objective-C的API。尽管如此,但是由于两种不同语言之间的差异性,它们之间仍然存在一些不兼容的地方。

或许你之前从来没有用过Objective-C,而且今后也不打算用,但是作为一个iOS开发者来说,多多少少应该是要知道一点的,因为历史原因,在今后的开发过程中,你仍然要接触相当多的Objective-C框架和类库。接下来,我们会介绍一些在处理集合类型和算法时会用到的一些Swift与Objective-C之间相互兼容的知识。

1、NSArray与Array之间进行桥接

我们可以使用类型参数化来实现NSArray和Array之间的桥接,在这一过程中,系统会创建一个[ObjectType]类型的数组。如果NSArray没有指定参数的类型,那么它就会创建一个[AnyObject]类型的数组。在稍早前我们说过,NSArray数组中的元素是不要求类型一致的。在桥接的过程中,如果你刚好碰到了这种情况,那么你就需要使用[AnyObject]类型的数组来对其进行处理。在具体操作时,你可以使用强制解包,或者类型转换。如果你不确定[AnyObject]数组中是否包含不同类型的元素,可以使用类型转换运算符来进行操作:

// 初始化一个数组,里面包含同一种类型的元素
let  myIntArray: NSArray = [1, 2, 3, 4, 5, 6, 7, 8]

// 使用强制解包来处理NSArray
let unwrapArray: [Int] = myIntArray as! [Int]

// 使用类型转换操作符来处理NSArray
if let castArray: [Int] = myIntArray as? [Int] {
    
    // 类型转换成功,这里面的代码会被执行
    print("数组中存放都是同一种类型的元素")
}

// 再初始化一个数组,里面包含不同类型的元素
let mixedNSArray: NSArray = NSArray(array: [1, 2, "3", 4, 5, "6", 7, 8])

// 这句代码会抛出异常,因为并不是所有的元素都是Int,所以强制解包要慎用
let unwrapMixedArray: [Int] = mixedNSArray as! [ Int]

if let castMixedArray: [Int] = mixedNSArray as? [Int]{
    // 类型转换失败,这里面的代码不会被执行,但是程序是安全的
    print("数组中存放的不是同一种类型的元素")
}

强制解包一定要慎用,因为一旦类型转换失败,整个程序都会直接崩掉。但是,使用类型转换操作就没有问题了,即便NSArray中存放的是不同类型的元素,顶多就是在类型转换时不成功,但是并不影响整个程序的安全。

2、NSDictionary和Dictionary之间的转换

从NSDictionary向Dictionary转换的过程中,同样会创建一个[ObjectType]类型的字典。如果如果NSDictionary没有明确指定类型参数,那么它就会创建一个[NSObject:AnyObject]的字典。跟NSArray向Array转化的方式一毛一样,使用强制解包,或者类型转换操作符来处理就可以了,具体操作不再赘述。

3、NSSet和Set之间的转换

从NSSet向Set转换的过程中,会创建一个Set<ObjectType>类型的Set集合。如果如果NSSet没有明确指定类型参数,那么它就会创建一个Set<AnyObject>的Set集合。其它操作和数组一样,这里也不再详述。

五、Swift面向协议编程

学习Swift的时候,你最好是从协议开始,而不是类。Swift协议中定义了相当多的方法、属性,以及某种情况下相互关联的类型和别名列表。协议有一个很重要的特点,就是任何遵守该协议的类型都会从协议那里获取相应的支持。协议在其它编程语言,比如说Java、C#或者是Go中被称作接口。

1、调度

Swift中的协议是Objective-C中协议的超集,因此,在Swift中,协议的功能远比Objective-C中的协议强大。在Objective-C中,所有方法都是通过在运行时中动态调度来发送消息的。Swift中使用了多种调度技术(multiple dispatch techniques),默认情况下,它使用一个vtable,列出了类中所有可用的方法。vtable在编译时创建,并且它包含了通过索引来访问的函数指针。编译器使用vtable作为查找表,将方法调用转换为对应的函数指针。如果一个Swift类是继承自一个Objective-C类,那么它就会在运行时使用动态调度。当然,你也可以强制Swift使用动态调度,只需要用@objc属性来标记该类方法就可以了。但是,@objc属性在将你的Swift API暴露给Objective-C运行时的过程中,并不保证会动态的调度属性,方法,下标或初始化器。通过绕开Objective-C运行时的方式,Swift编译器仍然可以进行虚拟化或者内联成员访问,以此来优化代码的性能。

除了上面这两种调度方式之外,其实还有第三种调度。如果编译器有足够的信息的话,那么它就可以使用静态调度,也即是直接以内联的形式调用该方法。

1、协议的语法

声明协议的语法和声明结构体的语法极为相似,也就是直接在关键字protocol后面直接跟上协议名:

protocol Particle {
    var name: String { get }
    func returnImage() -> UIImage
}

定义协议时,你必须明确指出它是只读(gettable)的还是可写(writable)的,或者既是只读有是可写的。定义方法时,写清楚方法名、参数,以及返回类型。如果该方法需要更改结构体的成员变量,那么在定义时,必须在它前面加上关键字mutating。一个协议可以继承自一个或者多个已经存在的协议。比如说,像下面这个协议就继承自CustomStringConvertible:

protocol Particle: CustomStringConvertible {
    // 代码
}

如果多个协议总是一起出现,那么你就可以用组合的方式将它们组合成单个需求。具体的操作是,使用关键字typealias给组合协议取一个名字,然后多个协议之间用&符号进行分割。需要注意的是,组合协议并没有重新定义一个新的协议,它只是起到临时本地协议的功能,也就是具有多个协议组合在一起的功能:

protocol Particle {
    var name: String { get }
    func returnImage() -> UIImage
}

protocol Insert {
    var name: String { get }
    func returnImage() -> UIImage
}

// 组合多个协议
typealias combine = protocol <Particle, Insert>  // Swift 3之前组合协议
typealias combine2 = Particle & Insert  // Swift 3之后组合协议

2、协议类型

作为类型使用时,协议可以先不必实现相关的代码,你可以在任何使用类型的地方使用协议类型,比如说:

  • 作为函数、方法或者构造器中的返回类型,或者参数类型来使用;
  • 作为Array、Dictionary和Set中元素的类型来使用;
  • 作为变量、常量,或者属性的类型来使用。

  在Swift中,所有你能够命名的类型都具有同等的地位,它们都可以使用OOP特性。而这一点最大的价值就在于,你可以在一个子类中重写协议。它使得复杂的逻辑能够被重复使用,从而真正实现灵活性和可定制化。

  3、协议的扩展

  协议扩展能够让你在没有协议实现代码的前提条件下,对现有的协议进行功能上的扩展。它可以为现有协议增加新的属性、方法和下标。协议扩展首次出现是在Swift 2中,它使得你不必再通过编写全局函数来对现有类型进行扩展。苹果在标准库中重构了很多集合类型,以方便它们可以实现协议扩展。

  通过协议扩展,你可以在代码中自定义属于自己的默认操作行为。下面我们来看一个具体的示例,它扩展了Collection协议,给任何遵守这个协议的类型都添加了一个新的方法:

extension Collection {
    
    func encryptElements(salt: String) -> [Iterator.Element] {
        
        guard !salt.isEmpty else { return [] }
        guard self.count > 0 else {return [] }
        
        var index = self.startIndex
        var result: [Iterator.Element] = []
        
        repeat {
            // 加密时加盐,然后再将其添加到结果中
            let el = self[index]
            
            result.append(el)
            index = self.index(after: startIndex)
        } while (index != self.endIndex)
        
        return result
        
    }
}

var myarr = [String]()
myarr.append("Mary")

var result = myarr.encryptElements(salt: "test")

  加盐是一种加密技术,就是在明文的固定位置插入一些随机串,然后再进行相应的加密,这样可以增大被破解的难度。

  4、集合中使用的协议

  Swift中用于定义集合类型相关的协议有很多,我们从中选几个看一下,主要是为了了解Swift是如何使用这些协议来设计可扩展类型的。

  与数组字面意思相关的协议(Array literal syntax)。定义数组时,可以使用两种形式的语法,最常用的是使用集合类型名称,然后在它后面的尖括号中跟上数据类型,也就是我们常说的完整的语法:

var myIntArray = Array<Int>()

  另一种方式是使用数组的字面意思,也就是在数组中括号里直接加上数据类型,也就是我们常说的快速定义数组的语法:

var myIntArray = [Int]()

  ExpressibleByArrayLiteral协议允许你使用类似于数组的语法来初始化结构体,类和枚举。这个协议需要实现一个方法来遵守协议:

init(arrayLiteral elements: Self.Element...)

  如果我们要创建自己的集合类型,并且让它支持数组字面意思语法,那么就应该这样开始我们的定义:

public struct ExampleList : ExpressibleByArrayLiteral {
    
    // 其它代码
    
    init(arrayLiteral: Example...)
}

  接下来,我们来完善上面的代码,看看ExampleList是如何实现支持数组字面意思语法的:

// 自定义一个结构体
struct Example {
    var name: String
    var age: Int
    var height: Double
}

// 再自定义一个结构体,并且遵守ExpressibleByArrayLiteral协议
struct ExampleList: ExpressibleByArrayLiteral {
    
    private let items: [Example]
    
    // 实现遵守ExpressibleByArrayLiteral协议必须实现的方法
    init(arrayLiteral: Example...) {
        self.items = arrayLiteral
    }
}

// 初始化数组中的元素
var p1 = Example(name: "Lily", age: 19, height: 1.57)
var p2 = Example(name: "Lucy", age: 21, height: 1.62)
var p3 = Example(name: "Jack", age: 18, height: 1.72)

// 根据数组字面意思声明一个数组
var exampleList = [Example]()
// 初始化数组
exampleList = [p1, p2, p3]

  让数组实现可以枚举。Sequence和IteratorProtocol协议提供了允许你使用for...in语法对集合类型进行遍历的功能。一个类型只有遵守了上述两个协议,它才支持对数组进行遍历。这两个协议是相互联系,不可分割的。序列(遵守Sequence协议)表示的是一连串的数值,而迭代器(遵守IteratorProtocol协议)能保证按照某种次序,一次一个的来使用序列中的值。Sequence协议只有一个要求,就是每个序列必须使用makeIterator()方法提供一个迭代器。

  Swift中常用数据结构相关的知识暂时先讲到这里,在后面的篇幅中,我们将继续学习常用的协议、基于堆栈的数组和基于栈结构的链表、常见的队列结构,以及其他链表结构。

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

推荐阅读更多精彩内容