类和结构体是两种很重要的数据结构,在Swift中类和结构体无论是从语法还是用法上都高度相似,但是功能和原理又大相径庭。
在Swift中,类和结构体的主要区别:
1 结构体不支持继承和类型转换。
2 结构体不支持定义析构方法。
3 结构体是值类型,类是引用类型。
由于他们分别是值类型和引用类型,导致他们在内存分布
、引用计数
、方法派发
等方面都有明显差异。
当然,他们也有很多共同点,比如属性
、方法
、构造
、下标
、扩展
、实现协议
等方面。
相似点
1 属性
这里涉及的知识点有存储属性
、计算属性
、延迟存储属性
、属性观察者
、闭包初始化
等。
1.1 存储属性
即我们平时常见的变量(var)或常量(let),可以在声明时指定默认值,也可以在构造过程中指定。
注意:
1 不能在枚举中使用
2 不能被子类重写,但是可以在子类中进行属性观察
// 常量存储属性
let id: Int
// 变量存储属性
var firstValue: Int
// 延迟存储属性(懒加载)
lazy var 存储属性名 : 类型 = { 创建变量代码 }()
值得注意的是,即便是声明为常量的存储属性,在构造阶段依然可以修改。但是这个常量必须不能有默认值,因为Swift中常量属性只能被初始化一次。
class Question {
let text: String // 如果text属性预先指定了初始值,则编译不通过
init(text: String) {
self.text = text
}
}
1.2 计算属性
计算属性不直接存储值,而是提供一个 getter
和一个可选的 setter
,来间接获取和设置其他属性或变量的值。
注意:
1 不能用let修饰计算属性
var 名称: 类型 {
get { return }
set(newValue) { }
}
这里setter
方法的newValue
可以省略,且setter
方法中依然可以使用省略的newValue
值。甚至可以直接省略setter
方法,将属名声明为只读计算属性
。
// 只读存储属性
struct Cuboid {
var width = 0.0, height = 0.0, depth = 0.0
var volume: Double { // 只读计算属性
return width * height * depth
}
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
1.3 属性观察者
为了让属性被赋值时获得执行代码的机会,在Swift中,属性观察者其实就是两个特殊的回调方法。
注意:
1 不要为没有重载的、普通的计算属性添加属性观察者(本身对应set部分已经获得执行)
2 不要为常量存储属性、只读计算属性添加属性观察者(它们不会发生改变)
// 语法格式
var 存储属性名 : 类型 = 初始值 {
willSet(newValue){ 即将赋值前 }
didSet(oldValue){ 赋值完成后 }
}
// newValue和oldValue这两个参数都可以省略
Q:setter
和willSet
共存?
我们知道,在同一个类中,属性观察和计算属性是不能同时共存的,而且在计算属性赋值时,你完全可以把想要做的检查放在setter方法中。所以不存在setter和willSet共存。但是假如我们不能修改setter方法中的代码,又想要做一些事情,我们可以通过继承并对父类的计算属性进行观察。
class A {
var number: Int {
get {
print("get")
return 0
}
set { print("set") }
}
}
class B: A {
override var number: Int {
willSet { print("willSet") }
didSet { print("didSet") }
}
}
1.4 闭包初始化
在定义存储属性之后,后面紧跟一个闭包进行初始化,这个闭包会在类初始化时直接调用。
注意:区分闭包初始化和懒加载的写法
let purpleView: UIView = {
// 在此初始化 view
// 直接叫 "view" 真的好吗?
let view = UIView()
view.backgroundColor = UIColor.purple
return view
}()
// 闭包初始化时面临一个问题:需要给中间变量进行命名
// 这种写法可以省略中间变量
let purpleView: UIView = {
$0.backgroundColor = UIColor.purple
return $0
}(UIView())
1.5 类型属性(静态属性)
在不加特定修饰的情况下,Swift在类、结构体和枚举中定义的属性都是实例属性
。把实例属性变成类型属性
需要用static
或class
修饰。
注意:
1 类型属性必须在声明时就添加默认值或声明为可选类型
2 类中计算属性可以用class或static修饰,存储属性只能用static修饰
3 结构体不论计算属性还是存储属性都用static修饰
struct SomeStructure {
// 结构体中都用static修饰
static var storedTypeProperty = "Some value"
static var computedTypeProperty: Int {
return 0
}
}
class SomeClass {
static var storedTypeProperty = "Some value" // 存储属性用static修饰
class var computedTypeProperty: Int { // 计算属性用class修饰
return 0
}
}
2 方法
在Swift中函数和方法语法格式相同,定义在枚举、类和结构体内部的称之为方法。外部的就是函数。结构体和枚举能够定义方法是 Swift 与Objective-C 的主要区别之一。方法同时也有实例方法
和类型方法
之分。
2.1 mutating关键字
默认情况下,结构体中实例方法内是不可以修改其属性值(因为结构体是值类型)。类中的实例方法中可以修改其属性值。
所以结构体中要用mutating
关键字来修饰实例方法,才能在方法中修改属性值。
// 结构体
struct Point {
var x = 0, y = 0
mutating func moveByPoint(x:Int, y:Int) {
self.x += x
self.y += y
}
}
var point = Point(x: 3, y: 3) // 这里要用var修饰,mutating关键字不能用在常量上
// 枚举
enum BlackOrWhite {
case Black, White
mutating func switchColor() {
switch self {
case .Black:
self = .White
case .White:
self = .Black
}
}
}
Swift 的协议不仅可以被类实现,也适用于结构体和枚举类型。因此,我们在写给别人用的接口时需要多考虑是否使用mutating来修饰方法。
// 协议
protocol Vehicle {
var numberOfWheels: Int {get}
var color: UIColor {get set}
mutating func changeColor(to color:UIColor)
}
class MyCar: Vehicle {
let numberOfWheels = 4
var color = UIColor.blue
// class中实现带mutating方法的接口时,不用进行mutating修饰
func changeColor(to color: UIColor) {
self.color = color
}
}
struct YourCar: Vehicle {
let numberOfWheels = 6
var color = UIColor.yellow
mutating func changeColor(to color: UIColor) {
self.color = color
}
}
思考:为什么结构体的实例方法中不能直接修改属性值呢?
我猜测是因为结构体在初始化阶段内部的属性和方法等所占用的内存空间已经分配完成。而且结构体本身值类型分配在没那么灵活的栈中,mutating
关键字修饰的方法会在被实例调用时强制要求该实例声明为var
。因为类作为引用类型,它修改属性时无所谓实例本身是let
或var
,所以不需要mutating
修饰。
思考:下面这个结构体,调用change()方法,结构体实例的地址是否改变?
struct CoordinateStruct {
var x: Double
var y: Double
mutating func change() {
self = CoordinateStruct(x: 10, y: 20)
}
}
2.2 ===、!==
在Swift中,引用类型可以直接通过===
和!==
比较两个引用类型的变量是否指向同一个对象。
class Dog {
var height = 0.0
var weight = 0.0
}
var dogA = Dog()
var dogB = dogA
print(dogA===dogB) // true
然而,如果直接使用==
或!=
比较的话会导致编译错误。但是我们可以通过实现Equatable
协议来进行运算符重载
。
extension Dog: Equatable {
static func == (lhs: Dog, rhs: Dog) -> Bool {
return lhs.height == rhs.height && lhs.weight == rhs.weight
}
static func != (lhs: Dog, rhs: Dog) -> Bool {
return lhs.height != rhs.height || lhs.weight != rhs.weight
}
}
值得注意的是,Swift中运算符本质上都是运算符重载
,值类型也同样可以重载它们。例如:===(left: Any, right:Any)->Bool
,其中Any
既可以代表值类型,又可以代表引用类型。
2.2 类型方法
在 Objective-C 里面,你只能为 Objective-C 的类定义类型方法。在 Swift 中,你可以为所有的类、结构体和枚举定义类型方法:每一个类型方法都被它所支持的类型显式包含。
注意:
1 结构体、枚举的类型方法使用static修饰
2 类的类型方法可以用staitc或class修饰,使用class修饰的可以被子类重写
3 构造
前面简单介绍了属性和方法,他们都是在定义结构体或类时重要的部分,那么创建实例就一定要讲到构造方法。
文档中涉及的知识点比较多,我把我认为重要的拿出来对比一下,分别是默认构造器
、便捷构造器
、可能失败的构造器
等。
3.1 默认构造器
对于类而言:
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()
情况1:只有当该类中所有实例存储属性显式指定了初始值,或者声明为可选类型,且没有提供任何构造器时,系统才会为该类提供一个无参数的构造器。即ShoppingListItem()
。
class ShoppingListItem {
var name: String // 存储属性name没有初始值
var quantity = 1
var purchased = false
init(name: String) {
self.name = name
}
}
var item = ShoppingListItem(name: "water") // 原本的空构造器失效
情况2: 如果提供了一个构造器,可以把没有指定初始值的属性在构造器中进行初始化,不过此时系统默认的空构造器就不能使用了,只能使用自己定义的构造器,即ShoppingListItem(name: "water")
。
对于结构体而言:
struct Size {
var width = 1.0, height = 1.0
var area: Double {
return width * height
}
}
let twoByTwo = Size(width: 2.0, height: 2.0)
let defaultSize = Size()
注意:结构体中不一定要给所有的实例存储属性初始值指定初始值,或者声明为可选,因为结构体会默认提供一个初始化所有属性的构造器。即Size(width: Double, height: Double)
。
如果给所有属性指定了初始值,除了包含所有属性的构造器以外,系统还会提供一个空的构造器。即Size()
。
总结:无论是类还是结构体
1 没有给所有存储属性初始值时,类会报错,结构体会提供一个所有属性的构造器。
2 给所有存储属性初始值时,类会提供一个空构造器,结构体会提供两个构造器(一个空,一个所有参数)。
3 如果他们自己实现了构造器,那么只能用自己的构造器(前提是所有属性都初始化了)。
这里补充一下关于OC和Swift中构造器的不同:
1 Swift的构造器名总为init方法(无需func修饰),且没有声明返回值类型,也无需return语句,系统隐式地返回self作为返回值。
2 OC的构造器必须声明返回值类型(id或instanceType),由于没有“重载”的概念,所以需要通过参数列表区分不同的构造方法。
3.2 便捷构造器和可失败的构造器
首先便捷构造器
就是一种便捷的写法,常用于嵌套自定义的构造器给定一些初始值,声明便捷构造器需要使用convenience
关键字。
class Cat {
let name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "hotdog")
}
}
let cat = Cat()
其次可失败的构造器
,无论是类,结构体或枚举类型的对象,在构造自身的过程中有可能失败,则为其定义一个可失败构造器,是非常有必要的。这里所指的“失败”是指,如给构造器传入无效的参数值,或缺少某种所需的外部资源,又或是不满足某种必要的条件等。
struct Animal {
let species: String
init?(species: String) {
if species.isEmpty { return nil }
self.species = species
}
}
let animal = Animal(species: "")
思考:下面是Person类的代码,构造person实例时会输出什么结果?
class Person {
var age: Int {
print("只读计算属性")
return 0
}
lazy var firstName: String = {
print("懒加载")
return ""
}()
var nickName: String = {
print("闭包初始化")
return ""
}()
var info: String = "" {
didSet{
print("属性观察" + oldValue)
}
willSet {
print("属性观察" + newValue)
}
}
}
let person = Person()
// 闭包初始化
很明显,实例化person时会打印“闭包初始化”,这个例子包含了之前属性的一些知识点,有可能会在笔试环节出现。
不同点
与其说类和结构体的不同点,不如放大命题为值类型和引用类型的不同点。
Swift 中几乎所有的内建类型都是值类型,不仅包括了传统意义像 Int,Bool 这些,甚至连 String,Array 以及 Dictionary 都是值类型的(与OC中不同,在OC中Int,Bool是基本数据类型,而NSString,NSArray等都派生自NSObject)。
对于值类型和引用类型的区别,网络上存在大量的理论论述的文章,很多都写得很深刻也很全面。下面给大家推荐几篇:
这篇文章就不赘述原理了,而是从结论直接出发,扩充一些常见的问题加以支撑。
题目整理(不断更新)
1 swift中Array是什么类型,大量复制时会不会影响性能?
首先,我们都知道在Swift中的Array是值类型,值类型是存储在栈中,每个实例保持一份数据拷贝,也就是说在一般情况下,值类型的赋值是深拷贝。那么大量复制必然会影响性能才对?这显然不合常理,所以带着疑问看下面的代码。
func address<T: AnyObject>(of object: T) -> String {
let addr = unsafeBitCast(object, to: Int.self)
return String(format: "%p", addr)
}
func address(of object: UnsafeRawPointer) -> String {
let addr = Int(bitPattern: object)
return String(format: "%p", addr)
}
定义两个查看变量内存地址的方法,参考Printing a variable memory address in swift,并做修改。
var arr = [1,2,3,4]
let newArr = arr
print(address(of: arr)) // 0x600000a18ca0
print(address(of: newArr)) // 0x600000a18ca0
可以看出,在数组进行简单赋值时,两个变量指向的内存地址是相同的。那么它什么时候才拷贝呢?
arr.append(5)
print(address(of: arr)) // 0x6000023f9ac0
print(address(of: newArr)) // 0x600001290760
我们发现,对于原数组进行修改操作之后,他们指向了不同的内存地址,说明此时数组进行了拷贝。其实对于值类型有这样一条结论:基本数据类型在赋值的时候复制,集合类型(Array, Set, Dictionary)是在写时复制的。这是Swift对结构体类型的一种优化,所以在Swift中大量复制Array时是不会影响性能的,因为只是做了指针移动。
2 模型拷贝
在实际开发中,经常需要把列表中的数据解析成模型封装在数组中。而Model大多定义为Class类型,那么在数据传递时,经常发现列表中的数据莫名其妙被修改了,这是什么原因呢?
我们模拟一下上述的场景:
class Person {
var name = ""
// ...
}
// 创建一个person对象,并封装在数组中。
let person = Person()
let arr = [person]
let newArr = arr
print(address(of: arr)) // 0x600000305220
print(address(of: newArr)) // 0x600000305220
截止到这里,我们都很熟悉,因为与上一个问题情况相同,数组没有发生写入操作时都指向相同的内存地址,不同的是上一个问题数组中封装的是值类型,而这次数组封装的是引用类型。这就涉及了类型嵌套,与上一题的内存分布形式不同。
person.name = "Rikcy"
print(arr[0].name) // Rikcy
print(newArr[0].name) // Rikcy
由于两个数组中的person对象指向同一个堆中的地址,所以他们数组中person.name都被修改了。注意:此时他们的内存地址依然相同,因为数组本身没有发生写入操作。
那么对于上述的问题,同样有两个解决方案:1 封装Model时尽量选择Struct类型(避免在结构体中嵌套额外的引用类型)2 对于Class类型的Model进行拷贝。
介绍一下第二种解决方案:定义一个Copying协议,同时实现对应的方法。
// 定义Copying协议
protocol Copying {
init(original: Self)
}
extension Copying {
func copy() -> Self {
return Self.init(original: self)
}
}
// 遵循Copying协议,并实现copy()方法
class Person: Copying {
var name = ""
required init(original: Person) {
self.name = original.name
}
}
// 在数组赋值时,循环调用copy方法。
var persons = [person]
var newpersons = [Person]()
for item in persons {
newpersons.append(item.copy())
}
这里的写法就很多了,你也可以运用之前讲到的mutating
关键字,同样是声明的协议中,简化的写法是在Model的基类中实现该协议,利用Runtime遍历ivarsList并赋值等。
3 ===、!==
关于引用类型的比较,上面涉及过。
4 PS:关于函数In-Out关键字
关于值类型或者引用类型作为函数参数传递的相关知识点,之前写过一篇文章,并在最近重读更新了一次。
有兴趣的可以看看:函数In-Out关键字
不断更新ing.