结构体和类(Structures and Classes)

642 阅读11分钟

原文

结构体和类是一般目的,灵活的结构,成为你项目代码的构造块。使用你用来定义常量,变量,方法一样的语法来定义属性和方法来给你的结构体和类增加功能。

不像其他编码语言,swift不需要你为自定义的结构体和方法创建独立的接口和实现文件。在swift中,你在一个文件里定义结构体或者类,对于这个类或者结构体的外部接口自动变得对其他代码获取使用是可获取的。

一个类的实例是一个对象。不过,swift结构体和类的功能比其他语言中的更接近,并且这个章节的大部分描述了用于类或者结构体的实例的功能。因为这个,使用更通常术语实例。

对比结构体和类(Comparing Structures and Classes)

swift中的结构体和类有一些共同的东西。两个都可以:

  • 定义属性来存储值
  • 定义方法来提供功能
  • 定义下标来提供使用下标语法访问他们的值
  • 定义初始化方法来设置他们初始化状态
  • 可以超过他们的默认实现扩展他们的方法
  • 遵守协议来提供一个具体种类的标准功能

更多的信息,查看Properties, Methods, Subscripts, Initialization, Extensions, and Protocols

雷友额外的结构体没有的特性:

  • 继承是一个类继承其他类的特性
  • 类型转换使你可以在运行时查看和说明一个类实例的类型
  • 清楚方法是类的实例可以释放它分配了的资源
  • 引用计数允许对一个类实例多余一个引用

更多的信息,查看Inheritance, Type Casting, Deinitialization, and Automatic Reference Counting

类支持的额外的功能是以增加复杂的的为代价而来的。作为一般准则,更偏爱使用结构体因为他们更容易推理并且当合适或者有必要的时候使用类。在实践中,这意味着你定义的大部分自定义数据类型将会是结构体或者枚举。更多详细的对比,查看Choosing Between Structures and Classes

定义语法(Definition Syntax)

结构体和类有一个相似的定义语法。使用struct关键字声明结构体,使用class关键字声明类。两个都把它们全部的定义放在一对大括号中:

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

任何时候你定义一个新的结构体或者类,你定义了一个新的swift类型。 用类型UpperCamelCase名来匹配大写的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的结构体,来描述一个显示像素的分辨率。这个结构体有两个名为width和height的存储属性。存储属性是附属和存储为结构体或者类一部分的常量或者变量。这两个属性通过将他们设置为一个0的初始化整型值推导为Int类型。

上面的例子也定义了一个名为VideoMode的新类,来为视频播放描述一个特定的视频模式。这个类有四个可变的存储属性。第一个,resolution,使用一个新的Resolution结构体实例初始化,推断一个Resolution的类型。其他的三个属性,新的VideoMOde实例将会使用一个设置为false(意味着”非交错的视频“)的interlaced初始化,一个回放帧率0.0,一个名为name的可选String值。name属性自动给了一个默认值nil,或者”no name value“,因为它是可选类型。

结构体和类实例(Structure and Class Instances)

Resolution结构体的定义和VideoMode类的定义只描述了Resolution或者VideoMode将来看起来是什么样。他们自己本身不描述特殊的分辨率或者视频模式。要做这些事情,你需要创建一个结构体或者类的实例。

结构体和类两个的创建实例的语法很相似:

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

结构体和类两个对新实例都是用初始化语法。初始化语法最简单的形式是用类或者结构体的类型名字后面跟一个空括号,例如Resolution()或者VideoModel()。这样创建类或者结构体的新的实例,全部的属性初始化为他们默认的值。类和结构体初始化的更多描述细节在Initialization

访问属性(Accessing Properties)

你可以使用点语法访问一个实例的属性。点语法中国,在实例名称后面立即写属性的名字,用点分隔开,没有任何空间:

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"

结构体类型成员初始化(Memberwise Initializers for Structure Types)

所有的结构体有一个自动生成的成员初始化方法,你可以用来初始化新结构体实体的成员属性。新实例的属性初始化值可以通过名字传给成员初始化方法:

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

不像结构体,类实例没有默认的成员初始化方法。初始化方法更多描述细节在Initialization

结构体和枚举时值类型(Structures and Enumerations Are Value Types)

值类型是一种当它分配给一个变量或者常量或者传给函数时拷贝值的类型。

实际上你在之前的章节中已经大量的使用了值类型。实际上,swift中全部的基础类型--整型,浮点型数字,布尔值,字符串,数组和字典--是值类型,在背后是用结构体实现的。

在swift中全部的结构体和枚举时值类型。这意味着任何你创建的结构体和枚举实例--他们有的任何作为属性的值类型--在他们在你的代码中传递的时候都是拷贝的。

通过标准库定义的集合像arrays,dictionaries,和strings使用最佳的优化来减少拷贝的性能消耗。取代立即进行拷贝,这些集合共享在原来实例和任何拷贝之间存储的元素的内存。如果集合的拷贝之一被修改了,元素在修改之前进行拷贝。你代码中看到的表现像立即进行了拷贝。

思考下面的例子,使用之前例子中的Resolution结构体:

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

这个例子声明了一个名为hd的常量并且将它设置为使用全HD视频的宽和高(1920点宽*1080点高)初始化的Resolution实例。

然后声明了一个名为cinema的变量并且将它设置为当前值hd。因为Resolution是一个结构体,生成了已存在的实例的拷贝,这个新的拷贝分配给cinema。即使hd和cinema现在有一样的宽和高,在这之后他们是两个完全不同的实例。

接下来,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实例中。最后结果是两个完全独立的实例,他们包含一样的numeric值。不过,一位他们是独立的实例,将cinema的width设置为2048没有应县存储在hd中的width,如下图所示:


用在枚举上一样的特性:

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中原先的值的拷贝。

类是引用类型(Classes Are Reference Types)

不像值类型,当他们分配给一个变量或者常量或者传给函数时引用类型没有拷贝。不像拷贝,使用一个指向同一个已存在的实例的引用。

这里的例子,使用上面定义的VideoMode类:

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

这个例子声明了一个名为tenEighty的新的常量并且将它设置为引用一个新的VideoMode类的实例。视频模型分配了一个之前1920*1080HD分辨率的拷贝。他被设置为interlaced,它的那么设置为”1080i“,它的帧率设置为每秒25帧。

接下来,tenEighty分配给一个新的常量,名为alsoTenEighty,并且alsoTenEighty的帧率修改了:

let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0

因为雷士引用类型,tenEighty和alseTenEighty实际上两个引用相同的VideoMode实例。实际上,特闷是同一个实例的不同名字,像下面图片展示的:


检查tenEighty的frameRate属性展示了它正确的报道了在下面的VideoMode实例的新的帧率为30:

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

这个例子展示了引用类型很难推理。如果tenEighty和alsoTenEighty在你的项目代码中离得很远。很难找到video mode修改的全部方式。不管哪里使用tenEighty,你不得不考虑使用alsoTenEighty的代码,反过来也是。另一面,值类型很容易推理因为和相同的值交互的全部代码在你的源文件中离的很近。

注意tenEighty和alseTenEighty声明为常量而不是变量。不过你仍然可以修改tenEighty.frameRate和alseTeneighty.frameRate因为tenEighty和alseTenEighty常量他们自己实际上没有改变。tenEighty和alseTenEighty他们自己不需要”存储“VideoMode实例--取而代之,他们在背后都引用了一个VideoMode实例。是后面VideoMode的frameRate属性修改了,不是引用那个VideoMode的常量值。

同样的运算符(Identity Operators)

因为类是引用类型,对多个常量和变量来说在背后可能引用同一个类的实例。(对于结构体和枚举的相同实际上不是真的,因为当他们分配给一个常量或者变量或者传给一个函数时他们通常是拷贝)。

有时候查看两个常量或者变量是否引用一个类的同一个实例挺有用的。为了做到这个,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."

注意恒等于(用三个等号表示,===)不代表和等于(==)一样的东西。恒等于意味着两个类型是类的常量或者变量引用明确的同样的类实例。等于意味着两个实例事项在值上是相等的或者相同的,对于一些相等的合适的意义,就像类型设计者所定义的。

当你定义你自己自定义的结构体或者类时,决定两个实例相等的条件是你的职责。定义你自己的==和!=操作符的实现的过程在Equivalence Operators中描述的。

指针(Pointers)

如果你有C,C++,或者Objective-C的经历,你可能知道这些语言使用指针来引用内存中的地址。swift中一个指向某个引用类型实例的常量或者变量和C中的指针很像,但是不是一个直接到内存中地址的指针,不需要你写asterisk(*)来指明你创建了一个引用。取而代之的,这些引用像swift中其他常量或者变量一样。标准库提供的指针和缓存类型如果你需要和指针直接交互的话可以使用--查看Manual Memory Management