类和结构体
类和结构体是常用的、灵活的结构用来组织你的代码。你可以在你的类和结构体内定义属性、方法来增加功能,使用常量、变量和函数的语法是一样的。
不同于其他语言,Swift不要求你去创建单独的接口和实现文件来定制类或者结构体。Swift中,你在单独文件中定义一个类或者结构体,关联到这个类或者结构体的外部接口会自动生效,供其它代码使用。
NOTE
一个类的引用通常被当作一个对象(Object)。然而,同其他语言相比,Swift的类和结构体在功能方面更加密切,本章的大部分会介绍可以用在类或者结构体实例上的功能。因此,实例(instance)是个常用的术语。
比较类和结构体
Swift的类和结构体有很多共同处,它们都可以:
1:定义属性来保存数据;
2:定义方法来实现功能;
3:定义下标提供访问值
4:定义构造方法设置初始值
5:默认实现之外,可以扩展功能
6:遵循协议(protocol)来提提供标准功能
更多的信息,参见: Properties,Methods,Subscripts,Initialization,Extensions,和Protocols.
类有的结构体没有:
1:一个类继承另一个类的特征;
2:类实例可以在运行时进行类型检查和判断;
3:类实例可以通过析构函数释放它分配到的资源;
4:引用计数允对于一个类的实例有多于一个引用。
更多信息,参见: Inheritance,Type Casting,Deinitialization,和Automatic Reference Counting.
定义的语法
类和结构体有相似的定义语法。使用class和struct关键字分别定义类和结构体。剩下的定义内容在一对花括号内:
class SomeClass { // class definition goes here } struct SomeStructure { // structure definition goes here }
NOTE
只要是你定义了一个新的类或者结构体,你都定义了一个新的Swift的类型。为了和Swift的类型名称拼写保持一致,类的名字请采用大骆驼拼写方式(UpperCamelCase )。相反,属性和方法名采用小骆驼拼写方式(lowerCamelCase ),用来和类型的名称区分开来。
这里是一个结构体和一个类的定义的例子:
struct Resolution { var width = 0 var height = 0 } class VideoMode { var resolution = Resolution() var interlaced = false var frameRate = 0.0 var name: String? }
上面例子定义了一个叫做Resolutino的结构体,用来描述一个像素分辨率。这个结构体保存了两个存储属性(stored property)分别叫做width和height。存储属性(stored property)是被绑定和存储到类或者结构体的常量或者变量。这两个属性被推断是Int类型的,因为它们被初始化设置为整型0。
上面的例子同样的定义了一个新的类叫做VideoMode,用来描述一个特定的显示模式。这个类有四个变量的存储属性。第一个,resolution被初始化为了一个新的Resolution结构体引用,它被推测为Resolution类型。剩下的三个属性,interlaced(是否交叉) 被设置为false,帧率被设置为0.0,一个字符串可选类型的值叫做name。name属性被自动赋予了默认值nil,因为它是一个可选类型。
类和结构体的引用
Resolution 结构体的定义和VideoMode类的定义分别描述了一个分辨率和显示模式应该的样子。他们不描述一个具体的分辨率或者显示模式。要描述具体的,需要创建结构体或者类的引用。
创建结构体和类的引用语法非常相似:
let someResolution = Resolution() let someVideoMode = VideoMode()
结构体和类都使用初始化语法创建新实例。最简单的初始化语法是使用结构体或类的名字跟随一个空的圆括号,就像Resolution()或者VideMode()一样。这中方式创建了类或者结构体的引用,其中的属性用他们的默认值初始化了。类和结构体的初始化更多的描述参见:Initialization。
访问属性
你可以使用点号访问一个实例的属性。引用名后跟一个点号再加上它的属性名就可以了,其间不要有任何的空白;
println("The width of someResolution is \(someResolution.width)") // prints "The width of someResolution is 0"
这个例子中,someResolution.width是someResolution的width属性的引用,返回默认的初始化值0.
你可以继续访问属性的子属性,比如一个VideoMode的resolution属性的width属性:
println("The width of someVideoMode is \(someVideoMode.resolution.width)") // prints "The width of someVideoMode is 0"
你可以通过点号给变量的属性赋值:
someVideoMode.resolution.width = 1280 println("The width of someVideoMode is now \(someVideoMode.resolution.width)") // prints "The width of someVideoMode is now 1280"
NOTE
和OC不同,Swift允许你可以直接对子属性赋值。上面的例子中,someVideoMode的属性resolution的属性width被直接赋值了,不需要你将整个resolution设置新值。
结构体的全体成员初始化函数
所有的结构体都有一项全员初始化的技能,你可以使用它初始化一个结构体的成员属性。新引用属性的初始值可以通过 属性的名称传递给全员初始化函数:
let vga = Resolution(width: 640,height: 480)
不同于结构体,类就没有这项技能。初始化的更多描述参见:Initialization.
结构体和枚举是值类型(value type)的
值类型的意思是这种类型的值被赋值给变量或者常量时,被当作参数传递给函数时会复制它的值。
前面的章节广泛使用了值类型。实际上Swift中所有的基本类型——整型、浮点型、布尔类型、字符串,数组和字典都是值类型,并且在后台以结构体的形式实现。
Swift中所有的结构体和枚举都是值类型的。这意味着你创建的任何结构体和枚举的引用、他们所有用的任意类型的属性,在代码中传递时都会被复制。
看看下面的例子,使用了上面讲到的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:
println("cinema is now \(cinema.width) pixels wide") // prints "cinema is now 2048 pixels wide"
然而,hd的width属性仍然是1920:
println("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 } var currentDirection = CompassPoint.West let rememberedDirection = currentDirection currentDirection = .East if rememberedDirection == .West { println("The remembered direction is still .West") } // prints "The remembered direction is still .West"
当remberedDirection被赋值为currentDirection的值,实际也是复制了一份值的副本。currentDirection值的改变不会影响remberedDirection中存储的原始值。
类是引用类型的
和值类型不同,引用类型当他们被赋值给变量或者常量、被当作函数的参数传递时他们不会被复制。取代复制的是,一个对同一个已经存在实例的引用。
这里是一个例子,使用了前文定义的VideoMode类:
let tenEighty = VideoMode() tenEighty.resolution = hd tenEighty.interlaced = true tenEighty.name = "1080i" tenEighty.frameRate = 25.0
这里例子定义了一个新的常量叫做tenEighty,然后将一个VideoMode类的新引用赋值给它。视频的模式被赋值为一个HD分辨率的副本(前面定义的1920*1080);interlaced设置为了true;名字被命名为“1080i”;帧率被设置为每秒25帧。
接下来,tenEighty被赋给了一个新的常量,叫做alsoTenEighty,它的帧率被修改了:
let alsoTenEighty = tenEighty alsoTenEighty.frameRate = 30.0
因为类是引用类型的,tenEighty和alsoTenEighty实际上是同一个VideoMode类的引用。直接一点说,他们是一个引用,只不过有不同的名字而已。
检查一下tenEighty的frameRate属性,它以经悄然改变成了30.0:
println("The frameRate property of tenEighty is now \(tenEighty.frameRate)") // prints "The frameRate property of tenEighty is now 30.0"
这里tenEighty和alsoTenEighty被定义为常量而不是变量。然而你依然能够修改tenEighty.frameRate 和alsoTenEighty.frameRate,因为tenEighty和alsoEighty他们自身的值没有发生变化。它们两个自身并不“存储”VideoMode引用,而是存储了一个到VideoMode实例的引用。不是到VideoMode引用的常量被改变,而是VideoMode的frameRate被修改了。
恒等运算符(Identity Operators)
因为类是引用类型的,所以可能出现多个常量或者变量指向同一个实例的情况。(结构体和枚举就不会这样,因为他们是值类型的)
有时需要判断两个变量或常量的引用是不是一个实例,为此Swift提供了两个恒等运算符:
恒等于(===)
非恒等于(!==)
使用着俩运算符判断是不是两个常量或变量的引用指向同一个实例:
if tenEighty === alsoTenEighty { println("tenEighty and alsoTenEighty refer to the same VideoMode instance.") } // prints "tenEighty and alsoTenEighty refer to the same VideoMode instance."
注意,恒等于(三个等于号,===)和等于(两等于号,==)不同的:
恒等于表示两个常量或变量的引用实际上是同一个实例。
等于表示两个实例比较值的情况下是相等、相当的。“相等”的意义需要在设计时定义。
当你定义了一个类和结构体,就有必要确定两个引用相等的条件。在 Equivalence Operators一节有专门讲述如果做这个工作。
指针(pointers)
如果你有C、C++或者OC的经验,你会知道这些语言使用指针(poointer)表示内存中的地址。一个Swift中的指向引用类型实例常量或者变量类似于C语言中的指针,但是不是一个直接指向内存地址的指针,也不需要用星号(*)表明你正在创建一个引用。而是像其他类型的常量或变量一样定义。
在类和结构体之间选择
你可以使用类或者结构体定义自己的数据类型。
然而,结构体总是传值的,而类实例总是传引用的。这意味着他们有各自的分工。根据你项目中的需要考虑数据结构和功能,决定数据结构是采用类还是结构体来定义。
作为通常的原则,如果有一条或一条以上的以下情形,考虑使用结构体:
1:结构的主要目标是封装一系列有关联的简单数据值;
2:赋值或传参数的时候期望传值而不是传引用;
3:属性也是值类型的;
4:不想从已有的类型中继承属性或者行为。
举一些适合采用结构体的例子:
1:描述一个几何形状的大小,或许可以封装宽度(width)和高度(height),而且它们都是Double类型的。
2:描述在一个序列中的范围的一种方式,或许可以封装一个开始(start)属性和一个长度(length)属性,而且它们都是整型。
3:描述一个在3D坐标系中的点,或许可以封装x,y,z属性,它们每一个都是Double类型的。
所有其他的情况,定义一个类吧。通过创建那个类的实例来操作它。实际上这意味着,多数的用户定制数据结构要采用类,而不是结构体。
字符串、数组、和字典的赋值和复制行为
Swift的字符串、数组和字典类型是以结构体类型来实现的。这意味着,字符串、数组、字典在经过赋值或者传参的时候会被复制。
这个行为和Foundation中的NSString、NSArray,NSDirctionary不同,他们是以类实现的,而不是结构体。它们三个的实例在赋值传值的时候都是使用的是引用,而不是复制。
NOTE 上面提到了字符串、数组和字典的“复制”问题。你在你代码看到地方复制就会发生。实际上,Swift只在有必要的时候才做实际的复制操作。Swift掌控了所有值复制,为了确保最佳表现,你不要试图掌控这个操作。