Swift5.x-结构体和类(中文文档)

1,460 阅读12分钟

引言

继续学习Swift文档,从上一章节:枚举,我们学习了Swift枚举相关的内容,如枚举的定义和使用、CaseIterable关键字、关联值、原始值、indirect关键字声明递归枚举等这些内容。现在,我们学习Swift的结构体和类相关的内容。由于篇幅较长,这里分篇来记录,接下来,Fighting!

插一句话,如果你已经熟练掌握了结构体和类的使用,那么,请移步下一章节:属性

结构体

结构体和类是灵活的通用结构,它们是程序代码的构建块。 您可以定义属性和方法,使用与定义常量,变量和函数相同的语法向结构体和类添加功能。

与其他编程语言不同,Swift不需要您为自定义结构体和类创建单独的接口和实现文件。 在Swift中,您可以在一个文件中定义结构体或类,并且该类或结构体的外部接口会自动提供给其他代码使用。

注意
传统上将类的实例称为对象。 但是,Swift结构体和类在功能上比其他语言要紧密得多,本章的大部分内容描述了适用于类或结构体类型的实例的功能。 因此,使用了更通用的术语实例。

1 比较结构体和类

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

  • 定义存储值的属性
  • 定义提供功能的方法
  • 定义下标以使用下标语法提供对其值的访问
  • 定义初始化程序以设置其初始状态
  • 扩展以扩展其功能,超出默认实现
  • 遵循协议以提供某种标准功能

更多信息,请参阅PropertiesMethodsSubscriptsInitializationExtensions, 和 Protocols.

类具有结构体所没有的其他功能:

  • 继承使一个类可以继承另一个类的特征。
  • 使用类型转换可以在运行时检查和解释类实例的类型。
  • 反初始化程序使类的实例释放其已分配的所有资源。
  • 引用计数允许对一个类实例进行多个引用。

更多信息,请参见 InheritanceType CastingDeinitialization, 和 Automatic Reference Counting.

类支持的附加功能是以增加复杂性为代价的。 作为一般准则,应首选结构体,因为它们更易于推理,并在适当或必要时使用类。 实际上,这意味着您定义的大多数自定义数据类型将是结构体和枚举。 有关更详细的比较,请参见Choosing Between Structures and Classes

1.1 定义语法

结构体和类具有相似的定义语法。 您可以使用struct关键字定义结构,使用class关键字定义类。 两者都将它们的整个定义放在一对大括号中:

struct SomeStructure {
    // structure definition goes here
}
class SomeClass {
    // class definition goes here
}

注意
每当定义新的结构或类时,就定义新的Swift类型。 提供类型UpperCamelCase的名称(例如此处的SomeStructure和SomeClass)以匹配标准Swift类型(例如String,Int和Bool)的大小写。 给属性和方法lowerCamelCase名称(例如frameRate和incrementCount)以将它们与类型名称区分开。

下面是结构体和类的定义:

struct Resolution {
    var width = 0
    var height = 0
}
class VideoMode {
    var resolution = Resolution()
    var interlaced = false
    var frameRate = 0.0
    var name: String?
}

上面的示例定义了一个称为“Resolution”的结构体,用于描述基于像素的显示分辨率。该结构具有两个存储的属性,称为宽度和高度。存储的属性是常量或变量,它们捆绑在一起并存储为结构体或类的一部分。通过将它们设置为初始整数值0,可以推断这两个属性为Int类型。

上面的示例还定义了一个称为VideoMode的类,以描述用于视频显示的特定视频模式。此类具有四个变量存储的属性。第一个分辨率是使用Resolution结构体实例初始化的,该实例可以推断Resolution的属性类型。对于其他三个属性,VideoMode实例将使用interlaced设置false(意为“非隔行视频”),frameRate为0.0以及可选的名为name的String值进行初始化。名称属性会自动提供默认值nil或“无名称值”,因为它是可选类型。

1.2 结构体和类的实例

Resolution结构体定义和VideoMode类定义仅描述Resolution或VideoMode的外观。 它们本身并没有描述特定的分辨率或视频模式。 为此,您需要创建结构体或类的实例。

对于结构体和类,创建实例的语法非常相似:

let someResolution = Resolution()
let someVideoMode = VideoMode()

结构体和类都对新的实例使用初始化程序语法。 初始化程序语法的最简单形式是使用类或结构体的类型名称,后跟空括号,例如Resolution()或VideoMode()。 这将创建类或结构体的实例,并将所有属性初始化为其默认值。 类和结构体的初始化在Initialization中有更详细的描述。

1.3 访问属性

您可以使用点语法访问实例的属性。 在点语法中,您应在实例名称之后立即写上属性名称,并用句点(.)分隔,不能有任何空格:

print("The width of someResolution is \(someResolution.width)")
// Prints "The width of someResolution is 0"

在此示例中,someResolution.width引用someResolution的width属性,并返回其默认初始值0。

您可以深入了解子属性,例如VideoMode的resolution属性中的width属性:

print("The width of someVideoMode is \(someVideoMode.resolution.width)")
// Prints "The width of someVideoMode is 0"

您还可以使用点语法将新值分配给变量属性:

someVideoMode.resolution.width = 1280
print("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// Prints "The width of someVideoMode is now 1280"

1.4 结构体类型的成员初始化器

所有结构体都有一个自动生成的成员初始化程序,您可以使用该初始化程序初始化结构体实例的成员属性。 可以通过名称将新实例的属性的初始值传递给成员初始化器:

let vga = Resolution(width: 640, height: 480)

与结构不同,类实例不会接收默认的成员初始化器。 初始化程序在Initialization中有更详细的描述。

2 结构体和枚举是值类型

值类型是一种在将值分配给变量或常量或将其传递给函数时会复制其值的类型。

实际上,在前几章中,您一直在广泛使用值类型。 实际上,Swift中的所有基本类型(整数,浮点数,布尔值,字符串,数组和字典)都是值类型,并在幕后实现为结构体。

所有结构体和枚举都是Swift中的值类型。 这意味着您创建的任何结构体和枚举实例以及它们具有的任何值类型作为属性,都将在它们在代码中传递时始终会被复制。

注意
由标准库定义的集合(例如数组,字典和字符串)使用了优化来降低复制的性能成本。 这些集合不是立即进行复制,而是共享内存,这些存储在原始实例和任何副本之间存储元素。 如果修改了集合的副本之一,则在修改之前就将元素复制。 您在代码中看到的行为始终就像是立即进行了复制一样。

思考以下示例,该示例使用上一个示例中的Resolution结构体:

let hd = Resolution(width: 1920, height: 1080)
var cinema = hd

本示例声明一个名为hd的常量,并将其设置为一个Resolution实例,该实例使用全高清视频的宽度和高度(1920像素宽x 1080像素高)初始化。

然后,它声明一个名为cinema的变量,并将其设置为hd的当前值。 因为Resolution是一种结构体,所以将复制现有实例,并将该新副本分配给cinema。 尽管hd和cinema现在具有相同的宽度和高度,但它们是幕后的两个完全不同的实例。

接下来,将电影院的width属性修改为用于数字电影院投影的稍宽的2K标准的宽度(宽2048像素,高1080像素):

cinema.width = 2048

检查cinema的width属性表明它的确已更改为2048:

print("cinema is now \(cinema.width) pixels wide")
// Prints "cinema is now 2048 pixels wide"

但是,原始hd实例的width属性仍然是1920的旧值:

print("hd is still \(hd.width) pixels wide")
// Prints "hd is still 1920 pixels wide"

当给cinema提供hd的当前值时,存储在hd中的值将被复制到新的cinema实例中。 最终结果是两个完全独立的实例,其中包含相同的数值。 但是,由于它们是单独的实例,因此将cinema的宽度设置为2048不会影响以hd存储的宽度,如下图所示:

枚举有相同的行为:

enum CompassPoint {
    case north, south, east, west
    mutating func turnNorth() {
        self = .north
    }
}
var currentDirection = CompassPoint.west
let rememberedDirection = currentDirection
currentDirection.turnNorth()

print("The current direction is \(currentDirection)")
print("The remembered direction is \(rememberedDirection)")
// Prints "The current direction is north"
// Prints "The remembered direction is west"

为RememberedDirection分配currentDirection的值时,它实际上是设置为该值的副本。 此后更改currentDirection的值不会影响存储在RememberedDirection中的原始值的副本。

3 类是引用类型

与值类型不同,将引用类型分配给变量或常量或将其传递给函数时,不会复制引用类型。 不是副本,而是使用对相同现有实例的引用。

这是一个使用上面定义的VideoMode类的示例:

let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0

本示例声明一个名为tenEighty的新常量,并将其设置为引用VideoMode类的新实例。 在视频模式之前,已为其分配了1920 x 1080高清分辨率的副本。 它设置为隔行扫描,其名称设置为“ 1080i”,其帧速率设置为每秒25.0帧。

接下来,将tenEighty分配给一个新的常量,也称为alsoTenEighty,并修改alsoTenEighty的帧速率:

let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0

由于类是引用类型,因此tenEighty和alsoTenEighty实际上都引用相同的VideoMode实例。 实际上,它们只是同一单个实例的两个不同名称,如下图所示:

检查tenEighty的frameRate属性表明它正确地打印了来自VideoMode实例的新帧速率30.0:

print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// Prints "The frameRate property of tenEighty is now 30.0"

此示例还显示了为何难以推理引用类型。如果tenEighty和alsoTenEighty在程序代码中相距甚远,可能很难找到改变视频模式的所有方式。无论在何处使用tenEighty,都还必须考虑使用alsoTenEighty的代码,反之亦然。相反,值类型更容易推论,因为与同一值交互的所有代码在源文件中都在一起。

请注意,tenEighty和alsoTenEighty被声明为常量,而不是变量。但是,您仍然可以更改tenEighty.frameRate和alsoTenEighty.frameRate,因为tenEighty和alsoTenEighty常量本身的值实际上并没有改变。 tenEighty和alsoTenEighty本身并不“存储” VideoMode实例,相反,它们都引用了幕后的VideoMode实例。更改的是基础VideoMode的frameRate属性,而不是对该VideoMode的常量引用的值的更改。

3.1 身份运算符

由于类是引用类型,因此多个常量和变量有可能在后台引用同一类的单个实例。 (对于结构体和枚举,情况并非如此,因为在将它们分配给常量或变量或传递给函数时,它们总是被复制。)

有时找出两个常量或变量是否引用了类的完全相同实例有时会很有用。 为此,Swift提供了两个身份运算符:

  • Identical to (===)
  • Not identical to (!==)

使用这些运算符检查两个常量或变量是否引用相同的单个实例:

if tenEighty === alsoTenEighty {
    print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")
}
// Prints "tenEighty and alsoTenEighty refer to the same VideoMode instance."

请注意,与(用三个等号或===表示)Identical to并不意味着和equal to (用两个等号或==表示)相同。 Identical to表示类类型的两个常量或变量引用完全相同的类实例。 equal to 表示两个实例的值相等,对于某些适当的含义,如类型设计者所定义的那样。

自定义结构体和类时,您有责任确定两个实例相等时的资格。 等价运算符中介绍了定义==和!=运算符的实现的过程。

3.2 指针

如果您有使用C,C ++或Objective-C的经验,您可能会知道这些语言使用指针来引用内存中的地址。 引用某种引用类型的实例的Swift常量或变量类似于C中的指针,但不是指向内存中地址的直接指针,并且不需要您编写星号(*)来表示您正在创建一个引用。 相反,这些引用的定义与Swift中的其他任何常量或变量一样。 如果需要直接与指针进行交互,则标准库提供了可以使用的指针和缓冲区类型-请参见Manual Memory Management

总结

这一章节的内容来个小结:

  • 结构体和类的定义语法,语法格式相似;实例化的语法也是一样的。
  • 使用点语法访问实例的属性。
  • 结构体的初始化可以根据其声明的属性,设置默认值进行初始化,类不可以。
  • 结构体和枚举是值类型,当给变量或常量赋值和传递给函数时会进行复制,如:
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd

hd和cinema是两个不同的实例,修改其中一个实例的属性值,不会影响另一个实例。

  • 类是引用类型:对一个对象添加引用,只会增加这个对象的引用计数。
  • Swift里可以用“===”和“!==”来判断两个类的实例是否是同一个。
  • Swift里的指针可以不用添加*号来表示,定义方法和定义Swift中其他任何常量和变量一样。

最后说一句,喜欢的朋友麻烦你手抖一下,点个赞哦,谢谢啦,嘿嘿~

上一章节:枚举

下一章节:属性

参考文档:Swift - Structures and Classes