Swift开发:定义对象时Struct&Class如何抉择?

对于初期接触Swift的人来说,了解了结构体的相关知识后,开发过程中可能会纠结当前这一对象是该定义为类好,还是结构体?下面让我们来了解下。

Swift 中结构体和类有很多共同点。两者都可以:

  1. 定义属性用于存储值
  2. 定义方法用于提供功能
  3. 定义下标操作用于通过下标语法(subscript)访问它们的值
  4. 定义构造器用于设置初始值
  5. 通过扩展(extension)以增加默认实现之外的功能
  6. 遵循协议(protocol)以提供某种标准功能

与结构体相比,类还有如下的附加功能:

  1. 继承允许一个类继承另一个类的特征
  2. 类型转换允许在运行时检查和解释一个类实例的类型
  3. 析构器允许一个类实例释放任何其所被分配的资源
  4. 引用计数允许对一个类的多次引用

所以,类可以利用继承实现重写、多态等特性,这是结构体所不能做到的。另外,结构体如果没有定义任何自定义构造器,它们将自动获得一个逐一成员构造器。不像默认构造器,即使存储型属性没有默认值,结构体也能会获得逐一成员构造器。

通过最后一点区别可以看出:类是引用类型(ReferenceType,赋值或作为参数传递时不会创建副本,大家都引用同一块内存),相对的结构体就属于值类型(ValueType,赋值或作为参数传递时会创建副本);通过lldb的调试命令 po 可以有个更直观的感受。

struct TestStruct {
    let key: String
    let value: Int
    
    subscript(i: Int) -> String {
        switch i {
        case 0: return key
        case 1: return "(value)"
        default: return ""
        }
    }
}

class TestClass {
    let key: String
    let value: Int
    
    init(key: String, value: Int) {
        self.key = key
        self.value = value
    }
    
    subscript(i: Int) -> String {
        switch i {
        case 0: return key
        case 1: return "(value)"
        default: return ""
        }
    }
}

func test() {
    var s1 = TestStruct(key: "first", value: 1)
    var s2 = s1

    var c1 = TestClass(key: "first", value: 1)
    var c2 = c1
    
    print("Hello, World!")
}

test()
复制代码

初步输出对象的值:

细看结构体:

细看类的实例:

扫描二维码关注公众号,回复: 14165162 查看本文章

通过上面的结果可以看出,结构体变量在内存中直接保存的就是其属性值,而类的实例对象存储的是对象的地址。

另外引用类型和值类型还有一个最直观的区别就是存储的位置不同:一般情况,值类型存储的在栈上,引用类型存储在堆上。 (此处可利用工具另做研究,能更直观看到内存分配信息。。。)

至此,你对于选择用类还是结构体是不是有了个你的判断呢?如果还不清楚,那我们继续结合引用类型与值类型来研究下。

首先可以知道的是值类型分配到栈上,所以它是线程安全的。与此同时,在内存分配上相对于类也是效率更高的。就拿test()方法中的代码var c1 = TestClass(key: "first", value: 1)来说:

1)它需要在栈上开辟8字节的控件,用来存放类实例对象的堆区地址

2)然后从堆上寻找到大小合适的内存区域为类对象分配内存控件来初始化实例

3)最后将这块内存的地址保存到前面申请的栈空间中

这就导致类的实例变量在创建和销毁时,都会存在查找内存的开销。

通过以下代码可以直观感受下两者的运行效率:

protocol ValueAutoIncrement {
    @discardableResult func run() -> Int
}

class TestOneClass {
    let value: Int
    init(_ val: Int) { self.value = val }
    
    subscript(add: Int) -> Int {
        return self.value + add
    }
}

struct TestOneStruct {
    let value: Int
    init(_ val: Int) { self.value = val }
    
    subscript(add: Int) -> Int {
        return self.value + add
    }
}

class TestTenClass {
    let value1, value2, value3, value4, value5, value6, value7, value8, value9, value10: Int
    
    init(_ val: Int) {
        self.value1 = val
        self.value2 = val
        self.value3 = val
        self.value4 = val
        self.value5 = val
        self.value6 = val
        self.value7 = val
        self.value8 = val
        self.value9 = val
        self.value10 = val
    }
    
    subscript(index: Int, add: Int) -> Int {
        switch index {
        case 2: return self.value2 + add
        case 3: return self.value3 + add
        case 4: return self.value4 + add
        case 5: return self.value5 + add
        case 6: return self.value6 + add
        case 7: return self.value7 + add
        case 8: return self.value8 + add
        case 9: return self.value9 + add
        case 10: return self.value10 + add
        default: return self.value1 + add
        }
    }
}

struct TestTenStruct {
    let value1, value2, value3, value4, value5, value6, value7, value8, value9, value10: Int
    
    init(_ val: Int) {
        self.value1 = val
        self.value2 = val
        self.value3 = val
        self.value4 = val
        self.value5 = val
        self.value6 = val
        self.value7 = val
        self.value8 = val
        self.value9 = val
        self.value10 = val
    }
    
    subscript(index: Int, add: Int) -> Int {
        switch index {
        case 2: return self.value2 + add
        case 3: return self.value3 + add
        case 4: return self.value4 + add
        case 5: return self.value5 + add
        case 6: return self.value6 + add
        case 7: return self.value7 + add
        case 8: return self.value8 + add
        case 9: return self.value9 + add
        case 10: return self.value10 + add
        default: return self.value1 + add
        }
    }
}

extension TestOneClass: ValueAutoIncrement {
    @discardableResult func run() -> Int {
        return self[1]
    }
}

extension TestOneStruct: ValueAutoIncrement {
    @discardableResult func run() -> Int {
        return self[1]
    }
}

extension TestTenClass: ValueAutoIncrement {
    @discardableResult func run() -> Int {
        return self[1, 1]
    }
}

extension TestTenStruct: ValueAutoIncrement {
    @discardableResult func run() -> Int {
        return self[1, 1]
    }
}

private func printRunningTime(_ name: String, block: @escaping () -> ()) {
    print("\n(name)")
    let t0 = CACurrentMediaTime()
    
    block()
    
    let dt = CACurrentMediaTime() - t0
    print("(dt)")
}

class Tests {
    static func runTests() {
        print("Test start")
        
        printRunningTime("class (1 field)") {
            for _ in 1...10000000 {
                let x = TestOneClass(1)
                x.run()
            }
        }
        
        printRunningTime("struct (1 field)") {
            for _ in 1...10000000 {
                let x = TestOneStruct(1)
                x.run()
            }
        }
        
        printRunningTime("class (10 fields)") {
            for _ in 1...10000000 {
                let x = TestTenClass(1)
                x.run()
            }
        }
        
        printRunningTime("struct (10 fields)") {
            for _ in 1...10000000 {
                let x = TestTenStruct(1)
                x.run()
            }
        }
        
        print("\nTest over")
    }
}
复制代码

输出结果:

综上,对于一般的数据结构来说,我们可以考虑优先选用结构体。除此之外,创建相同属性的结构体比类更加节省内存,而且类类型在使用时还存在引用计数方面的内存开销(这里涉及到对Swift源码进行分析,探索类的底层结构 github.com/apple/swift,此处暂不展开)。

那么,到底该选择类还是结构体?Swift的官方文档中有这么段话:

The additional capabilities that classes support come at the cost of increased complexity. As a general guideline, prefer structures because they’re easier to reason about, and use classes when they’re appropriate or necessary. In practice, this means most of the custom data types you define will be structures and enumerations.

在实践过程中,你或许可以遵循以下原则:以下情况下,应该使用类:

  • 需要使用 === 来比较两个实例标识
  • 数据需要用来共享状态
  • 必须要与Objective-C相互调用

以下情况下,应该使用结构体:

  • 需要使用 == 来比较两个实例数据
  • 需要具有独立状态的唯一副本
  • 数据在多个线程中使用

猜你喜欢

转载自juejin.im/post/7097082089022947358