swift 集合_swift set集合 - CSDN
  • Swift提供三种主要的集合类型,为数组,集合和字典,用于存储集合值。数组是有序的值集合集合是唯一值的无序集合。字典是键值关联的无序集合Swift中的数组,集合和字典总是清楚它们可以存储的值和键的类型。这...

    案例代码下载

    集合类型

    Swift提供三种主要的集合类型,为数组,集合和字典,用于存储集合值。数组是有序的值集合。集合是唯一值的无序集合。字典是键值关联的无序集合。
    image

    Swift中的数组,集合和字典总是清楚它们可以存储的值和键的类型。这意味着您不能错误地将错误类型的值插入到集合中。这也意味着您可以对从集合中检索的值的类型充满信心。

    注意: Swift的数组,集合和字典类型实现为泛型集合。有关泛型类型和集合的更多信息,请参阅泛型。

    可变集合

    如果创建数组,集合或字典,并将其分配给变量,则创建的集合将是可变的。这意味着,你可以改变(或变异它是由添加,删除或改变创建后集合中的项目)的集合。如果将数组,集合或字典分配给常量,则该集合是不可变的,并且其大小和内容不能更改。

    注意: 在集合不需要更改的所有情况下,最好创建不可变集合。这样做可以使您更容易推理代码,并使Swift编译器能够优化您创建的集合的性能。

    数组

    一个阵列存储在有序列表中的相同类型的值。相同的值可以在不同位置多次出现在数组中。

    注意: Swift的Array类型被桥接到Foundation的NSArray类。有关Array与Foundation和Cocoa一起使用的更多信息,请参阅数组和NSArray之间的桥接。

    数组类型速记语法

    Swift数组的类型是完整写写法是Array,其中Element是允许数组存储的值的类型。您也可以用简写形式编写数组类型[Element]。虽然这两种形式在功能上是相同的,但是简写形式是优选的,并且在引用数组的类型时在本指南中使用。

    创建一个空数组

    您可以使用初始化语法创建某个类型的空数组:

    var someInts = [Int]()
    print("someInts is of type [Int] with \(someInts.count) items.")
    

    请注意,someInts变量的类型推断为[Int]来自初始化程序的类型。

    如果上下文已经提供了类型信息,例如函数参数或已经键入的变量或常量,则可以使用空数组文字创建一个空数组,该数组写为[](一对空方括号):

    someInts.append(3)
    someInts = []
    

    创建具有默认值的数组

    Swift的Array类型还提供了一个初始化器,用于创建一个特定大小的数组,并将其所有值设置为相同的默认值。您将此初始值设定项传递给相应类型(称为repeating)的默认值:以及在新数组(被调用count)中重复该值的次数:

    var threeDoubles = Array(repeating: 0.0, count: 3)
    // threeDoubles is of type [Double], and equals [0.0, 0.0, 0.0]
    

    通过添加两个数组来创建数组

    您可以通过使用加法运算符(+)将两个具有兼容类型的现有数组相加来创建新数组。新数组的类型是从您添加的两个数组的类型推断出来的:

    var anotherThreeDoubles = Array(repeating: 2.5, count: 3)
    // anotherThreeDoubles is of type [Double], and equals [2.5, 2.5, 2.5]
    
    var sixDoubles = threeDoubles + anotherThreeDoubles
    // sixDoubles is inferred as [Double], and equals [0.0, 0.0, 0.0, 2.5, 2.5, 2.5]
    

    使用Array Literal创建数组

    您还可以使用数组文字初始化数组,这是将一个或多个值写为数组集合的简便方法。数组文字被写为值列表,用逗号分隔,由一对方括号括起来:[value 1, value 2, value 3]

    下面的示例创建一个名为shoppingList存储String值的数组:

    var shoppingList: [String] = ["Eggs", "Milk"]
    // shoppingList has been initialized with two initial items
    

    该shoppingList变量被声明为“字符串值数组”,写为[String]。由于此特定数组已指定值类型String,因此仅允许存储String值。这里,shoppingList数组初始化为两个String值(“Eggs"和"Milk”),写在数组文字中。

    注意: 该shoppingList阵列被声明为一个变量(由var声明),而不是一个常数(用let声明因为更多的项目被添加到以下实例中的购物清单)。

    在这种情况下,数组文字包含两个String值,而不包含任何其他值。这匹配shoppingList变量声明的类型(只能包含String值的数组),因此允许使用数组文字的赋值作为初始化shoppingList两个初始项的方法。

    感谢Swift的类型推断,如果要使用包含相同类型值的数组文字对其进行初始化,则不必编写数组类型。初始化shoppingList可能是用较短的形式编写的:

    var shoppingList = ["Eggs", "Milk"]
    

    因为数组文字中的所有值都是相同的类型,所以Swift可以推断出这[String]是用于shoppingList变量的正确类型。

    访问和修改数组

    您可以通过其方法和属性或使用下标语法来访问和修改数组。

    要查找数组中的项数,请访问其只读属性count:

    print("The shopping list contains \(shoppingList.count) items.")
    // Prints "The shopping list contains 2 items."
    

    使用Boolean值isEmpty属性作为检查count属性是否等于式0的快捷方:

    if shoppingList.isEmpty {
        print("The shopping list is empty.")
    } else {
        print("The shopping list is not empty.")
    }
    // Prints "The shopping list is not empty."
    

    您可以通过调用数组的append(_:)方法将新项添加到数组的末尾:

    shoppingList.append("Flour")
    // shoppingList now contains 3 items, and someone is making pancakes
    

    使用添加赋值运算符(+=)追加一个或多个兼容项的数组:

    shoppingList += ["Baking Powder"]
    // shoppingList now contains 4 items
    shoppingList += ["Chocolate Spread", "Cheese", "Butter"]
    // shoppingList now contains 7 items
    

    使用下标语法从数组中检索值,在数组名称后面的方括号内传递要检索的值的索引:

    var firstItem = shoppingList[0]
    // firstItem is equal to "Eggs"
    

    注意: 数组中的第一项具有索引0,而不是1。Swift中的数组始终为零索引。

    您可以使用下标语法更改给定索引处的现有值:

    shoppingList[0] = "Six eggs"
    // the first item in the list is now equal to "Six eggs" rather than "Eggs"
    

    使用下标语法时,您指定的索引必须有效。例如,写入尝试将项目附加到数组的末尾shoppingList[shoppingList.count] = "Salt"会导致运行时错误。

    您还可以使用下标语法一次更改值范围,即使替换值集的长度与要替换的范围不同。下面的示例替"Chocolate Spread"换,“Cheese"和"Butter"以及"Bananas"与"Apples”:

    shoppingList[4...6] = ["Bananas", "Apples"]
    // shoppingList now contains 6 items
    

    要在指定索引处将项插入数组,请调用数组的insert(_:at:)方法:

    shoppingList.insert("Maple Syrup", at: 0)
    // shoppingList now contains 7 items
    // "Maple Syrup" is now the first item in the list
    

    此insert(_:at:)方法调用会在购物清单的最开头插入一个新项目,其值由索引表示。"Maple Syrup"0

    同样,您使用该remove(at:)方法从数组中删除项。此方法删除指定索引处的项并返回已删除的项(尽管如果不需要,可以忽略返回的值):

    let mapleSyrup = shoppingList.remove(at: 0)
    // the item that was at index 0 has just been removed
    // shoppingList now contains 6 items, and no Maple Syrup
    // the mapleSyrup constant is now equal to the removed "Maple Syrup" string
    

    注意: 如果尝试访问或修改数组现有边界之外的索引的值,则会触发运行时错误。通过将索引与数组的count属性进行比较,可以在使用之前检查索引是否有效。数组中最大的有效索引是count - 1因为数组从零开始索引 - 但是,当count是0(意味着数组为空)时,没有有效的索引。

    删除项时,数组中的任何间隙都会关闭,因此index 0处的值再次等于:“Six eggs”

    firstItem = shoppingList[0]
    // firstItem is now equal to "Six eggs"
    

    如果要从数组中删除最终项,请使用removeLast()方法而不是remove(at:)方法来避免查询数组的count属性。与remove(at:)方法一样,removeLast()返回已删除的项:

    let apples = shoppingList.removeLast()
    

    迭代数组

    可以用数组for-in循环遍历整个集合值:

    for item in shoppingList {
        print(item)
    }
    // Six eggs
    // Milk
    // Flour
    // Baking Powder
    // Bananas
    

    如果需要每个项的整数索引及其值,请使用该enumerated()方法迭代数组。对于数组中的每个项,该enumerated()方法返回由整数和项组成的元组。整数从零开始,每个项目计数加1; 如果枚举整个数组,则这些整数与项目的索引相匹配。您可以将元组分解为临时常量或变量,作为迭代的一部分:

    for (index, value) in shoppingList.enumerated() {
        print("Item \(index + 1): \(value)")
    }
    // Item 1: Six eggs
    // Item 2: Milk
    // Item 3: Flour
    // Item 4: Baking Powder
    // Item 5: Bananas
    

    有关for- in循环的更多信息,请参阅For-In循环。

    集合

    集合在集合中存储相同类型的不同值,没有定义的顺序。当项目的顺序不重要时,或者当您需要确保项目仅出现一次时,您可以使用集合而不是数组。

    注意: Swift的Set类型被桥接到Foundation的NSSet类。

    有关Set与Foundation和Cocoa一起使用的更多信息,请参阅Set和NSSet之间的桥接。

    集合类型的哈希值

    必须是可哈希化的类型才能存储在集合中 - 即,类型必须提供计算自身哈希值的方法。哈希值是Int值对于所有对象的相等比较的相同值,如果是a == b,则遵循a.hashValue == b.hashValue。

    所有Swift的基本类型(例如String,Int,Double,和Bool)默认情况下可哈希,并可以作为设定值类型或字典密键类型。默认情况下,没有关联值的枚举案例值(如枚举中所述)也是可哈希的。

    注意: 可以使用自己的自定义类型作为设置值类型或字典键类型,使它们符合Swift标准库中的Hashable协议。符合Hashable协议的类型必须提供一个Int名为gettable的属性hashValue。hashValue类型属性返回的值在同一程序的不同执行或不同程序中不需要相同。

    由于Hashable协议符合Equatable,符合类型还必须提供equals operator的实现。Equatable协议要求任何符合要求的实现是等价关系。也就是说,一个==实现必须满足以下三个条件,所有a,b以及c值:

    • a == a (自反)
    • a == b暗示(对称)b == a
    • a == b && b == c暗示(传递性)a == c

    有关符合协议的更多信息,请参阅协议。

    集合类型语法

    Swift集合类型写为Set,Element允许集存储的类型在哪里。与数组不同,集合没有等效的简写形式。

    创建和初始化空集

    可以使用初始化程序语法创建某个类型的空集:

    var letters = Set<Character>()
    print("letters is of type Set<Character> with \(letters.count) items.")
    

    注意: 根据初始化程序的类型推断letters变量为Set类型。

    或者,如果上下文已经提供了类型信息,例如函数参数或已经键入的变量或常量,则可以使用空数组文字创建一个空集:

    letters.insert("a")
    // letters now contains 1 value of type Character
    letters = []
    // letters is now an empty set, but is still of type Set<Character>
    

    使用Array Literal创建集合

    您还可以使用数组文字初始化集合,作为将一个或多个值写为集合集合的简写方式。

    下面的示例创建一个名为favoriteGenres存储String值的集合:

    var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"]
    // favoriteGenres has been initialized with three initial items
    

    favoriteGenres变量被声明为“一组String值”,写为Set。由于此特定集已指定值类型String,因此仅允许存储String值。在此,favoriteGenres集合被初始化具有三个String值(“Rock”,“Classical”,和),阵列字面内写入。“Hip hop”

    注意: favoriteGenres集合被声明为变量(使用var声明)而不是常量(使用let声明),因为在下面的示例中添加和删除了项目。

    无法仅从数组文字中推断出集合类型,因此Set必须显式声明该类型。但是,由于Swift的类型推断,如果使用包含仅一种类型的值的数组文字初始化它,则不必编写集合元素的类型。初始化favoriteGenres是用较短的形式编写的:

    var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]
    

    因为数组文字中的所有值都是相同的类型,所以Swift可以推断出这Set是用于favoriteGenres变量的正确类型。

    访问和修改集

    可以通过其方法和属性访问和修改集。

    要查找集合中的项目数,请访问其只读属性count:

    print("I have \(favoriteGenres.count) favorite music genres.")
    // Prints "I have 3 favorite music genres."
    

    使用Boolean类型的isEmpty属性作为检查count属性是否等于0的快捷方式:

    if favoriteGenres.isEmpty {
        print("As far as music goes, I'm not picky.")
    } else {
        print("I have particular music preferences.")
    }
    // Prints "I have particular music preferences."
    

    可以通过调用set的insert(_:)方法将新项添加到集合中:

    favoriteGenres.insert("Jazz")
    // favoriteGenres now contains 4 items
    

    可以通过调用set的remove(_:)方法从集合中删除项目,如果该项目是该集合的成员,则删除该项目,并返回已删除的值,或者如果该集合不包含该项目则返回nil。或者,可以使用其removeAll()方法删除集合中的所有项目。

    if let removedGenre = favoriteGenres.remove("Rock") {
        print("\(removedGenre)? I'm over it.")
    } else {
        print("I never much cared for that.")
    }
    // Prints "Rock? I'm over it."
    

    要检查集合是否包含特定项目,请使用该contains(_:)方法。

    if favoriteGenres.contains("Funk") {
        print("I get up on the good foot.")
    } else {
        print("It's too funky in here.")
    }
    // Prints "It's too funky in here."
    

    遍历集合

    您可以使用for- in循环遍历集合中的值。

    for genre in favoriteGenres {
        print("\(genre)")
    }
    // Classical
    // Jazz
    // Hip hop
    

    有关for- in循环的更多信息,请参阅For-In循环。

    Swift的Set类型没有定义的顺序。要以特定顺序迭代集合的值,请使用该sorted()方法,该方法将集合的元素作为使用<运算符排序的数组返回。

    for genre in favoriteGenres.sorted() {
        print("\(genre)")
    }
    // Classical
    // Hip hop
    // Jazz
    

    集合运算

    可以有效地执行基本集合操作,例如将两个集合组合在一起,确定两个集合具有哪些值,或者确定两个集合是否包含全部,一些或不包含相同的值。

    基本集合运算

    下图描绘了两个集a和b由阴影区域表示的各种运算操作的结果。
    image

    • 使用intersection(_:)方法创建一个仅包含两个组共有的值的新集。
    • 使用symmetricDifference(_:)方法创建一个新集合,其中包含任一集合中的值,但不包含任两个集合都存在的值。
    • 使用union(_:)方法创建包含两个集中所有值的新集。
    • 使用此subtracting(_:)方法创建一个值不在指定集中的新集。
    let oddDigits: Set = [1, 3, 5, 7, 9]
    let evenDigits: Set = [0, 2, 4, 6, 8]
    let singleDigitPrimeNumbers: Set = [2, 3, 5, 7]
    oddDigits.union(evenDigits).sorted()
    oddDigits.intersection(evenDigits).sorted()
    oddDigits.subtracting(singleDigitPrimeNumbers).sorted()
    oddDigits.symmetricDifference(singleDigitPrimeNumbers).sorted()
    

    集合会员资格和平等

    下图描绘了三个集合a,b和c具有表示在集合之间共享的元素的重叠区域。集合a是集合b的超集,因为a包含了所有元素b。相反,集合b是集合a的子集,因为b其中的所有元素也包含在a内。集合b和集合c彼此不相交,因为它们没有共同的元素。
    image

    • 使用“is equal”运算符(==)来确定两个集合是否包含所有相同的值。
    • 使用此isSubset(of:)方法确定集合的所有值是否都包含在指定的集合中。
    • 使用此isSuperset(of:)方法确定集合是否包含指定集合中的所有值。
    • 使用isStrictSubset(of:)或isStrictSuperset(of:)方法确定集合是否是指定集合的子集或超集,但不等于。
    • 使用该isDisjoint(with:)方法确定两个集合是否没有共同的值。
    let houseAnimals: Set = ["?", "?"]
    let farmAnimals: Set = ["?", "?", "?", "?", "?"]
    let cityAnimals: Set = ["?", "?"]
    houseAnimals.isSubset(of: farmAnimals)//ture
    farmAnimals.isSuperset(of: houseAnimals)//ture
    farmAnimals.isDisjoint(with: cityAnimals)//ture
    

    字典

    典存储相同类型的key和相同类型的值且没有定义排序之间的关联的集合。每个值都与唯一key相关联,唯一key充当字典中值的标识符。与数组中的项目不同,字典中的项目没有指定的顺序。当您需要根据标识符查找值时,可以使用字典,这与使用真实字典查找特定单词的定义的方式非常相似。

    注意: Swift的Dictionary类型被桥接到基金会的NSDictionary类。

    有关Dictionary与Foundation和Cocoa一起使用的更多信息,请参阅字典和NSDictionary之间的桥接。

    字典类型速记语法

    Swift字典Dictionary<Key, Value>的类型是完整的,Key是以用作字典键的值的类型,Value典】为这些键存储的值的类型。

    注意: 字典Key类型必须符合Hashable协议,如集合的值类型。

    您也可以用简写形式编写字典类型[Key: Value]。虽然这两种形式在功能上是相同的,但是简写形式是优选的,并且在本指南中在引用字典的类型时使用。

    创建一个空字典

    与数组一样,您可以Dictionary使用初始化程序语法创建某个类型的空:

    var namesOfIntegers = [Int: String]()
    

    此示例创建一个[Int: String]类型的空字典,以存储整数值的可读名称。它的键是Int类型,其值是String类型。

    如果上下文已经提供了类型信息,您可以创建一个空字典,其中包含一个空的字典文字,其写为[:](一对方括号内的冒号):

    namesOfIntegers[16] = "sixteen"
    namesOfIntegers = [:]
    

    使用Dictionary Literal创建字典

    您还可以使用字典文字初始化字典,字典文字与前面看到的数组文字具有相似的语法。字典文字是将一个或多个键值对编写为Dictionary集合的简便方法。

    键值对是一个键和值的组合。在字典文字中,每个键值对中的键和值由冒号分隔。键值对被写为列表,以逗号分隔,由一对方括号括起:[key 1: value 1, key 2: value 2, key 3: value 3]

    以下示例创建一个字典来存储国际机场的名称。在这个字典中,key是三个字母的国际航空运输协会代码,值是机场名称:

    var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
    

    airports字典被声明为[String: String]类型的,意思是Dictionary的key为String类型,并且其值也是String类型的。

    注意: airports字典被声明为一个变量(用var声明),而不是一个常数(用let声明),因为在下面的例子有更多的机场被添加到词典中。

    使用airports包含两个键值对的字典文字初始化字典。第一对有一个键值"YYZ"和值"Toronto Pearson"。第二对有一个键值"DUB"和值"Dublin"。

    这个字典文字包含两String: String对。airports键值类型匹配变量声明的类型(仅包含String键和String值的字典),因此允许分配字典文字作为使用两个初始项初始化airports字典的方法。

    与数组一样,如果要使用其键和值具有一致类型的字典文字对其进行初始化,则不必编写字典类型。初始化airports可能是用较短的形式编写的:

    var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
    

    因为文字中的所有键都是彼此相同的类型,并且同样所有值都是彼此相同的类型,Swift可以推断这是用于airports字典的正确类型是[String: String]。

    访问和修改字典

    可以通过其方法和属性或使用下标语法来访问和修改字典。

    与数组一样,可以通过检查只读属性count来查找Dictionary中的项目数:

    print("The airports dictionary contains \(airports.count) items.")
    

    使用Boolean类型的isEmpty属性作为检查count属性是否等于0的快捷方式:

    if airports.isEmpty {
        print("The airports dictionary is empty.")
    } else {
        print("The airports dictionary is empty.")
    }
    

    可以使用下标语法将新项添加到字典中。使用适当类型的新键作为下标索引,并指定适当类型的新值:

    airports["LHR"] = "London"
    

    还可以使用下标语法来更改与特定键关联的值:

    airports["LHR"] = "London Heathrow"
    

    作为下标的替代方法updateValue(:forKey:),使用字典的方法来设置或更新特定键的值。与上面的下标示例一样,updateValue(:forKey:)如果键不存在,则该方法为设置值,或者如果该键已存在则更新值。但是,与下标不同,updateValue(_:forKey:)方法在执行更新后返回旧值。这可以检查是否进行了更新。

    updateValue(_:forKey:)方法返回字典值类型的可选值。例如,对于存储String值的字典,该方法返回type String?或“optional String”的值。如果在更新之前存在该键则此可选值包含该键的旧值,如果不存在任何键或则为nil:

    if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") {
        print("The old value for DUB was \(oldValue).")
    }
    

    还可以使用下标语法从字典中检索特定键的值。因为可以请求不存在值的键,所以字典的下标返回字典值类型的可选值。如果字典包含所请求键的值,则下标返回包含该键的现有值的可选值。否则,返回nil:

    if let airportName = airports["DUB"] {
        print("The name of the airport is \(airportName).")
    } else {
        print("That airport is not in the airports dictionary.")
    }
    

    可以使用下标语法通过nil为该键指定值来从字典中删除键值对:

    airports["APL"] = "Apple International"
    airports["APL"] = nil
    

    使用removeValue(forKey:)方法从字典中删除键值对。此方法删除键值对(如果存在)并返回已删除的值,如果不存在值则返回nil:

    if let removedValue = airports.removeValue(forKey: "DUB") {
        print("The removed airport's name is \(removedValue).")
    } else {
        print("The airports dictionary does not contain a value for DUB.")
    }
    

    遍历字典

    可以用字典遍历键值对for- in环。字典中的每个项都作为元组(key, value)返回,可以将元组的成员分解为临时常量或变量,作为迭代的一部分:

    for (airportCode, airportName) in airports {
        print("\(airportCode): \(airportName)")
    }
    

    有关for- in循环的更多信息,请参阅For-In循环。

    还可以通过访问字典的键keys和values属性来检索字典的键或值的可迭代集合:

    for airportCode in airports.keys {
        print("Airport code: \(airportCode)")
    }
    for airportName in airports.values {
        print("Airport name: \(airportName)")
    }
    

    如果需要API使用字典的键或值来创建Array实例,请使用keys或values属性初始化新数组:

    let airportCodes = [String](airports.keys)
    let airportNames = [String](airports.values)
    

    Swift的Dictionary类型没有定义顺序。要按特定顺序迭代字典的键或值,请在其keys或values属性上使用sorted()方法。

    展开全文
  • Swift3-集合类型

    2017-03-03 16:13:13
    一、引言Swift中提供了3种集合类型,Array数据类型,Set集合类型,Dictionary字典类型。 Array用于存放一组有序的数据,数据角标从0开始一次递增; Set用于存放一组无序的数据,数据不可以重复; Dictionary也...

    一、引言

    Swift中提供了3种集合类型,Array数据类型,Set集合类型,Dictionary字典类型。

    Array用于存放一组有序的数据,数据角标从0开始一次递增;
    Set用于存放一组无序的数据,数据不可以重复;
    Dictionary也用于存放一组无序的数据,只是其是按照键值对的方式存储,键值必须唯一。

    这里借用官方文档中的一张图来表示3种集合类型的特点:
    这里写图片描述

    二、Array类型

    Array通常也被称为数组,Swift是一种类型安全语言,其中的Array类型也必须确定其元素的类型,声明数组类型有两种方法,示例如下:

    //将数组声明为Int类型值集合的数组
    var array1:[Int]
    var array2:Array<Int>
    //创建空数组
    array1 = []
    array2 = Array()

    数组对象如果通过var变量也接收,则其为可变的数组,可以通过append方法来追加元素,示例如下:

    //向数组中追加元素
    array1.append(3)    //[3]

    在创建数组时,也可以对数组进行初始化,示例如下:

    //创建数组[0,0,0]
    var array3 = [Double](repeating: 0, count: 3)
    //创建数组[2.5,2.5,2.5]
    var array4 = Array(repeating: 2.5, count: 3)
    //数组可以使用+号直接进行追加 [0,0,0,2.5,2.5,2.5]
    var array5 = array3+array4

    Swift中提供了许多访问和修改数组的方法,示例代码如下:

    //获取数组中元素个数
    array5.count    //6
    //判断数组是否为空
    array5.isEmpty  //false
    //判断数组是否包含某个元素,有则返回所包含的元素个数
    array5.index(of: 1);    //nil
    array5.index(of: 2.5);  //3
    
    //通过下标访问数组中的元素
    array5[1]   //0
    
    //通过下标修改数组元素
    array5[1]=2 //2
    //修改数据中的一组数据
    array5[0...3] = [1,1]   //[1, 1, 2.5, 2.5]
    
    //替换数据中的一组数据,作用与修改一样
    array5.replaceSubrange(Range(1..<2), with: [66,77,88,99]) //[1, 66, 77, 88, 99, 2.5, 2.5]
    
    //向数组中某个位置插入一个数据
    array5.insert(3, at: 1) //[1, 3, 66, 77, 88, 99, 2.5, 2.5]
    
    //移除数组某个角标处的元素
    array5.remove(at: 1)    //[1, 66, 77, 88, 99, 2.5, 2.5]
    
    //移除数组的最后一个元素
    array5.removeLast() //[1, 66, 77, 88, 99, 2.5]
    
    //移除数组第一个元素
    array5.removeFirst()    //[66, 77, 88, 99, 2.5]
    
    //遍历整个数组
    for item in array5 {
        print(item)
    }
    //遍历数组枚举
    for (index,item) in array5.enumerated() {
        print(index,item)
    }
    //移除数组所有元素
    array5.removeAll()  //[]

    三、Set类型

    Set类型集合不关注元素的顺序,但是其可以保证其中元素的唯一性。和Array类型一样,Set类型来声明时也需要确定其内元素的类型,示例如下:

    var sets1 = ["qwe","asd","zxc"] //["qwe", "asd", "zxc"]
    type(of:sets1)  //Array<String>.Type
    //Set类型不能从数组字面量中被单独推断出来,因此Set类型必须显式声明,但是可以不声明具体类型,具体类型可以自检
    var sets2: Set = ["qwe","asd","zxc"]    //{"zxc", "qwe", "asd"}
    type(of:sets2)  //Set<String>.Type

    下面示例代码演示对集合进行操作:

    var set1:Set<Character> = ["a","b","c","d"]
    //向集合中插入元素
    set1.insert("z")    //(.0 true, .1 "z") 返回值中true表示成员不重复,插入成功
    set1    //{"b", "a", "d", "c", "z"}
    set1.insert("z")    //(.0 false, .1 "z") 返回值中false表示成员重复,插入失败
    
    //获取集合中元素个数
    set1.count  //5
    //判断集合是否为空
    set1.isEmpty    //false
    //判断集合中是否包含某个元素
    set1.contains("e")  //false
    //将集合中的某个元素移除
    set1.remove("a")    //{"b", "d", "c", "z"}
    
    //遍历集合
    for item in set1 {
        print(item)
    }
    //进行从小到大的排序遍历
    for item in set1.sorted() {
        print(item)
    }
    
    //移除集合中的所有元素
    set1.removeAll()    //Set([])

    Set也支持进行一些集合的数学运算,例如交集,并集,补集等,下面一张图演示了Set进行集合运算的一些特性:
    这里写图片描述
    - intersection(_:) 交集,根据两个集合中都包含的值 创建 的一个新的集合。
    - symmetricDifference(_:) —> 交集的补集,根据在一个集合中但不在两个集合中的值 创建 一个新的集合。
    - union(_:) —> 并集,根据两个集合的值 创建 一个新的集合。
    - subtracting(_:) —> 第二个集合的补集,在第一个集合中 根据不在第二个集合中的值 创建 一个新的集合。
    - subtract(_:) —> 第二个集合的补集,并且从第一个集合中移除交集的元素。没有返回值!

    示例代码如下:

    let sets10: Set = [1, 2, 3, 4, 5, 6]
    var sets11: Set = [4, 5, 6, 7, 8]
    
    
    //交集
    let set01 = sets11.intersection(sets10) //返回{4, 5, 6},sets11和sets10值不变
    sets11
    sets10
    
    //交集的补集
    let set02 = sets11.symmetricDifference(sets10)  //返回{1, 2, 3, 7, 8},sets11和sets10值不变
    
    //并集
    let set03 = sets11.union(sets10)   //返回{1, 2, 3, 4, 5, 6, 7, 8},sets11和sets10值不变
    
    //第二个集合的补集
    let set04 = sets11.subtracting(sets10)  //返回[7, 8],sets11和sets10值不变
    sets11
    
    //第二个集合的补集,并且从第一个集合中移除交集的元素。没有返回值
    sets11.subtract(sets10)  // sets11的值变为[7, 8] 和 sets10值不变
    //let set05:Set<Int> = sets11.subtract(sets13)  //错误,subtract没有返回值!
    let set05 = sets11  //[7, 8]
    • “相等”运算符 ( == ) —> 判断两个集合是否相等;
    • isSubset(of:) —> 判断第一个集合是否是第二个集合的子集;
    • isSuperset(of:) —> 判断第一个集合是否是第二个集合的超集,即第一个集合是否包含第二个集合的所有值;
    • isStrictSubset(of:) —> 判断是否是某个集合的真子集,即这个集合是某一个集合的子集,但它们并不相等;
    • isStrictSuperset(of:) —> 判断是否是某个集合的真超集,即这个集合是某一个集合的超集,但它们并不相等;
    • isDisjoint(with:) —> 判断两个集合是否没有相同的值。没有相同的值则返回true,有则false。

    下面代码显示了与子集相关的运算:

    sets11 = [4, 5, 6, 7, 8]
    let sets12: Set = [4, 6, 8]
    let sets13: Set = [4, 5, 6, 7, 8]
    let sets14: Set = [1, 3, 5, 7, 9]
    
    //判断是否相等
    sets11 == sets12    //false
    sets11 == sets13    //true
    
    //判断第一个集合是否是第二个集合的子集
    sets11.isSubset(of: sets12) // false
    sets12.isSubset(of: sets11) // true
    
    //判断第一个集合是否是第二个集合的超集
    sets11.isSuperset(of: sets12)   // true
    sets12.isSuperset(of: sets11)   // false
    
    //判断是否是某个集合的真子集,
    sets12.isStrictSubset(of: sets11)   //true
    sets13.isStrictSubset(of: sets11)   //false
    
    //判断是否是某个集合的真超集
    sets11.isStrictSuperset(of: sets12) //true
    sets11.isStrictSuperset(of: sets13) //false
    
    //判断两个集合是否没有相同的值,没有则返回true
    sets11.isDisjoint(with: sets12)// false
    sets12.isDisjoint(with: sets14)// true

    四、Dictionary类型

    Swift中的Dictionary在声明时必须明确键的类型和值的类型,示例如下:

    var dic:Dictionary<Int,String>
    var dic1:[Int:String] = [1:"one",2:"Two"]

    访问与操作Dictionary的方法,代码示例如下:

    var dic2:[Int:String] = [1:"One",2:"Two",3:"Three",4:"Four"]
    //获取字典键值对个数
    dic2.count  //4
    //判断字典是否为空
    dic2.isEmpty    //false
    //通过键获取值
    dic2[1] //"One"
    
    //添加或修改: 字典变量[键] = 值
    //通过键修改值
    dic2[1] = "First"   //[2: "Two", 3: "Three", 1: "First", 4: "Four"]
    //添加键值
    dic2[0] = "Zero"    //[0: "Zero", 2: "Two", 3: "Three", 1: "First", 4: "Four"]
    
    
    //updateValue 方法将更新一个键值 如果此键存在 则更新键值 并且将旧的键值返回 如果此键不存在 则添加键值 返回nil 其返回的为一个Optional类型值 可以使用if let进行处理
    dic2.updateValue("9", forKey: 1)    //"First"
    //使用if let 处理updateValue的返回值
    if let oldValue = dic2.updateValue("One", forKey: 1) {
        print("Old Value is \(oldValue)")   //"Old Value is 9\n"
    }
    //通过键值获取的数据也将是有个Optional类型的值 也可以使用if let
    if let value = dic2[1] {
        print("The Value is \(value)")  //"The Value is One\n"
    }
    
    //如果有此键,则移除某个键值对
    dic2    //[0: "Zero", 2: "Two", 3: "Three", 1: "One", 4: "Four"]
    dic2[9]=nil //没有此键,忽略
    dic2[0]=nil //[2: "Two", 3: "Three", 1: "One", 4: "Four"]
    //如果有此键,则返回其值,并移除键值对,没有则返回nil
    dic2.removeValue(forKey: 0) //没有此键,忽略
    dic2.removeValue(forKey: 1) //返回“One”,dic2变为[2: "Two", 3: "Three", 4: "Four"]
    
    
    //对字典进行遍历
    for (key,value) in dic2 {
        print(key,value)
    }
    //遍历所有键
    for key in dic2.keys {
        print(key)
    }
    //遍历所有值
    for value in dic2.values {
        print(value)
    }
    //进行从小到大的排序遍历
    for key in dic2.keys.sorted() {
        print(key)
    }
    

    原文https://my.oschina.net/u/2340880/blog/673359,仅针对swift3做少量更改。

    展开全文
  • 数组、字典和集合是常见的集合类型,它们都内置在 Swift 标准库中。但如果它们不能满足你的 App 的需要的时候怎么办?一种最常见的办法是使用 Array 或 Dictionary,然后用一堆业务逻辑去保存你的数据结构。但这种...

    原文:Building a Custom Collection in Swift
    作者:Eric Cerney
    译者:kmyhy

    数组、字典和集合是常见的集合类型,它们都内置在 Swift 标准库中。但如果它们不能满足你的 App 的需要的时候怎么办?

    一种最常见的办法是使用 Array 或 Dictionary,然后用一堆业务逻辑去保存你的数据结构。但这种方式太过于直接且难于维护。

    这样,创建自定义集合类型就变得有意义了。在本文,你将学习用 Swift 的 collection 协议创建自定义集合类型。

    当文本结束,你会拥有一个强大的自定义集合类型,拥有 Swift 内置集合的所有功能。

    注:本文用 Swift 3.0。小于次的版本无法编译,因为 Swift 标准库发生了剧烈改变。

    开始

    在本文中,你将从头开始创建一个“多集合”(Bag)类型。

    一个 Bag 对象就像一个 Set,用于存储不会重复的对象。在一个 Set 集合中,重复对象会被忽略。在一个 Bag 中,每个对象都会被算进去。

    一个好例子是购物清单。你拥有一个清单,每个商品都和一个数量关联。如果添加了重复的商品,则我们会增加已有商品的数量,而不是重新插入一条商品记录。

    在介绍 collection 协议前,首先来实现一个基本的 Bag。
    创建一个新的 Playground:在 Xcode 中,选择 File\New\Playground… 然后给 playground 命名为 Bag。 你可以选择任何平台,因为本教程是和平台无关的,你只需要选择 Swift 语言就可以了。
    点击 Next,选择一个地方保存 playground,然后点 Create。

    编辑 playground 文件为:

    struct Bag<Element: Hashable> {
    
    }

    Bag 结构是一个泛型结构,需要元素类型必须是 Hashable 的。Hashable 允许你对元素进行比较,在 0(1)时间复杂度上只存储唯一值。也就是说,无论内容有多复杂,Bag 存取速度相同。你通过定义一个结构体,强制让它具备值语义(C++ 术语),这就和 Swift 标准库保持一致了。

    然后为 Bag 添加属性:

    // 1
    fileprivate var contents: [Element: Int] = [:]
    
    // 2
    var uniqueCount: Int {
      return contents.count
    }
    
    // 3
    var totalCount: Int {
      return contents.values.reduce(0) { $0 + $1 }
    }

    这是 Bag 的基本属性:

    • 用一个作为内部存储结构。因为字典的键是唯一的,你可以用它来存储数据。字典的值则表示每个元素的个数。fileprivate 关键字表示这个属性是隐藏的,在外部不可访问。
    • uniqueCount 返回了字典的每一种对象的统计值,并不累加它们每一个的数量。例如,一个 Bag 中有 4 个橙子和 2 个苹果只会返回 2。
    • totalCount 返回的是 Bag 中所有对象的总计。以同一个例子为例,totalCount 返回值为 6。

    现在,需要几个方法以便增减 Bag 中的内容。在属性声明下面加入:

    // 1
    mutating func add(_ member: Element, occurrences: Int = 1) {
      // 2
      precondition(occurrences > 0, "Can only add a positive number of occurrences")
    
      // 3
      if let currentCount = contents[member] {
        contents[member] = currentCount + occurrences
      } else {
        contents[member] = occurrences
      }
    }

    代码解释如下:

    • add(_:occurrences:) 方法提供了增加元素的方法。它需要两个参数:泛型参数 Element 和一个 Optional 的元素个数。如果 Bag 实例以常量形式 let 定义而不是变量 var 形式定义的话,则这个方法无效。
    • precondition(_:_:)方法的第一个参数是一个 Boolean 表达式,如果为 false,则程序会中断,并在 Debug 窗口输出第二个参数的内容。这个方法有一个前置条件,以保证 Bag 能够被正确使用。这个方法检查了调用 add 方法时符合我们的预设。
    • if 语句判断元素是否已经存在,如果存在,则累加它的计数器,否则加入新的元素。

    另外,你还需要一个删除元素的方法。在 add 方法后新增方法:

    mutating func remove(_ member: Element, occurrences: Int = 1) {
      // 1
      guard let currentCount = contents[member], currentCount >= occurrences else {
        preconditionFailure("Removed non-existent elements")
      }
    
      // 2
      precondition(occurrences > 0, "Can only remove a positive number of occurrences")
    
      // 3
      if currentCount > occurrences {
        contents[member] = currentCount - occurrences
      } else {
        contents.removeValue(forKey: member)
      }
    }

    remove(_:occurrences:) 方法使用的参数和 add 方法一模一样,只不过做了相反的事情:

    • 首先判断元素是否存在,确保在删除它时起码有足够的数目能够被删除。
    • 然后保证元素个数大于 0.
    • 最后,如果元素存在则将元素个数减少。如果元素个数减少后小于等于 0,直接删除整个元素。

    这里,Bag 还不能干更多的事情,甚至无法访问它的内容。你还不能使用 Dictionary 中存在的高阶方法。

    但亡羊补牢为时未晚。我们开始在 Bag 中一一添加这些代码。现在的任务是保持你的代码整洁。

    请先等一下!Swift 提供了让 Bag 符合传统集合的所有工具。
    你需要先了解一下在 Swift 中,让一个对象变成集合需要做些什么。

    自定义集合需要做些什么?

    要理解什么是 Swift 集合,首先需要它继承的协议层次:

    Sequence 协议表示类型支持排序、以迭代的方式访问其元素。你可以把一个 Sequence 对象视作一个元素的列表,允许你挨个挨个地访问其中的元素。

    迭代(Iteration)是一个简单概念,但它能给你的对象提供许多功能。它允许你各种强大的操作比如:

    • map(_:): 用一个闭包将 Sequence 中的每个元素挨个进行转换,并构成另一个数组返回。
    • filter(_:): 用一个闭包过滤所需的元素,将符合闭包谓词所指定条件的元素放到新数组中返回。
    • reduce(_:_:): 用一个闭包将 Sequence 中的所有元素合并成一个值返回。
    • sorted(by:): 根据指定的闭包谓词,将 Sequence 中的元素进行排序并返回排序后的数组。

    这只是其中很少的一部分功能。要查看 Sequence 中提供的所有方法,请查看 Sequence 的文档

    需要说明的一点是,采用 Sequence 协议的类型强制要求是破坏性的或者是非破坏性的。这意味着,在迭代之后,无法保证下一次迭代会从头开始。

    这是一个大问题,如果你的数据准备迭代不止一次的话。要实现非破坏性的迭代,你的对象需要使用 Collection 协议。

    Collection 协议继承了 Sequence 和 Indexable 协议。Collection 和 Sequence 的主要区别是,你可以迭代多次,而且可以用索引来访问。

    实现 Collection 协议之后,你会获得更多“免费”的方法和属性,例如:

    • isEmpty: 返回一个布尔值,表示集合是否为空。
    • first: 返回集合中的第一个元素。
    • count: 返回集合中的元素个数。

    依据集合中的元素类型的不同,你还可能拥有更多的方法和属性。如果你想了解更多,请查看Collection 的文档

    在实现这些协议之前,Bag 还有一个地方需要改进。

    打印对象

    当前,Bag 对象可以用 print(_:) 方法或在 Result Sidebar 视图中暴露出的信息很少。
    在 Playground 中加入如下代码:

    var shoppingCart = Bag<String>()
    shoppingCart.add("Banana")
    shoppingCart.add("Orange", occurrences: 2)
    shoppingCart.add("Banana")
    shoppingCart.remove("Orange")

    这里创建了一个 Bag 对象,并加入了几种水果。如果你查看Playground 的调试窗口,你会看到这些对象的类型信息而不是它保存的内容。

    你可以用 Swift 标准库中的一个协议来解决这个问题。在 shoppingCart 变量上面的 Bag 类型定义结束的 } 之后添加:

    extension Bag: CustomStringConvertible {
      var description: String {
        return String(describing: contents)
      }
    }

    采用 CustomStringConvertible 协议需要实现一个属性,叫做 description。这个属性返回一个实例对象的文字表示。

    在这里,你可以放入任何足以表示你的数据的逻辑。因为字典已经继承了这个协议,你可以简单调用 contents 对象的 description 值。

    看一眼 shopingCart 的 debug 信息:

    漂亮!现在你已经为 Bag 添加了功能,你可以对它的 contents 进行校验了。

    在 Playground 中编写代码时,你可以使用 precondition(_:_:) 来检验返回结果。这会避免你突然破坏之前编写的功能。可以用这个工具作为你的单元测试——将它放到你的日常编码中去做是一个不错的主意!

    在最后一次调用 remove(_:occurrences:) 之后加入:

    precondition("\(shoppingCart)" == "\(shoppingCart.contents)", "Expected bag description to match its contents description")

    如果 shoppingCart 的 description 属性不等于 contents 的 description,则会导致一个错误。

    为了创建我们屌爆了的集合类型,接下来的步骤自然就是初始化。

    初始化

    每次只能加一个元素真的很烦。通常的办法是在初始化的时候用另一个集合来进行初始化。

    这正是我们希望 Bag 能够做到的。在 Playground 的最后加入:

    let dataArray = ["Banana", "Orange", "Banana"]
    let dataDictionary = ["Banana": 2, "Orange": 1]
    let dataSet: Set = ["Banana", "Orange", "Banana"]
    
    var arrayBag = Bag(dataArray)
    precondition(arrayBag.contents == dataDictionary, "Expected arrayBag contents to match \(dataDictionary)")
    
    var dictionaryBag = Bag(dataDictionary) 
    precondition(dictionaryBag.contents == dataDictionary, "Expected dictionaryBag contents to match \(dataDictionary)")
    
    var setBag = Bag(dataSet)
    precondition(setBag.contents == ["Banana": 1, "Orange": 1], "Expected setBag contents to match \(["Banana": 1, "Orange": 1])")

    无法进行编译,因为还没有定义针对这些类型的初始化方法。不要为每种类型创建一种初始化方法,你可以使用泛型。
    在 Bag 定义的 totalCount 后面添加:

    // 1
    init() { }
    
    // 2
    init<S: Sequence>(_ sequence: S) where S.Iterator.Element == Element {
      for element in sequence {
        add(element)
      }
    }
    
    // 3
    init<S: Sequence>(_ sequence: S) where S.Iterator.Element == (key: Element, value: Int) {
      for (element, count) in sequence {
        add(element, occurrences: count)
      }
    }

    代码解释如下:

    • 首先,创建一个空的 init 方法。在定义了其他初始化方法之后,你必须添加这个方法,否则编译器会报错。
    • 然后,定义一个初始化方法并接受一个元素为 Sequence 集合的参数。sequence 参数的类型必须和元素类型匹配。例如,Array 和 Set 对象。然后,对 sequence 进行迭代,挨个添加元素。
    • 最后一个方法类似,但元素类型变成元素了(Element, Int)。这种情况最典型的例子就是字典。 这里,你依然对 sequence 中的元素进行迭代并以指定的个数来添加元素。

    这些泛型初始化方法为 Bag 对象添加了大量的数据源。但是,它们仍然你初始化另一个 Sequence 对象然后传递给 Bag。

    为了避免这个,Swift 标准库提供了两个协议。这两个协议支持以 Sequence 的写法进行初始化。这种写法能让你用更简短的方式定义数据,而不必显式地创建一个对象。

    在 Playground 最后加入下列代码,来看看如何使用这种写法:

    var arrayLiteralBag: Bag = ["Banana", "Orange", "Banana"]
    precondition(arrayLiteralBag.contents == dataDictionary, "Expected arrayLiteralBag contents to match \(dataDictionary)")
    
    var dictionaryLiteralBag: Bag = ["Banana": 2, "Orange": 1]
    precondition(dictionaryLiteralBag.contents == dataDictionary, "Expected dictionaryLiteralBag contents to match \(dataDictionary)")

    没说的,编译器报错了,我们后面再来解决。这种写法用初始化数组和字典的写法来进行初始化,而不需要创建对象。

    在 Bag 的其它扩展之后定义两个扩展:

    extension Bag: ExpressibleByArrayLiteral {
      init(arrayLiteral elements: Element...) {
        self.init(elements)
      }
    }
    
    extension Bag: ExpressibleByDictionaryLiteral {
      init(dictionaryLiteral elements: (Element, Int)...) {
        // The map converts elements to the "named" tuple the initializer expects.
        self.init(elements.map { (key: $0.0, value: $0.1) })
      }
    }

    ExpressibleByArrayLiteral 和 ExpressibleByDictionaryLiteral 扩展需要实现一个初始化方法,以处理它们对应的参数的那种写法。由于前面已经定义的初始化方法,它们的实现都非常简单。

    现在 Bag 已经非常像原生的集合类型了,我们该来点猛货了。

    Sequence

    集合类型的最常用的操作是对其元素进行迭代。来看一个例子,在 Playground 最后添加如下代码:

    for element in shoppingCart {
      print(element)
    }

    超级简单。就像数组和字典一样,你可以遍历一个 Bag 对象。因为 Bag 还没有实现 Sequence 协议,编译不能通过。
    在 ExpressibleByDictionaryLiteral 扩展后加入另一个扩展:

    extension Bag: Sequence {
      // 1
      typealias Iterator = DictionaryIterator<Element, Int>
    
      // 2
      func makeIterator() -> Iterator {
        // 3
        return contents.makeIterator()
      }
    }

    不需要实现太多方法。代码解释如下:

    • 定义了一个类型别名,叫做 Iterator,这是 Sequence 中指定的,需要实现 IteratorProtocol 协议。DictionaryIterator 类型是字典用于迭代其元素的。你可以用它,因为 Bag 在底层使用了字典来存储数据的。
    • makeIterator() 方法返回一个 Iterator,它会用来对序列中的每个元素进行迭代。
    • 调用 contents 的 makeIterator() 方法创建一个 Iterator,它已经实现了 Sequence 协议。

    这就是 Bag 对 Sequence 协议进行实现的全部。
    你现在可以迭代 Bag 对象中的每个元素了,并可以获得每个对象的个数。在之前的 for-in 循环后加入:

    for (element, count) in shoppingCart {
      print("Element: \(element), Count: \(count)")
    }

    打开 Debug 视图,你会看到每个元素都打印出来了:

    实现 Sequence 协议之后,你就可以使用许多 Sequence 中有的方法了。

    在 Playground 最后加入代码试一试:

    // 查找所有数目大于 1 的对象
    let moreThanOne = shoppingCart.filter { $0.1 > 1 }
    moreThanOne
    precondition(moreThanOne.first!.key == "Banana" && moreThanOne.first!.value == 2, "Expected moreThanOne contents to be [(\"Banana\", 2)]")
    
    // 获取所有对象的数组(不需要数量)
    let itemList = shoppingCart.map { $0.0 }
    itemList
    precondition(itemList == ["Orange", "Banana"], "Expected itemList contents to be [\"Orange\", \"Banana\"]")
    
    // 获得所有对象的加总数据
    let numberOfItems = shoppingCart.reduce(0) { $0 + $1.1 }
    numberOfItems
    precondition(numberOfItems == 3, "Expected numberOfItems contents to be 3")
    
    // 所有商品按照数量降序排序
    let sorted = shoppingCart.sorted { $0.0 < $1.0 }
    sorted
    precondition(sorted.first!.key == "Banana" && moreThanOne.first!.value == 2, "Expected sorted contents to be [(\"Banana\", 2), (\"Orange\", 1)]")

    所有 Sequence 对象能有的方法都能用——它们完全是免费的。

    现在,你可能满足于以这种方式使用 Bag,但还有比这更好玩的吗?当前的 Squence 实现仍然还有改进的余地。

    加强版的 Sequence

    当前,你依赖于字典为你提供底层支持。这很好,对你来说,这不乏为一种轻松实现功能强大的集合的方式。问题是,它会让 Bag 的用户感到奇怪和困惑。

    例如,Bag 会返回一个 DictionaryIterator 类型的 Iterator 好像不妥。你可以创建自己的 Iterator 类型,但这次不是免费的了。

    Swift 提供了一个 AnyIterator 类型,将底层的 itertator 隐藏起来。
    将 Sequence 的实现修改为:

    extension Bag: Sequence {
      // 1
      typealias Iterator = AnyIterator<(element: Element, count: Int)>
    
      func makeIterator() -> Iterator {
        // 2
        var iterator = contents.makeIterator()
    
        // 3
        return AnyIterator {
          return iterator.next()
        }
      }
    }

    Playground 报了一堆错,等会来解决。除了使用了一个 AnyIterator 外,这个实现和之前没有太大区别:

    1. AnyIterator 是一个无类型的 Iterator,它只暴露出底层实际的 Iterator 的 next() 方法给你。这样你就可以将实际使用的 Iterator 类型隐藏起来。
    2. 跟前面一样,通过 contents 创建了一个新的 DictionaryIterator 实例。
    3. 最后,将 Iterator 包装成 AnyIterator 以传递其 next() 方法。

    现在来解决先前的报错。你看到的是这两个错误:

    前面,你用 DictionaryIterator 的元组命名为 key 和 value。现在你已经将 DictionaryIterator 隐藏起来了,并且将元组的名字修改为 element 和 count。要解决这个错误,将 key 和 value 替换成 element 和 count。

    这样你的 precondition 语句的问题就解决了。这就是前置条件的好处了,它能保证某些东西不会被意外修改。

    现在,任何人都不知道你在用字典进行所有的工作。
    现在的 Bag 让你感觉更好了吧?是时候让它回家了。呃,将你激动的心情收起来吧,它是属于集合的!

    Collectdion

    闲话少说,接下来还有一道大菜,创建一个集合……即 Collection 协议!再次声明,集合是能够通过索引进行访问并进行多次非破坏性迭代的结合。

    为了符合 Collection 协议,你需要提供这些数据:

    1. startIndex 和 endIndex: 指定集合的边界,并说明遍历的起点。
    2. subscript (position:): 允许你通过索引找到集合中的任意元素。这个访问方法的时间复杂度应控制在 0(1) 上。
    3. index(after:): 返回传入的索引的下一个索引。

    使用 Cellection 协议时只需要这 4 个数据。在 Sequence 扩展后新增扩展:

    extension Bag: Collection {
      // 1
      typealias Index = DictionaryIndex<Element, Int>
    
      // 2
      var startIndex: Index {
        return contents.startIndex
      }
    
      var endIndex: Index {
        return contents.endIndex
      }
    
      // 3
      subscript (position: Index) -> Iterator.Element {
        precondition(indices.contains(position), "out of bounds")
        let dictionaryElement = contents[position]
        return (element: dictionaryElement.key, count: dictionaryElement.value)
      }
    
      // 4
      func index(after i: Index) -> Index {
        return contents.index(after: i)
      }
    }

    代码非常简单:

    1. 首先用 DictionaryIndex 来声明一个 Index 类型,DictionaryIndex 在 Collection 协议中已定义。注意,其实编译器会基于你后面的实现来推断这个类型,但为了保持代码的清晰和可维护性,我们显示地指定了类型。
    2. 然后用 contents 来返回第一个索引和最后一个索引。
    3. 用一个前置条件来强制校验索引的有效性。然后,以元组的方式返回 contents 中位于该索引的元素。
    4. 最后,调用 contents.index(after:) 方法并返回结果。

    通过添加几个属性和方法,你创建了一个功能完整的集合!在 Playground 最后用几行代码来测试这些功能:

    // 读取 Bag 中的第一个对象
    let firstItem = shoppingCart.first
    precondition(firstItem!.element == "Orange" && firstItem!.count == 1, "Expected first item of shopping cart to be (\"Orange\", 1)")
    
    // 判断 Bag 是否为空
    let isEmpty = shoppingCart.isEmpty
    precondition(isEmpty == false, "Expected shopping cart to not be empty")
    
    // 获取 Bag 中的商品种类数
    let uniqueItems = shoppingCart.count
    precondition(uniqueItems == 2, "Expected shoppingCart to have 2 unique items")
    
    // 查找第一个名为 Banana 的元素
    let bananaIndex = shoppingCart.indices.first { shoppingCart[$0].element == "Banana" }!
    let banana = shoppingCart[bananaIndex]
    precondition(banana.element == "Banana" && banana.count == 2, "Expected banana to have value (\"Banana\", 2)")

    漂亮!(当你对自己所做的一切感到心满意足时,“等等,你还可以做得更好”这句话又在等着你了……)

    是的,你说的没错!你可以做得更好。仍然能够从 Bag 中看出一丝字典的模样。

    加强版的 Collection

    Bag 暴露了太多底层实现细节。Bag 的用户仍然需要使用 DictionaryIndex 对象去访问集合中的元素。
    这个很好搞定。在 Collection 扩展后面增加:

    // 1
    struct BagIndex<Element: Hashable> {
      // 2
      fileprivate let index: DictionaryIndex<Element, Int>
    
      // 3
      fileprivate init(_ dictionaryIndex: DictionaryIndex<Element, Int>) {
        self.index = dictionaryIndex
      }
    }

    没有任何新奇的玩意儿,但我们还是来过一下吧:

    1. 定义了一个泛型类型 BagIndex,和 Bag 一样,为了访问字典对象,它需要一个 Hashable 的泛型参数。
    2. index 类型的真实类型是一个私有的 DictionaryIndex 对象。BagIndex 仅仅是一个封装,隐藏了它真正的 index 类型。
    3. 最后,创建一个私有的初始化方法,接收一个 DictionaryIndex 参数。

    Collection 对象需要索引对象实现 Comparable 协议,能够对两个索引进行比较以进行某些操作。因此,BagIndex 必须实现 Comparable 协议。在 BagIndex 扩展之后添加:

    extension BagIndex: Comparable {
      static func == (lhs: BagIndex, rhs: BagIndex) -> Bool {
        return lhs.index == rhs.index
      }
    
      static func < (lhs: BagIndex, rhs: BagIndex) -> Bool {
        return lhs.index < rhs.index
      }
    }

    这个逻辑非常简单;方法返回的结果直接调用 DictionaryIndex 已实现的 Comparable 协议的相同方法。

    现在将 Bag 修改为使用 BagIndex。将 Collecdtion 扩展替换成:

    extension Bag: Collection {
      // 1
      typealias Index = BagIndex<Element>
    
      var startIndex: Index {
        // 2.1
        return BagIndex(contents.startIndex)
      }
    
      var endIndex: Index {
        // 2.2
        return BagIndex(contents.endIndex)
      }
    
      subscript (position: Index) -> Iterator.Element {
        precondition((startIndex ..< endIndex).contains(position), "out of bounds")
        // 3
        let dictionaryElement = contents[position.index]
        return (element: dictionaryElement.key, count: dictionaryElement.value)
      }
    
      func index(after i: Index) -> Index {
        // 4
        return Index(contents.index(after: i.index))
      }
    }

    注释中的数字标出了修改之处。分别解释如下:

    1. 首先将 Index 的类型从 DictionaryIndex 修改为 BagIndex。
    2. 然后,是 startIndex 和 endIndex,你换成新的 BagIndex。
    3. 接着,用这个 BagIndex 从 contents 后访问对应元素并返回。
    4. 最后,结合上面几个步骤。从 contents 中获取 DictionaryIndex,然后用它来创建一个 BagIndex。

    就这样!用户不会知道你底层是用什么来存储数据的了。你未来还有可能对索引对象的获得更大的控制。

    在完成之前,还有一个很重要的地方。除了基于索引来访问元素,你还以通过一段连续索引来访问集合中的值。

    要实现这个,你可以看一下集合中是如何进行切片操作的。

    切片

    切片就是查看集合中多个连续的元素。它允许你在集合元素的子集上进行某些操作,而不用复制这些元素。
    切片只会保存原来集合中的元素的引用。它还存储了元素子集的起、始索引。切片的时间复杂度为 0(1),因为它们直接引用了它的原始集合。

    切片直接重用原始集合的索引,这使得它们尤其有用。
    要测试切片操作,在 Playground 最后添加代码:

    // 1
    let fruitBasket = Bag(dictionaryLiteral: ("Apple", 5), ("Orange", 2), ("Pear", 3), ("Banana", 7))
    
    // 2
    let fruitSlice = fruitBasket.dropFirst() // No pun intended ;]
    
    // 3
    if let fruitMinIndex = fruitSlice.indices.min(by: { fruitSlice[$0] > fruitSlice[$1] }) {
      // 4
      let minFruitFromSlice = fruitSlice[fruitMinIndex]
      let minFruitFromBasket = fruitBasket[fruitMinIndex]
    }

    我们来看一下这些代码做了些什么,以及它们的意思:

    1. 首先,创建一个由 4 种水果构成的水果篮。
    2. 然后拿走第一种水果。这会创建一个水果篮的切片,但第一种水果不见了。
    3. 通过切片找到数量最少的水果的索引。
    4. 尽管上一步的索引是从切片中得到的,你可以把这个索引同时用在原来的集合和后面的切片中来访问对象。

    注意:切片对于基于哈希的字典和 Bag 来说,用处不大,因为它们的顺序在任何方向上都是不确定的。而数组则相反,数组是集合中的一个极端例子,在执行顺序操作时,切片扮演很重要的角色。

    祝贺你——你现在是一个集合方面的专家了!你可以创建任意内容的 Bag 对象来表示庆祝。

    结束

    完整的 Playground 代码可以在这里下载。如果你想了解或者实现更完整的 Bag,请从 github 中 checkout 项目。

    在这篇文章里,你学习了在 Swift 中,如何通过一个数据结构来创建自己的集合。你使用了 Sequence 、Collection 、CustomStringConvertible、 ExpressibleByArrayLiteral、ExpressibleByDictionaryLiteral 协议以及资第一的索引类型。

    这仅仅是对 Swift 提供的用于创建健壮、使用的结合类型的协议的一点尝试。如果你想看一下还有什么其他的协议,你可以参考:

    希望你能喜欢这篇教程!创建自定义的集合并不是一个常见的需求,当它能加深你对 Swift 内置集合类型的裂解。

    如果有任何疑问或建议,请在下面留言。

    展开全文
  • 假设你有一个需要处理许多数据的应用。你会把收据放在哪儿?...在那种情况下,你将会需要一种基本的集合类数据结构。幸运的是,这篇教程就是关于那个主题的。下面是这篇教程的构成: 你将会复习什么是数据

    原文链接:Collection Data Structures In Swift
    原文日期:2015-04-21 Xcode6.3 Swift1.2

    译者:Yake
    校对:shanks
    定稿:numbbbbb

    假设你有一个需要处理许多数据的应用。你会把收据放在哪儿?你怎么样高效地组织并处理数据呢?

    如果你的项目只处理一个数字,你把它存在一个变量中。如果有两个数字你就用两个变量。
    如果有1000个数字,10,000个字符串或者终极模因库呢(能马上找到一个完美的模因不是很好吗)?在那种情况下,你将会需要一种基本的集合类数据结构。幸运的是,这篇教程就是关于那个主题的。

    下面是这篇教程的构成:

    • 你将会复习什么是数据结构并学习大O符号。这是用来描述不同的数据结构性能的标准工具。
    • 下一步:通过衡量arrays,disctionaries 以及 sets 这些 Cocoa 开发中最基本的数据结构的性能进行练习。同时这篇文章也会介绍一些基础的性能测试的方法。
    • 继续学习,你将会比较成熟的 Cocoa 数据结构与对应的纯 Swift 结构的性能。
    • 最后,你会简要地复习一下 Cocoa 中提供的一些相关类型。下面关于这些数据结构的内容可能让你很惊讶,但其实都是你常用的那些。

    你想让你的数据结构表现出怎样的性能呢?

    开始

    在你深入探索iOS中使用的数据结构之前,你应该先复习一下他们是什么,以及怎么测量他们的性能这些基本的概念。

    有许多集合类数据结构的类型,每一个都代表了一个特定的方法去组织并获取一个数据的集合。集合类型可能会使一些操作变得十分高效,例如添加新的数据,查找最小的数据以及保证不会重复添加数据。

    没有集合数据类型,你将会陷入试图一个一个地管理数据的困境中。集合能够让你实现:
    * 将所有的数据作为一个实体来处理
    * 给他们设置一些结构
    * 高效地插入、删除、并检索数据

    什么是大O标记

    大O,说的是字母O,而不是数字0,符号是一种描述某种数据结构上的某种操作的效率的方法。有很多种衡量效率的方法:你可以衡量这种数据结构消耗了多少内存,在最坏的情况下消耗的时间,它花费的平均时间是多少等等。

    在这篇教程中,你将会衡量一种操作消耗的平均时长。

    通常,大量的数据不会使得操作更快。大多数情况下是相反的,但有时候差别很小甚至没有差别。大O标记是一种描述这种情况的精确的方法。你能够写出一个具体的函数式来大概表示基于结构中数据的数量运行时间的变化情况。

    当你看到大O标记被写作O(与n相关的函数),括号中的n就表示数据结构中数据的数量,而”与n相关的函数”则大约表示操作将要消耗的时间。

    “大约”,确实有点讽刺,但是它有特定的含义:当n非常大时函数的渐进值。假设n是很大很大的数,当你将参数从n修改为n+1时,考虑一些操作的性能会怎样变化。

    注意:这些都是算法复杂度分析中的一部分,如果你想深入探索的话,就多读些计算机科学类的书籍。你将不再束手无策,你会发现一些分析复杂度的数学方法,不同效率之间的微小差异,关于未知机器模型的更精细化的公式化的假设,以及更多其他你能想到或者想象不到的有趣的东西。

    常见的大O性能测量如下:排列顺序是由最好的性能到到最差的性能

    • O(1)(稳定的时间):无论数据结构中有多少数据,这个函数都调用同样数量的操作。这是理想的性能。
    • O(log n)(对数级):这个函数调用的操作数量随着数据结构中的数据量对数级增长。这是很好的性能了,因为相比较数据结构中的数据的数量它的增长速度要慢得多。
    • O(n)(线性):函数调用的操作数随着数据结构的数据量线性增长。这是较为合适的性能,但是如果是较大的数据集合就不合适了。
    • O(n (log n)):这个函数调用操作的数量是由数据结构中的数据量的对数乘以数据结构中的数据量。可以预见的是,这是现实中对性能的最低容忍度。较大的数据结构会执行更多的操作,对于数据量较小的数据结构来说增长是较为合理的。
    • O(n²)(平方):这个函数调用操作的数量是数据结构大小的平方 - 是差性能中最好的。即使你是在处理一些很小的数据结构它也会很快就变得特别慢。
    • O(2^n)(指数):函数调用的操作数与数据结构是2的n次方的关系。这会导致很差的性能并很快就变得特别慢。
    • O(n!) (阶乘):函数调用的操作数与数据结构之间是阶乘的关系。实际上,这是性能最差的情况。例如,在一个有100个数据的结构中,操作的数量的乘积是个158位的数。

    下面是一个更直观的性能图。当集合中的数据量从1变化至25时,性能会怎么降低:

    图片

    你有没有发现你几乎看不到绿色的O(log n)线?因为在这个范围内它几乎与理想中的O(1) 重合。那太好了!另一方面, O(n!)和O(2^n)标记的操作性能下降得特别快,当集合中有超过10个数据时操作的数量在图表中飙升了起来。
    是的!正如图表中清楚展示的,你处理的数据越多,选择正确的数据结构就越重要。

    现在你已经知道怎么比较基于某种数据结构的数据操作的性能了。下面我们来复习一下iOS中最常用的三种数据类型以及他们在理论和实践中是如何发挥作用的。

    常见iOS数据结构

    iOS中三种最常用的数据结构是arrays,dictionaries 和 sets。首先你要考虑他们与作为基本抽象的完美类型的区别,然后你需要检查iOS提供的代表这些抽象的具体类的性能。(First you’ll consider how they differ in ideal terms as fundamental abstractions, and then you’ll examine the performance of the actual concrete classes which iOS offers for representing those abstractions.)首先你得从字面上去理解他们作为基本的抽象类型的区别,然后你需要检查在 iOS 中这些具体类表现的性能,从而知道 iOS 为啥提供这些抽象命名给它们。

    对于这三种主要结构的类型来说,iOS 提供了多种具体的类来支持抽象的结构。除了在 Swift 和Objective-C 中旧的 Foundation 框架中的数据结构,现在又有了新的仅支持 Swift 版本的数据结构与语言紧密结合在一起。

    Arrays

    数组就是以一定顺序排列的一组数据,你可以通过索引来获取每一个数据元素,索引代表数据在排列中的位置。当你在数组变量名称后面的括号中写上索引时,这就叫做下标。

    在 Swift 中,如果你用let将数组作为常量来定义,他们就是不可变的,如果用 var 定义为变量他们就是可变的。

    作为对比,Foundation 框架中的 NSArray 默认是不可变类型,如果你想在数组创建之后添加、删除或者修改数据,你必须使用可变类 NSMuatbleArray

    NSArray 是异质的,那也就意味着他可以包含不同类型的 Cocoa 对象。 Swift 数组是同质的,意味着每一个 Swift 数组都只包含一种类型的对象。

    不过,你仍然可以通过将一个类型定义为 AnyObject 类型,使定义的 Swift 数组能够存储可变的 Cocoa 对象类型。

    期望性能以及什么时候使用数组

    使用数组来存储变量的一个首要原因是顺序能发挥重要作用。例如,你会通过名或者姓来排列联系人,按日期写一个计划列表,或者任何很需要通过某种顺序来查找或者展示数据的情形。

    苹果的文档在CFArray header一篇中阐明了三种关键的预期:

    • 在数组中通过一个特定的索引获取值的时间复杂度最坏是O(log n),但通常应该是O(1)。
    • 搜索一个未知索引的对象的时间复杂度最坏是O(n (log n)),但一般应该是O(n)。
    • 插入或者删除一个对象最坏的时间复杂度是O(n (log n)),但经常是O(1)。

    这些保证了数组会基本如你在计算机科学的教材或者是C语言中那样,总是一段连续内存中的数据序列,是一种“理想型”数组。

    将它作为一个有效的提醒去查看文档。

    实际上,当你考虑下面这些的时候这些预期很有意义:

    • 当你已经知道一个数据在哪儿并且需要查找时,那应该是很快的。
    • 如果你不知道某个数据在哪儿,你可能需要将数组从头到尾遍历一遍,查找的过程可能会比较慢。
    • 如果你知道要把某个数据添加到哪里或者从哪儿删除,那不是很困难,虽然后来你可能需要调整数组的剩余部分,那得花费些时间。

    这些预期与现实是一致的吗?继续往下看:

    示例App测试结果

    从这篇教程中下载示例项目并在 Xcode 中打开。里面有些方法可以创建或者/并且测试数组,并且展示给你执行每个任务消耗的时间。

    需要注意的是:在应用中,测试配置会自动将优化选项设置成和发布配置一样的选项。这样当你测试app的时候,你就能获得和真实环境一样的优化选项。

    你需要至少1000个数据才可以用示例app进行测试。当你编译运行的时候,滑块会被设置到1000.点击Create Array and Test按钮,你将马上进入测试:

    将滑块向右拖动直到数据达到10,000,000,再次按下Create Array and Test按钮,看看这个明显增大的数组的创建时间与刚才有什么不同:

    这些测试是在 Xcode6.3(内含Swift1.2),iOS 8.3系统,iPhone6 模拟器上运行的。在数据增加了10,000倍的情况下,创建数组花费的时间只消耗了714倍。

    O(n)函数意味着增加10,000倍的数据将会消耗10,000倍的时间,O(log n)函数意味着增加10,000倍的数据将会消耗4倍的时间。这个例子中的性能处于O(log n)和O(n)之间,这是相当稳定的。

    这个过去一直都消耗了较长的时间。很久之前写的这篇教程的最初版本,是运行在Xcode6.0 Gold Master版(Swift6.0),iOS8 Gold Master版,iPhone5 上,测试消耗了相当长的时间:

    图图1

    图图2

    在这个例子中,大约106.5倍的数据,消耗了大约1,417倍的时间去创建数组。

    如果要实现O(n)的性能,106.5倍的数据预期消耗时间应该是3.7秒。而以O(n log n)增长,预期消耗时间是33.5秒。以O(n²)增长,预期消耗时间395秒。

    所以在这个例子中,Swift 中的 Array 的创建时间大约在O(n log n)和O(n²)之间。如你所见,当你的数据增加数十倍的时候时间消耗就过大了。

    这个时间就不容乐观了。不过,更新至 Swift1.2 之后,你创建多大的数组都没有问题!

    那么 NSMutableArray 呢?你仍然可以在 Swift 中调用 Foundation 的类而不需要使用Objective-C,你会发现还有一个Swift类叫做 NSArrayManipulator 遵守同样的协议 ArrayManipulator

    正因如此,你可以轻松将 NSMutableArray 替换成 Swift 中的数组。项目代码很简单,你可以尝试修改一行来对比 NSMuatbleArray 的性能。

    打开ArrayViewController.swift文件并将第27行由:

    let arrayManipulator: ArrayManipulator = SwiftArrayManipulator()

    修改为:

    let arrayManipulator: ArrayManipulator = NSArrayManipulator()

    再次编译运行,点击 Create Array and Test 测试有1000个数据的NSMutableArray 数组的创建:

    然后再次修改数据为10,000,000:

    创建操作原始的性能不如 Swift1.2 中的好 - 虽然一部分可能是因为需要在那些可以存储在NSArray 中和他们对应的Swift的类型中的对象之间来回操作。

    不管怎样,你创建一个数组只需要一次 - 但是需要经常执行的是其他一些操作例如查找,添加,或者是移除对象。

    在更深入的测试中,例如当你使用 Xcode6 中的一些性能测试方法分别调用每一个方法50次,一种模式就开始呈现了:

    • 创建一个 Swift 数组和 NSArray 数组消耗相同的时间 - 介于O(log n)和O(n)之间。而至于拥有5,000到25,000个数据的数组,Swift 比 Foundation 花费大约三倍的时间 - 但是都低于0.02秒。
    • 在 Swift 中向一个数组结构的对象的开始或者是中间添加数据,相对于 Foundation 中(大约在O(1)左右)是慢很多的。向 Swift 数组的结尾添加数据(时间少于O(1))实际上比向 NSArray 的结尾添加数据(多于O(1))要快。
    • 在 Foundation 和 Swift 数组结构中移除对象操作的性能表现是很相似的:从开头,之间,或者结尾移除一个对象时间大约在O(log n)和O(n)之间。从数组的开头移除数据这一操作在Swift中性能略差,但是时间差异是毫秒级的。
    • 通过索引查找所消耗的时间在Swift数组以及 NSArray 中以相同的速率增长。当你通过索引查找数据,数组很大时,Swift 数组消耗的时间相比使用 NSMutableArray 类型的对象快将近10倍。

    注意:你可以在iPhone6性能测试电子表格程序中获得将每个方法运行50次的结果(环境:Xcode 6.3 已发布版本,Swift 1.2,iOS 8.3系统,iPhone6)。你也可以尝试运行应用中包含的测试看看每运行10次的平均结果,以及他们在你手机上是如何运行的。

    字典

    字典是一种不需要特定的顺序且数据都与唯一的键相联系的储值方式。你可以使用键来存储或者是查询一个值。

    字典也使用下标语法。当你写dictionary["hello"],你就会得到与键hello对应的值。

    像数组一样,在 Swift 中你如果用let来声明字典它就是个常量,如果用var
    来声明它就是可变的。在 Foundation 框架方面也是类似的,有 NSDictionaryNSMutableDictionary 两种类型供你使用。

    与 Swift 数组相似的另外一个特征就是字典也是强类型的(即对于程序里的每一个变量,在使用前必须声明类型,赋值的时候也必须与变量的类型相同),你必须有已知类型的键值对。NSDictionary 类型的对象可以以任何 NSObject 的子类对象作为 key,可以存储任意类型的对象为值。

    当你调用某个使用或者返回一个字典类型的 Cocoa 的 API 时,你就会看到它在实际中的样子。在 Swift 中,这个类型以 [NSObject: AnyObject] 这种形式来描述。也就是说这个类型必须以 NSObject 的子类作为键,而值可以是任何兼容 Swift 的对象。

    什么时候使用字典

    当你要存储的数据没有特定的顺序,而又有某种意义上的关联的时候,字典是最好的选择。

    为了帮你检验字典以及这篇教程中其他的数据结构是如何工作的,通过File…\New\Playground创建一个Playground,并把它命名为 DataStructures。

    例如,你需要存储一个数据结构,他们包含了你所有的朋友以及他们的猫的名字,这样你就可以通过你朋友的名字查找他们的猫的名字。这样一来,你就不需要记住猫的名字以获得朋友的亲睐。

    首先,你需要存储朋友和猫的字典:

    let cats = [ "Ellen" : "Chaplin", "Lilia" : "George Michael", "Rose" : "Friend", "Bettina" : "Pai Mei"]

    由于 Swift 的类型推断,上述类型被定义为 [String: String] - 键值均为字符串类型的字典。

    现在尝试在里面获取数据:

    cats["Ellen"] //returns Chaplin as an optional
    cats["Steve"] //Returns nil

    注意字典中的下标语法返回一个可选类型。如果字典中不包含某个特定键的值,可选类型为nil;如果包含那个键对应的值,你就可以将其解包获取值。

    这样一来,使用if let可选类型解包语法从字典中获取值就是个很不错的办法:

    if let ellensCat = cats["Ellen"] {
        println("Ellen's cat is named \(ellensCat).")
    } else {
        println("Ellen's cat's name not found!")
    }

    由于确实存在‘Ellen’对应的值,你的 playground 中就会打印‘Ellen’s cat is named Chaplin’

    预期的性能

    苹果再一次在 Cocoa 的CFDictionary.h头文件中概括了字典的期望性能:

    1.从字典中获取值的时间复杂度最坏是O(n),但一般应该是O(1)
    2.插入和删除数据的时间复杂度最坏是O(n²),但由于顶层的优化通常更接近O(1)

    这些没有数组中的性能表现那样明显。由于存储键值对相较于一个有序的数组有更复杂的本质,所以性能不那么好预测。

    示例程序测试结果

    DictionaryManipulator是跟ArrayManipulator类似的一个协议,它可以测试字典。你可以用它测试SwiftDictionaryNSMutableDictionary在执行相同的操作时的性能。

    为了比较 Swift 和 Cocoa 字典,你可以使用与上述数组的测试程序相似的步骤。编译运行app,选择底部的Dictionary标签。

    运行几个测试程序 - 你会发现创建一个字典比创建数组消耗的时间要长得多。如果你把数据滑块滑到10,000,000的位置,你可能会收到内存警告甚至内存溢出崩溃!

    回到 Xcode 中,打开DictionaryViewController.swift文件并找到dictionaryManipulator属性:

    let dictionaryManipulator: DictionaryManipulator = SwiftDictionaryManipulator()

    用下面的代码替换它:

    let dictionaryManipulator: DictionaryManipulator = NSDictionaryManipulator()

    现在应用底层将会使用 NSDictionary。再次编译运行app,多进行几次测试。如果你是在 Swift1.2 上运行的,你的发现可能与一些扩展测试中的发现相一致:

    • 在时间消耗上来看,创建 Swift 字典比创建 NSMutableDictionaries 要慢得多 - 但性能都以O(n)的速率降低。
    • 向 Swift 字典中添加数据的时间比向 NSMutableDictionaries 类型的对象中添加数据大约快3倍,并且性能如苹果的文档中保证的那样,最好能达到O(1)的速率。
    • Swift 和 Foundation 的字典都能以大约O(1)的速率进行查找。不像 Swift 数组,字典并不比 Foundation 中的字典性能要好,但是他们非常得接近。

    现在,进入 iOS 中最后一个主要的数据结构:集合!

    集合

    集合是一种存储无序的,值唯一的数据结构。当你向一个集合中添加同样的数据时,数据不会被添加。这里的“同样地”即满足方法isEqual()

    Swift 在1.2版本中添加了对原生Set类型的支持 - 早期的Swift版本,你只能获取Foundation框架下的NSSet类型。Swift 集合中的元素,也必须拥有相同的类型。

    注意,就像数组和字典那样,对于一个原生的 Swift 集合来说,如果你用let关键字进行声明它就是常量,如果用var声明就是可变的。在 Foundation 方面,也都有 NSSetNSMutableSet 供你使用。

    什么时候使用集合

    当数据的唯一性很重要而顺序无所谓时可以使用集合。例如,如果你想从八个名字的数组中随机选出四个名字并且没有重复时,你会选择什么呢?

    在你的 Playground 中输入下面的代码:

    let names = ["John", "Paul", "George", "Ringo", "Mick", "Keith", "Charlie", "Ronnie"]
    var stringSet = Set<String>() // 1
    var loopsCount = 0
    while stringSet.count < 4 {
        let randomNumber = arc4random_uniform(UInt32(count(names))) //2
        let randomName = names[Int(randomNumber)] //3
        println(randomName) //4
        stringSet.insert(randomName) //5
        ++loopsCount //6
    }
    
    //7
    println("Loops: " + loopsCount.description + ", Set contents: " + stringSet.description)

    在这一小段代码中,你在做下面的事情:

    1.初始化集合,这样你就可以在里面添加数据。
    2.在0和名字的个数中间选一个随机值。
    3.获取所选数字为索引对应的名字。
    4.打印出选择的名字。
    5.将选择的名字添加至可变的集合。记住,如果名字已经在集合里面了,集合就不会变化,因为集合不会存储重复的数据。
    6.loop计数器一直在增加,这样你就可以看到循环执行了多少次。
    7.一旦循环结束,打印loop计数器的值和可变集合中的内容。

    在这个例子中我们使用了一个随机数字生成器,你每次都能获取到一个不同的结果。下面的示例是写这篇教程时的其中一次运行结果:

    John
    Ringo
    John
    Ronnie
    Ronnie
    George
    Loops: 6, Set contents: ["Ronnie", "John", "Ringo", "George"]

    在这儿,为了得到唯一的名字循环执行了6次。它分别选择了 Ronnie 和 John 两次,但是只在集合中添加了一次。

    当你在 playground 中写下这个循环时,你会注意到它一遍一遍地执行,每次循环你都会得到一个不同的数字。每次运行都至少有四次循环,因此集合中必须有四个数据才能跳出循环。

    现在你已经看到了一个较小的集合是怎样工作的,下面我们来看看存储较大数据量的集合的性能。

    示例应用测试结果

    不像数组和字典,苹果没有概括集合性能的大概的预期(Swift集合文档中对几个方法的性能有预期描述,但是没有 NSMutableSet 对应的预期),所以在这里你只需要观察实际情况中的性能。

    Swift1.2 版本的示例项目在 SetViewController 类中有 NSSetManipulatorSwiftSetManipulator 对象,这与 ArrayDictionary 的视图控制器中的配置类似,并且也可以以同样地方式进行替换。

    在这两种情况下,如果你追求纯粹的速度,使用Set 可能不能令你满意。SetNSSet 相较于 ArrayNSMutableArray ,你会发现集合的时间要慢得多 - 这就是你为了检查每一个数据在数据结构中是否是唯一所付出的代价。

    进一步的测试显示由于 Swift 的集合相对于数组和字典来说,处理数据花费的时间要多一些,他的性能以及执行大多数操作所消耗的时间都与 NSSet 非常的相似:创建,移除,以及查找操作对于 FoundationSwift 来说消耗的时间几乎是相同的。

    SwiftFoundation 中创建一个集合类型的对象消耗时间大约都以O(n)增长 - 这与预期相符,因为集合中的每一个数据被添加进来之前都要检查是否与已有数据相等。

    SwiftFoundation 中删除和查找操作的性能大约是O(1)。这种情况是很稳定的,因为集合结构使用哈希值来检查是否相等,而哈希值可以以某个顺序进行计算和存储。这就使得查找操作明显比数组中的查找要快。

    NSMutableSet 和 Swift 原生的 Set 性能上主要的差异在于添加对象的操作。

    总之,向一个 NSSet 中添加对象时间复杂度约为O(1),而在 Swift 的 Set 结构下可能就高于O(n)了。

    Swift 在它短暂的公众生涯中,已经在集合类数据结构的性能方面有了很大的提升,并且随着 Swift 的进化还有可能继续看到他们的变化。

    鲜为人知的Foundation数据结构

    数组,字典和集合肩负着数据处理的重任。然而,Cocoa 提供了许多了解的人很少甚至不被欣赏的集合类型。如果字典,数组,集合都不能胜任某个功能,那么你可以试试这些是否管用。

    NSCache

    使用 NSCache 与使用 NSMutableDictionary 是类似的 - 就是通过键来添加和获取值。区别在于 NSCache 是用来暂时存储一些你总是可以重新计算和生成的东西。如果可用内存降低,NSCache 可能会移除一些对象。他们是线程安全的,但是苹果的文档提示:

    如果缓存被要求释放内存,它就会自动执行异步的更新操作。

    从根本上说,NSCacheNSMutableDictionary 很像,除了NSCache可以在你的代码没有做任何事情的时候从另一个线程中移除一些对象。这对于管理缓存所使用的内存来说是很好的,但是如果你试图使用一个突然消失的对象,那么就会出问题了。

    NSCache对键采取弱引用而不是强引用。

    NSCountedSet

    NSCountedSet 可以检测到你向某个集合中添加某个对象操作执行的次数。它继承自NSMutableSet,所以如果你再次向集合中添加同样的对象,集合中也只会有一个那样的对象。

    不管怎样,在一个 NSCountedSet 类型的对象中,集合记录下对象被添加了多少次。你可以通过countForObject()查看某个对象被添加的次数。

    注意当你调用 NSMutableSetcount 属性时,它只返回唯一对象的数量,而不是所有被添加到集合中的元素的数量。

    例如,在你的 Playground 中,使用你之前做 NSMutableSet 的测试时,创建的数组的名字,将每个名字都向 NSCountedSet 对象中添加两次。

    let countedMutable = NSCountedSet()
    for name in names {
        countedMutable.addObject(name)
        countedMutable.addObject(name)
    }

    然后,打印集合,看看“Ringo”被添加了多少次:

    let ringos = countedMutable.countForObject("Ringo")
    println("Counted Mutable set: \(countedMutable)) with count for Ringo: \(ringos)")

    你的打印结果应该是:

    Counted Mutable set: {(
    George,
    John,
    Ronnie,
    Mick,
    Keith,
    Charlie,
    Paul,
    Ringo
    )}) with count for Ringo: 2

    注意集合中元素的顺序可能不同,但是你应该看到“Ringo”在名字列表中出现了一次,即使你看到它是被添加了两次的。

    NSOrderedSet

    NSOrderedSet 以及它对应的可变类型 NSMutableOrderedSet,是一种允许你以特定顺序存储一组不重复的对象的数据结构。
    “特定顺序” - 天哪,这听起来很像数组对不对?苹果简单总结了你为什么会使用NSOrderedSet 而不是数组:

    当元素顺序以及测试某个元素是否存在于集合中的性能需要被考虑在内时,你可以使用有序的集合作为数组的备选项。

    因此,当你需要存储一组有序的数据而数据又不能重复时使用NSOrderedSet是最好的。

    注意:由于 NSCountedSet 继承自 NSMutableSet,因此 NSOrderedSet 也继承自 NSObject 。这个例子很好地表明了苹果命名一个类是基于他们的功能,而不一定是基于他们在底层是如何工作的。

    NSHashTable and NSMapTable

    NSHashTable 是另外一种与集合类似的数据结构,但是与 NSMutableSet 有几处关键的不同之处。

    你可以使用任意指针类型而不仅仅是对象来搭建一个 NSHashTable 类型对象,所以你可以向 NSHashTable 中添加结构体和非对象类型的数据。你也可以使用 NSHashTableOptions 枚举值来设置内存管理和相等的条件等。

    NSMapTable 是一种类字典的数据结构,但是在内存管理方面与 NSHashTable 有着相似的行为。

    NSCache 那样,NSMapTable 对键是弱引用。然而,不管什么时候键被释放它都能够自动删除与那个键对应的数据。这些选项都可以在 NSMapTableOptions 枚举值中进行设置。

    NSIndexSet

    NSIndexSet 是一个不可变的集合,用来存储唯一的无符号整数,来表示数组的索引值。

    如果你有一个拥有十个数据的数组,而你通常需要使用其中某几个固定位置的数据,你就可以将索引存储在 NSIndexSet 对象中,并使用 NSArray 的 objectsAtIndexes 来直接获取那些对象:

    let items : NSArray = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"]
    
    let indexSet = NSMutableIndexSet()
    indexSet.addIndex(3)
    indexSet.addIndex(8)
    indexSet.addIndex(9)
    
    items.objectsAtIndexes(indexSet) // returns ["four", "nine", "ten"]

    你所指定的某个“数据”现在是一个数组了,Swift 数组没有对应的方法可以通过 NSIndexSet 或者别的什么类来获取多个数据。

    NSIndexSet 保留了 NSSet 中只允许某个值出现一次的特性。因此,你不能用它来存储任意一组数据,除非这组数据没有重复值。

    有了 NSIndexSet,你就可以将索引按一定的顺序存储,这比仅仅存储一个整数数组效率更高。

    小测试

    已经学到了这里,下面这个小测试是关于你想用哪种数据结构来存储某种类型的数据的,你可以通过小测试来检测你的记忆。

    为了做下面的测试,假设你有一个可以在图书馆展示数据的应用程序。

    Q:你会用什么来创建图书馆中所有作者的列表?

    答案:集合!它会自动移除重复的数据,也就是说你可以任意输入作者的名字,多少次都可以,但是你只会有一次录入 - 除非你输错了作者的名字!

    Q:你怎么按字母排序的方式存储一个多产作者的所有作品的标题?

    答案:用数组!这种情况下你有许多相似的数据对象(所有的标题都是字符串类型的),并且他们有顺序(标题必须按照字母顺序排列),这绝对是使用数组的最佳场景。

    Q:你怎样存储每个作者最受欢迎的作品?

    答案:字典!如果你使用作者的名字作为键,使用作者最受欢迎的作品为值,你可以以下面的方式获取这个作者最受欢迎的作品:

    mostPopularBooks["Gillian Flynn"] //Returns "Gone Girl"

    扩展阅读

    我想特别感谢我的一个同事Chris Wagner,在我们接触到 Swift 之前他写了这篇教程的 OC 版本,为了可以使用他的笔记和示例项目,把那篇教程也放在这里。

    我也要感谢苹果公司的 Swift 团队 - 尽管在原生的数据结构方面还有很大的提升空间,我已经很享受用 Swift 编码和测试了。一段时间内我可能还会使用 Foundation 下的数据结构,但也可能开发一些 Swift 小插件。

    不管怎样,如果你想学习更多 iOS 的数据结构,这儿有一些很棒的资源:
    * NSHipster是一个很棒的资源,可以让你去探索Cocoa的包括数据结构在内的一些鲜为人知的API。
    * PSPDFKit公司的Peter Steinberger的最著名的关于Foundation 数据结构的excellent article in ObjC.io issue 7
    * 前任UIKit工程师Andy Matuschak的一篇关于 Swift 中基于结构的数据结构的文章article in ObjC.io issue 16
    * AirspeedVelocity,一个研究Swift本质特征的博客 - 由于在标题中引用了Monty Python的台词而获得了加分(Monty Python 1975年有一个电影 叫 Monty Python and the Holy Grail,里面有一个角色叫bridgekeeper ,他有一句台词:What… is the air-speed velocity of an unladen swallow? )。
    * 一篇很棒的文章deep dive into the internals of NSMutableArray深入研究了NSMutableArray,以及修改NSMutableArray中的数据对内存的影响。
    * 一篇关于NSArray and CFArray performance changes with extremely large data sets很棒的研究。这篇文章进一步证明了苹果对类的命名并不是基于类在后台的行为,而是他们于开发者的行为。
    * 如果你想学习更多的算法复杂度分析Introduction to Algorithms会教你比你在实际应用中可能了解到的还要多的内容,但能使你顺利通过工作面试。

    如果你想仔细分析呈现在这篇文章中的数据,你可以自己下载the numbers spreadsheet used to track all the testing runs with Swift 1.2分析数据。

    更多问题或者想进一步分析数据的话,在下面尽情地留言吧!

    更多swift优秀译文,请关注:http://swift.gg

    展开全文
  • Swift 中的集合 (Set)

    2017-09-01 16:55:29
    Set Set 用来存储相同类型并且没有明确的顺序的值,与数组不同的是,Set里面的元素是无序的,并且每个元素都不能重复 Set 类型的基本格式为: Set()创建 Set 创建一个空的Set let letters = Set() ...
  • Swift集合类型优化

    2020-06-08 14:11:29
    集合类型是swift语言的核心抽象概念之一,标准库中的主要集合类型包括:数组,集合,字典,从小脚本到大应用,它们几乎被用在所有的Swift程序中;该书讲解了如何设计一个新的通用的集合类型,怎样效仿标准库中已经...
  • 本节文本内容转载于 ...本页包含内容: ...Swift 语言提供Arrays、Sets和Dictionaries三种基本的集合类型用来存储集合数据。数组(Arrays)是有序数据的集。集合(Sets)是无序无重复数据的集。字典(Dic...
  • Swift集合类型中,有许多十分便捷的函数。相比于Objective-C,这些高阶函数会引起你的极度舒适。因为在Swift的许多函数中引入了闭包元素,这就直接造就了它的灵活性,简直就是极致的便捷。
  • swift集合

    2017-03-07 17:31:54
    swift集合与数组比较相近,最大的区别在于集合无序,并且所含有的元素不能重复1、集合的定义//集合定义 var skillsOfA:Set<String> = ["swift","OC"] //集合是无序的,会自动删除重复的元素值 var skillsOfB:Set...
  • 写了一段小代码,总结了一下关于Swift集合类之数组和数据字典相关的使用方法。 // 集合类型:数组和数据字典(类型保持一致)  // 数组  //1、 定义空数组和创建非空数组  var arr:[String] = [String]() ...
  • swift 集合类型

    2019-08-09 15:47:08
    swift语言提供了数组和字典两种集合类型,数组用来按顺序存储相同的数据类型,字典是键值对的形式存储相通类型的数据。在swift中,数据和集合存储的数据类型必须明确,他只能存取指定的数据类型。 数组 数组是有序...
  • swift 集合(Collection)类型的赋值和拷贝行为 Swift 中数组(Array)和字典(Dictionary)类型均以结构体的形式实现。然而当数组被赋予一个常量或变量,或被传递给一个函数或方法时,其拷贝行为与字典和其它结构体有...
  • Swift 集合

    2017-02-10 11:44:30
    Swift包含三种集合类 数组 Array字典 Dictionary集合 Set 数组
  • Swift 3.0-集合

    2016-09-30 15:53:26
    // // main.swift // Swift-集合 // // Created by yidong on 16/9/28. // Copyright © 2016年 东哥. All rights reserved. // import Foundation var sets1 = Set() sets1.insert("a") sets1.insert
  • Swift 集合类型(Collection Type) 之 字典(dictionary)(官方文档翻译及总结)
  • Swift 集合类型(Collection Type) 之 set(官方文档翻译及总结)
  • Swift 集合 Dictionary

    2019-08-29 00:13:06
    Swift 集合 Dictionary 从 Swift 中 Dictionary 的声明,可知,其是一个结构体,且保存的是键值对集合,并且作为作为键的类型必须是可哈希的,这使其如同一个哈希表一样可以通过键来快速访问相对应的值。 public ...
  • Swift 集合类型(Collection Type) 之 数组(array)(官方文档翻译及总结) ‘[NSObject]’ is not convertible to ‘[String]'
  • swift 6.1 集合

    2016-09-03 17:57:56
    swift 6.1 集合标签:swift集合和数组基本是一样的类型,只不过是集合是无序的,而且是没重复的。集合的声明声明集合用关键字Set。必须显示声明类型,不然会被认为是数组var skillsOfA: Set<String> = ["swift","OC...
  • Swift- 集合

    2018-06-15 16:23:09
    //一. 数组(Array) //同 OC 一样, array 是有序的, 其它两个无序 //可变 var, 不可变 let //Array(Dictionarie, Sett)类型写法: Array&lt;存储数据类型&gt; 或者 [存储数据类型] 一般使用第二种 ...
1 2 3 4 5 ... 20
收藏数 13,558
精华内容 5,423
关键字:

swift 集合