关于Swift中Struct,Class和Enum的哪些事儿
Swift type System
Swift是强类型的,尽管只有六种类型。
命名类型: protocol, class , struct , enum复合类型:tuple, functionStruct Class and Enum 比较可能会有疑问,那些基本类型:Bool,Int,UInt, Float, Double, Character, String, Array, Set, Dictionary, Optional。实际上他们都是通过命名类型创建的。
Swift中提供了多种可以结构化存储数据的方式,它们是: struct
、enum
和class
。Swift标准库中的绝大多数类型都是struct
,甚至Foundation中的一些类也提供了它们在Swift中的struct
版本,而class
和enum
只占很少一部分。
Class,Struct and Enum对比表
共同点:
都可以当作protocol
都可以使用extension
,扩充method
都可以使用泛型如何抉择?通常,在平时的编程中,按照对象的生命周期形态,可以把使用的类型分成两大类:
一类必须有明确生命周期的,它们必须被明确的初始化、使用、最后明确的被释放。例如:文件句柄、数据库连接、线程同步锁等等。这些类型的初始化和释放都不是拷贝内存这么简单,通常,这类内容,我们会选择使用class
来实现。另一类,则是没有那么明显的生命周期。 例如:整数、字符串、URL等等。这些对象一旦被创建之后,就很少被修改,我们只是需要使用这些对象的值,用完之后,我们也无需为这些对象的销毁做更多额外的工作,只是把它们占用的内存回收就好了。这类内容,通常我们会选择使用struct
或enum
来实现。StructStruct的定义和初始化 定义结构体
下面定义了一个二维空间坐标的类型:
struct Point { var x: Double var y: Double}
这个结构体包含两个名x和y的存储属性。存储属性是被绑定和存储在结构体中的常量或变量。
初始化
结构体类型的逐一初始化所有的结构体都有一个自动生成的成员逐一构造器
var pointA = Point(x: 10, y: 20)
默认初始化
我们也可以在定义的时候直接给属性初始化
struct Point {var x = 0.0var y = 0.0}var pointB = Point()
使用这种方法,必须给每一个属性指定默认值。因为Swift中要求init
方法必须初始化自定义类型每个属性。如果无法做到,我们可以自定义逐一初始化方法。
struct Point {var x : Doublevar y : Doubleinit(_ x : Double = 0.0, y : Double = 0.0) { self.x = x self.y = y}}
当我们自定义init
方法之后,Swift将不会再自动创建逐一初始化方法。
var pointB = Point(200, y: 100)var pointC = Point(100, y: 200) { didSet { print("\(pointC)") }}pointC = pointB// Point(x: 200.0, y: 100.0)pointC.x = 200//Point(x: 200.0, y: 100.0)
通过didSet
观察pointC
的变化。当修改pointC
变量值时,控制台输出Point(x: 200.0, y: 100.0)
, 但是,修改pointC
的修改某个属性,也会触发didSet
。
这就是值语义的本质:即使字面上修改了pointC
变量的某个属性,但实际执行的逻辑是重新给pointC
赋值一个新的Point
对象。
给struct
添加的方法,默认的都是只读的。计算Point之间的距离
extension Point { func distance(to: Point) -> Double { let distX = self.x - to.x let distY = self.y - to.y return sqrt(distX * distX + distY * distY) }}pointC.distance(to: Point(0, y: 0))
当我们定义一个移动X轴坐标点的方法时,会导致编译错误:
extension Point { func move(to: Point) { self = to }}
这里提示self is immutable , 必须使用mutating
修饰这个方法, Swift编译器就会在所有的mutating
方法第一个参数的位置,自动添加一个 inout Self
参数。
extension Point { /* self: inout Self */ mutating func move(to: Point) { self = to }}
以上,是关于Struct类型的基本内容。
init
方法的合成规则值语义在struct
上的表现Enum在Swift中,对enum
做了诸多改进和增强,它可以有自己的属性,方法,还可以遵从protocol
。
定义了一个colorName
枚举
enum ColorName { case black case silver case gray case white case red //.... and so on ....}// 也可以写在同一行上,用逗号隔开:enum Month { case january, februray, march, april, may, june, july, august, september, october, november, december}
使用
let black = ColorName.blacklet jan = Month.january
理解Enum的“Value”case 本身就是值注意:
与C和Objective-C不同,Swift的枚举成员在被创建时不会被赋予一个默认的整数值。上面定义的枚举成员是完备的值,这些值的类型就是定义好的枚举ColorName
或Month
。
func myColor(color: ColorName) -> String { switch color { case .black: return "black" case .red: return "red" default : return "other" }}
绑定值(raw values)注意
color的类型可以通过type inference
推导出是ColorName
。因此,可以省略enum
的名字。当Switch...case...将color的所有的值都列举出来时,可以省略default
。
在Swift中,enum
默认不会为case
绑定一个整数值。但是我们可以手动的绑定值,这个“绑定”来的值,叫做raw values。
enum Direction : Int { case east case south case west case north}
现在定义Direction,Swift就会依次把case
绑定上值。
let east = Direction.east.rawValue // 0
关联值(Associated value)
在Swift中, 我们可以给每一个case
绑定不同类型的值,我们管这种值叫做Associated value
。
定义了一个表示CSSColor的enum
:
enum CSSColor { case named(ColorName) case rgb(UInt8, UInt8, UInt8)}
使用:
var color1 = CSSColor.named(.black)var color2 = CSSColor.rgb(0xAA, 0xAA, 0xAA)switch color2 {case let .named(color): print("\(color)")case .rgb(let r, let g, let b): print("\(r), \(g), \(b)")}
协议和方法(Protocol and Method)注意:
提取”关联值“的内容时,可以把let
和var
写在case
前面或者后面。例如:named
和rgb
。
在Swift中,enum
和其他的命名类型一样,也可以采用protocol
。
例如: 给CSSColor添加一个文本表示。
extension CSSColor: CustomStringConvertible { var description: String { switch self { case .named(let colorname): return colorname.rawValue case .rgb(let red, let green, let blue): return String(format: "#%02X%02X%02X", red, green, blue) } }}
结果:
let color3 = CSSColor.named(.red)let color4 = CSSColor.rgb(0xBB, 0xBB, 0xBB)print("color3=\(color3), color4=\(color4)") //color3=red, color4=#BBBBBB
什么是Copy on write (COW) ?
COW是一种常见的计算机技术,有助于在复制结构时提高性能。例如:一个数组中有1000个元素,如果你复制数组到另一个变量,Swift将复制全部的元素,即使最终两个数组的内容相同。
这个问题可以使用COW解决:当将两个变量指向同一数组时,他们指向相同的底层数据。两个变量指向相同的数据可能看起来矛盾。解决方法:当修改第二个变量的时候,Swift才会去复制一个副本,第一个不会改变。
通过延迟复制操作,直到实际使用到的时候 才去复制,以此确保没有浪费的工作。
值类型和引用类型(Value vs. Reference Type)注意:COW是特别添加到Swift数组和字典的功能,自定义的数据类型不会自动实现。
Class和Struct有很多相似的地方,他们都可以用来自定义类型、都可以有属性、都可以有方法。作为Swift中的引用类型,class
表达的是一个具有明生命周期的对象,我们关心的是类的生命周期。而值类型,我关注的是值本身。
Swift中
class
不会自动生成init
方法。如果不定义编译器报错。引用类型关注的是对象本身
Circle (定义为Class)
var a = Circle()a.radius = 80var b = aa.radius = 1000b.radius // 1000
Circle(定义为Struct)
var a = Circle()a.radius = 80var b = aa.radius = 1000b.radius // 80
使用值类型创建新对象时,将复制;使用引用类型时,新变量引用同一个对象。这是两者的关键区别。
引用类型的默认值是可以修改的我们之前提到过,给
struct
添加的方法,默认的都是只读的。如果要修改必须用mutating
来修饰。class
中则不同,我们可以直接给 self
赋值。Class理解class类型的各种init方法由于class
之间可以存在继承关系,因此它的初始化过程要比struct
复杂,为了保证一个class
中的所有属性都被初始化,Swift中引入一系列特定规则。
class Point2D { var x : Double var y : Double}
这项写是不行了,因为没有定义初始化方法。
指定构造器(Designated init)上面的Point2D有一个默认的初始化方法,有两种办法:第一种给每一个属性都添加默认值。
class Point2D { var x : Double = 0 var y : Double = 0}let origin = Point2D()
这种方法只能创建一个固定的class
。另外一种,添加一个memberwise init
方法
class Point2D { var x : Double = 0 var y : Double = 0 init(x: Double, y: Double) { self.x = x self.y = y }}
添加个一个memberwise init
方法,我们可以使用
let point = Point2D(x: 1, y: 1)
但是,如果你现在使用
let point = Point2D() // Error
结果会导致编译错误。 因为,我们接手了init
的定义后,编译就不会插手init
工作。所以,在定义init
方法时添加默认参数, 我们称这种初始化为 designated init
。
class Point2D { var x : Double = 0 var y : Double = 0 init(x: Double = 0, y: Double = 0) { self.x = x self.y = y }}
便利构造器 (convenience init)
class Point2D { var x : Double = 0 var y : Double = 0 init(x: Double = 0, y: Double = 0) { self.x = x self.y = y } convenience init(at: (Double, Double) ) { self.init(x: at.0, y: at.1) }}
使用convenience
关键字修改;必须调用designated init
完成对象的初始化;如果直接调用self.x或self.y,会导致编译错误。可失败构造器 (Failable init )
class Point2D { // .... convenience init?(at: (String, String)) { guard let x = Double(at.0), let y = Double(at.1) else { return nil } self.init(at:(x, y)) }}
由于String tuple
版的init可能失败,所以需要用init?
形式定义。在实现里面,如果String
无法转换为成Double
, 则返回nil
。
类的继承和构造过程注意:
严格来说,构造器都不支持返回值。因为构造器本身的作用,只是为了确保对象能被正确构造。因此,return nil
表示构造失败,而不能return
表示成功。
当类之间存在继承关系的时候,为了保证派生类和基类的属性都被初始化,Swift采用以下三条规则限制构造器之间的代理调用:
指定构造器必须调用其直接父类的指定构造器便利构造器必须调用同类中定义的其它构造器便利构造器必须最终导致一个指定构造器被调用简单说:
指定构造器必须总是向上代理便利构造器必须总是横向代理init的继承class Point3D: Point2D { var z: Double = 0}let origin3D = Point3D()let point31 = Point3D(x: 1, y: 1)let point33 = Point3D(at: (2, 3)) // 继承基类 convenience init
如果派生类没有定义任何designated initializer
,那么它将自动继承所有基类的designated initializer
。如果一个派生类定义了所有基类的designated init
,那么它将自动继承基类所有的convenience init
。重载init方法
class Point3D: Point2D { var z: Double init(x: Double = 0, y: Double = 0, z: Double = 0) { self.z = z super.init(x: x, y: y) }}
在派生类自定义designated init
, 表示明确控制派生类的初始化构造过程, Swift 就不会干涉构造过程。那么,之前创建Point3D
就会出现错误。
let point33 = Point3D(at: (2, 3)) // Error
如果想让Point3D
从Point2D
继承所有的convenience init
,只有在派生类中实现所有的designated init
方法。
class Point3D: Point2D { var z: Double init(x: Double = 0, y: Double = 0, z: Double = 0) { self.z = z super.init(x: x, y: y) } override init(x: Double, y: Double) { // 注意先后顺序 self.z = 0 super.init(x: x, y: y) }}
此时,就可以正常工作了。只要派生类拥有基类所有的designated init
方法,他就会自动获得所有基类的convenience init
方法。另外,重载基类convenience init
方法,是不需要override
关键字修饰的。
Swift为了保证在一个继承关系中,派生类和基类的属性都可以正确初始化而约定的初始化机制。简单来说,这个机制把派生类的初始化过程分成了两个阶段。
阶段一: 从派生类到基类,自下而上让类的每个属性有初始值阶段二:所有属性都有初始值之后,从基类到派生类,自上而下对类的每个属性进行进一步加工。两段式构造过程让构造过程更安全,同时整个类层级结构中给予了每个类完全的灵活性。两段式构造过程可以防止属性值在初始化之前被访问,也可以防止属性被另外一个构造器意外赋予不同的值。
参考[The swift Programming Language]()
Swift Standard Library
如何学习Swift编程语言-泊学
Getting to Know Enums, Structs and Classes in Swift - raywenderlich
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。