美文网首页Swift开发那些事
Swift - mutating & inout

Swift - mutating & inout

作者: just东东 | 来源:发表于2021-01-05 10:43 被阅读0次

    Swift - mutating & inout

    前言

    曾几何时,刚用swift的时候,我想修改传入的参数,发现不能修改,于是就有了如下代码:

    func testSwap(a: Int, b: Int) -> (a: Int, b: Int) {
        return (b, a)
    }
    

    其实我的意思是:

    func testSwap(a: Int, b: Int) {
        let temp = a
        a = b
        b = temp
    }
    

    但是这样写会出现编译错误的:

    16094054869591.jpg

    因为在swift中结构体和枚举是值类型,直接修改其属性是不行的,如下:

    struct Point {
        var x = 0
        var y = 0
        
        func movePoint(x: Int, y: Int) {
            self.x += x
        }
    }
    
    enum Switch {
        case top, bottom, left, right
        
        func next() {
            switch self {
            case .top:
                self = .bottom
            case .bottom:
                self = .left
            case .left:
                self = .right
            case .right:
                self = .top
            }
        }
    }
    
    16094069507649.jpg

    那么该如何解决上述问题呢?

    1. mutating

    1.1 mutating 使用

    默认情况下,实例方法中是不可以修改值类型的属性的,使用mutating后可修改属性的值,上面编译报错的解决方法如下:

    struct Point {
        var x = 0
        var y = 0
        
        mutating func movePoint(x: Int, y: Int) {
            self.x += x
        }
    }
    
    enum Switch {
        case top, bottom, left, right
        
        mutating func next() {
            switch self {
            case .top:
                self = .bottom
            case .bottom:
                self = .left
            case .left:
                self = .right
            case .right:
                self = .top
            }
        }
    }
    

    加上mutating后则不会存在编译报错的问题

    16094071472114.jpg

    如果使用mutating修饰,类中的方法是会编译报错的,因为类是引用类型,在其方法中默认情况下就可以修改属性值,不存在以上问题。

    16094081327495.jpg

    1.2 mutating 实现

    1. 那么为什么使用mutating就可以修改值类型中的属性了呢?
    2. 使用前后的差别是什么呢?
    3. mutating的底层原理是什么呢?

    使用前:

    示例代码:

    struct Point {
        var x = 0
        var y = 0
        
        func movePoint(x: Int, y: Int) {
            print(x)
        }
    }
    

    sil代码:


    16094086930611.jpg

    我们可以通过sil代码看到,在不使用mutating修饰前,方法中的selflet不可修改的

    那么使用mutating修饰是什么样子的呢?下面我们就来看看

    使用后:

    示例代码:

    struct Point {
        var x = 0
        var y = 0
    
        mutating func movePoint(x: Int, y: Int) {
            self.x += x
        }
    }
    

    sil代码:


    16097404014498.jpg

    在使用mutating修饰后,我们在sil代码中可以看到我们的Point结构体使用了inout修饰,并且在方法内部的self也变成可变类型的了。

    所以在添加mutating修饰后:

    • 我们的值类型前面加上了inout修饰
    • 使用前后方法内的self分别是letvar
    • 所以mutating的本质就是inout

    2. inout

    通过上面对mutating的探索我们知道它的本质就是inout,那么inout是怎么实现的呢?下面我们就一起来研究一下。

    inoutSwift 中的关键字,可以放置于参数类型前,冒号之后。使用 inout 之后,函数体内部可以直接更改参数值,而且改变会保留。

    我们知道Swift中有值类型和引用类型,为了区分开来,我们先准备一些代码:

    为了方便打印,PointClass遵守了CustomStringConvertible协议

    struct PointStruct {
        var x = 0.0
        var y = 0.0
    }
    
    class PointClass: CustomStringConvertible {
        var x = 0.0
        var y = 0.0
        
        var description: String {
            return "PointClass(x: \(x), y: \(y))"
        }
    }
    

    2.1 参数

    为了更好的说明,下面我们先来说说参数:

    Swift中,方法中的参数默认是常量类型,也就是说在函数内只能访问参数,不能修改参数值:

    1. 对于值类型的参数,不能修改其值
    2. 对于引用类型的参数,我们不能修改其指向的内存地址,但是其内部的可变的变量时可以修改的
    16097449717039.jpg

    如果我们要改变参数的值或引用,我们可以采用笨方法,就是在函数体内声明一个同类型的同名变量,然后操作这个新的变量,修改其值或引用。那么这样做会改变函数参数的生命周期吗?或者说函数参数的作用域会改变吗,下面我们来测试一下,定义一函数,交换改点的坐标。

    值类型:

    func swap(ps: PointStruct) -> PointStruct {
        var ps = ps
        withUnsafePointer(to: &ps) { print("地址1: \($0)") }
        
        let temp = ps.x
        ps.x = ps.y
        ps.y = temp
        
        return ps
    }
    
    var ps1 = PointStruct(x: 10, y: 20)
    print(ps1)
    withUnsafePointer(to: &ps1) { print("地址2: \($0)") }
    print(swap(ps: ps1))
    print(ps1)
    withUnsafePointer(to: &ps1) { print("地址3: \($0)") }
    

    打印结果:

    16097456030165.jpg

    根据打印结果我们可以知道,对于值类型的参数:

    1. 函数调用前后,外界变量的和其值并没有因为内部对参数的修改而改变
    2. 函数体内的参数的内存地址和外界是不一样的,一个值栈区,一个是全局区
    3. 按照这种方法,相当于在函数体内深拷贝了一份参数,作用域仅在函数体内

    引用类型:

    func swap(pc: PointClass) -> PointClass {
        withUnsafePointer(to: pc) { print("参数地址: \($0)") }
        print("地址1: \(Unmanaged.passUnretained(pc).toOpaque())")
        let temp = pc.x
        
        pc.x = pc.y
        pc.y = temp
        
        return pc
    }
    
    let pc1 = PointClass()
    withUnsafePointer(to: pc1) { print("外部变量地址: \($0)") }
    pc1.x = 10
    pc1.y = 20
    print(pc1)
    print("地址2: \(Unmanaged.passUnretained(pc1).toOpaque())")
    print(swap(pc: pc1))
    print(pc1)
    print("地址3: \(Unmanaged.passUnretained(pc1).toOpaque())")
    

    打印结果:

    16097487777079.jpg

    根据打印结果我们可以知道,对于引用类型的参数:

    1. 函数内外的指针指向是一致的
    2. 作为参数时时创建了一个新的常量,指向源变量的引用
    3. 如果在函数体内修改其内部变量的值,外部也会受到影响

    2.2 使用 inout

    对于上面提到的,我们使用inout来实现:

    值类型:

    func swap(ps: inout PointStruct) -> PointStruct {
        withUnsafePointer(to: &ps) { print("地址1: \($0)") }
    
        let temp = ps.x
        ps.x = ps.y
        ps.y = temp
    
        return ps
    }
    
    var ps1 = PointStruct(x: 10, y: 20)
    print(ps1)
    withUnsafePointer(to: &ps1) { print("地址2: \($0)") }
    print(swap(ps: &ps1))
    print(ps1)
    withUnsafePointer(to: &ps1) { print("地址3: \($0)") }
    

    打印结果:

    16097494590963.jpg

    根据打印结果我们可以知道,对于使用inout修饰的值类型的参数:

    1. 外界和参数的地址保值一致
    2. 在函数内对值类型的修改得到了保留

    引用类型:

    func swap(pc: inout PointClass) -> PointClass {
        withUnsafePointer(to: pc) { print("参数地址: \($0)") }
        print("地址1: \(Unmanaged.passUnretained(pc).toOpaque())")
        let temp = pc.x
    
        pc.x = pc.y
        pc.y = temp
    
        return pc
    }
    
    var pc1 = PointClass()
    withUnsafePointer(to: pc1) { print("外部变量地址: \($0)") }
    pc1.x = 10
    pc1.y = 20
    print(pc1)
    print("地址2: \(Unmanaged.passUnretained(pc1).toOpaque())")
    print(swap(pc: &pc1))
    print(pc1)
    print("地址3: \(Unmanaged.passUnretained(pc1).toOpaque())")
    

    打印结果:

    16097514968779.jpg

    根据打印结果我们可以知道,对于使用inout修饰的引用类型的参数:

    1. 参数的地址和外部变量的地址是一致的
    2. 它们指向的内存空间的地址也是一样的
    3. 对于修改,当然也是会保持一致的
    4. 其实对于引用类型的参数,使用inout的意义不是很大

    2.3 注意事项

    通过上面的一些解释,我们可以知道使用inout关键字可以在函数体内修改参数,但其中也有一些注意事项:

    1. 使用inout关键字的函数,在调用时需要在该参数前加上&符号
    2. 使用inout的参数在传入时必须为变量,不能为常量
    3. 使用inout的参数不能有默认值,不能为可变参数
    4. 使用inout的参数不等同与函数返回值,是一种使参数的作用域超出函数体的方式
    5. 多个使用inout的参数不能同时传入一个变量,因为拷入拷出的顺序不定,那么最终值也不能确定

    2.4 inout 的原理

    我们使用一段简单的代码,编译成sil代码,看看其底层实现是什么样子的:

    func testSwap(a: inout Int, b: inout Int) {
        let temp = a
        a = b
        b = temp
    }
    

    sil代码:

    // testSwap(a:b:)
    sil hidden @main.testSwap(a: inout Swift.Int, b: inout Swift.Int) -> () : $@convention(thin) (@inout Int, @inout Int) -> () {
    // %0 "a"                                         // users: %11, %4, %2
    // %1 "b"                                         // users: %14, %8, %3
    bb0(%0 : $*Int, %1 : $*Int):
      debug_value_addr %0 : $*Int, var, name "a", argno 1 // id: %2
      debug_value_addr %1 : $*Int, var, name "b", argno 2 // id: %3
      %4 = begin_access [read] [static] %0 : $*Int    // users: %5, %6
      %5 = load %4 : $*Int                            // users: %15, %7
      end_access %4 : $*Int                           // id: %6
      debug_value %5 : $Int, let, name "temp"         // id: %7
      %8 = begin_access [read] [static] %1 : $*Int    // users: %9, %10
      %9 = load %8 : $*Int                            // user: %12
      end_access %8 : $*Int                           // id: %10
      %11 = begin_access [modify] [static] %0 : $*Int // users: %12, %13
      store %9 to %11 : $*Int                         // id: %12
      end_access %11 : $*Int                          // id: %13
      %14 = begin_access [modify] [static] %1 : $*Int // users: %15, %16
      store %5 to %14 : $*Int                         // id: %15
      end_access %14 : $*Int                          // id: %16
      %17 = tuple ()                                  // user: %18
      return %17 : $()                                // id: %18
    } // end sil function 'main.testSwap(a: inout Swift.Int, b: inout Swift.Int) -> ()'
    

    通过sil代码我们可以看到:

    1. 使用inout修饰的参数,在函数体内部都是var可变类型的
    2. 在修改的时候,都是修改的其内存中的数据

    官方文档——In-Out Parameters的解释:

    As an optimization, when the argument is a value stored at a physical address in memory, the same memory location is used both inside and outside the function body. The optimized behavior is known as call by reference; it satisfies all of the requirements of the copy-in copy-out model while removing the overhead of copying. Write your code using the model given by copy-in copy-out, without depending on the call-by-reference optimization, so that it behaves correctly with or without the optimization.
    译:作为一种优化,当实参是存储在内存中的物理地址中的值时,函数体内外都使用相同的内存位置。优化的行为称为引用调用;它满足了copy-in copy-out模型的所有要求,同时消除了复制的开销。使用copy-in copy-out给出的模型编写代码,而不依赖引用调用优化,这样无论优化与否,它都能正确地运行。

    3. 总结

    本文主要讲述了Swift中的mutatinginout

    1. mutating主要用于值类型中的方法,修改其属性
    2. inout主要用于想在方法中修改参数的场景

    相关文章

      网友评论

        本文标题:Swift - mutating & inout

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