第十二章 属性

属性

属性是和一个特定类、结构、枚举等相关联的值。存储属性存储了一些常量和变量作为实例的一部分,但是计算属性不仅存储值,还计算值。计算属性由类、结构和枚举提供。存储属性仅由类和结构提供。

存储属性和计算属性通常都和一个特定类型的实例相关联。但是,属性也可以和自身的类型相关联。这样的属性被称为类型属性。

你可以定义属性观察者来监视属性值的变化,来执行自定义的动作。属性观察者可以添加到你定义的属性上,也可以添加到一个由父类继承而来的属性上。

存储属性

简单来说,存储属性是特定类型类或结构的实例的一部分。存储属性可以是变量存储属性(使用 var 关键字) 也可以是常量存储属性(使用 let 关键字)。

你可以为一个存储属性提供一个默认值作为属性声明的一部分,在 Default Property Values 这一章节中有介绍。同时你也可以在属性初始化的时候设置或者修改其初始化值。这甚至对常量存储属性也是有效的,在 “Modifying Constant Properties During Initialization” 这一章节中有介绍。

下面的例子定义了一个结构称作 FixedLengthRange,其定义了一个整型区间,其区间长度一旦被创建就不能被修改:

<此处添加代码2.10.1 - 1>

FixedLengthRange 的实例有一个变量存储属性称为 firstValue 以及一个常量存储属性称为 length。上面的例子中,length 在实例被创建的时候初始化,并且之后将不能被修改,因为它是一个常量属性。

常量结构实例的存储属性

如果你创建了一个结构实例并且将其赋值给一个常量,那么你将不能修改这个实例的属性,即使这些属性是变量属性:

<此处添加代码2.10.1 - 2>

rangeOfFourItems 被声明为常量(使用 let 关键字),因此不能修改 firstValue 属性值,即使 firstValue 被定义为变量属性。

这是由于结构是值类型。当一个值类型被标记为常量时,其所有的属性也同时被标记为常量。

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

延迟存储属性

延迟属性的初始值直到它第一次被使用时才会计算。在存储属性的声明前面添加 lazy 修饰符来指定延迟存储属性。

注意:

必须将延迟存储属性声明为变量(使用 var 关键字),因为它的初始值可能在实例初始化完成后才会被获取。常量属性必须在初始化完成前有一个值,所以不能被声明为延迟属性。

当属性的初始化值依靠于外部因素,并且在初始化完成之前值都不确定的时候,延迟属性就非常有用了。当属性在初始化时需要复杂或富计算的准备,并仅在需要的时候才执行时,延迟属性也非常有用。

如下的例子中展示了使用延迟属性来避免复杂类的初始化。这个例子中定义了两个类称为 DataImporter 和 DataManager,均没有展示完全:

<此处添加代码2.10.1 - 3>

DataManager 类有一个存储属性称为 data,其初始化为一个新的空 String 类型的数组。类的其他功能没有展示出来,也能看出 DataManager 类的目的是管理和提供这个 String 数组的访问途径。

DataManager 的部分功能是从一个文件导入数据。这个功能由 DataImporter 类提供,假定可以在非平凡时间内初始化完成。因为当 DataImporter 实例化时,需要打开一个文件并且把它的内容读入到内存中。

DataManager 的实例可以管理数据而不需要从文件导入数据,因此没有必要在 DataManager 实例创建的时候同时创建 DataImporter 实例。而在第一次被使用到时再创建 DataImporter 实例更为合理。

因为它使用了 lazy 修饰符,DataImporter 实例中的 importer 属性在第一次被访问的时候才会创建,例如它的属性 fileName 被访问时:

<此处添加代码2.10.1 - 4>

存储属性和实例变量

如果你有 Objective-C 编程经验,你应该知道有两种存储值和属性的方式。除了属性外,你可以使用实例变量来作为属性存储值的备份存储。

Swift 将这些概念统一到属性申明中。Swift 中的属性没有对应的实例变量,并且备份存储无法直接访问。这避免了在不同场景下值如何被访问的歧义,也将属性的声明简化到一个语句中。所有与属性有关的信息 —— 包括名称、类型、内存管理特征 —— 都在一个地方被定义。


计算属性

除了属性外,类、结构和枚举还可以计算属性,其并不实际存储值。而是提供一个 getter 函数和一个可选 setter 函数来获取和设置属性的值:

<此处添加代码2.10.2 - 1>

这个例子中定义了三个几何结构体:

Point 封装一个 (x, y) 坐标

Size 封装一个 width 和 height

Rect 起始点和尺寸定义了一个矩形

Rect 结构体提供了一个称为 center 的计算属性。Rect 的当前中心位置可以通过 origin 和 size 计算得出,因此没有必要显式的存储一个中心点 Point 值。Rect 为计算属性 center 定义了自定义的 getter 和 setter 函数,让你可以像真正的存储属性一样访问矩形的 center。

前面的例子中创建了一个新的 Rect 变量,称为 square。square 变量原点为 (0, 0),宽和高都是10。如下图所示的蓝色矩形。

之后 square 变量的 center 属性可用点运算符访问 (square.center),这会使 center 的 getter 函数被调用,以获取当前属性值。getter 函数并不是直接返回已经存在的值,getter 函数实际上计算并返回一个新的代表矩形中心的 Point。如上所示,getter 函数正确返回了中心点 (5, 5)。

center 属性随后被设置为 (15, 15),这让矩形向右上方移动,移动到如下图所示的橙色矩形位置。设置 center 属性会调用其 setter 函数,修改了 origin 属性的 x 和 y 值,使矩形移动到新的位置。

<图表>

Setter 声明简写

如果计算属性的 setter 函数没有为新值定义名称,则使用默认的名称 newValue。如下是 Rect 结构的另一种版本,使用了 setter 函数简写标记:

<此处添加代码2.10.2 - 2>

只读计算属性

只有 getter 没有 setter 的计算属性就是只读计算属性。一个只读计算属性只能返回值,可以使用点运算符访问,但不能被设置为其他值。

注意:

必须使用 var 关键字来定义计算属性为变量属性,包括只读计算属性,因为他们的值并不是固定的。let 关键字仅使用于常量属性,来表示他们的值在实例初始化被设置后就不能被修改。

可以简单的去除 get 关键字及花括号来定义一个只读计算属性:

<此处添加代码2.10.2 - 3>

这个例子定义了一个新的结构称为 Cuboid,用 width, height 和 depth 属性来表示了一个三维立方体的长、宽和高。还有一个只读计算属性称为 volume,用来计算和返回当前立方体的容积。立方体的容积可以被设置是不合理的,可能会造成歧义,因为体积是用 width,height 和 depth 计算出的。但是,Cuboid 提供一个只读计算属性来让外部用户获取体积是很有用的。


属性监视器

属性监视器监视并且响应属性值的变化。即使新的值与属性的当前值相同,每次属性值被设置时都会调用属性监器。

你可以为任何定义的存储属性添加属性监视器,除了延迟存储属性。同时你也可以在子类中重写来为任何继承属性(无论存储属性或计算属性)添加监视器。属性重写在 Overriding 这一章节中有描述。

注意:

你不需要为无覆盖的计算属性定义属性监视器,因为你可以直接在他们的 setter 函数中监视并且处理变化。

你可以为属性添加如下一种或两种监视器:

willSet 在值被存储的时候被调用

didSet 在新的值被存储的时候立刻调用

如果实现一个 willSet 监视器,它以敞亮参数的形式传递新的属性。你可以为这个参数指定一个名称作为 willSet 实现的一部分。如果不指明参数名称并且在实现中不写圆括号的话,那么参数将仍然可以使用默认参数名 newValue 来访问。

类似地,如果实现 didSet 监视器,它将传递一个包含旧属性值的常量参数。你可以为该参数命名,也可以使用默认参数名 oldValue。

注意:

willSet 和 didSet 监视器在属性首次初始化的时候不会被调用。仅当在初始化外部环境中属性值被设置的时候才会调用。

如下是一个使用 willSet 和 didSet 的示例。例子中定义了一个新的类称为 StepCounter,用来记录一个人的行走步数。这个类可以使用计数器或者其他计步器作为数据输入来记录人们在日常生活中的锻炼:

<此处添加代码2.10.3 - 1>

StepCounter 类定义了一个 int 类型的属性 totalSteps。这是一个拥有 willSet 和 didSet 监视器的存储属性。

任何时候当 totalSteps 被赋予新值时,willSet 和 didSet 监视器豆浆被调用。即使新值和旧的值相同时也同样。

例子中 willSet 监视器使用了一个名为 newTotalSteps 的自定义参数来表示新值。在这个例子中,它仅仅是打印出了将要被设置的值。

在 totalSteps 值更新之后 didSet 监视器被调用。它对比了旧的和新的值。如果总步数值有增加,则打印一条信息来显示新增了多少步数。didSet 监视器并没有为旧的值提供自定义的参数名,仅仅使用了默认的参数名 oldValue。

注意:

如果你在一个属性自己的 didSet 监视器中给它赋值,那么这个值会替换掉之前设置的值。


全局变量和局部变量

如上所述的计算属性和属性监视器的特性也适用于全局变量和局部变量。全局变量是没有定义在任何函数,方法,闭包或者类型的上下文中的变量。局部变量是定义在函数,方法或者闭包上下文中的。

前面章节中遇到的全局变量和局部变量都属于存储变量。存储变量,类似存储属性,可以存储一个特定类型的值,并且该值可以被设置和访问。

但是,在全局或者局部作用域中,你仍然可以定义计算变量并且为其定义监视器。计算变量并不存储一个值而只是计算一个值,写法通计算属性相同。

注意:

全局常量和变量都是延迟计算的,与延迟存储属性的方式相同。与延迟存储属性不同的是,全局常量和变量不需要使用 lazy 修饰符标识。局部常量和变量不是延迟计算的。


类型属性

实例属性是属于一个特定类型的实例。每次创建此类型的一个实例时,都独自拥有一套属性值,与其他的实例相互独立。

也可以为类型本身而不是为此类型的实例来定义属性。这些属性只会有唯一版本,无论创建了多少这个类型的实例。这样的属性称作为类型属性。

类型属性对于定义属于某类型所有实例的值非常有用,比如所有实例都可以使用的常量属性(像 C 语言中的静态常量),或者一个变量属性存储了一个属于某类型所有实例的值(像 C 语言中的静态变量)。

对于值类型(即,结构和枚举)来说,你可以定义存储和计算类型属性。对于类来说,你只可以定义计算类型属性。

值类型的存储类型属性可以是变量或者常量。计算类型属性可以声明为变量属性,对于计算实例属性来说也是同样的。

注意:

与存储实例类型不同,你必须给存储类型属性一个默认值。这是因为在初始化的时候类型本身没有初始化函数为其赋值。

类型属性语法

在 C 和 Objective-C 中,可以定义某种类型的一个静态常量或者变量来作为全局静态变量。然而在 Swift 中,类型属性被作为类型定义的一部分来书写,写在类型外面的花括号中,并且每个类型属性的作用域也就是类型的作用域。

使用 static 为值类型定义类型属性,使用 class 为类定义类型属性。下面的例子展示了存储和计算类型属性的语法:

<此处添加代码2.10.5 - 1>

注意:

上面的计算类型属性是只读的,但你可以使用与计算实例类型相同的语法来定义可读写的类型属性。

获取和设置类型属性

类型属性可以使用点运算符来获取和设置,和实例属性相同。但是,类型属性是在类型上进行获取和设置,并不是在一个此类型的实例上进行。例如:

<此处添加代码2.10.5 - 2>

例子中定义了一个结构体, 使用两个存储类型属性来表示多个声道的声音电平。每个声道的声音电瓶使用 0 到 10 之间的整数来表示。

下面的图中展示了如何使用两个声道结合来模拟立体声道的电平。当一个声道的电平为 0 时,此声道的所有灯都不亮。当声道的电平为 10 时,此声道的所有灯都亮。在此图中,左边的声道电平为 9,右边的声道电平为 7:

<图表>

上面描述的声道使用 AudioChannel 结构来表示:

<此处添加代码2.10.5 - 3>

AudioChannel 结构定义了两个存储类型属性来支持其功能。第一个,thresholdLevel,定义了声音电平的最大阈值。对于所有 AudioChannel 的实例,这个值是一个常量 10.如果一个输入声音信号超过 10,它会被限制到最大阈值。

第二个类型属性是一个存储变量属性称为 maxInputLevelForAllChannels。这个变量持续记录所有 AudioChannel 实例接受到的最大输入值。它的初始值为 0。

AudioChannel 结构同时也定义了一个存储实例属性称为 currentLevel,它用 0 到 10 表示声道当前的电平值。

currentLevel 属性有一个 didSet 属性监视器来检查 currentLevel 值是否被设置。这个监视器执行两个检查:

1. 如果新的 currentLevel 值比允许的最大阈值 thresholdLevel 要大,属性监视器将 currentLevel 限制到 thresholdLevel。

2. 如果新的 currentLevel 值(在限制之后)比之前所有 AudioChannel 实例接收到的值都要大,属性监视器将新的 currentLevel 值存储到 maxImputForAllChannels 静态属性。

注意:

在第一项检查中,didSet 监视器将 currentLevel 设置为不同值。但是这并不会使监视器再一次被调用。

你可以使用 AudioChannel 结构来创建两个新的声道称为 leftChannel 和 rightChannel,来表示一个立体声系统:

<此处添加代码2.10.5 - 4>

如果你设置左声道的 currentLevel 值为 7,你会发现 maxInputLevelForAllChannels 类型属性被更新到值 7:

<此处添加代码2.10.5 - 5>

如果试着将右声道的 currentLevel 调整到 11,可以发现 currentLevel 属性值将被限制到最大值 10,并且 maxInputLevelForAllChannels 类型属性被更新到10:

<此处添加代码2.10.5 - 6>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容