代码

多类型和容器

Swift 中常用的原生容器类型有三种,它们分别是 ArrayDictionarySet。它们都是泛型的,也就是说我们在一个集合中只能放同一个类型的元素。

let numbers = [1,2,3,4,5]  // numbers 的类型是 [Int]
let strings = ["hello", "world"]  // numbers 的类型是 [String]

如果我们要把不相关的类型放到同一个容器类型中的话,需要做一些转换的工作:

import UIKit

let mixed: [Any] = [1, "two", 3]  // Any 类型可以隐式转换
let objectArray = [1 as NSObject, "two" as NSObject, 3 as NSObject]  // 转换为 [NSObject]

这样的转换会造成部分信息的损失,我们从容器中取值时只能得到信息完全丢失后的结果,在使用时还需要进行一次类型转换。这其实是在无其他可选方案后的最差选择: 因为使用这样的转换的话,编译器就不能再给我们提供警告信息了。我们可以随意地将任意对象添加进容器,也可以将容器中取出的值转换为任意类型,这是一件十分危险的事情。

我们注意到,Any 其实不是具体的某个类型。因此就是说其实在容器类型泛型的帮助下,我们不仅可以在容器中添加同一具体类型的对象,也可以添加实现了同一协议的类型的对象。绝大多数情况下,我们想要放入一个容器中的元素或多或少会有某些共同点,这就使得用协议来规定容器类型会很有用。

        import Foundation

        let mixed1:[CustomStringConvertible] = [1,"two", 3]
        for obj in mixed1 {
            print(obj.description)
        }

这种方法虽然也损失了一部分类型信息,但是相对于 Any 或者 AnyObject 还是改善很多,在对于对象中存在某种共同特性的情况下无疑是最方便的。另一种做法是使用 enum 可以带有值的特点,将类型信息封装到特定的 enum。下面的代码封装了Int 或者 String 类型:

        //使用枚举封装数据
       enum IntOrString {
              case intValue(Int)
              case stringValue(String)
        }
        // mixed: [IntOrString]
        let mixed = [IntOrString.intValue(1),
                     IntOrString.stringValue("two"),
                     IntOrString.intValue(3)]
        for value in mixed {
            switch value {
            case let .intValue(i):
                print(i*2)
            case let .stringValue(s):
                print(s.capitalized)//首字母大写
            }
        }

通过这种方法,我们完整地在编译时保留了不同类型的信息。为了方便,我们甚至可以进一步为 IntOrString 使用字面量转换的方法编写简单的获取方式。

字面量表达

所谓字面量,就是指像特定的数字,字符串或者是布尔值这样,能够直截了当地指出自己的类型并为变量进行赋值的值。比如在下面:

let aNumber = 3
let aString = "Hello"
let aBool = true

中的 3Hello 以及 true 就称为字面量。在Swift中,ArrayDictionary 在使用简单的描述赋值的时候,使用的也是字面量,比如:

let anArray = [1, 2 ,3]
let aDictionary = ["key1": "value1", "key2": "value2"]

Swift为我们提供了一组非常有意思的协议,使用字面量来表达特定的类型。对于那些实现了字面量表达协议的类型,在提供字面量赋值的时候,就可以简单地按照协议方法中定义的规则“无缝对应”地通过赋值的方式将值表达为对应类型。这些协议包括了各个原生的字面量,在实际开发中我们经常可能用到的有:

  • ExpressibleByNilLiteral

  • ExpressibleByIntegerLiteral

  • ExpressibleByFloatLiteral

  • ExpressibleByBooleanLiteral

  • ExpressibleByStringLiteral

  • ExpressibleByArrayLiteral

  • ExpressibleByDictionaryLiteral

所有的字面量都定义了一个 associatetype和对应的 init方法。拿 ExpressibleByBooleanLiteral举个例子:

public protocol ExpressibleByBooleanLiteral {

    associatedtype BooleanLiteralType
    //Creates an instance initialized to the given Boolean value.
    public init(booleanLiteral value: Self.BooleanLiteralType)
}

于是在我们需要自己实现一个字面量表达的时候,可以简单地只实现定义 init 的方法就行了。举个不太有实际意义的例子,比如我们想实现一个自己的 Bool 类型,可以这么做:

enum MyBool: Int {
    case myTrue, myFalse
}

extension MyBool : ExpressibleByBooleanLiteral {
    init(booleanLiteral value: Bool) {
        self = value ? .myTrue : .myFalse
    }
}

这样我们就能很容易地直接使用 Booltruefalse 来对类型进行赋值了:

    let myTrue: MyBool = true
    let myFalse: MyBool = false

    print(myTrue.rawValue) //0
    print(myFalse.rawValue) //1

BooleanLiteralType 大概是最简单的形式,如果我们深入一点,就会发现像是 ExpressibleByStringLiteral 这样的协议要复杂一些。这个协议不仅类似于上面布尔的情况,定义了 StringLiteralType 及接受其的初始化方法,这个协议本身还要求实现下面两个协议:

ExpressibleByExtendedGraphemeClusterLiteral
ExpressibleByUnicodeScalarLiteral

这两个协议我们在日常项目中基本上不会使用,它们对应字符簇和字符的字面量表达。虽然复杂一些,但是形式上还是一致的,只不过在实现时我们需要将这三个init 方法都进行实现。

还是以例子来说明,比如我们有个类 Person ,里面有这个人的名字:

class Person {
    let name: String

    init (name value: String) {
        self.name = value
    }

}

如果想要通过 String 赋值来生成 Person 对象的话,可以改写这个类:

//通过名字(字符串)创建Person对象
class Person: ExpressibleByStringLiteral {
    let name: String

    init (name value: String) {
        self.name = value
    }

    required init(stringLiteral value: String) {
       self.name = value    
    }
    required init(extendedGraphemeClusterLiteral value: String) {
        self.name = value
    }
    required init(unicodeScalarLiteral value: String) {
         self.name = value
    }

}

在所有的协议定义的 init 前面我们都加上了 required 关键字,这是由初始化方法的完备性需求所决定的,这个类的子类都需要保证能够做类似的字面量表达,以确保类型安全。

在上面的例子里有很多重复的对 self.name 赋值的代码,这是我们所不乐见的。一个改善的方式是在这些初始化方法中去调用原来的 init(name value: String) ,这种情况下我们需要在这些初始化方法前加上 convenience :

//通过名字(字符串)创建Person对象
class Person: ExpressibleByStringLiteral {
    let name: String

    init (name value: String) {
        self.name = value
    }

    required  convenience init(stringLiteral value: String) {
        self.init(name: value)
    }
    required convenience init(extendedGraphemeClusterLiteral value: String) {
        self.init(name: value)
    }
    required convenience init(unicodeScalarLiteral value: String) {
         self.init(name: value)
    }

}

let xiaoming:Person = "xiaoming"
print(xiaoming.name) //xiaoming

上面的 Person 的例子中,我们没有像 MyBool 中做的那样,使用一个 extension 的方式来扩展类使其可以用字面量赋值,这是因为extension 中,我们是不能定义 required 的初始化方法的。 也就是说,我们无法为现有的非 finalclass 添加字面量表达(不过也许这在今后的Swift版 本中能有所改善)。

results matching ""

    No results matching ""