草庐IT

关于Swift中Struct,Class和Enum的哪些事儿

Owenli_千 2023-03-28 原文

前言

Swift type System

Swift是强类型的,尽管只有六种类型。

  • 命名类型: protocol, class , struct , enum
  • 复合类型:tuple, function
可能会有疑问,那些基本类型:Bool,Int,UInt, Float, Double, Character, String, Array, Set, Dictionary, Optional。实际上他们都是通过命名类型创建的。

Struct Class and Enum 比较

Swift中提供了多种可以结构化存储数据的方式,它们是: structenumclass。Swift标准库中的绝大多数类型都是struct,甚至Foundation中的一些类也提供了它们在Swift中的struct版本,而classenum只占很少一部分。

Class,Struct and Enum对比表

copy by inheritance static variable instance variable static method instance method
Class Reference
Struct Value
Enum Value
共同点:

  1. 都可以当作protocol
  2. 都可以使用extension,扩充method
  3. 都可以使用泛型

如何抉择?

通常,在平时的编程中,按照对象的生命周期形态,可以把使用的类型分成两大类:

  • 一类必须有明确生命周期的,它们必须被明确的初始化、使用、最后明确的被释放。例如:文件句柄、数据库连接、线程同步锁等等。这些类型的初始化和释放都不是拷贝内存这么简单,通常,这类内容,我们会选择使用class来实现。
  • 另一类,则是没有那么明显的生命周期。 例如:整数、字符串、URL等等。这些对象一旦被创建之后,就很少被修改,我们只是需要使用这些对象的值,用完之后,我们也无需为这些对象的销毁做更多额外的工作,只是把它们占用的内存回收就好了。这类内容,通常我们会选择使用structenum来实现。

Struct

Struct的定义和初始化

** 定义结构体 ** 下面定义了一个二维空间坐标的类型:

struct Point { var x: Double var y: Double } 这个结构体包含两个名x和y的存储属性。存储属性是被绑定和存储在结构体中的常量或变量。

** 初始化 **

  • 结构体类型的逐一初始化 所有的结构体都有一个自动生成的成员逐一构造器
var pointA = Point(x: 10, y: 20)
  • 默认初始化 我们也可以在定义的时候直接给属性初始化
struct Point { var x = 0.0 var y = 0.0 } var pointB = Point() 使用这种方法,必须给每一个属性指定默认值。因为Swift中要求init方法必须初始化自定义类型每个属性。如果无法做到,我们可以自定义逐一初始化方法。

struct Point { var x : Double var y : Double init(_ x : Double = 0.0, y : Double = 0.0) { self.x = x self.y = y } } 当我们自定义init方法之后,Swift将不会再自动创建逐一初始化方法。

Struct 值类型本质

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添加方法

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

定义enum

定义了一个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.black let jan = Month.january
注意: 与C和Objective-C不同,Swift的枚举成员在被创建时不会被赋予一个默认的整数值。上面定义的枚举成员是完备的值,这些值的类型就是定义好的枚举ColorNameMonth

理解Enum的“Value”

case 本身就是值

func myColor(color: ColorName) -> String { switch color { case .black: return "black" case .red: return "red" default : return "other" } }
注意

  • color的类型可以通过type inference推导出是ColorName。因此,可以省略enum的名字。
  • 当Switch...case...将color的所有的值都列举出来时,可以省略default

绑定值(raw values)

在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)") }
注意: 提取”关联值“的内容时,可以把letvar写在case前面或者后面。例如:namedrgb

协议和方法(Protocol and Method)

在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才会去复制一个副本,第一个不会改变。 通过延迟复制操作,直到实际使用到的时候 才去复制,以此确保没有浪费的工作。

注意:COW是特别添加到Swift数组和字典的功能,自定义的数据类型不会自动实现。

值类型和引用类型(Value vs. Reference Type)

Class和Struct有很多相似的地方,他们都可以用来自定义类型、都可以有属性、都可以有方法。作为Swift中的引用类型,class表达的是一个具有明生命周期的对象,我们关心的是类的生命周期。而值类型,我关注的是值本身。

差异对比

  1. 引用类型必须明确指定init方法 Swift中class不会自动生成init方法。如果不定义编译器报错。
  2. 引用类型关注的是对象本身 Circle (定义为Class)
var a = Circle() a.radius = 80 var b = a a.radius = 1000 b.radius // 1000 Circle(定义为Struct)

var a = Circle() a.radius = 80 var b = a a.radius = 1000 b.radius // 80 使用值类型创建新对象时,将复制;使用引用类型时,新变量引用同一个对象。这是两者的关键区别。

  1. 引用类型的默认值是可以修改的 我们之前提到过,给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 如果想让Point3DPoint2D继承所有的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

有关关于Swift中Struct,Class和Enum的哪些事儿的更多相关文章

  1. ruby-on-rails - 您希望看到哪些 Rails 插件? - 2

    您认为可以作为插件很好地存在于您的Rails应用程序中必须实现的哪些行为?您过去曾搜索过哪些插件功能但找不到?哪些现有的Rails插件可以改进或扩展,如何改进或扩展? 最佳答案 我希望在管理界面中看到一个引擎插件,它提供了应用程序中所有模型的仪表板摘要,以及可配置的事件图表。 关于ruby-on-rails-您希望看到哪些Rails插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questio

  2. Ruby - 如何处理子类意外覆盖父类(super class)私有(private)字段的问题? - 2

    假设您编写了一个类Sup,我决定将其扩展为SubSup。我不仅需要了解你发布的接口(interface),还需要了解你的私有(private)字段。见证这次失败:classSupdefinitialize@privateField="fromsup"enddefgetXreturn@privateFieldendendclassSub问题是,解决这个问题的正确方法是什么?看起来子类应该能够使用它想要的任何字段而不会弄乱父类(superclass)。编辑:equivalentexampleinJava返回"fromSup",这也是它应该产生的答案。 最佳答案

  3. ruby - 无法理解 `puts{}.class` 和 `puts({}.class)` 之间的区别 - 2

    由于匿名block和散列block看起来大致相同。我正在玩它。我做了一些严肃的观察,如下所示:{}.class#=>Hash好的,这很酷。空block被视为Hash。print{}.class#=>NilClassputs{}.class#=>NilClass为什么上面的代码和NilClass一样,下面的代码又显示了Hash?puts({}.class)#Hash#=>nilprint({}.class)#Hash=>nil谁能帮我理解上面发生了什么?我完全不同意@Lindydancer的观点你如何解释下面几行:print{}.class#NilClassprint[].class#A

  4. ruby - [1,2,3].to_enum 和 [1,2,3].enum_for 在 Ruby 中的区别 - 2

    在Ruby中,我试图理解to_enum和enum_for方法。在我提出问题之前,我提供了一些示例代码和两个示例来帮助理解上下文。示例代码:#replicatesgroup_bymethodonArrayclassclassArraydefgroup_by2(&input_block)returnself.enum_for(:group_by2)unlessblock_given?hash=Hash.new{|h,k|h[k]=[]}self.each{|e|hash[input_block.call(e)]示例#1:irb(main)>puts[1,2,3].group_by2.ins

  5. ruby-on-rails - 关于 Ruby 的一般问题 - 2

    我在我的rails应用程序中安装了来自github.com的acts_as_versioned插件,但有一段代码我不完全理解,我希望有人能帮我解决这个问题class_eval我知道block内的方法(或任何它是什么)被定义为类内的实例方法,但我在插件的任何地方都找不到定义为常量的CLASS_METHODS,而且我也不确定是什么here,并且有问题的代码从lib/acts_as_versioned.rb的第199行开始。如果有人愿意告诉我这里的内幕,我将不胜感激。谢谢-C 最佳答案 这是一个异端。http://en.wikipedia

  6. ruby - 使用 Class.new 时访问外部范围 - 2

    是否有可能以某种方式访问​​Class.new范围内的a?a=5Class.new{defb;aend}.new.b#NameError:undefinedlocalvariableormethod`a'for#:0x007fa8b15e9af0>#:in`b' 最佳答案 即使@MarekLipka的回答是正确的——改变变量范围总是有风险的。这是可行的,因为每个block都带有创建它的上下文,因此您的局部变量a突然变得不那么局部了——它变成了一个“隐藏的”全局变量:a=5object=Class.new{define_method(

  7. ruby - 实现k最近邻需要哪些数据? - 2

    我目前有一个reddit克隆类型的网站。我正在尝试根据我的用户之前喜欢的帖子推荐帖子。看起来K最近邻或k均值是执行此操作的最佳方法。我似乎无法理解如何实际实现它。我看过一些数学公式(例如k表示维基百科页面),但它们对我来说并没有真正意义。有人可以推荐一些伪代码,或者可以查看的地方,以便我更好地了解如何执行此操作吗? 最佳答案 K最近邻(又名KNN)是一种分类算法。基本上,您采用包含N个项目的训练组并对它们进行分类。如何对它们进行分类完全取决于您的数据,以及您认为该数据的重要分类特征是什么。在您的示例中,这可能是帖子类别、谁发布了该项

  8. ruby - 模块中的 instance_eval 与 class_eval - 2

    classFooincludeModule.new{class_eval"deflab;puts'm'end"}deflabsuperputs'c'endendFoo.new.lab#=>mc======================================================================classFooincludeModule.new{instance_eval"deflab;puts'm'end"}deflabsuperputs'c'endend注意这里我把class_eval改成了instance_evalFoo.new.labresc

  9. ruby - 我怎样才能更好地了解/了解更多关于 Ruby 的知识? - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭9年前。我最近开始学习Ruby,这是我的第一门编程语言。我对语法感到满意,并且我已经完成了许多只教授相同基础知识的教程。我已经写了一些小程序(包括我自己的数组排序方法,在有人告诉我谷歌“冒泡排序”之前我认为它非常聪明),但我觉得我需要尝试更大更难的东西来理解更多关于Ruby.关于如何执行此操作的任何想法?

  10. ruby-on-rails - Rails - 父类(super class)不匹配 - 2

    玩转Rails和Controller继承。我创建了一个名为AdminController的Controller,其中一个名为admin_user_controller的子类位于/app/controllers/admin/admin_user_controller.rb这是我的routes.rbnamespace:admindoresources:admin_user#Havetheadminmanagethemhere.endapp/controllers/admin/admin_user_controller.rbclassAdminUserController应用程序/Contr

随机推荐