Swift 4.0 编程语言(四)

109.关联值

上部分实例展示枚举分支是一个定义值(类型值)。 你可以把一个常量或者变量设置成 Planet.earth, 然后稍晚再判断这个值。 不过, 有时候存储其他类型的对应值是很有用的。这让你可以随着分支存储其他自定义信息。

你可以定义 Swift 枚举来存储给定类型的对应值, 如果需要每个分支的值类型可以不同。枚举和其他语言的可辨识联合,标签联合或者变体很类似。

例如, 假设库存跟踪系统需要用两种不同类型的条形码来追踪产品。一些产品用UPC格式标记为 1D 条形码, 这些使用数字 0 到 9. 每个条形码有一个“数字系统” 位, 后面是5个 “制造商码” 位然后是5个“产品码”位。 最后是“校验” 位来校验扫码正确性:


Swift 4.0 编程语言(四)_第1张图片

其他产品用二维码标记, 它使用任意的 ISO 8859-1 字符并且可以编码2953个字符长度:


Swift 4.0 编程语言(四)_第2张图片

这就方便库存跟踪系统能够用四个整数元组存储 UPC 条形码, 用一个任意长度的字符串存储二维码。

在 Swift 里, 枚举定义两种类型条形码可能如下所示:

enum Barcode {

case upc(Int, Int, Int, Int)

case qrCode(String)

}

可以这样解读:

“定义一个枚举类型 Barcode, 有一个upd的值对应 (Int, Int, Int, Int)类型, 和一个 qrCode 值对应 String 类型。”

定义没有提供任何实际的 Int 或者 String 值—它仅仅定义了对应值的类型。

新的条形码可以用任何一个类型来创建:

var productBarcode = Barcode.upc(8, 85909, 51226, 3)

这个例子创建了一个新的变量 productBarcode 然后用一个元组值(8, 85909, 51226, 3)赋给它一个 Barcode.upc值。

相同的产品可以指定一个不同的二维码类型:

productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

这时候, 原先的 Barcode.upc 和它的整数值被新的 Barcode.qrCode 和它的字符串值取代。类型的常量或者变量可以存储 .upc 或者 .qrCode (带着对应的值), 不过在给定时间它们只能存储其中之一。

可以用一个switch语句判断不同的条形码类型。 这次, 无论如何, 对应的值被提取作为switch语句的部分。提取的值作为常量或者变量在switch语句的分支中使用:

switch productBarcode {

case .upc(let numberSystem, let manufacturer, let product, let check):

print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")

case .qrCode(let productCode):

print("QR code: \(productCode).")

}

// 打印 "QR code: ABCDEFGHIJKLMNOP."

如果一个分支的所有对应值都提取成常量或者变量, 或者都提取成变量, 你可以在这个分支 名字前单独放一个let 或者 var, 为简洁性考虑:

switch productBarcode {

case let .upc(numberSystem, manufacturer, product, check):

print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")

case let .qrCode(productCode):

print("QR code: \(productCode).")

}

// 打印 "QR code: ABCDEFGHIJKLMNOP."

110.原始值

Associated Values 中的条形码例子展示枚举分支如何声明它们存储的不同类型的对应值。作为关联值的替代方式, 枚举分支 可以填充默认值 (raw values), 它们都是相同的类型。

这里有个例子,在指定的枚举分支旁存储原始 ASCII 值:

enum ASCIIControlCharacter: Character {

case tab = "\t"

case lineFeed = "\n"

case carriageReturn = "\r"

}

这里, 枚举 ASCIIControlCharacter 的原始值定义为字符类型, 设成成一些比较常见的 ASCII 控制字符。

原始值可以是字符串, 字符, 或者任意整型或者浮点型数字类型。 每个原始值在枚举声明里必须是唯一的。

备注

原始值作为对应值是不相同的。原始值用来在首次声明枚举时填充值, 就像上面三个 ASCII 码。特定分支的原始值总是相同的。基于枚举分支之一创建一个新的常量或者变量时设置关联值, 每次设置都可以不同。


隐式赋原始值

使用存储整型或者字符串类型的原始值时, 你不需要显式为每个分支指定一个原始值。如果你不设置, Swift 会为你自动指定值。

例如, 当整型用作原始值时, 每个分支的隐式值比前一个分支多一。如果第一个分支没有设置值, 它的值是0.

下面的枚举是早期 Planet 枚举的细化版本, 带有整数原始值来代表距离每个星球距离太阳的顺序:

enum Planet: Int {

case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune

}

上面的例子, Planet.mercury有个显式的原始值1, Planet.venus 有个显式的值 2, 以此类推。 字符串用作原始值时, 每个分支的隐式值是分支名字的文本字符串。 下面的枚举是早期 CompassPoint 枚举的细化版本, 使用字符串原始值表示每个方向的名字:

enum CompassPoint: String {

case north, south, east, west

}

上面的例子, CompassPoint.south 有个隐式的原始值 “south”, 以此类推。

使用 rawValue 属性来访问枚举分支的原始值:

let earthsOrder = Planet.earth.rawValue

// earthsOrder is 3

let sunsetDirection = CompassPoint.west.rawValue

// sunsetDirection is "west"

用原始值初始化

如果你用原始值类型定义一个枚举, 枚举自动得到一个初始化方法, 这个初始化方法有一个原始值类型 (参数 rawValue) 并且返回一个枚举分支或者nil. 你可以用这个初始化方法试着创建新的枚举实例。

这个例子用原始值7标识 Uranus :

let possiblePlanet = Planet(rawValue: 7)

// possiblePlanet 是 Planet?类型并且等于Planet.uranus

不是所有的整数值都能找到一个对应的星球, 不过。 因为如此, 原始值初始化方法总是返回一个可选的枚举分支. 上面的例子, possiblePlanet 是Planet?类型, 或者 “可选的 Planet.”

备注

原始值初始化方法是灵活的, 因为不是每个原始值都会返回一个枚举分支.

如果尝试查找位置是11的星球, 原始值初始化方法返回的 Planet 值为nil:

let positionToFind = 11

if let somePlanet = Planet(rawValue: positionToFind) {

switch somePlanet {

case .earth:

print("Mostly harmless")

default:

print("Not a safe place for humans")

}

} else {

print("There isn't a planet at position \(positionToFind)")

}

// 打印 "There isn't a planet at position 11"

这个例子使用可选绑定访问原始值是11的星球。 语句 if let somePlanet = Planet(rawValue: 11) 创建了一个可选的 Planet, 然后设置 somePlanet 值为这个可选绑定的 Planet. 这种情况, 不可能获取到位置是11的星球, 所有else分支被执行了。

111.递归枚举

递归枚举是这样一种枚举,它有另外一个枚举的实例,这个实例作为一个或者多个分支的关联值。通过在枚举分支前书写indirect 来标明枚举是递归的。告诉编译器插入必要的间接层。

例如, 这里有一个枚举存储了简单的算术表达式:

enum ArithmeticExpression {

case number(Int)

indirect case addition(ArithmeticExpression, ArithmeticExpression)

indirect case multiplication(ArithmeticExpression, ArithmeticExpression)

}

你也可以在枚举开始写 indirect, 来保证所有需要的枚举分支是间接的:

indirect enum ArithmeticExpression {

case number(Int)

case addition(ArithmeticExpression, ArithmeticExpression)

case multiplication(ArithmeticExpression, ArithmeticExpression)

}

这个枚举可以存储三种算术表达式: 一个简单数字, 两个表达式相加, 两个表达式相乘。 加法和乘法分支对应值也是算术表达式—内嵌表达式。 例如, 表达式 (5 + 4) 2 乘法右边有一个数字,乘法左边有一个表达式。 因为数据是嵌套的, 枚举存储这个数据也需要支持嵌套—意思就是说枚举需要递归。 下面的代码展示为(5 + 4) 2创建了一个递归枚举 ArithmeticExpression :

let five = ArithmeticExpression.number(5)

let four = ArithmeticExpression.number(4)

let sum = ArithmeticExpression.addition(five, four)

let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

递归函数直接使用具有递归结构的数据。例如, 这里有个函数执行一个算术表达式:

func evaluate(_ expression: ArithmeticExpression) -> Int {

switch expression {

case let .number(value):

return value

case let .addition(left, right):

return evaluate(left) + evaluate(right)

case let .multiplication(left, right):

return evaluate(left) * evaluate(right)

}

}

print(evaluate(product))

// 打印 "18"

这个函数通过返回关联值执行一个简单的数字。它执行一个加法和一个乘法,执行左边的表达式和右边的表达式,然后相加或者相乘它们。

类和结构体

类和结构体是通用的,灵活的结构, 成为你程序代码的构建块。通过对常量,变量,函数使用相同的语法,你定义属性和方法来给类和结构体添加功能

跟其他语言不一样, Swift 不要你给类和结构体创建单独的接口和实现文件。在 Swift里, 你在一个单独的文件中定义类和结构体。 这些类和结构的对外接口对于其他代码是自动可用的。

备注:类的实例一般认为是一个对象。 不过, Swift 的类和结构体在功能上比其他语言更加接近, 这种描述的功能即可用在类的实例也可以用在结构体的实例上。因为如此, 更多通用术语实例被使用了。

比较类和结构体

在Swift里类和结构体有很多相同的地方,它们都可以:

定义属性来存储值

定义函数提供功能

定义下标,提供下标语法访问值

定义初始化方法来设定初始状态

扩展默认实现的功能

遵守协议提供特定类型的标准功能

类具备结构体没有的额外能力:

继承,让一个类继承另外一个类的特征。

类型转换,类型转换让你可以在运行是判断和解释类的实例。

析构器可以释放类实例分配的任何资源。

引用计数允许对类实例的多次引用。

备注 结构体传值时都是拷贝,它不使用引用计数。

定义语法

类和结构体有相同的定义语法。 使用class 关键字引入类,使用struct关键字引入结构体。两者都把全部定义放在一堆大括号内:

class SomeClass {

// class definition goes here

}

struct SomeStructure {

// structure definition goes here

}

备注:一旦你定义了一个新的类或者结构体, 你就有效定义了一个全新的Swift类型。给一个大写驼峰式命名 (比如 SomeClass 和 SomeStructure here) 来匹配标准 Swift 类型大写。(比如 String, Int, 和 Bool). 相反, 属性和方法总是小写驼峰式命名 (比如 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而被推断为整型。

上面的例子同时定义了一个新类 VideoMode, 来描述一个指定的视频模式用来视频显示。 这个类有四个变量存储属性。 第一个是 resolution, 用一个新的 Resolution 结构体实例来初始化, 推断为 Resolution 属性类型。 其他三个属性, 新的 VideoMode 实例将会用一个设置为false的交错设置来初始化 (意思是 “非交错视频”), 一个回放帧率是 0.0, 一个可选字符串 name. name 属性自动得到默认值nil, 或者 “没有名字值”, 因为它是一个可选类型。

类和结构体实例

Resolution 结构体定义和 VideoMode 类定义仅仅描述了一个 Resolution 或者 VideoMode 看起来是什么样子。 他们没有描述一个指定的 resolution 或者video mode. 为了实现这个, 你需要创建结构体或者类的实例。

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

let someResolution = Resolution()

let someVideoMode = VideoMode()

结构体和类都使用初始化方法创建实例。 最简单的初始化语法就是使用类或者结构的类型名后面跟着一个空的括号, 比如 Resolution() 和 VideoMode(). 这个创建了类或者结构体的实例,给所有属性设置默认值。

访问属性

你可以用点语法访问实例的属性。 在点语法中, 属性名跟着实例名, 用点分开 (.), 没有任何空格:

print("The width of someResolution is \(someResolution.width)")

// 打印 "The width of someResolution is 0"

在这个例子里, someResolution.width 调用 someResolution 的宽度属性, 然后返回它的默认值0.

你可以获取子属性, 比如 width 属性就是 VideoMode 中的resolution 的属性:

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

// 打印 "The width of someVideoMode is 0"

你还可以用点语法为变量属性指定新值:

someVideoMode.resolution.width = 1280

print("The width of someVideoMode is now \(someVideoMode.resolution.width)")

// 打印 "The width of someVideoMode is now 1280"

备注:跟 Objective-C 不同, Swift 让你可以直接设置结构体属性的子属性。 上面最后一个例子, someVideoMode 属性resolution的子属性width 就是直接设置的, 无需你把整个resolution 属性设置成新值。

结构体类型成员初始化方法

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

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

跟结构体不同, 类实例不会接受一个默认的成员初始化方法。

结构体和枚举是值类型

值类型指的是赋给变量或者常量的时候会进行拷贝的类型, 或者在传给函数使用的时候。

通过前面的章节,你实际上已经广泛使用了值类型。 事实上, 所有Swift 的基础类型—整型, 浮点数, 布尔类型, 字符串类型, 数组和字典都是值类型, 背后都是用结构体实现。

Swift 中所有的结构体和枚举都是值类型。意思就是你创建的任何结构体和枚举—任何它们作为属性的值类型—当它们在你的代码中传递时总是拷贝的。

看一下这个例子, 使用了前一个例子中的 Resolution 结构体:

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

var cinema = hd

这个例子定义了一个常量 hd 然后用初始化方法赋值给它。

然后声明一个变量 cinema 并且把hd 的当前值赋给它。因为 Resolution 是一个结构体, 存在实例的拷贝就创建了。新的拷贝赋值给 cinema. 尽管 hd 和 cinema 现在有了相同的宽和高, 它们背后是完全不同的两个实例。

下一步, cinema 宽度属性修改为2K标准的宽度,用于数字电影项目(2048 像素宽和 1080 像素高):

cinema.width = 2048

检查cinema的宽度属性,显示它的确被修改为 2048:

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

// 打印 "cinema is now 2048 pixels wide"

不过, 原来hd 的宽度属性依然是旧值 1920:

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

// 打印 "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 {

print("The remembered direction is still .west")

}

// 打印 "The remembered direction is still .west"

当 rememberedDirection 被赋值为 currentDirection的值时, 实际上设置的是一个值的拷贝。 改变 currentDirection 的值之后不会影响原先存储在rememberedDirection 中的值。

112.类是引用类型

跟值类型不同, 它们赋值给变量或者常量的时候不会发生拷贝, 传递给函数也一样。 相对一个拷贝, 用的是引用已经存在的实例。

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

let tenEighty = VideoMode()

tenEighty.resolution = hd

tenEighty.interlaced = true

tenEighty.name = "1080i"

tenEighty.frameRate = 25.0

这个例子声明了一个新的变量 tenEighty 并且把一个新的VideoMode 实例赋值给它。视频模式还是用的19020乘以1080 的HD 的拷贝。它设置成交错的,给了一个名字 “1080i”. 最后, 设置了25 帧每秒的帧速。

下一步, tenEighty 赋值给一个新的常量 alsoTenEighty, 然后改变 alsoTenEighty 帧速:

let alsoTenEighty = tenEighty

alsoTenEighty.frameRate = 30.0

因为类是引用类型, tenEighty 和 alsoTenEighty 实际上引用的是相同的 VideoMode 实例。它们不过是同一个实例的两个不同的名字。

查看tenEighty 的帧速属性, 显示它变成了新的帧速率 30.0:

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

// 打印 "The frameRate property of tenEighty is now 30.0"

注意 tenEighty 和 alsoTenEighty 声明成常量而不是变量。 不过, 你仍然可以改变 tenEighty.frameRate 和 alsoTenEighty.frameRate, 因为 tenEighty 和 alsoTenEighty 常量的值实际没有改变。 tenEighty 和 alsoTenEighty 它们不保存 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."

注意“相同” (用三个等号表示) 跟“等于” (用两个等号表示)还不同:

“相同” 意思是两个类实例常量或者变量引用相同的类实例。

“等于” 意思是两个实例值相等, 一些“等于”的相关意思, 由类型设计者定义。

当你自定义类和结构体时, 你有责任决定两个实例“等于”在符合条件。实现“等于” and “不等于” 运算符的过程在等于运算符中描述。


113.指针

如果你熟悉 C, C++, 或者 Objective-C, 你可能会知道这些语言使用指针来指向内存地址。 Swift 的常量或者变量,引用一个实例很像C语言的指针, 但是不会直接指向内存地址, 并且不需要你书写 (*)来表明你要创建一个引用。 相反, 这些引用和Swift 中的其他任何常量或者变量的定义很像。

114.在类与结构体中选择

你可以在你的代码块中同时使用类和结构体来定义自定义数据类型。

不过, 结构体实例总是用值传递, 而类总是使用引用传递。这就意味着它们适用于不同的任务种类。在你需要考虑数据构建和功能时, 觉得是否每一个数据构建应该选用类还是结构体。

一个通用的指导是, 下面一个或者多个条件符合时考虑使用一个结构体:

结构体的主要目的是封装一些相关的简单数据值。

传递结构体实例是拷贝而非引用。

结构体存储的任意属性也是值类型, 这些属性期望是拷贝而非引用使用。

结构体不需要继承其他存在类型的属性或者行为。

适合结构体的好例子:

几何形状的大小, 可能要封装一个宽度属性和一个高度属性, 两者都是浮点型。

使用一个系列的范围, 可能封装一个起点属性和一个长度属性, 两者都是整型。

3D坐标系的点, 可能要封装 x, y 和 z 属性, 每个都是浮点型。

其他情况, 定义一个类, 然后创建类的实例用来管理和传递引用。 在实践中, 意思自定义数据构建使用类而不是结构体。


115.字符串,数组和字典的赋值和拷贝行为

在 Swift 中, 一些基本的数据类型像String, Array, 和 Dictionary 都是用结构体实现的。 这就意味着例如字符串,数组和字典在赋值给常量或者变量时,也是被拷贝的, 传递给函数或者方法时也一样。

这个行为不同于 Foundation: NSString, NSArray, 和 NSDictionary, 它们是用类实现的。字符串,数组和字典在 Foundation 中赋值时用的引用而非拷贝。

备注:上面描述的拷贝。 在你的代码里这种行为似乎总是发生。 然而, Swift 只有在真正需要这么做的时候才进行实际的拷贝。 Swift管理所有的值拷贝来确保性能最优, 所以你不应避免赋值来抢占这种优化。

116.属性

属性关联特定的类,结构体或者枚举的值。 存储属性作为实例的一部分用来存储常量或者变量的值, 而计算属性用来计算一个值。 计算属性由类,结构体和枚举提供。 存储属性仅由类和结构体提供。

存储和计算属性通常关联特定类型的实例。 不过, 属性也可以管理类型本身。 比如类型属性。

另外, 你可以定义观察者来监视属性值的改变, 自定义响应的行为。 属性观察者可以添加到你定义的存储属性, 也可以是子类集成父类的属性。

存储属性

最简单的形式, 一个存储属性是作为特定类和结构体的常量或者变量。 存储属性既可以是变量也可以是常量。

你可以提供一个默认值作为存储属性定义的一部分。你也可以在初始化时改变它的初始值。对于常量存储属性也是如此。

下面的例子定义了一个结构体 FixedLengthRange, 描述了一个整型的范围, 它有一个长度一旦创建就不可修改:

struct FixedLengthRange {

var firstValue: Int

let length: Int

}

var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)

// the range represents integer values 0, 1, and 2

rangeOfThreeItems.firstValue = 6

// the range now represents integer values 6, 7, and 8

FixedLengthRange 实例有一个变量存储属性 firstValue 和一个常量存储属性 length. 上面这个例子, length 在新实例创建时初始化而后不可修改, 因为它是一个常量属性。

常量结构体的存储属性

如果你创建一个结构体实例并把它赋值给一个常量, 你就不能修改这个实例的属性, 即使它们定义成变量属性:

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)

// this range represents integer values 0, 1, 2, and 3

rangeOfFourItems.firstValue = 6

// 这里会报错, 即使 firstValue 是变量属性

因为rangeOfFourItems 声明成一个常量, 所以不能修改 firstValue 属性, 即使 firstValue 是一个变量属性。

这个行为是因为结构体是值类型。 一个值类型的实例被标记为常量, 那么它所有的属性也变成了常量。

对于类来说不是这样, 类是引用类型。 如果你把类实例赋值给一个常量, 你依然可以修改类的变量属性。

延迟存储属性

延迟存储属性的初始值在首次使用时才会计算。标示延迟存储属性通过在声明前书写 lazy 修饰符实现。

备注

你应该总是把延迟属性声明成变量, 因为它的初始值可能在实例初始化完成后才获取到。常量属性在初始化完成前总是有值, 因为不可以声明为懒加载。

延迟属性在初始化值需要依赖外部因素时很有用,它们的值直到实例初始化完成后才知道。 当属性初始值需要复杂和大量计算时,延迟属性需要时再执行加载也非常有用。

下面的例子使用一个延迟存储属性来避免一个复合类的不必要的初始化。这个例子定义了两个类 DataImporter 和 DataManager, 两个都没有完整显示:

class DataImporter {

/*

DataImporter is a class to import data from an external file.

The class is assumed to take a non-trivial amount of time to initialize.

*/

var fileName = "data.txt"

// the DataImporter class would provide data importing functionality here

 }

class DataManager {

lazy var importer = DataImporter()

var data = [String]()

// the DataManager class would provide data management functionality here

 }

let manager = DataManager()

manager.data.append("Some data")

manager.data.append("Some more data")

// the DataImporter instance for the importer property has not yet been created

DataManager 类有一个存储属性 data, 用一个新的,空的字符串数组初始化。 尽管它其余的功能没有显示, DataManager 类的目的是管理和提供对字符串数据数组的访问。

类的部分功能是可以从文件导入数据。 这个功能由 DataImporter 类提供, 假设初始化时间微不足道。 这是因为当DataImporter实例被初始化时, DataImporter 实例需要读取文件内容到内存。

DataManager 实例在没有任何文件输入的情况下管理数据也是有可能的, 所以 DataManager 创建时不需要创建一个新的 DataImporter 实例。 相反, 真正需要使用DataImporter的时候再创建更有意义。

因为它标记了lazy 修饰符, DataImporter 实例只会在 importer 属性首次被访问的时候创建, 比如当它的 fileName 属性需要的时候:

print(manager.importer.fileName)

// the DataImporter instance for the importer property has now been created

// 打印 "data.txt"

备注

如果lazy 标示的属性被多线程同时访问并且这个属性还未初始化, 不能保证这个属性只会初始化一次。

存储属性和实例变量

如果你熟悉 Objective-C, 你可能了解它提供了两种方式来存储值和引用。 除了属性, 你可以使用实例变量作为属性的后备来存储值。

Swift 把这些概念结合到单独的属性声明。 一个 Swift 属性没有相应的实例变量, 属性的后备存储不能直接访问。 这个方法避免了在不同上下文访问值的混乱并且把属性的声明简化到单独明确的语句。所有的属性信息—包括它的名字, 类型, 和内存管理特性—作为类型定义的部分定义在单独的位置。

计算属性

除了存储属性, 类, 结构体, 和枚举还可以定义计算属性, 计算属性不存储值。相反, 它们提供了一个 getter 和一个可选的 setter 方法来简介获取或者设定其他属性和值。

struct Point {

var x = 0.0, y = 0.0

}

struct Size {

var width = 0.0, height = 0.0

}

struct Rect {

var origin = Point()

var size = Size()

var center: Point {

get {

let centerX = origin.x + (size.width / 2)

let centerY = origin.y + (size.height / 2)

return Point(x: centerX, y: centerY)

}

set(newCenter) {

origin.x = newCenter.x - (size.width / 2)

origin.y = newCenter.y - (size.height / 2)

}

}

}

var square = Rect(origin: Point(x: 0.0, y: 0.0),

size: Size(width: 10.0, height: 10.0))

let initialSquareCenter = square.center

square.center = Point(x: 15.0, y: 15.0)

print("square.origin is now at (\(square.origin.x), \(square.origin.y))")

// 打印 "square.origin is now at (10.0, 10.0)"

这个例子定义了三个结构体使用几何形状:

Point 封装了一个点的x和y坐标。

Size 封装了一个宽度和高度。

Rect 用一个原点和大小定义了一个矩形。

Rect 结构体也提供了一个计算属性 center. 一个Rect 的当前中心位置总是取决于它的原点和大小。所以你不需要显示存储中心的值。 相反, Rect 为计算center自定义了 getter 和 setter 方法。确保你像使用真正的存储属性一样使用矩形的中心。

前面的例子创建了一个新的 Rect 变量 square. square 原点初始化为 (0, 0), 宽度和高度都是 10. 这个正方形在下面的图表里用蓝色正方形表示。

square 变量的 center 属性通过点语法访问, 这个导致 getter 方法被调用, 来获取当前属性值。 相比返回存在的值, getter 实际上计算并返回一个新的 Point 来表示正方形的中心。从上面可以看到, getter 正确返回了中心点 (5, 5).

center 属性然后被设置成新值 (15, 15), 这会把正方形往右上方移动, 如下图中的橙色正方形。 设置 center 属性会调用 setter 方法, 这个会修改原点属性的x和y值, 然后移动正方形到新的位置。


Swift 4.0 编程语言(四)_第3张图片

117.Setter 简化声明

如果一个计算属性的 setter 没有定义设置用的新值名, 默认的名字 newValue 会被使用。这里有一个Rect 结构体的替代版本, 使用了这种简化符号:

struct AlternativeRect {

var origin = Point()

var size = Size()

var center: Point {

get {

let centerX = origin.x + (size.width / 2)

let centerY = origin.y + (size.height / 2)

return Point(x: centerX, y: centerY)

}

set {

origin.x = newValue.x - (size.width / 2)

origin.y = newValue.y - (size.height / 2)

 }

 }

}

只读计算属性

只有getter 没有 setter 的计算属性就是人们所说的只读计算属性。一个只读计算属性总是返回一个值, 可以通过点语法访问, 但是不可以设置为其他值。

备注

你必须把计算属性包含只读计算属性声明为变量, 因为它们的值是不固定的。let 关键字只有用于常量属性, 表明它们一旦初始化就不能再更改。

去掉 get 关键字和它的括号,你可以简化一个只读计算属性的声明:

struct Cuboid {

var width = 0.0, height = 0.0, depth = 0.0

var volume: Double {

return width * height * depth

  }

}

let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)

print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")

// 打印 "the volume of fourByFiveByTwo is 40.0"

这个例子定义了一个新的结构体 Cuboid, 表示一个三维长方体, 具有 width, height, 和 depth 属性。 这个结构体同时有一个只读计算属性 volume, 用来计算和返回长方体的体积。 设置体积是没有意义的, 因为特定体积值用哪些长宽高是不清楚的。 尽管如此, 给外部用户提供一个只读计算属性来得到立方体的计算后的体积还是有用的。

属性观察者

属性观察者观察并响应一个属性值的变化。 每次属性值设置都会调用属性观察者, 即使新值和属性当前值一样也会调用。

你可以给任何你定义的存储属性添加观察者, 除了延迟加载的存储属性。 你也可以给一个继承来的属性添加观察者,只需要在子类重写这个属性即可。你不需要给一个非重写的计算属性添加属性观察者, 因为你可以在它们的setter 方法里观察和响应变化。

你可以选择定义这些属性观察者之一或全部:

willSet 在值存储前调用。

didSet 在新值存储后立即调用。

如果你实现一个 willSet 观察者, 它会以常量参数形式传递新属性的值。 你可以简化参数名作为 willSet 的实现部分。 如果你在实现中不写参数名和括号, 参数名默认是 newValue.

类似的, 如果你实现 didSet 观察者, 它包含旧的属性值作为常量产生传递。 你可以命名这个参数或者使用默认参数名 oldValue. 如果你在它自己的didset 观察者里给这个属性设置值, 你赋给它的新值会替换刚刚设置的值。

备注

父类初始化方法调用后, 父类属性的 willSet 和 didSet 观察者在子类初始化属性的时候调用。 在父类初始化调用之前, 当一个类设置它自己的属性时它们不会再被调用。

这里有一个 willSet 和 didSet 的例子。 下面的例子定义了一个新类 StepCounter, 用来计算一个人走路的总步数。 这个类用在输入数据或者其他记录一个人日常锻炼的计步器。

class StepCounter {

var totalSteps: Int = 0 {

willSet(newTotalSteps) {

print("About to set totalSteps to \(newTotalSteps)")

}

didSet {

if totalSteps > oldValue  {

print("Added \(totalSteps - oldValue) steps")

  }

 }

 }

}

let stepCounter = StepCounter()

stepCounter.totalSteps = 200

// About to set totalSteps to 200

// Added 200 steps

stepCounter.totalSteps = 360

// About to set totalSteps to 360

// Added 160 steps

stepCounter.totalSteps = 896

// About to set totalSteps to 896

// Added 536 steps

StepCounter 类声明了一个整型 totalSteps. 这是一个存储属性,带有 willSet 和 didSet 观察者。

totalSteps 属性设置新值的时候 willSet 和 didSet 观察者会调用。即使这个新值跟当前值一样, 也会调用。

这个例子 willSet 观察值使用一个自定义的参数 newTotalSteps 来更新值。在这个例子里, 它简单打印将要设置的值。

didSet 观察者在 totalSteps 值更新后调用。它比较 totalSteps 的新旧值。如果总步数增加了, 打印增加步数的信息。didSet 观察者没有提供一个自定义参数名, 用的是and the default name of oldValue is used instead.

备注

你如果传递一个带有观察者的属性给函数作为输入输出参数的话, willSet 和 didSet 观察者总会被调用。这是因为输入输出参数的内存赋值模型: 函数结尾这个值总会写回属性。

118.全局变量和局部变量

上述描述的计算和观察属性的能力也可以用于全局变量和局部变量。全局变量定义在函数,方法,闭包或者类型上下文之外。局部变量则是定义在函数,方法或者闭包上下文内。

前面章节遇到的全局和局部变量都是存储变量。 存储变量很像存储属性, 提供特定类型值的存储并且允许值的设置和获取。

不过, 不过你可以在全局获取局部范围,定义计算变量,为存储变量定义观察者。计算变量计算它们的值, 而非存储它, 它们和计算属性写法一样。

备注:Global 常量和变量总是延迟计算, 跟延迟的存储属性类似方式。但是和延迟存储属性不同的是, 全局常量和变量不需要标记 lazy 修饰符。

局部常量和变量不能是延迟计算的。

类型属性

Instance 属性属于特定类型的实例。 每次你创建这种类型的实例, 它都有自己一组属性值, 和其他实例区分开来。

你还可以定义类型自己的属性, 而不是这种类型实例的属性。这里只会有这些属性一个拷贝, 不管你创建多少那种类型的实例。这些属性就是类型属性。

定义某种类型实例的通用值时类型属性很有用, 比如常量属性所有实例都可以用 (像C里面的静态常量), 或者变量属性用来存储那种类型的通用值 (想C里面的静态变量).

存储类型属性可以是常量或者变量。计算类型属性总是变量, 和计算实例属性一样。

备注:跟存储实例属性不同, 你必须给存储类型属性一个默认值。这是因为类型本身没有初始化方法, 不能在初始化时给存储类型属性指定一个初始值。

存储类型属性首次访问时是延迟初始化的。它们保证只进行一次初始化, 即使被多线程同时访问, 它们不需要用lazy 修饰符标记。


类型属性语法

在 C 和 Objective-C 中, 你定义静态常量和变量作为全局静态变量。 在 Swift 里, 类型属性写作类型定义的一部分, 在类型外部的大括号里, 并且每种类型属性显式局限于它支持的类型。

用static关键字定义类型属性。 对于类的计算类型属性, 你可以用 class 关键字去允许子类重写父类的实现。下面的例子展示存储和计算类型属性的语法:

struct SomeStructure {

static var storedTypeProperty = "Some value."

static var computedTypeProperty: Int {

return 1

     }

}

enum SomeEnumeration {

static var storedTypeProperty = "Some value."

static var computedTypeProperty: Int {

return 6

   }

}

class SomeClass {

static var storedTypeProperty = "Some value."

static var computedTypeProperty: Int {

return 27

}

class var overrideableComputedTypeProperty: Int {

return 107

   } 

}

备注:上面的例子是只读类型的计算属性, 不过你要可以定义读写类型的计算属性。

查询和设置类型属性

类型查询和设置使用点语法, 跟实例属性很像。不过, 类似属性使用类型查询和设置, 不是使用类型的实例。 例如:

print(SomeStructure.storedTypeProperty)

// 打印 "Some value."

SomeStructure.storedTypeProperty = "Another value."

print(SomeStructure.storedTypeProperty)

// 打印 "Another value."

print(SomeEnumeration.computedTypeProperty)

// 打印 "6"

print(SomeClass.computedTypeProperty)

// 打印 "27"

下面的例子用了两个存储属性作为结构体的一部分, 对多个音轨模拟了一个音频水平计。每个音轨有一个在0和10之间的整数音频水平。

下面图展示了两个音轨怎样合成一个立体音频水平计。当一个音轨的音频水平是0时, 那个音轨的没有灯会亮。 当这个音频水平是10时, 那个音轨的灯全部点亮。 这个图中, 左边音轨当前水平值是 9, 右边音轨当前水平是 7:

Swift 4.0 编程语言(四)_第4张图片

上面描述的音轨用 AudioChannel 结构体的实例表示:

struct AudioChannel {

static let thresholdLevel = 10

static var maxInputLevelForAllChannels = 0

var currentLevel: Int = 0 {

didSet {

if currentLevel > AudioChannel.thresholdLevel {

// cap the new audio level to the threshold level

currentLevel = AudioChannel.thresholdLevel

     }

if currentLevel > AudioChannel.maxInputLevelForAllChannels {

// store this as the new overall maximum input level

AudioChannel.maxInputLevelForAllChannels = currentLevel

       }

    }

  }

}

AudioChannel 结构体定义了两个存储类型属性来支持它的功能。 第一个是 thresholdLevel, 定义了音频水平最大阈值。对于所有AudioChannel 实例这是一个常量值10. 如果音频信号遇到大于10的值, 它封顶就是阈值。

第二个类型属性是一个变量存储属性 maxInputLevelForAllChannels. 这个保持追踪 AudioChannel 实例接收的最大输入值。 它的初始值是 0.

AudioChannel 结构体同时定义了一个存储实例属性currentLevel, 用来表示0到10之间的音轨当前音频水平。

currentLevel 属性有一个didSet 属性观察者来判断 currentLevel 设置时的值。这个观察者执行两个判断:

如果新值大于阈值, currentLevel 封顶值就是阈值。

如果新值(还没有封顶) 大于之前 AudioChannel 实例接收的值, 属性观察者把新的 currentLevel 值存储到 maxInputLevelForAllChannels 类型属性中。

备注:第一个判断, didSet 观察值把 currentLevel 设置成一个不同值。这个不会导致观察者被再次调用。

你可以用 AudioChannel 结构体创建两个新的音轨 leftChannel 和 rightChannel, 来表示这个音频水平是一个立体声系统:

var leftChannel = AudioChannel()

var rightChannel = AudioChannel()

If you set the currentLevel of the left channel to 7, you can see that the maxInputLevelForAllChannels type property is updated to equal 7:

leftChannel.currentLevel = 7

print(leftChannel.currentLevel)

// 打印 "7"

print(AudioChannel.maxInputLevelForAllChannels)

// 打印 "7"

If you try to set the currentLevel of the right channel to 11, you can see that the right channel’s currentLevel property is capped to the maximum value of 10, and the maxInputLevelForAllChannels type property is updated to equal 10:

rightChannel.currentLevel = 11

print(rightChannel.currentLevel)

// 打印 "10"

print(AudioChannel.maxInputLevelForAllChannels)

// 打印 "10"

119.方法

方法是对应特定类型的函数。类,结构体和枚举都可以定义实例方法, 封装了特定的任务和功能用于给定类型的实例。类, 结构体和枚举也可以定义类型方法, 它们对应类型本身。类型方法很像 Objective-C 中的类方法。

结构体与枚举可以定义方法,是Swift 与 C 和 Objective-C之间一个巨大的区别。在Objective-C, 只有类可以定义方法。在 Swift, 你可以选择是否定义一个类,结构体或者枚举, 并且可以灵活定义你创建类型下的方法。

实例方法

实例方法属于特定类,结构体或者枚举实例的函数。或者提供方式访问和修改实例的属性,或者提供实例目的相关的功能。实例方法和函数语法一样。

在它所属的类型开闭括号里书写一个实例方法。 一个实例方法可以隐式访问这个类型所有其他的实例方法和属性。一个实例方法只能被它属于的类型调用。 没有实例不能单独调用。

这里有个例子,定义了一个简单的类 Counter , 来计算一个行为的次数:

class Counter {

var count = 0

func increment() {

count += 1

}

func increment(by amount: Int) {

count += amount

}

func reset() {

count = 0

   }

}

Counter 类定义了三个实例方法: increment() 把计数器加1. increment(by: Int) 把计数器增加指定整数。 reset() 重置计数器为0 Counter 类同时声明了一个变量属性, count, 来跟踪当前计步器的值。 调用实例方法跟属性一样使用点语法:

let counter = Counter()

// the initial counter value is 0

counter.increment()

// the counter's value is now 1

counter.increment(by: 5)

// the counter's value is now 6

counter.reset()

// the counter's value is now 0

Function 参数可以用一个名字和一个参数标签。

120.self 属性

每个类型的实例都有一个隐式的属性 self, 等于实例自己。 你可以用在实例方法内引用当前实例。

上面例子里的increment() 方法可以这样写:

func increment() {

self.count += 1

}

在实践中, 你不需要经常在代码中写self. 如果不显示书写 self, 一旦你在一个方法里使用了已知的属性或者方法名, Swift 就会假设你引用的是当前实例的属性或者方法。这个假设在Counter三个实例方法中count被证明了。

这个规则的例外情况是, 一个实例方法的参数名和实例的属性名一样。这种情况下, 参数优先, 有必要用更有效的方式调用属性。用self 属性来区分参数名和属性名。

这里, self 避免一个方法参数叫 x 与一个实例属性叫 x 的混乱:

struct Point {

var x = 0.0, y = 0.0

func isToTheRightOf(x: Double) -> Bool {

return self.x > x

     }

}

let somePoint = Point(x: 4.0, y: 5.0)

if somePoint.isToTheRightOf(x: 1.0) {

print("This point is to the right of the line where x == 1.0")

}

// 打印 "This point is to the right of the line where x == 1.0"

不带self 前缀, Swift 会假设两个 x 都是引用方法的参数 x.

121.在实例方法中修改值类型

结构体和枚举是值类型。默认情况下, 值类型的属性不能在实例方法内部修改。

不过, 如果你需要再特定方法中修改结构体和枚举的属性, 你可以选择这个方法的变异行为。方法就可以在内部变异它的属性, 方法结束后这个种改变会写回先前的结构体。这个方法也可以给隐式的self属性指定一个完全全新的实例, 方法结束后这个新值会替代原先存在的。

可以在方法的func 关键字前加上 mutating 关键字来实现这个行为:

struct Point {

var x = 0.0, y = 0.0

mutating func moveBy(x deltaX: Double, y deltaY: Double) {

x += deltaX

y += deltaY

   }

}

var somePoint = Point(x: 1.0, y: 1.0)

somePoint.moveBy(x: 2.0, y: 3.0)

print("The point is now at (\(somePoint.x), \(somePoint.y))")

// 打印 "The point is now at (3.0, 4.0)"

Point 结构体定义了一个变异方法 moveBy(x:y:), 使用一个特定数量移动一个点。不是返回一个新的点, 这个方法实际上修改了这个点。mutating 关键字加在定义是为了保证它可以修改自己的属性。

注意你不能用结构体类型的常量来调用变异方法, 因为它的属性不能改变, 即使它们是变量属性:

let fixedPoint = Point(x: 3.0, y: 3.0)

fixedPoint.moveBy(x: 2.0, y: 3.0)

// 这会报一个错误

在变异方法里赋值给self

变异方法可以把一个全新的实例赋给隐式 self 属性。上面的 Point 例子可以用下面的写法代替:

struct Point {

var x = 0.0, y = 0.0

mutating func moveBy(x deltaX: Double, y deltaY: Double) {

self = Point(x: x + deltaX, y: y + deltaY)

   }

}

这个版本的变异 moveBy(x:y:) 方法创建了一个全新的结构体,它的x和y的值被设置为目标位置。调用这个版本的结果和早前版本是完全一样的。

枚举的变异方法可以把隐式self属性设置为一个不同的分支:

enum TriStateSwitch {

case off, low, high

mutating func next() {

switch self {

case .off:

self = .low

case .low:

self = .high

case .high:

self = .off

     }   

  }

}

var ovenLight = TriStateSwitch.low

ovenLight.next()

// ovenLight is now equal to .high

ovenLight.next()

// ovenLight is now equal to .off

这个例子定义了三联开关的枚举。 每次调用 next() 方法这个开关就在三种不同的状态下循环。

类型方法

实例方法, 如上所述, 是特定类型实例调用的方法。 你也可以定义类型本身调用的方法。这些方法称为类型方法。在方法的func 关键字前加上static 关键来标示类型方法。类可以用class 关键字来允许子类覆盖父类的方法实现。

备注:在 Objective-C 中, 你只能给类定义类型级别的方法。 而在 Swift 里, 你可以给类,结构体和枚举都定义类型级别的方法。每个类型方法仅限于它支持的类型使用。

类型方法使用点语法调用, 和实例方法一样。 不过, 你使用类型调用方法而不是类型的实例。这里有一个类,展示在一个类如何调用类型方法:

class SomeClass {

class func someTypeMethod() {

// type method implementation goes here

      }

}

SomeClass.someTypeMethod()

在类型方法的函数体中, 隐式self属性调用类型自己而非类型的实例。 这就意味着你可以用self属性来消除类型属性和类型方法参数之间的歧义, 跟处理实例属性和实例方法参数一样。

更一般的是, 用在类型方法内的任何不合适的方法和属性名将会引用其他类型级别的方法和属性。一个类型方法可以用其他方法名调用另外一个类型方法, 不需要再前面加上类型名。相似的, 结构体和枚举的类型方法可以用类型属性名访问类型属性。

下面的例子定义了一个结构体 LevelTracker, 用来跟踪玩家通过游戏不同级别或者阶段的过程。它是一个单人游戏, 但是可以在单机上存储多个玩家的信息。

第一次玩游戏时所有游戏的关都是锁定的 (除了第一关)。 每当玩家完成一关, 这一关在设备上对所有玩家打开。LevelTracker 结构体使用类型属性和方法来跟踪哪些关已经打开了。它同时跟踪单个玩家所在的当前关。

struct LevelTracker {

static var highestUnlockedLevel = 1

var currentLevel = 1

static func unlock(_ level: Int) {

if level > highestUnlockedLevel { highestUnlockedLevel = level }

}

static func isUnlocked(_ level: Int) -> Bool {

return level <= highestunlockedlevel="" }="" @discardableresult="" mutating="" func="" advance(to="" level:="" int)="" -=""> Bool {

if LevelTracker.isUnlocked(level) {

currentLevel = level

return true

} else {

return false

      }

   }

}

LevelTracker 结构体保持记录任何玩家已经打开的最高关。这个值存储在类型属性 highestUnlockedLevel.

LevelTracker 同时定义了两个类型方法来使用highestUnlockedLevel 属性。第一个类型方法是 unlock(:), 一旦新的关通过了就更新 highestUnlockedLevel 的值。第二个是便利类型方法 isUnlocked(:), 如果特定关已经通过就返回真。 (注意,这两个方法访问 highestUnlockedLevel 类型属性需要写成 LevelTracker.highestUnlockedLevel.)

除了它的类型属性和方法, LevelTracker 还跟踪了个体玩家通过这个游戏的过程。它用一个实例属性 currentLevel 来跟中玩家当前正在玩的关。

为了帮助管理 currentLevel 属性, LevelTracker 定义了一个实例方法 advance(to:). 在更新currentLevel的值之前, 这个方法会判断请求的关是否打开。advance(to:) 方法返回布尔值是否可以设置currentLevel. 因为调用 advance(to:) 方法去忽略这个返回值是不需要一个错误, 这个函数被标记为 @discardableResult 属性。

LevelTracker 结构体和 Player 类一起使用, 下面展示跟踪和更新个体玩家的过程:

class Player {

var tracker = LevelTracker()

let playerName: String

func complete(level: Int) {

LevelTracker.unlock(level + 1)

tracker.advance(to: level + 1)

        }

init(name: String) {playerName = name

  }

}


Player 创建了新的LevelTracker 实例来跟踪玩家的过程。它同时提供了一个方法 complete(level:), 一旦玩家通过一关就会调用。这个方法为所有玩家打开下一关并且更新玩家的过程,把他们带到下一关。(advance(to:) 的布尔返回值被忽略了, 因为通过调用 LevelTracker.unlock(_:) 已经知道这个关被打开了)

你可以为一个新的玩家创建一个Player 类的实例, 并且看看玩家通过第一关会发生什么:

var player = Player(name: "Argyrios")

player.complete(level: 1)

print("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")

// 打印 "highest unlocked level is now 2"

如果你创建第二个玩家, 如果尝试移动他到没有任何玩家打开的关, 那么尝试设置玩家的当前关就会失败:

player = Player(name: "Beto")

if player.tracker.advance(to: 6) {

print("player is now on level 6")

} else {

print("level 6 has not yet been unlocked")

}

// 打印 "level 6 has not yet been unlocked"

122.下标

类,结构体和枚举都快要定义下标, 它是用来访问集合元素的速写方式。你无需单独的设置和获方法,使用下标索引就可以设置和获取元素值。例如, 你访问数组实例的元素用 someArray[index] ,访问字典实例元素用 someDictionary[key].

你可以为单个类型定义多个下标, 选择适当的下标重载使用取决于你传给下标的索引值的类型。一维下标没有限制, 你可以用多个输入参数定义下标来满足自定义类型的需要。

下标语法

在一个方括号里写一个或多个值,下标就可以查询一个类型的实例。这个语法跟实例方法和计算属性的语法很像。使用 subscript 关键字定义下标, 同时指定一个或多个输入参数和一个返回值, 跟实例方法一样。和实例方法不同的是, 下标可以读写或者只读。和计算属性方式一样,这个用 setter 和 getter 实现:

subscript(index: Int) -> Int {

get {

// return an appropriate subscript value here

}

set(newValue) {

// perform a suitable setting action here

    }

}

newValue 类型和返回值类型一样。 和计算属性一样, 你可以不指定setter 的参数名。默认的参数名 newValue 会提供。

和只读计算属性一样, 只读下标可以去掉get 关键字:

subscript(index: Int) -> Int {

// return an appropriate subscript value here

}

这里有个只读下标实现的例子, 它定义了一个结构体 TimesTable 来表示整数n倍数表:

struct TimesTable {

let multiplier: Int

subscript(index: Int) -> Int {

return multiplier * index

        }

}

let threeTimesTable = TimesTable(multiplier: 3)

print("six times three is \(threeTimesTable[6])")

// 打印 "six times three is 18"

这个例子里, 一个新的 TimesTable 实例被创建来表示整数的3倍数表。数字3传入结构体的初始化方法来作为乘法的参数。

你可以用下标获取 threeTimesTable 实例, 通过调用 threeTimesTable[6]. 这请求三倍数表中的第六个实体, 返回18, 或者3乘以 6.

备注:n倍数表基于固定的数学规则。把 threeTimesTable[someIndex] 设置为一个新值是不合适的。所以 TimesTable 的下标定义为只读的。

使用下标

下标的确切意思取决于它使用的上下文。下标是访问集合元素的速写方式。你可以自由的用最合适的方式为特定的类或者结构体实现下标。

例如, Swift的字典类型实现一个下标去存取存储在字典实例中的值。你可以在下标括号里使用字典的键来设置值, 也可以给下标设置值:

var numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]

numberOfLegs["bird"] = 2

上面的例子定义了一个变量 numberOfLegs , 然后用一个包含三个键值对的字典字面量来初始化它。numberOfLegs 字典类型被推断为 [String: Int]. 字典创建后, 这个例子使用下标赋值,向字典添加一个键值对 “bird” 和 2 .

备注:Swift 的字典类型实现的键值下标接受和返回一个可选类型。 对于上面的 numberOfLegs 字典, 键值下标接受和返回一个 Int? 类型值, 或者 “可选 int”. 字典类型使用了一个可选的下标类型来模拟这个事实,不是每一个键都有对应的值, 同时提供了一个方法去删除值,只需要给这个键赋一个nil即可。

下标选项

下面可以接受任意数量的输入参数, 并且这些输入参数可以是任意类型。 下标也可以返回任意类型。 下标可以使用可变参数, 但是它们不能使用输入输出参数也不能给参数提供默认值。

一个类或者结构体可以提供需要的多个下标实现, 使用的下标基于值类型或者包含在下标括号里的值来推断。这个多个下标的定义就是人们常说的重载。

虽然下标接受一个参数很常见, 但是如果它符合你的类型,你还是可以定义个带多个参数的下标。下面的例子定义了一个结构体 Matrix, 用来表示一个二维的浮点值矩阵。Matrix 结构体的下标带有两个参数:

struct Matrix {

let rows: Int, columns: Int

var grid: [Double]

init(rows: Int, columns: Int) {

self.rows = rows

self.columns = columns

grid = Array(repeating: 0.0, count: rows * columns)

  }

func indexIsValid(row: Int, column: Int) -> Bool {

return row >= 0 && row < rows && column >= 0 && column < columns

    }

subscript(row: Int, column: Int) -> Double {

get {

assert(indexIsValid(row: row, column: column), "Index out of range")

return grid[(row * columns) + column]

      }

set {

assert(indexIsValid(row: row, column: column), "Index out of range")

grid[(row * columns) + column] = newValue

   }

   }

}


Matrix 提供了一个初始化方法,它有两个参数 rows 和 columns, 然后创建一个足够存储 rows * columns 个浮点值的数组。矩阵的每个位置的值初始化为 0.0. 为了实现这个, 数组的大小, 初始的cell 值是 0.0, 传给数组的初始化方法, 初始化一个正确大小的数组。

传入一个合适的行列值给初始化方法,你可以创建一个 Matrix 的实例:

var matrix = Matrix(rows: 2, columns: 2)


前面的例子创建两行两列的 Matrix 实例。实例的grid 数组实际上是一个平的矩阵:


Swift 4.0 编程语言(四)_第5张图片

通过传入行列值给下标来设置矩阵中的值,用逗号分开:

matrix[0, 1] = 1.5

matrix[1, 0] = 3.2

这两个语句调用下标的setter 设置矩阵右上方位置的值为1.5 (行是0列是1), 然后设置左下位置的值为3.2(行是1列是0):


Swift 4.0 编程语言(四)_第6张图片

矩阵下标的 getter 和 setter 都包含一个断言,用来判断下标的行列值是有效的。 为了协助这些断言, Matrix 含有一个便利方法 indexIsValid(row:column:), 用来判断需要的行列是否在矩阵的边界内:

func indexIsValidForRow(row: Int, column: Int) -> Bool {

return row >= 0 && row < rows && column >= 0 && column < columns

}

如果你访问越界的下标,就会触发断言:

let someValue = matrix[2, 2]

// 触发断言, 因为 [2, 2] 超出了矩阵的边界

你可能感兴趣的:(Swift 4.0 编程语言(四))