Swift Learning (11) - Classes and Structures (Fully Ver.)

Swift Learning (11) - Classes and Structures (Fully Ver.)

August 30, 2021·Jensen
Jensen

image

结构体和类作为一种通用而又灵活的结构,成为了人们构建代码的基础。可以使用定义常量、变量和函数的语法,从而为结构体和类定义属性、添加方法。

与其他编程语言所不同的是,Swift并不要求为自定义的结构体和类的接口与实现代码分别创建文件。只需要在单一文件中定义一个结构体或类,系统将会自动生成面向其他代码的外部接口。

注意

通常一个类的实例被称为对象。然而相比其他语言,Swift中结构体和类的功能更加相近,本章中所讨论的大部分都可以用在结构体或者类上。因此,这里会使用实例这个更通用的术语。


结构体和类对比

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

  • 定义属性用于存储值
  • 定义方法用于提供功能
  • 定义下标操作用于通过下标语法访问它们的的值
  • 定义构造器用于设置初始值
  • 通过扩展以增加默认实现之外的功能
  • 遵循协议以提供某种标准功能 更多信息请参见属性方法下标构造过程扩展协议

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

  • 继承允许一个类继承另一个类的特征
  • 类型转换允许在运行时检查和解释一个类实例的类型
  • 析构器允许一个类实例释放任何其所被分配的资源
  • 引用计数允许对一个类的多次引用 更多信息请参见继承类型转换析构过程自动引用计数

类支持的附加功能是以增加复杂性为代价的。作为一般准则,优先使用结构体,因为它们更容易理解,仅在适当或者必要时才使用类。实际上,这意味着大多数自定义数据类型都会是结构体和枚举。

注意

类和actors共享很多特性。

类型定义语法

结构体和类有着相似的定义方式,通过struct关键字引入结构体,通过class关键字引入类,并将它们的具体定义放在一对大括号中:

struct SomeStructure {
    // 在这里定义结构体
}
class SomeClass {
    // 在这里定义类
}

注意

每当定义一个新的结构体或者类时,都是定义了一个新的Swift类型。请使用UpperCamelCase这种方式来命名类型(如这里的SomeClass和SomeStructure),以便复合标准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的结构体,用来描述基于像素的分辨率。这个结构体包含了名为widthheight的两个存储属性。存储属性是与结构体或者类绑定的,并存储在常量或者变量。当这两个属性被初始化为整数0的时候,它们会被推断为Int类型。

在上面的示例还定义了一个名为VideoMode的类,用来描述视频显示器的某个特定视频模式。这个类包含了四个可变的存储属性。第一个,resolution,被初始化为一个新的Resolution结构体实例,属性类型被推断为Resolution。新VideoMode实例同时还会初始化其它三个属性,它们分别是初始值为falseinterlaced(意为“非隔行视频”),初始值为0.0的frameRate,以及值为可选Stringname。因为name是一个可选类型,它会被自动赋予一个默认值nil,意为“没有name值”。

结构体和类的实例

Resolution结构体和VideoMode类的定义仅描述了什么是ResolutionVideoMode。它们并没有描述一个特定的分辨率(Resolution)或者视频模式(VideoMode)。为此,需要创建结构体或者类的一个实例。

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

结构体和类都使用构造器语法来创建新的实例。构造器语法的最简单形式就是在结构体或者类的类型名称后跟随一对空括号,如Resolution()VideoMode()。通过这种方式所创建的类或者结构体实例,其属性均会被初始化为默认值。构造过程章节将会对类和结构体的初始化进行更详细的讨论。

属性访问

可以通过点语法访问实例的属性,其语法规则是,实例后面紧跟属性名,两者以.分隔,中间不带空格。

print("The width of someResolution is \(someResolution.width).")  // 打印输出“The width of someResolution is 0.”
---
output: The width of someResolution is 0.

在上面的例子中,someResolution.width引用someResolutionwidth属性,返回width的初始值0。

也可以访问子属性,如someVideoMode中的resolution属性的width属性:

print("The width of someVideoMode is \(someVideoMode.resolution.width).")  // 打印输出“The width of someVideoMode is 0.”
---
output: The width of someVideoMode is 0.

也可以使用点语法为可变属性赋值:

someVideoMode.resolution.width = 1280
print("Now, the width of someVideoMode is \(someVideoMode.resolution.width).")  // 打印输出“Now, the width of someVideoMode is 1280.”
---
output: Now, the width of someVideoMode is 1280.

结构体类型的成员逐一构造器

所有的结构体都有一个自动生成的成员逐一构造器,用于初始化新结构体实例中的成员属性。新实例中各个属性的初始值可以通过属性的名称传递到成员逐一构造器中:

let vga = Resolution(width: 640, height: 480)
print("The width of vga is \(vga.width) and the height is \(vga.height).")  // 打印输出“The width of vga is 640 and the height is 480.”
---
output: The width of vga is 640 and the height is 480.

与结构体不同,类实例没有默认的成员逐一构造器。构造过程章节会对构造器进行更详细的讨论。


结构体和枚举是值类型

值类型是这样一种类型,当它被赋值给了一个变量、常量或者传递给一个参数时,其值会被拷贝。

在之前的章节中,我们已经大量使用了值类型。其实,Swift中所有的基本类型:整数(Integer)、浮点数(Floating-Point Number)、布尔值(Boolean)、字符串(String)、数组(Array)和字典( Dictionary)都是值类型,其底层也是使用结构体实现的。

Swift中所有的结构体和枚举都是值类型。这意味着它们的实例,以及实例中所包含的任何值类型的属性,在代码中传递的时候都会被复制。

注意

标准库定义的集合,如数组、字典和字符串,都对复制进行了优化以降低性能成本,新集合不会立即复制而是跟原集合共享同一份内存,共享同样的元素。在集合的某个副本要被修改之前,才会复制它的元素。而开发者在代码中看起来就像是立即发生了复制。

请看下面的示例,其使用了上一个示例中的Resolution结构体:

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

在以上的示例中声明了一个名为hd的常量,其值初始化为全高清视频分辨率(1920像素宽,1080像素高)的Resolution实例。

然后示例中又声明了一个名为cinema的变量,并将hd赋值给它。因为Resolution是一个结构体,所以会创建一个现有实例的副本,并将副本赋值给cinema。尽管hdcinema有着相同的宽(Width)和高(Height),但是在幕后这两个是完全不同的实例。

下面,为了满足数码影院的放映需求(2048像素宽,1080像素高),cinemawidth属性被修改为稍宽一点的2K标准:

cinema.width = 2048

查看cinemawidth属性,其值确实被改为了2048:

print("The width of cinema is \(cinema.width).")  // 打印输出“The width of cinema is 2048.”
---
output: The width of cinema is 2048.

然而,初始的hdwidth属性值还是1920:

print("The width of hd is still \(hd.width).")  // 打印输出“The width of hd is still 1920.”
---
output: The width of hd is still 1920.

hd赋值给cinema时,hd中所存储的值会拷贝到新的cinema实例中。结果就是两个完全独立的实例包含了相同的数值。由于两者相互独立,因此将cinemawidth修改为2048并不会影响hd中的width的值,如下图所示:

SharedState Struct

枚举也遵循相同的行为准则:

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

print("The current direction is \(currentDirection).")  // 打印输出“The current direction is north.”
print("The remember direction is \(rememberDirection).")  // 打印输出“The remember direction is west.”
---
output: The current direction is north.
The remember direction is west.

rememberDirection被赋予了currentDirection的值,实际上它被赋予的是一个值拷贝,赋值过程结束后再修改currentDirection的值并不rememberDirection所存储的原始值的拷贝。


类是引用类型

与值类型不同,引用类型被赋予到一个常量、变量或者被传递到一个函数时,其值不会被拷贝。因此使用的是已存在实例的引用而不是其拷贝。

请看下面这个示例,其使用了之前定义的VideoMode类:

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

在以上示例中,声明了一个名为tenEighty的常量,并让其引用一个VideoMode类的新实例。它的视频模式(Video Mode)被赋值为之前创建的HD分辨率(1920 * 1080)的一个拷贝,然后将其设置为隔行视频,名字为“”,并将帧率设置为25.0帧每秒。

接下来,将tenEighty赋值个一个名为alsoTenEighty的新常量,并修改alsoTenEighty的帧率:

let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0

因为类是引用类型,所以alsoTenEightytenEighty引用的是同一个VideoMode实例。换句话说,它们是同一个实例的两种叫法,如下图所示:

SharedState Class

通过查看tenEightyframeRate属性,可以看到其正确地显示了底层VideoMode实例的新帧率30.0:

print("The frameRate property of tenEighty is now \(tenEighty.frameRate).")  // 打印输出“The frameRate property of tenEighty is now 30.0.”
---
output: The frameRate property of tenEighty is now 30.0.

这个例子也就显示了为何引用类型难以理解。如果tenEightyalsoTenEighty在代码中的位置相距很远,那么就很难找到所有修改视频模式的地方。无论在哪里使用tenEighty都需要考虑alsoTenEighty的代码,反之亦然。相反,值类型就显得容易理解多了,因为源码中,同一个位置交互的代码都很近。

需要注意的是tenEightyalsoTenEighty被声明为常量而不是变量。然而你依然可以改变tenEighty.frameRatealsoTenEighty.frameRate,这是因为tenEightyalsoTenEighty这两个常量的值并未改变。它们并不“存储”这个VideoMode实例,而仅仅是对VideoMode实例的引用。所以,改变的是底层VideoMode实例的frameRate属性,而不是指向VideoMode的常量引用的值。

恒等运算符

因为类是引用类型,所以多个常量和变量可能在幕后同时引用同一个类实例。(对于结构体和枚举来说,这并不成立。因为它们作为值类型,在被赋予给常量、变量或者传递到函数时,其值总是会拷贝。)

判定两个常量或者变量是否引用同一个类实例时很有用。为了达到这个目的,Swift提供了两个恒等运算符:

  • 相同(===)
  • 不同(!==)

使用这两个运算符检测两个常量和变量是否引用了同一个实例:

if tenEighty === alsoTenEighty {
    print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")  // 打印输出“tenEighty and alsoTenEighty refer to the same VideoMode instance.”
}
---
output: tenEighty and alsoTenEighty refer to the same VideoMode instance.

请注意,“相同”(用三个等号表示,===)与“等于”(用两个等号表示,==)不同,“相同”表示两个类类型(class type)的常量或变量引用同一个类实例。“等于”表示两个实例的值“相等”或“等价“,判定时要遵循设计者定义的评判标准。

当在定义自定义结构体和类时,开发者有义务来决定判定两个实例“相等”的标准。在章节高级运算符中将会详细介绍实现自定义==!=运算符的流程。

指针

如果有CC++、Objective-C语言的开发经验,那么大家也许会知道这些语言使用指针来引用内存中的地址。Swift中引用了某个引用类型实例的常量或变量,与C语言中的指针类似,不过它并不直接指向某个内存地址,也不要求使用*来表明正在创建一个引用。相反,Swift中引用的定义方式与其他常量和变量的一样。如果需要直接与指针交互,可以使用标准库提供的指针和缓冲区类型–参见手动内存管理

Last updated on