美文网首页
Swift进阶五:结构体

Swift进阶五:结构体

作者: Trigger_o | 来源:发表于2022-03-09 12:37 被阅读0次

    在swift中,类,结构体,枚举以及闭包捕获变量都可以作为存储结构化数据的方法.但在标准库中,绝大多数类型都是结构体,类和枚举只占很小一部分,并且很多foundation的类都对应成了结构体.另外枚举的特性和结构体非常相似,所以放在一起对比.

    对比结构体(枚举)和类:
    1.体是值类型,类是引用类型,在设计结构体时,我们可以要求编译器保证不可变性。而对于类来说,我们就得自己来确保这件事情。
    2.结构体是直接持有并访问,类只能通过引用间接访问,结构体不会被持有只能被复制,但是类可以被多个指针引用,有多个持有者.
    3.类可以通过继承来共享代码,但是结构体想要共享代码需要按情况使用不同的技术,比如组合,泛型,协议扩展.

    值类型

    值类型和引用类型
    在OC中,我们经常需要一些有明确生命周期的对象,比如viewController,创建它,改变它,最后销毁它,如果想要比较他们,则需要比较他们是否指向同样的地址,创建和销毁时都需要做专门的操作.
    但是也有一些对象不需要明确的生命周期,创建之后就不再更改,比如NSURL,并且在销毁时也不需要做什么操作,比较两个URL也不用比较地址,只要对比url即可,因此,在swift中,URL是一个结构体.

    swift结构体
    值永远不会改变,它们具有不可变的特性。这 (在绝大多数情况下) 是一件好事,因为使用不变的数据可以让代码更容易被理解。不可变性也让代码天然地具有线程安全的特性,因为不能改变的东西是可以在线程之间安全地共享的.
    swift中结构体是值类型,但是结构体的属性却可以是可变的,你可以使用var来声明结构体的属性,这是为什么.
    实际上这个var虽然指属性可以被修改,但是修改这个属性和创建一个新的结构体是等价的,当修改了这个 属性之后,新的结构体被创建出来,这个属性使用了新的值,然后替换了原来的结构体.结构体本身没有被修改,他就好像一个Int型变量,值从0换成1,0还是0,1还是1.
    因此,结构体只能有一个持有者,当把结构体传给函数时,函数获得结构体的一份复制,函数也只能修改它的这份复制,这叫做值语义.而对象传递引用,可以有多个持有者,叫做引用语义.
    值总是需要复制,因此编译器自然会做优化,需要注意的是,这个值语义优化并不是写时复制,写时复制是开发者自己实现的,后面会讲到.
    如果一个结构体只由其他结构体组成,那编译器可以确保不可变性。同样地,当使用结构体时,编译器也可以生成非常快的代码。举个例子,对一个只含有结构体的数组进行操作的效率,通常要比对一个含有对象的数组进行操作的效率高得多。这是因为结构体通常要更直接:值是直接存储在数组的内存中的。而对象的数组中包含的只是对象的引用。最后,在很多情况下,编译器可以将结构体放到栈上,而不用放在堆里。

    可变性

    可变性经常是导致异常的原因.

    let mutableArray: NSMutableArray = [1,2,3]
    for _ in mutableArray { 
        mutableArray.removeLastObject() 
    }
    

    这是一段会crash的代码,迭代器以数组为基础工作,改变数组会改变迭代器的状态,引发异常.

    var mutableArray = [1, 2, 3]
    for _ in mutableArray { 
        mutableArray.removeLast() 
    }
    

    这段代码不会crash,因为mutableArray是值类型,迭代器持有的mutableArray是值语义,它一直都是三个元素,

        在类中,我们可以使用 var 和 let 来控制属性的可变和不可变性。比如,我们可以创建一个和Foundation 中Scanner 类似的扫描器,不过区别是它将读取二进制数据。在 Scanner 类中,你可以从一个字符串中扫描值,每次成功获得一个扫描值后就进行步进。类似地,我们的BinaryScanner 类将持有一个位置属性 (它是可变的,因为它是用 var 声明的),以及原始的数据 (它是不可变的,因为它是用 let 声明的)。
    

    同时它还有一个函数,来扫描字节,并且改变position.

    class BinaryScanner { 
        var position: Int 
        let data: Data 
        init(data: Data) { 
              self.position = 0
              self.data = data 
         } 
    
        func scanByte() -> UInt8? { 
            guard position < data.endIndex else { return nil } 
            position += 1
            return data[position-1]
         }
    }
    
    

    定义一个函数来使用BinaryScanner,然后来试一下.

    func scanRemainingBytes(scanner: BinaryScanner) { 
        while let byte = scanner.scanByte() { 
             print(byte)
         } 
    }
    let scanner = BinaryScanner(data: Data("hi".utf8))
    /* 104 105 */
    

    现在到了关键的时候,position是可变的,如果在两个不同的线程调用scanRemainingBytes的话,就会进入竞态条件,在线程1中,position < data.endIndex为真,但是在position += 1之前,线程2先执行了position += 1,然后cpu切换到线程1,这时就会越界.

    for _ in 0..<Int.max {
          let newScanner = BinaryScanner(data: Data("hi".utf8)) 
          DispatchQueue.global().async { 
                scanRemainingBytes(scanner: newScanner)
           } 
           scanRemainingBytes(scanner: newScanner) 
    }
    

    这里套在一个0..<Int.max迭代里是因为遇到越界的概率不大.

    结构体

    首先简单的回忆一下,一个值类型的变量赋值给另一个变量时,值会被复制

    var a = 42 
    var b = a 
    b += 1 
    b // 43 
    a // 42
    

    在swift中,结构体就是这么工作的.

    struct Point { 
        var x: Int 
        var y: Int 
    }
    let origin = Point(x: 0, y: 0)
    //origin.x = 10 // 错误
    

    虽然x和y是var定义的,但是结构体origin是let,因此依然不能改变它的属性,好处也很明显,看到"let x = 结构体构造"的时候就知道x是个不能被修改的值类型.

    同变量a和b的例子一样,换成结构体也成立

    var thirdPoint = origin 
    thirdPoint.x += 10 
    thirdPoint // (x: 10, y: 0) 
    origin // (x: 0, y: 0)
    

    **构造方法 **
    上面定义的Point,swift自动生成了基于成员的构造方法,但是如果自定义构造方法,就没有swift提供的构造方法了.

    struct Size { 
        var width: Int 
        var height: Int 
       init(w:Int, h:Int){
            width = w
            height = h
        }
    }
    var p1 = Point.init(w: 10, h: 10)
    

    这个时候就只有init(x:Int, y:Int)这一个构造方法了,不过可以通过扩展Size来保留原来的构造方法

    extension Point{
        init(w:Int, h:Int){
            width = w
            height = h
        }
    }
    

    这样两种构造方法就都有了

    可变语义

    一开始的时候说到,改变结构体的成员,实际上是生成了一个新的结构体,那么如果给类的一个结构体成员变量设置didSet会发生什么.
    首先构造一个嵌套的结构体,加一个构造方法

    struct Rectangle { 
        var origin: Point 
        var size: Size 
    }
    extension Rectangle {
        init(x: Int = 0, y: Int = 0, width: Int, height: Int) {
            origin = Point(x: x, y: y)
            size = Size(width: width, height: height)
        }
    }
    

    然后声明一个全局变量,设置didSet,修改两次成员的值

    var screen = Rectangle.init(width: 100, height: 100){
            didSet {
                print("Screen changed: \(screen)")
            }
    }
    
    screen.origin.x += 10
    screen.size.height += 10
    
    //Screen changed: Rectangle(origin: TestS.Point(x: 10, y: 0), size: TestS.Size(width: 100, height: 100))
    //Screen changed: Rectangle(origin: TestS.Point(x: 10, y: 0), size: TestS.Size(width: 100, height: 110))
    

    运行可以看到打印了两次,也就是说即便修改的是结构体深处的成员,didSet也会执行,那么如果Point的成员也设置didSet呢.
    给Size的width属性添加didSet然后修改screen.size.width,运行

    struct Size {
        var width: Int{
            didSet{
                print("width changed: \(width)")
            }
        }
        var height: Int
    }
    
     screen.size.width += 10
    
    //width changed: 110
    //Screen changed: Rectangle(origin: TestS.Point(x: 0, y: 0), size: TestS.Size(width: 110, height: 100))
    
    

    可以看到两层didSet都执行了;
    对结构体进行改变,在语义上来说,与重新为它进行赋值是相同的。即使在一个更大的结构体上只有某一个属性被改变了,也等同于整个结构体被用一个新的值进行了替代。在一个嵌套的结构体的最深层的某个改变,将会一路向上反映到最外层的实例上,并且一路上触发所有它遇到的 willSet 和 didSet.

    这种影响还会继续扩展,比如标准库的内建集合类型是结构体,比如数组,向数组添加元素,修改某个元素都会触发数组的didSet,因此,如果数组中放的是值类型,这个值类型内部发生变化最终也会触发数组的didSet

    var screens: [Rectangle] = [] { 
        didSet { print("Screens array changed: \(screens)")
         }
     }
    screens.append(Rectangle(width: 320, height: 480)) // Screens array changed: [(0, 0, 320, 480)] 
    screens[0].origin.x += 100 // Screens array changed: [(100, 0, 320, 480)]
    

    如果Rectangle是类就不会触发didset,因为数组存储的引用没有发生变化.

    可变方法

    假设我们要为 Rectangle 添加一个 translate 方法,用来将矩形以给定的偏移量进行位移。我们需要在矩形当前的原点值上增加这个偏移量,所以我们首先需要添加一个 + 操作符的重载,用来将两个 Point 相加到一起,并返回一个新的 Point.
    然后添加translate方法.

    func +(lhs: Point, rhs: Point) -> Point {
        return Point(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
    }
    
    extension Rectangle {
        func translate(by offset: Point) {
    // 错误:不能赋值属性: 'self' 是不可变的
            origin = origin + offset
        }
    }
    

    但是translate这里标红了,并且提示给func添加mutating关键字.

    extension Rectangle {
         mutating func translate(by offset: Point) { 
              origin = origin + offset
         } 
    }
    screen.translate(by: Point(x: 10, y: 10)) screen // (10, 10, 320, 480)
    

    函数并不知道实例将会被声明为可变还是不可变 ,加上mutating相当于告诉函数,这个结构体将创建出可变的实例,并且这个函数要描述实例被修改的行为.对此编译器会进行相关的处理.

    let otherScreen = screen 
    // 错误:不能对不可变的量使⽤可变成员
     otherScreen.translate(by: Point(x: 10, y: 10))
    

    因此mutating标记的方法只有可变实例才能调用 .

    回想一下内建集合一章,我们现在可以理解将 let 和 var 应用到集合上的区别了。数组的append 方法被定义为mutating,所以当数组被定义为 let 时,编译器不让我们调用这个方法。属性的 setter 自身就是 mutating 的,你无法调用一个 let 变量的 setter:

    let point = Point.zero 
    // 错误:⽆法赋值属性:'point' 是⼀个 'let' 常量 
    point.x = 10
    

    mutating 同时也是 willSet 和 didSet “知道” 合适进行调用的依据:任何 mutating 方法的调用或者隐式的可变 setter 都会触发这两个事件。

    在很多情况下,一个方法会同时有可变和不可变版本。比如数组有 sort() 方法 (这是个mutating 方法,将在原地排序) 以及 sorted() 方法 (返回一个新的数组)。我们也可以为我们的translate(by:_) 提供一个非 mutating 的版本。这次我们不再改变 self,而是创建一个复制,改变它,然后返回这个新的 Rectangle:

    extension Rectangle { 
        func translated(by offset: Point) -> Rectangle { 
            var copy = self 
            copy.translate(by: offset)
             return copy
         } 
    }
    screen.translated(by: Point(x: 20, y: 20)) // (30, 30, 320, 480)
    

    mutating是如何工作的

    首先引入一个例子,定义一个全局函数,来将一个矩形在两个轴方向上各移动 10 个点,传进来的是值语义,只能使用translated(by:)

    func translatedByTenTen(rectangle: Rectangle) -> Rectangle {
        return rectangle.translated(by: Point(x: 10, y: 10))
    }
    
    screen = translatedByTenTen(rectangle: screen)
    

    如过rectangle是var,那么就可以使用translate()了,而mutating就是在做这件事,可以把 self 想像为一个传递给Rectangle 所有方法的额外的隐式参数。你不需要自己去传递这个参数,但是在函数体内部你可以随时使用self,而mutating 关键字可以将隐式的 self 参数变为可变的.

    将一个或多个参数标记为 inout 来达到相同的效果。就和一个普通的参数一样,值被复制并作为参数被传到函内。不过,我们可以改变这个复制 (就好像它是被var 定义的一样)。然后当函数返回时,Swift 会将这个 (可能改变过的) 值进行复制并将其返回给调用者,同时将原来的值覆盖掉.
    mutating标记func,实际上是用inout标记了隐式的self参数.

    func translateByTwentyTwenty(rectangle: inout Rectangle) {
         rectangle.translate(by: Point(x: 20, y: 20)) 
    }
    translateByTwentyTwenty(rectangle: &screen)
    

    那些像是 += 这样,可以对左侧值进行变更的运算符,需要其参数为 inout。下面是 Point 的+= 实现。如果我们将 inout 去掉的话,编译器就不会允许我们对 lhs 进行赋值了

    func +=(lhs: inout Point, rhs: Point) { 
        lhs = lhs + rhs 
    }
    var myPoint = Point.zero 
    myPoint += Point(x: 10, y: 10)
    myPoint // (x: 10, y: 10)
    

    现在再来看最开始的问题代码

    for _ in 0..<Int.max { 
        let newScanner = BinaryScanner(data: Data("hi".utf8)) 
        DispatchQueue.global().async { 
            scanRemainingBytes(scanner: newScanner)
         } 
        scanRemainingBytes(scanner: newScanner) 
    }
    

    如果 BinaryScanner 是一个结构体,而非类的话,每次 scanRemainingBytes 的调用都将获取它自己的 newScanner 的独立的复制。这样一来,这些调用将能够在数组上保持安全的迭代,而不必担心结构体被另一个方法或者线程所改变。因为两个线程现在并没有共享一个单一的position 值,每个循环中将会把每个字节打印两次。

    相关文章

      网友评论

          本文标题:Swift进阶五:结构体

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