结构体

作者: 曹来东 | 来源:发表于2018-10-17 16:38 被阅读16次

    在大部分命令式编程语言中都有结构体(structure)这个概念。结构体对于一个结构化编程语言而言是一个非常重要的特性,它用于构造一个我们自定义的数据集抽象类型,这也使得代码可通过这些结构体而变得结构化。
    Swift中的结构体属于四大基本类型之一,它属于值类型。在Swift标准库中很多类型都是结构体类型,比如 Int、Float、String、Array 等都是结构体类型。而在Swift中,结构体拥有许多十分灵活的语法特性,很多类类型才有的特性结构体也有,比如:
    1、可定义存储式实例与类型属性;
    2、可定义计算式实例与类型属性;
    3、可使用属性观察者;
    4、可定义实例与类型方法;
    5、可定义初始化器;
    6、可定义数组下标;
    7、可对结构体进行扩展;
    8、可遵循协议。
    下面我们将分别介绍结构体定义的相关语法特性。

    结构体基本语法简介

    一个结构体类型用关键字 struct 引出,然后在它内部可定义属性(properties)与方法(methods)。我们看以下代码片段:

    /// 定义了一个结构体
    struct Structure {
     
    /// 一个常量存储式实例属性,
    /// 并直接为它初始化
    let constProperty = 10
     
    /// 一个变量存储式实例属性
    var property: Int
     
    /// 初始化器
    init() {
    property = 0
    }
     
    /// 实例方法
    func method() {
    print("This is a structure")
    }
    }
    

    上述代码片段中定义了一个名为Structure的结构体。然后在它内部定义了一些存储式实例属性、初始化器以及实例方法。下面我们看一下如何创建一个结构体的对象实例,然后通过该对象来访问其实例属性以及实例方法。

    /// 定义了一个结构体
    struct Structure {
     
    /// 一个常量存储式实例属性,
    /// 并直接为它初始化
    let constProperty = 10
     
    /// 一个变量存储式实例属性
    var property: Int
     
    /// 初始化器
    init() {
    property = 0
    }
     
    /// 实例方法
    func method() {
    print("This is a structure")
    }
    }
     
    /// 使用无参数的初始化器创建一个Structure结构体的常量对象实例
    let s1 = Structure()
     
    /// 访问s1对象的实例属性
    var a = s1.constProperty + s1.property
    print("a = \(a)")
     
    /// 访问s1的实例方法
    s1.method()
     
    /// 使用无参数的初始化器创建一个Structure结构体的变量对象实例
    var s2 = Structure()
     
    /// 访问s2对象的实例属性
    s2.property += 5
    a = s2.property + s2.constProperty
    print("a = \(a)")
     
    /// 访问s2的实例方法
    s2.method()
    

    我们可以看到,创建一个结构体的实例对象时根据初始化器的参数使用类型名后面跟圆括号的方式进行。这就好比将结构体类型作为一个函数标志来使用一样,当然我们也可以显式地调用结构体的初始化器.我们访问一个实例对象的实例方法与实例属性的时候都使用 . 成员访问操作符。

    存储式实例属性

    存储式实例属性(Stored Instance Properties)是结构体与类类型中最简单的属性形式。我们直接在这些类型中用 var 或 let 来声明一个变量或常量,另外也可以直接对它们进行初始化,那么这些声明在类型中的对象就称为该类型的存储式实例属性。
    实例属性是每个对象实例自己独立拥有的,每个对象拥有属于自己的实例属性。与之相对的是类型属性,它们则是类型独有的,并且可以为其他对象所共同访问。我们先看下面的例子。

    /// 定义了一个名为Test的结构体
    struct Test {
     
    /// 声明了一个整数常量作为存储式实例属性
    let constant = 100
     
    /// 声明了一个字符串变量作为存储式实例属性
    var str = "Hello"
     
    /// 声明了一个数组变量作为存储式实例属性
    var array = [1, 2, 3]
    /// 声明了一个元组常量作为存储式实例属性
    let tuple = (1.0, "a", true)
     
    /// 声明了Void类型的常量作为存储式实例属性
    let null: Void = ()
    }
     
    // 创建一个Test对象实例test
    var test = Test()
     
    // 将test对象的str实例属性后面再追加一个字符串
    test.str += ", world"
     
    // 将test对象的array实例属性后面再添加3个元素
    test.array += [4, 5, 6]
     
    // 这里创建了Test的对象实例test2,
    // 然后将test对象的所有属性值复制给它
    var test2 = test
     
    // 这里将test2对象的str实例属性进行修改
    test2.str = "Bye"
     
    // 然后移除test2对象的array实例属性中的最后一个元素
    test2.array.removeLast()
     
    // 这里输出:test2.str = Bye
    print("test2.str = \(test2.str)")
     
    // 这里输出:test2 value is: 105
    print("test2 value is: \(test2.constant + test2.array.count)")
     
    // 输出:test.str = Hello, world
    print("test.str = \(test.str)")
     
    // 输出:test value = 106
    print("test value = \(test.constant + test.array.count)")
     
    // 这是一个空表达式,
    // 即便前面不加通配符 _ 编译也不会报warning
    test.null
     
    struct MyStruct {
    var a = 10
     
    /// 这里我们可以发现,
    /// Void类型的实例属性不占任何存储空间
    let null: Void = ()
    }
     
    print("size is: \(MemoryLayout.size(ofValue: MyStruct()))")
    

    上述这段代码比较简单。首先我们可以看到,我们可以用任意类型来声明结构体的存储式实例属性,即便是 Void 类型,当然 Void 类型的实例属性不会占用对象的存储空间。此外,这段代码还体现出结构体作为值类型的特质。像 var test2 = test 这条语句并不像Java里的那样,将test2指向test对象;而是将test中的属性值全都复制给新建的test2对象实例中。在Swift中,结构体属于值类型,所以用一个对象对另一个对象进行初始化时,所采取的策略是属性复制,而不是引用机制。在此之后,我们可以看到test与test2两个对象就有各自的实例属性了。当test2的实例属性进行修改之后,test对象中的实例属性不会受任何影响。

    惰性存储式属性

    在默认情况下,当一个对象实例被创建时,其所有的存储式实例属性都会完成初始化。不过我们有时可能需要将一些构建起来成本会比较昂贵的资源在我们需要访问的时候才被创建,由于这些属性一方面构造起来比较费时、费存储空间,并且可能在某些场合还未必能用到,所以此时我们可以将它们声明为惰性存储式属性(Lazy Stored Properties)。惰性存储式属性的声明只需要在前面添加 lazy 关键字,并且只能通过 var 来声明,而不能使用 let。惰性存储式属性只有在它第一次被访问的时候才会做相应的初始化,否则它是不会被初始化的。下面我们来举些例子。

    /// 定义一个函数,用于获取一个整数值
    func fetchData() -> Int {
    print("data fetched!")
    return 100
    }
     
    struct Test {
    /// 声明了一个惰性存储式实例属性prop,
    /// 当它被第一次访问的时候才会调用fetchData函数对它初始化
    lazy var prop = fetchData()
    }
     
    // 创建Test结构体的一个对象实例
    var test = Test()
     
    // 为了证明此时test对象的prop属性尚未被建立,
    // 我们先输出一条打印
    print("test has been created!")
     
    // 我们这里对prop惰性存储式实例属性进行第一次访问,
    // 此时将会调用fetchData函数,
    // 这里才会输出:data fetched!
    test.prop += 10
     
    // 第二次访问test对象的prop属性,
    // 输出:test.prop = 110
    print("test.prop = \(test.prop)")
    

    上述代码中我们定义了一个fetchData函数用于观察惰性存储式实例属性在什么时候进行初始化。通过这段代码例子我们可以知道,惰性存储式实例属性只有当它第一次被访问的时候才会进行初始化。也就是说,只有当惰性存储式实例属性第一次被访问的时候,才会用声明在结构体中的针对它的初始化器对它进行初始化,此时该初始化器中的表达式也才会被执行。

    计算式属性

    Swift是一门主张简洁、直观的编程语言。有时在描述一个物体的时候有些属性可以不通过用户输入得到,而是通过已有的属性进行计算得到,此时我们就可以使用计算式属性(Computed Properties)。
    计算式属性不能对它在声明时直接初始化,而是为它提供一个getter方法用来获取当前计算式属性的值;提供一个setter方法用来设置它的值。因此,我们在定义计算式属性的时候必须显式给出它的类型,并且只能用 var 来声明,不能用 let。下面我们举一个定义一个圆的结构体的例子来说明计算式实例属性的使用。

    /// 定义了一个Circle结构体,表示圆
    struct Circle {
     
    /// radius是一个Double类型的存储式实例属性
    var radius = 0.0
     
    /// 这里定义了用于表示直径的计算式实例属性
    var diameter: Double {
     
    // 定义它的getter方法。
    // 这里要注意的是,
    // getter方法的返回类型必须缺省!
    // 其返回类型就是该计算式实例属性的类型
    get {
    return radius * 2.0
    }
     
    // 定义它的setter方法。
    // 这里各位要注意的是,
    // setter方法形参的类型必须缺省!
    // 该形参类型就是该计算式属性的类型
    set(value) {
    radius = value / 2.0
    }
    }
     
    /// 这里定义了用于表示周长的计算式实例属性
    var perimeter: Double {
     
    // 定义它的getter方法
    get {
    // 这里对周长的计算借用了diameter计算式实例属性
    return Double.pi * self.diameter
    }
    // 当setter方法缺省参数时,
    // 其隐式的形参标识符为newValue
    set {
    radius = newValue / (Double.pi * 2.0)
    }
    }
     
    /// 这里定义了用于表示面积的计算式实例属性
    var area: Double {
     
    // 定义它的getter方法
    get {
    return Double.pi * radius * radius
    }
     
    // 定义它的setter方法
    set {
    radius = sqrt(newValue / Double.pi)
    }
    }
    }
     
    // 创建一个circle对象实例
    var circle = Circle()
     
    // 将circle的半径设置为3
    circle.radius = 3.0
     
    // 访问circle的直径计算式实例属性
    // 这里调用的是diameter的getter方法
    print("diameter = \(circle.diameter)")
     
    // 这里将circle的面积设置为10,
    // 即调用了area计算式实例属性的setter方法
    circle.area = 10.0
     
    // 这里得到circle的半径值为:1.78412411615277
    print("radius = \(circle.radius)")
    

    我们通过定义一个圆周能看到计算式实例属性的定义以及其使用方法。我们看到,通过计算式实例属性我们可以像访问属性那样的语法来访问原本需要通过方法调用计算得到的属性。也就是说,我们把这种计算过程给抽象掉了。因为就面向对象的概念而言,像直径、周长、面积等概念都属于圆的属性,而不是操作。方法定义的是对象的一个操作。上面还提到了setter方法的形参可省,此时该setter方法所具有的隐式形参标识符为:newValue。
    计算式属性的setter方法可缺省,但getter方法绝不能缺省。如果setter方法缺省了,那么该计算式属性也就变为只读属性了。下面我们通过定义一个矩形来展示只读计算式实例属性的例子。

    /// 定义了矩形结构体
    struct Rectangle {
     
    /// 声明了矩形宽度存储式实例属性
    var width = 0.0
     
    /// 声明了矩形高度存储式实例属性
    var height = 0.0
     
    /// 定义矩形对角线长度计算式实例属性
    /// 此计算式实例属性是只实现了getter方法,
    /// 因此是只读的
    var diagonal: Double {
    get {
    return sqrt(width * width + height * height)
    }
    }
     
    /// 定义了矩形的周长计算式实例属性
    /// 如果计算式属性只有getter方法,
    /// 那么get { } 可省
    var perimeter: Double {
    // 这里直接将get { }中的内容放到了最外层的 { }之中
    return 2.0 * (width + height)
    }
     
    /// 定义了矩形的面积计算式实例属性
    var area: Double {
    return width * height
    }
    }
     
    // 创建了Rectangle的一个对象实例rect
    var rect = Rectangle()
     
    // 将rect的宽高分别设定为3和4
    rect.width = 3.0
    rect.height = 4.0
     
    // 这里访问了rect的diagonal计算式实例属性,
    // 调用了diagonal的getter方法
    print("diagonal = \(rect.diagonal)")
     
    print("perimeter = \(rect.perimeter)")
     
    print("area = \(rect.area)")
    

    通过上述代码片段,我们可以看到了计算式实例属性的只读属性语法。我们在使用一个对象的属性时一般无需关心它是属于只读属性还是读写属性,如果我们要观察的话其实通过Xcode开发环境中的option键加鼠标左键也能看到指定属性的声明。比如,如果我们option加鼠标左键点击了rect对象的area属性,那么Xcode编辑界面中会弹出 var area: Double { get } 这种声明。这其实也就一目了然了。

    属性观察者

    有时为了逻辑上的简化需要,我们可能需要获悉某个存储式属性的值当前被修改了,从而可以做一些输入值的过滤或其他操作。Swift编程语言中引入了属性观察者(Property Observers)这一语法特性,从而提供了针对存储式属性值得到变化的响应。
    属性观察者的语法跟计算式属性比较类似,也是在属性声明后添加 { } ,然后在此花括号中添加 willSet 方法以及 didSet 方法,当然我们也可以实现这两个方法的其中之一,不需要全都实现。当指定的属性要被修改前,会调用它设定的 willSet 方法;当指定的属性已经被修改之后则会调用它所设定的 didSet 方法。下面我们先来举一个简单的例子来看看实例属性观察者的一般使用。

    /// 定义一个Test结构体
    struct Test {
     
    /// 这里声明了number存储式实例属性,
    /// 其类型为Int,并且初始化为0
    var number = 0 {
     
    // 这里定义了number的willSet属性观察者,
    // 当number属性的值发生改变之前会调用此方法。
    // 这里参数value是即将传给number的新值,
    // 其类型与number相应。
    willSet(value) {
    print("current value = \(number)")
    print("new value = \(value)")
    }
     
    // 这里定义了number的didSet属性观察者,
    // 当number属性的值修改完之后就会调用此方法。
    // 这里参数orgValue是指在修改number属性之前的值,
    // 其类型与number相应。
    didSet(orgValue) {
    print("original value = \(orgValue)")
    print("modified value = \(number)")
    }
    }
    }
     
    // 这里创建一个Test结构体对象实例test
    var test = Test()
     
    // 将test的实例存储式属性number设置为10
    test.number = 10
    

    上述代码演示了属性观察者的一般使用与工作机制。像属性观察者中 willSet 方法以及 didSet 方法中的参数均可缺省。如果 willSet 方法的参数缺省,那么它对应的一个隐式参数标识符为 newValue。如果 didSet 方法中的参数缺省,那么它对应的一个隐式参数标识符为 oldValue。下面我们再举个例子来进一步说明属性观察者的用法。

    struct Test {
     
    /// 这里声明了一个Float类型的存储式实例属性,
    /// 并为它设定了属性观察者。
    /// 这里由于没有为number直接初始化,
    /// 所以必须显式给定其类型
    var number: Float {
     
    willSet {
    print("Current value is: \(number)")
    print("Specified value is: \(newValue)")
    // 在willSet方法中不要对当前所观察的属性值进行修改,
    // 因为即便你修改了number的值最终也会被newValue给覆盖掉。
    // 这里编译器会报warning:
    // 企图在属性'number'自己的willSet中存储值,
    // 这将会被newValue所覆盖
    number = -100.0
    }
     
    didSet {
    // 我们可以看到,上述number即便显式地用-100.0对它赋值,
    // 但最终出现在didSet中的number的值依然为0.0
    let value = number == 0.0 ? 0.00001 : number
     
    number = oldValue / value
    }
    }
     
    init(value: Float) {
    // 在初始化器中为number存储式实例属性进行初始化
    number = value
    }
    }
     
    var test = Test(value: 1.0)
     
    test.number = 0.0
     
    print("The number is: \(test.number)")
    

    上述代码片段进一步展示了属性观察者的使用。这里各位要注意,在 willSet 方法中不要对被观察的存储式属性进行值修改,因为最终这些修改操作都是无效的。此外,在初始化器方法以及 didSet 方法中对被观察的存储式属性进行修改不会触发 willSet 方法的调用,即不会引发属性观察的消息通知。一般来说,我们在属性观察者中 didSet 方法使用更多些,许多过滤操作都在此方法中进行。而 willSet 方法中则可记录一些数据统计,或对其他一些对象发送某些消息等。

    类型属性

    之前我们谈论的都是实例属性(Instance Properties),实例属性的特点是当前枚举、结构体、类等类型的对象实例各自持有各自的实例属性,它们占据着对象实例自身的存储空间。我们将介绍类型属性(Type Properties),顾名思义类型属性就是属于类型自己的,与用该类型所创建的对象实例无关。
    枚举、结构体以及类类型都能定义属于自己的类型属性,而且声明方法非常简单,只需要在属性声明最前面添加 static 关键字即可。类型属性与实例属性一样,也具有存储式类型属性、计算式类型属性以及针对存储式类型属性的属性观察者。由于类型属性本身具有惰性特质,所以我们不能用 lazy 去修饰它们。此外,对于计算式类型属性,如果定义在一个类类型中,那么还可以使用 class 关键字去声明,表示允许其子类覆盖当前类的实现。
    访问类型属性也是通过 . 成员访问操作符。只不过我们只能通过相应的类型标识符去访问其内部的类型属性,而不能通过用它所构建的对象实例。这意味着一个类型中可以同时存在相同名称的实例属性与类型属性,两者不会产生冲突。我们下面来看一个比较详细的例子。

    struct Test {
     
    /// 定义了Test结构体的存储式类型属性
    static var ti = 10;
     
    /// 定义了Test结构体的计算式类型属性
    static var cp: Int {
    get {
    print("getter ti = \(ti)")
    return ti - 10
    }
     
    set {
    print("setter value = \(newValue)")
    ti = newValue + 10
    }
    }
     
    /// 定义了Test结构体的一个存储式类型属性,
    /// 并为它建立了属性观察者
    static var str = "Hello" {
    willSet {
    print("current string: \(str)")
    print("specified string: \(newValue)")
    }
     
    didSet {
    print("original string: \(oldValue)")
    print("modified string: \(str)")
    }
    }
     
    /// 这里定义了Test结构体的常量存储式类型属性
    static let si = 1.5
     
    /// 这里定义了存储式实例属性
    var si = -1
    }
     
    var test = Test()
     
    // 这里访问的是test对象实例的存储式实例属性si
    test.si += 2
    print("instance si = \(test.si)")
    // 这里访问的是Test结构体的存储式类型属性si
    print("type si = \(Test.si)")
     
    // 这里访问的是Test结构体的存储式类型属性ti
    Test.ti += 100
    print("ti = \(Test.ti)")
     
    // 这里访问了Test结构体的计算式类型属性cp
    Test.cp += 5
     
    // 这里访问了Test结构体的存储式类型属性str
    // 这次对str的修改触发了其属性观察者
    Test.str += ", world"
     
    /// 定义一个函数foo
    func foo() {
     
    /// 在函数foo中定义一个Test结构体
    struct Test {
    static var ti = -10;
    static let si = -1.5
    }
     
    // 这里所访问的Test结构体类型都是属于foo函数中所定义的。
    // 因此这里的Test将文件作用域的给覆盖掉了
    print("foo ti = \(Test.ti)")
    print("foo si = \(Test.si)")
    }
     
    // 调用foo函数
    foo()
    

    上述代码介绍了类型属性及其相关使用方法。我们可以看到,类型属性的访问与实例属性十分类似,只不过所属不同。类型属性所属于类型,而实例属性则所属于对象实例。

    实例方法

    当我们在枚举、类、结构体类型中定义一个函数时,该函数则被称为方法(Methods)。Swift官方文档则是把方法定义为:与一个特定类型相关联的函数。方法与属性类似,也主要分为实例方法与类型方法两大类。实例方法中也有一些变种,稍后会讲解到。我们先介绍一般的实例方法。
    定义一个类型的实例方法非常简单,我们直接将一个函数定义在一个类型之中即可。实例方法与实例属性类似,也是作用于单个对象实例。此外,每个实例方法都具有一个隐式的属性 self,它指向调用此方法的对象实例,所以 self 的类型为当前对象的类型。一般情况下,我们在实例方法中访问当前类型的实例属性或其他实例方法时,不需要显式使用 self 去访问,也就是说 self. 可省。如果当前方法中声明了与当前类型中的属性名称相同的对象,那么我们需要显式加上 self. 加以区分。
    对于结构体与枚举,如果我们要在实例方法中修改当前类型的实例属性的值或当前对象实例的值,那么我们必须在 func 前面添加 mutating 关键字,表示当前方法将会修改它相关联的对象实例的实例属性值。如果不加 mutating 关键字,那么对于结构体与枚举来说,在方法中对其任何实例属性的修改都将引发编译报错。而对于类类型的实例方法则不需要,也不能添加 mutating 关键字。
    下面我们先看一组简单的例子,对实例方法有个初步认识。

    /// 定义了一个结构体类型Test
    struct Test {
     
    /// 定义了一个存储式实例属性a
    var a = 10
     
    /// 定义了一个存储式实例属性s
    let s = "Hello"
     
    /// 定义了一个方法method,
    /// 其类型为:() -> Void
    func method() {
     
    // 这里访问实例属性a
    print("a = \(a)")
     
    // 这里访问实例属性s,
    // 其中 self. 可以省略
    print("s = \(self.s)")
    }
     
    /// 这里定义了方法method2,
    /// 其类型为:(Int) -> Void,
    /// 由于method2方法中需要对实例属性进行修改,
    /// 所以这里使用了mutating限定符进行修饰
    mutating func method2(a: Int) {
    // 这里是将形参对象a的值做加1操作后给当前Test对象的实例属性a。
    // 这里 += 左边的 self. 不能省,
    // 因为实例属性a的标识符与形参标识符重名。
    self.a += a + 1
    }
     
    /// 这里定义了方法method3,
    /// 由于此方法的功能为将其相关联的对象实例重新修改为默认值的状态,
    /// 所以这里需要添加mutating限定符
    mutating func method3() {
    // 将整个对象实例修改为默认值状态
    self = Test()
    }
    }
     
    // 创建了Test结构体的对象实例test
    var test = Test()
     
    // 对test对象实例调用了method2方法
    test.method2(a: 10)
     
    // 对test对象实例调用了method方法
    test.method()
     
    // 对test对象调用了method3方法
    test.method3()
     
    // 最后对test对象再次调用method方法
    test.method()
    

    上述代码展示了实例方法的基本定义与使用方法。方法与函数的声明形式几乎一样,并且也具有与函数一样结构的语法特性,比如实参标签、默认形参值、不定个数的形参、输入输出形参、方法重载以及方法签名都几乎一样。下面要描述的是实例方法的引用,由于实例方法必须要与某一对象实例进行关联,所以我们用一个函数引用对象指向某一对象实例的方法时需要将对象实例也一起带上。下面我们将举一些例子来描述针对实例方法的引用。

    struct Test {
     
    /// 定义一个Int类型的存储式实例属性property
    var property = 100
     
    /// 定义了一个mutating实例方法
    mutating func method(a: Int) {
    property += a
    }
     
    /// 定义了一个foo实例方法
    func foo(_: Void = ()) {
    print("property = \(property)")
    }
     
    /// 定义了另一个foo实例方法,
    /// 这就是实例方法的重载
    func foo(a: Int) {
    print("value = \(property + a)")
    }
    }
     
    // 创建了Test结构体的对象实例test
    var test = Test()
     
    // 将test对象的property存储式实例属性的值加10
    test.property += 10
     
    // 这里通过method方法签名来对它进行调用。
    // 由于mutating方法不允许通过函数引用对象对它进行引用,
    // 所以这里只能直接通过方法签名做直接调用,
    // 这是允许的。
    test.method(a:)(5)
     
    // 不过我们可以通过闭包做一个简单封装,
    // 使得一个函数引用对象能指向这个闭包
    // 这里ref的类型为:() -> Void
    let ref = { test.method(a: 5) }
     
    // 对ref进行调用
    ref()
     
    // 声明了函数引用ref1,
    // 直接指向test的foo(_:)实例方法
    let ref1 = test.foo(_:)
     
    // 调用函数引用ref1
    ref1(())
     
    // 声明了函数引用ref2,
    // 直接指向test的foo(a:)实例方法
    let ref2 = test.foo(a:)
    // 调用函数引用ref2
    ref2(10)
    

    通过上述代码片段,我们可以看到,对实例方法的引用必须包含与它所关联的对象实例。此外, mutating 的实例方法不允许被引用,只能通过方法签名做直接调用。
    上面讲述了实例方法的基本概念以及常用的使用形式。下面我们进一步介绍实例方法的本质类型。就拿上述代码中 Test 结构体类型中的 func foo(a: Int) 实例方法而言,其真正的类型为 (Test) -> (Int) -> Void。这其实非常好地诠释了实例方法的本意,也就是以方法调用者(即调用此方法的对象)作为该方法的第一个实参,然后它所返回的类型才是我们在其声明上所看到的函数类型。所以在Swift中,我们也可以像C++编程语言那样直接以类型名来访问其实例方法,作为一个对该类型实例方法的引用。下面我们举一个例子进行说明。

    /// 定义一个结构体类型**MyStruct**
    struct MyStruct {
     
    /// 定义了一个实例方法**method**
    ///
    /// 它具有一个类型为`Int`的形参**a**
    func method(a: Int) {
    print("a = \(a)")
    }
    }
     
    /// 这里定义了一个函数引用对象**methodRef**,
    ///
    /// 其类型为:`(MyStruct) -> (Int) -> Void`
    let methodRef = MyStruct.method(a:)
     
    let s = MyStruct()
     
    // 我们可以这么调用methodRef,
    methodRef(s)(100)
    

    上述代码中对 methodRef 函数对象引用的调用其实是要经过两步:第一步是将对象调用者作为第一个实参传入,所以 methodRef(s) 其实就相当于 s.method(a:);而第二步就是做真正的方法调用了,即 methodRef(s)(100),它就相当于 s.method(a:)(100)。所以我们这里能清晰地观察到通过类型对实例方法的直接引用与通过对象访问其实例方法的不同以及对应关系。而这也能体现出Swift编程语言其类型系统的丰富性及灵活性。

    类型方法

    类型方法与类型属性类似,是与类型相关联的方法,而不是对象实例。我们在类型方法中可以直接访问当前类型的类型属性,但不能直接访问当前类型的实例属性。从Swift 3.1版本起,我们也可以在类型方法中使用 self 了,此时 self 指代的是调用此类型方法的类型本身。
    我们定义一个类型方法也非常简单,直接在 func 前面添加 static 关键字即可。如果当前类型是类类型,那么我们还能使用 class 关键字修饰,表示当前类型方法能被子类重写。如果在类类型中用了 static 关键字去修饰类型方法,那么该类型方法就不允许被子类重写了。
    由于类型方法的定义方式以及使用与实例方法类似,我们看下面的代码例子。

    struct Test {
    /// 定义了存储式类型属性
    static var a = 100
     
    /// 定义了类型方法method()
    /// 与实例方法不同的是,
    /// 类型方法中如果对类型属性进行修改,
    /// 无需用mutating关键字进行修饰
    static func method() {
    // 这里将Test的类型属性a加20处理,
    // 这里的 self. 可省
    self.a += 20
     
    print("This is a type method")
    }
     
    /// 定义了类型方法getValue(),
    /// 返回一个Int类型
    static func getValue(a: Int) -> Int {
    // 这里直接返回类型属性a的值与形参a的值的和
    // 这里 + 左边的 self. 不可省,
    // 否则访问的就是形参对象a了
    return self.a + a
    }
     
    /// 定义了类型方法foo()
    static func foo(_: Void) {
    print("This is a foo!")
    }
     
    /// 定义了重载的类型方法foo()
    static func foo(a: Int) {
    print("a = \(a)")
    }
    }
     
    // 直接调用Test的类型方法method
    Test.method()
     
    // 直接调用Test的类型方法getValue
    let a = Test.getValue(a: 5)
     
    print("a = \(a)")
     
    // 这里通过定义函数引用对象ref1指向Test的类型方法foo(_:)
    var ref1 = Test.foo(_:)
     
    // 调用ref1
    ref1(())
     
    // 这里通过定义函数引用对象ref2指向Test的类型方法foo(a:)
    let ref2 = Test.foo(a:)
    // 调用ref2
    ref2(1)
     
    // ref1再次指向Test的method类型方法
    ref1 = Test.method
     
    // 调用ref1
    ref1(())
     
    

    上述代码例子比较详细地描述了类型方法的使用以及功能。我们这里要注意的是,类型方法是属于类型自己的,与类型所创建的对象实例无关。

    初始化器方法

    初始化器方法(initializer methods)用于在创建一个类、结构体或枚举类型的对象实例时为该对象的实例属性进行初始化。Swift编程语言语法规定,一个创建好的对象实例,其所有存储式实例属性都必须完成初始化,使得它们的值都是确定的。也就是说,当我们在定义一个类型时,如果在定义其中一个存储式实例属性时没有直接为它初始化,那么我们就必须显式地在初始化器方法中对这些实例属性进行初始化赋值。如果一个类型中所有定义的存储式实例属性都直接完成初始化了,并且没有提供任何自己实现的初始化器方法,那么此时我们可以使用Swift所提供的默认缺省定义的初始化方法。我们之前的代码示例都采用的是缺省初始化器方法的。

    我们在Swift中直接使用 init 关键字表示当前类型的初始化器方法,然后后面跟形参列表。由于一个类型的初始化器方法肯定返回它所创建的对象实例,因此其返回类型不需要写,就是当前类型本身,在初始化方法最后也不需要添加 return 语句。除此之外,它跟一般的类型方法也没有什么不同。比如,初始化方法也可以进行重载,其方法签名形式也与类型方法的一样。

    当我们在创建一个类型的对象实例时,直接用该类型名,后面跟实参列表的形式进行构建。这里,该类型的名称就指代了使用该类型的初始化器方法,后面的实参列表就作为传递给初始化方法的实参。此外,我们也可以显式地使用类型名 .init() 的形式。保留这种形式,其实也是方便对类型的初始化方法进行引用。这里我们可以看到,初始化方法在性质上看上去类似于类型方法。但是这里各位需要注意的是,初始化方法由于其特殊性,所以在它里面使用 self 的时候, self 指向的是当前正在被创建的对象,而此对象是隐式的。

    下面我们举一些例子先简单介绍一下初始化器的定义以及使用。

    /// 定义一个结构体类型Test
    struct Test {
     
    /// 定义一个存储式实例属性a,
    /// 由于它已经直接被初始化,
    /// 因此可以不在初始化器方法中进行初始化赋值
    var a = 10
     
    /// 定义一个存储式实例属性b,
    /// 由于它没有被直接初始化,
    /// 因此需要在初始化器方法中进行初始化赋值
    let b: Float
     
    /// 定义一个存储式实例属性c,
    /// 由于它没有被直接初始化,
    /// 因此需要在初始化器方法中进行初始化赋值
    var c: String
     
    /// 定义一个存储式实例属性d,
    /// 由于它是一个Optional对象,
    /// 因此会直接被默认初始化为空值,
    /// 所以可以不在初始化器方法中进行初始化赋值
    var d: Int?
     
    /// 定义了Test结构体类型的初始化器方法,
    /// 该初始化器方法不带任何参数
    init() {
    // 在此初始化器方法中,仅仅对属性b与属性c做初始化赋值。
    // 此外,这里 self.c 中的 self 就指向当前正被创建的对象实例,
    // 而此对象实例是隐式的,且这里的 self. 也可省。
    b = 1.0
    self.c = "Hello"
    }
    }
     
    // 这里直接通过Test的不带参数的初始化器方法创建对象实例test
    var test = Test()
     
    // 我们也可以直接通过 Test.init() 来创建Test对象实例
    test = Test.init()
     
    // 这里使用ref函数引用对象指向Test结构体的初始化器方法,
    // ref的类型为:() -> Test
    let ref = Test.init
     
    // 这里通过ref函数引用对象间接调用Test的初始化器方法创建对象实例
    test = ref()
    

    上述代码片段介绍了初始化器的定义方式以及各种使用方式。我们也可以从中看到,哪些属性需要在初始化器方法中做初始化赋值,而哪些则不需要。后面我们将进一步介绍初始化器方法的其他形式及用法。

    逐成员的初始化器方法

    对于结构体类型有一种默认的初始化器形式,叫做逐成员的初始化器方法(Memberwise Initializers)。当我们在一个结构体类型中定义了某些存储式实例属性,但没有对它们直接初始化,并且也没有显式提供初始化器方法,那么我们在用该结构体去创建一个对象实例时就可使用逐成员的初始化方法来为该结构体对象中的每个存储式实例属性进行指定具体的值。我们先来看一个简单的例子:

    /// 定义一个结构体类型Test
    struct Test {
     
    /// 定义一个存储式实例属性a,
    /// 它已经直接被初始化
    var a = 10
     
    /// 定义一个存储式实例属性b,
    /// 它没有被直接初始化
    let b: Float
     
    /// 定义一个存储式实例属性c,
    /// 它没有被直接初始化
    var c: String
     
    /// 定义一个存储式实例属性d,
    /// 由于它是一个Optional对象,
    /// 因此会直接被默认初始化为空值
    var d: Int?
    }
     
    // 这里通过Test结构体类型所提供的默认的逐成员初始化器方法来创建对象实例test
    let test = Test(a: 10, b: 1.0, c: "Hello", d: nil)
    

    上述代码就使用了Test结构体所默认提供的逐成员初始化器方法来创建其对象实例test。由于是逐成员的,所以我们必须指定每一个该结构体中的存储式实例属性的值,即使它在结构体类型中已经做了初始化。

    当然,每一个结构体都会有默认提供的逐成员的初始化器方法,只要该结构体中没有提供显式实现的初始化器方法,即便该结构体中所有存储式实例属性都已经被初始化了。下面我们再看一个简单的例子。

    struct Test2 {
     
    var a = 10, b = 20
    }
     
    let t1 = Test2()
    let t2 = Test2(a: 1, b: 2)
    

    上述代码分别使用默认提供的无参数的初始化器方法以及默认提供的逐成员的初始化器方法各自创建了一个对象实例。

    值类型的初始化器代理

    Swift编程语言为枚举以及结构体类型这些值类型提供了初始化器代理(Initializer Delegation)语法特性。其实所谓的初始化器代理语法很简单,而且也很自然。我们之前已经知道初始化器方法与类型方法很类似,所以也有重载特性。当我们在一个初始化器方法中调用另一个初始化器方法以执行对一个对象实例的部分初始化,那么这个过程就称为初始化器代理。下面我们举一个简单的例子。

    /// 定义一个结构体类型Test
    struct Test {
     
    /// 定义一个存储式实例属性a,
    /// 它已经直接被初始化
    var a = 10
     
    /// 定义一个存储式实例属性b,
    /// 它没有被直接初始化
    let b: Float
     
    /// 定义一个存储式实例属性c,
    /// 它没有被直接初始化
    var c: String
     
    /// 定义一个存储式实例属性d,
    /// 由于它是一个Optional对象,
    /// 因此会直接被默认初始化为空值
    var d: Int?
     
    /// 定义了一个初始化器方法,
    /// 对存储式实例属性b进行初始化
    init(b: Float) {
    // 各位注意!
    // 我们必须在一个初始化器方法中
    // 为当前结构体对象的所有尚未被初始化的存储式实例属性进行初始化赋值。
    // 所以这里我们也需要对存储式实例属性c进行初始化赋值
    self.b = b
    c = ""
    }
     
    /// 定义了一个初始化器方法,
    /// 分别对存储式实例属性b与存储式实例属性c进行初始化
    init(b: Float, c: String) {
     
    // 各位注意,这里我们调用了init(b:)的初始化方法,
    // 这就是所谓的初始化器代理。
    // 此外,这里的 self. 不能省!
    self.init(b: b)
     
    self.c = c
    }
     
    /// 定义了一个初始化器方法,
    /// 分别对存储式实例属性b、存储式实例属性c与存储式实例属性d进行初始化
    init(b: Float, c: String, d: Int) {
     
    // 这里我们调用了init(b:c:)初始化器方法
    self.init(b: b, c: c)
     
    self.d = d
    }
    }
     
    // 我们这里使用Test结构体的init(b:)初始化器方法创建对象实例。
    // 这里我们还需要注意的是,
    // 一旦我们自己为某个类型提供了显式的初始化器方法,
    // 那么默认的初始化器方法,
    // 包括逐成员的初始化器方法就都不再提供了。
    // 此时我们只能使用我们自己实现的初始化器方法来创建对象实例。
    var test = Test(b: 1.0)
     
    // 使用Test结构体类型的init(b:c:)初始化器方法创建对象实例
    test = Test(b: 2.0, c: "OK")
     
    // 使用Test结构体类型的init(b:c:d:)初始化器方法创建对象实例
    test = Test(b: 3.0, c: "Yes", d: 5)
     
    print("b = \(test.b), c = \(test.c), d = \(String(describing: test.d))")
    

    上述代码例子详细介绍了初始化器代理的使用方式。我们在结构体以及枚举类型中可以通过这种方式将当前类型中对某些存储式实例属性的初始化给整理为一个初始化器方法,然后可以提供更多参数的初始化器方法进行扩展,这样可以省去不少重复代码。

    可失败的初始化器

    我们有时需要定义某些类型,这些类型根据用户的输入或当前执行环境可能造成其对象实例的创建失败,此时我们可以使用可失败的初始化器(Failable Initializers)。可失败的初始化器方法可根据当前条件返回空值。因此当我们使用可失败的初始化器来创建一个对象时,该对象的类型为此类型的Optional类型。

    可失败的初始化器方法的声明非常简单,我们直接在 init 关键字后面紧跟着放一个 ? 即可,然后后面再跟形参列表。下面我们举一个例子进行说明。

    /// 定义一个结构体类型Test
    struct Test {
     
    /// 定义一个存储式实例属性a,
    /// 它未被直接被初始化
    var a: Int
     
    /// 这里定义了一个可失败的初始化器方法。
    /// 大家注意,这里的 init 与 ? 之间不允许出现任何空白字符。
    init? (value: Int) {
    if value == 0 {
    // 若形参value的值等于0,那么直接返回空
    return nil
    }
     
    a = 100 / value
     
    // 在可失败的初始化器方法中只能使用 return nil 语句,
    // 所以return后面不能添加其他对象或值
    }
    }
     
    // 这里使用可失败的初始化器尝试创建一个对象实例。
    // 这里test的类型为:Test?
    let test = Test(value: 0)
     
    if test == nil {
    // 这里将会输出Failed!
    print("Failed!")
    }
    

    上述代码简单地演示了可失败的初始化器方法的使用场景以及效果。可失败初始化器除了可用 init? 之外,还能使用 init!。我们对上述例子稍作修改来演示 init! 的使用。

    /// 定义一个结构体类型Test
    struct Test {
     
    /// 定义一个存储式实例属性a,
    /// 它未被直接被初始化
    var a: Int
     
    /// 这里定义了一个可失败的初始化器方法。
    /// 大家注意,这里的 init 与 ! 之间不允许出现任何空白字符。
    init! (value: Int) {
    if value == 0 {
    // 若形参value的值等于0,那么直接返回空
    return nil
    }
     
    a = 100 / value
    }
    }
     
    // 这里使用可失败的初始化器尝试创建一个对象实例。
    // 注意,这里test的类型仍然为:Test?
    var test = Test(value: 0)
     
    if test == nil {
    // 这里将会输出Failed!
    print("Failed!")
    }
     
    test = Test(value: 10)
    print("value = \(String(describing: test?.a))")
    

    我们可以看到, init? 与 init! 在一般使用上差不多。不过如果用于初始化器代理,那么会稍微有些差别。

    下标语法

    我们之前已经谈到了数组 Array 以及字典 Dictionary 可以通过下标操作符来访问与其相对应的元素对象。Swift编程语言允许我们在自定义类型中使用下标(Subscript)。Swift中下标只有实例方法形式,没有类型方法形式。

    我们通过 subscript 关键字来定义一个下标,其后面的形参列表即为下标 [ ] 中的索引列表。索引列表中的每个表达式使用逗号分隔。对下标的实现风格与计算式属性比较类似,我们使用 get 块(即getter方法)来实现通过指定的下标索引获得相应的值;使用 set 块(即setter方法)来实现通过指定的下标索引设置相应的值。此外,下标也可以缺省 set 块表示当前下标是只读的,而 get 块则不可缺省。

    下标同其他实例方法类似,也有类似于其他实例方法的语法特性,只不过对于结构体和枚举这种值类型来说,我们无需在下标方法前添加 mutating 关键字。下标方法也可进行重载。但有一点各位要注意:下标本质上不属于类型的实例方法,它其实更偏向于计算式实例属性,因此我们不能像初始化器方法那样去显式地调用 subscript,也不能用一个函数引用对象指向某一个下标。此外,对于下标的形参,我们不能为它添加默认值。下面我们举一些例子来说明下标方法的使用。

    /// 定义一个结构体类型Test
    struct Test {
     
    /// 定义一个存储式实例属性a
    var a = 10
     
    /// 定义下标方法,
    /// 它具有一个Int类型参数,
    /// 返回值也为Int
    subscript(index: Int) -> Int {
     
    // 我们可以看到,
    // 下标方法的getter与setter同计算式属性的十分类似
    get {
    return a + index
    }
     
    // 这里各位要注意的是,
    // 下标的setter方法的形参类型为下标方法的返回类型,
    // 而不是下标方法的形参类型
    set(value) {
    a = value + index
    }
    }
     
    /// 这里又定义了一个下标方法,
    /// 它具有一个String类型参数,
    /// 返回类型为Int。
    /// 此外,它只有getter方法,因此是只读的
    subscript(str: String) -> Int {
     
    // 同计算式属性一样,
    // 如果下标方法中只含有getter方法,
    // 那么get块可省
    return str.count
    }
     
    /// 这里又定义了一个下标方法,
    /// 它具有两个参数,分别为Int与String,
    /// 最后返回一个 Int? 类型
    subscript(value: Int, str: String) -> Int? {
     
    get {
    guard let count = Int(str) else {
    return nil
    }
     
    return value + count
    }
     
    set {
    // 这里同计算式属性一样,
    // setter后面的形参可省,
    // 如果缺省形参,
    // 那么Swift提供了一个隐式形参标识符:newValue
    if let data = newValue, let strValue = Int(str) {
    a = value + data + strValue
    }
    }
    }
     
    /// 这里又定义了一个下标方法,
    /// 其形参为Void类型,返回Int类型
    /// 这里还需注意,下标的形参不能带有默认值
    subscript(_: Void) -> Int {
     
    get {
    return a
    }
     
    set {
    a = newValue
    }
    }
    }
     
    // 这里使用可失败的初始化器尝试创建一个对象实例。
    // 注意,这里test的类型仍然为:Test?
    var test = Test()
     
    // 这里调用了test对象的下标方法 subscript(index:)
    // 的setter方法
    test[1] = 10
     
    // 这里调用了test对象的下标方法 subscript(index:)
    // 的getter方法
    print("test[5] = \(test[5])")
     
    // 这里调用了test对象的下标方法 subscript(str:)
    // 的getter方法
    print("count = \(test["abc"])")
     
    // 这里调用了test对象的下标方法 subscript(value:str:)
    // 的setter方法。
    // 这里各位需要注意下标中多个索引表达式的使用
    test[2, "123"] = 10
     
    // 这里调用了test对象的下标方法 subscript(value:str:)
    // 的getter方法
    print("value = \(String(describing: test[5, "10"]))")
     
    // 这里调用了test对象的下标方法 subscript(_:) 的getter方法
    print("a = \(test[()])")
     
    // 这里调用了test对象的下标方法 subscript(_:) 的setter方法。
    test[()] = 0
     
    print("a = \(test[()])")
    

    上述代码展示了结构体中下标的使用。枚举与类类型的下标使用也与此相同。我们可以看到,下标的参数列表中可以含有多个形参,并且每个形参的类型也可以是任意类型,包括 Void 类型。此外,下标的语法上与计算式实例属性十分类似,通过getter方法与setter方法进行定义。不过这里要注意的是,setter方法的形参类型为该下标的返回类型,而不是该下标的形参类型,更何况下标的形参可以有多个。

    下标与其他函数和方法还有一个不同之处在于,下标默认不带有实参标签,所以像上述代码中的下标声明:subscript(value: Int, str: String),其下标签名为: subscript(::)。不过我们也可以显式地给下标形参添加实参标签,我们请看以下简单的例子。

    /// 定义了一个Test结构体
    struct Test {
     
    private var dict = [1:"1", 2:"2", 3:"3"]
     
    /// 定义了一个下标,第一个形参具有实参标签index,
    /// 第二个形参具有实参标签value
    /// - parameter index: 指定字典的key
    /// - parameter value: 指定字典的默认值
    subscript(index index: Int, default value: String) -> String? {
    get {
    return dict[index] ?? "null"
    }
    set {
    newValue == nil ? (dict[index] = value) : (dict[index] = newValue!)
    }
    }
    }
     
    var test = Test()
    test[index: 10, default: "10"] = "-10"
    test[index: 20, default: "20"] = nil
     
    let str10 = test[index: 10, default: ""]
    print("str10 = \(str10!)")
     
    let str20 = test[index: 20, default: ""]
    print("str20 = \(str20!)")
    

    从上述代码我们可以看到,Test结构体的下标增加了形参标签,因此我们在使用该下标时也需要显式地加上形参标签。此时,该下标的签名为:subscript(index:default:)。

    KEY PATH

    我们在本章介绍了结构体以及属性与方法相关的概念。有时候,一个结构体、枚举或类类型中的某个属性的类型比较复杂,类型嵌套比较深。因此Swift 4中引入了 Smart KeyPaths 这一概念来简化对一些嵌套比较深的属性的访问。

    Smart KeyPaths 采用一种字面量的方式来构造一个访问属性或方法的关键路径。其形式为:<类型名>.<路径>。然后某一对象实例在通过关键路径访问相关属性时采用<对象实例标识符>.[keyPath: <关键路径标识符>]这种方式进行。我们下面先举一个简单的例子来引出 Smart KeyPaths 这个概念,先给各位有一个感性的认识。

    /// 我们这里自己定义了一个矩形
    struct MyRect {
     
    /// 在里面定义一个Point类型
    struct Point {
    var x: Float
    var y: Float
    }
     
    /// 在里面定义一个Size类型
    struct Size {
    var width: Float
    var height: Float
    }
     
    /// 定义position实例属性
    var position: Point
     
    /// 定义size实例属性
    var size: Size
    }
     
    /// 定义MyStruct结构体类型
    struct MyStruct {
     
    /// 定义property实例属性
    var property: Int
     
    /// 用上面定义的MyRect类型
    /// 定义rect实例属性
    var rect: MyRect
    }
     
    /// 这里用Smart KeyPath字面量
    /// 定义一个widthKeyPath关键路径,
    /// 它是对MyStruct.rect.size.width
    /// 这一实例属性的访问路径
    let widthKeyPath = \MyStruct.rect.size.width
     
    /// 创建obj这一MyStruct的对象实例
    var obj = MyStruct(property: 10, rect: MyRect(position: MyRect.Point(x: 0.0, y:1.0), size: MyRect.Size(width: 10.0, height: 20.0)))
     
    /// 我们可以简单地通过widthKeyPath关键路径
    /// 来访问obj.rect.size.width
    let width = obj[keyPath: widthKeyPath]
     
    print("width = \(width)")
     
    // 这里直接用关键路径字面量构造
    // 访问obj对象的rect.position.x实例属性
    obj[keyPath: \MyStruct.rect.position.x] += obj.rect.position.y * 8.0
     
    print("x = \(obj.rect.position.x)")
    

    上述代码例子中,我们声明的 widthKeyPath 就是一个关键路径对象。正如我们早先所提到的,Swift中任何类型都不会无中生有,这里的关键路径也同样如此!上述代码中用字面量所构造的关键路径对象均属于 WritableKeyPath 这一泛型类型,它是一个类类型,因此关键路径之间的赋值都以引用的方式进行,而不是值拷贝,这对于运行时效率来说是件好事。

    相关文章

      网友评论

          本文标题:结构体

          本文链接:https://www.haomeiwen.com/subject/vsmlzftx.html