Swift编程十一(属性)

案例代码下载

属性

属性将值与特定类,结构或枚举相关联。存储的属性将常量和变量值存储为实例的一部分,而计算属性则计算(而不是存储)值。计算属性由类,结构和枚举提供。存储的属性仅由类和结构提供。

存储和计算属性通常与特定类型的实例相关联。但是,属性也可以与类型本身相关联。这些属性称为类型属性。

此外,可以定义属性观察器以监视属性值的更改,可以使用自定义操作进行响应。可以将属性观察器添加到自定义的存储性中,也可以添加到子类从其超类继承的属性中。

存储属性

在其最简单的形式中,存储属性是一个常量或变量,存储为特定类或结构的实例的一部分。存储的属性可以是变量存储属性(由var关键字引入),也可以是常量存储属性(由let关键字引入)。

可以为存储属性提供默认值作为其定义的一部分,如“ 默认属性值”中所述。还可以在初始化期间设置和修改存储属性的初始值。即使对于常量存储属性也是如此,如初始化期间分配常量属性中所述。

下面的示例定义了一个名为FixedLengthRange的结构,它描述了一系列整数,其范围长度在创建后无法更改:

struct FixedLengthRange {
    var firstValue: Int
    let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3);
rangeOfThreeItems.firstValue = 6;

FixedLengthRange的实例具有变量存储属性firstValue和常量存储属性length。在上面的示例中,length在创建新范围时初始化,此后无法更改,因为它是常量属性。

常数结构实例的存储性质

如果创建结构的实例并将该实例分配给常量,则无法修改实例的属性,即使它们被声明为变量属性:

let rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
rangeOfThreeItems.firstValue = 6

因为rangeOfFourItems声明为常量(使用let关键字),无法更改其firstValue属性,即使firstValue是变量属性。

此行为是由于结构是值类型。当值类型的实例标记为常量时,其所有属性也都标记为常量。

对于类情况不同,类是引用类型。如果将引用类型的实例分配给常量,则仍可以更改该实例的变量属性。

懒加载存储属性

懒加载存储属性是一个初始值是不计算的第一次直到使用它。通过在声明之前编写lazy修饰符来指示懒加载存储的属性。

注意: 必须始终将懒加载属性声明为变量(使用var关键字),因为在实例初始化完成之后,可能无法检索其初始值。常量属性在初始化完成之前必须始终具有值,因此不能声明为懒加载属性。

当属性的初始值依赖于外部因素时,懒加载属性非常有用,这些外部因素的值在实例初始化完成之后才知道。当属性的初始值的设置需要复杂或昂贵的计算时,懒加载属性也很有用,除非需要,否则不应执行。

下面的示例使用懒加载存储属性来避免复杂类的不必要的初始化。本实施例中定义了两个类叫DataImporter和DataManager:

class DataImporter {
    var filename = "data.txt"
}
class DataManager {
    lazy var importer = DataImporter()
    var data = [String]()
}
let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")

DataManager类有一个存储属性叫做data,用一个新的,空String数组初始化。虽然未显示其余功能,但DataManager类的目的是管理并提供对此String数据数组的访问。

DataManager类的部分功能是从文件导入数据的能力。此功能由DataImporter类提供,假设需要花费大量时间进行初始化。这可能是因为DataImporter实例需要在DataImporter初始化实例时打开文件并将其内容读入内存。

DataManager实例可以在不从文件导入数据的情况下管理其数据,因此在创建DataManager自身时无需创建新DataImporter实例。相反,在第一次使用DataImporter时,创建实例更有意义。

因为它用lazy修饰符标记,所以只有在首次访问importer属性时才会创建importer属性的DataImporter实例,例如其filename属性被访问时:

print(manager.importer.filename)

注意: 如果同时由多个线程访问标记有lazy修饰符的属性且该属性尚未初始化,则无法保证该属性仅初始化一次。

存储属性和实例变量

如果有使用Objective-C的经验,可能知道它提供了两种方法来存储值和引用作为类实例的一部分。除了属性之外,还可以使用实例变量作为存储在属性中的值的后备存储。

Swift将这些概念统一到一个属性声明中。Swift属性没有相应的实例变量,并且不直接访问属性的后备存储。这种方法避免了在不同的上下文中如何访问值的混淆,并将属性的声明简化为单个明确的语句。有关属性的所有信息(包括其名称,类型和内存管理特征)都在单个位置定义,作为类型定义的一部分。

计算属性

除了存储属性之外,类,结构和枚举还可以定义计算属性,这些属性实际上不存储值。相反,它们提供了一个getter和一个可选的setter来间接检索和设置其他属性和值。

struct Point {
    var x = 0.0, y = 0.0
}
struct Size {
    var with = 0.0, height = 0.0
}
struct Rect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + size.with/2
            let centerY = origin.y + size.height/2
            
            return Point(x: centerX, y: centerY)
        }
        set(newCenter) {
            origin.x = newCenter.x - size.with/2
            origin.y = newCenter.y - size.height/2
        }
    }
}
var square = Rect(origin: Point(x: 0.0, y: 0.0), size: Size(with: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print(square)

此示例定义了三种用于处理几何形状的结构:

  • Point 封装了一个点的x坐标和y坐标。
  • Size封装a width和a height。
  • Rect 按原点和大小定义矩形。

Rect结构还提供了一个名为center的计算属性。Rect的当前中心位置总是可以从它的origin和size确定,所以不需要中心点存储为一个明确的Point值。Rect而是自定义一个getter和setter来计算center变量,使得能够使用矩形center,就像它是一个真正的存储属性一样。

上面的例子创建了一个Rect名为square的新变量。square变量被原点(0, 0),和宽度与高度为10初始化。该正方形由下图中的蓝色方块表示。

该square变量的center属性,然后通过点语法访问(square.center),这会导致需要getter center被调用,获取当前的属性值。getter实际上不是返回现有值,而是实际计算并返回一个新的Point来表示方形的中心。如上所示,getter正确返回中心点(5, 5)。

然后将center属性设置为新值(15, 15),该值将方块向上和向右移动到下图中橙色方块所示的新位置。设置center属性会调用center的setter方法,它会修改存储属性origin的x值和y值,并将方块移动到新位置。


image

速记setter声明

如果计算属性的setter没有为要设置的新值定义名称,newValue则使用默认名称。这是该Rect结构的替代版本,它利用了这种速记符号:

struct AlternativeRect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + size.with/2
            let centerY = origin.y + size.height/2
            
            return Point(x: centerX, y: centerY)
        }
        set {
            origin.x = newValue.x - size.with/2
            origin.y = newValue.y - size.height/2
        }
    }
}

只读计算属性

具有getter但没有setter的计算属性称为只读计算属性。只读计算属性始终返回一个值,可以通过点语法访问,但不能设置为其他值。

注意: 必须将计算属性(包括只读计算属性)声明为带有var关键字的变量属性,因为它们的值不固定。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(fourByFiveByTwo.volume)

这个例子定义了一个新的结构叫做Cuboid,其表示立方体的width,height和depth属性。此结构还具有一个只读的计算属性volume,它可以计算并返回立方体的当前体积。设置volume是没有任何意义的,因为这将弄不清width,height以及depth的哪个值应该用于特定的volume值。尽管如此,Cuboid提供只读计算属性以使外部用户能够发现其当前计算的体积是有用的。

属性观察者

属性观察者观察并响应属性值的变化。每次设置属性值时都会调用属性观察者,即使新值与属性的当前值相同。

可以将属性观察者添加到定义的任何存储属性,但惰加载存储属性除外。还可以通过覆盖子类中的属性,将属性观察者添加到任何继承的属性(无论是存储还是计算属性)。不需要为非重写的计算属性定义属性添加观察者,因为可以在计算属性的setter中观察并响应其值的更改。Overriding中描述了属性覆盖。

可以选择在属性上定义其中一个或两个观察者:

  • willSet 在存储值之前调用。
  • didSet 在存储新值后立即调用。

如果实现了一个willSet观察者,它会将新属性值作为常量参数传递。可以在willSet实现过程中指定此参数的名称。如果未在实现中编写参数名称和括号,则该参数的默认参数名称为newValue。

类似地,如果您实现了一个didSet观察者,它会传递一个包含旧属性值的常量参数。可以为参数命名或使用默认参数名称oldValue。如果为其自己的didSet观察者中的属性分配值,则分配的新值将替换刚刚设置的值。

注意: 在父类的初始化调用之后,子类初始化时设置一个属性,父类属性的willSet与didSet会被调用。在调用父类初始化程序之前,类在设置自己的属性时不会调用它们。

下面是一个willSet,和didSet功能的例子。下面的示例定义了一个名为StepCounter的新类,它跟踪一个人在行走时所采取的步伐总数。该类可以与来自计步器或其他步数计数器的输入数据一起使用,以记录人们在日常生活中的运动。

class StepCounter {
    var totalSteps = 0 {
        willSet {
            print("willSet \(newValue)")
        }
        didSet {
            if totalSteps > oldValue {
                print("didSet \(totalSteps)")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
stepCounter.totalSteps = 360
stepCounter.totalSteps = 896

在StepCounter类声明了一个Int类型的totalSteps属性。这是一个有willSet和didSet观察者的存储属性。

每当totalSteps属性分配一个新的值时willSet和didSet观察者被调用。即使新值与当前值相同,也是如此。

此示例的willSet观察者使用自定义参数名称newTotalSteps来表示即将到来的新值。在此示例中,它只是打印出即将设置的值。

在更新totalSteps值之后调用didSet观察者。它将totalSteps新值与旧值进行比较。如果步伐总数增加,则会打印一条消息,指示已执行了多少新步数。didSet观察者不提供旧值自定义参数名称,用默认的名称oldValue来代替。

注意: 如果将具有观察者的属性作为输入输出参数传递给函数,则始终会调用willSet和didSet观察者。这是因为in-out参数的copy-in copy-out内存模型:该值总是在写回属性。有关输入输出参数行为的详细讨论,请参阅输入输出参数。

全局和局部变量

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

在前面章节中遇到的全局变量和局部变量都是存储变量。存储变量(如存储属性)为特定类型的值提供存储,并允许设置和检索该值。

还可以在全局或局部区域内定义计算变量、存储变量观察者。计算变量计算它们的值,而不是存储它们,它们的编写方式与计算属性相同。

注意: 全局常量和变量总是懒加载计算的,与懒加载属性类似。与懒加载存储属性不同,全局常量和变量不需要使用lazy修饰符标记。局部常量和变量永远不会懒加载计算。

类型属性

实例属性是属于特定类型的实例的属性。每次创建该类型的新实例时,它都有自己的设置属性值,与任何其他实例分开。

还可以定义属于该类型本身的属性,而不是该类型的任何一个实例。无论创建的该类型的实例有多少,这些属性都只会有一个副本。这些属性称为类型属性。

类型属性对于定义对特定类型的所有实例通用的值很有用,例如所有实例都可以使用的常量属性(如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)
SomeStructure.storedTypeProperty = "another value"
print(SomeStructure.storedTypeProperty)
print(SomeEnumeration.computedTypeProperty)
print(SomeClass.computedTypeProperty)

以下示例使用两个存储类型属性作为的结构体的一部分建模多个音频通道音量。每个通道都有一个介于0和10之间的整数音量。

下图说明了如何组合其中两个音频通道来模拟立体音频音量。当通道的音量为0时,该通道的任何灯都不会亮起。当音频音量为10时,该通道的所有灯都会亮起。在此图中,左声道的当前音量为9,右声道的当前音量为7:

image

上述音频通道由AudioChannel结构实例表示:

struct AudioChannel {
    static let thresholdLevel = 10
    static var maxInputLevelForAllChannels = 0
    var currentLevel: Int = 0 {
        didSet {
            if currentLevel > AudioChannel.thresholdLevel {
                currentLevel = AudioChannel.thresholdLevel
            }
            if currentLevel > AudioChannel.maxInputLevelForAllChannels {
                AudioChannel.maxInputLevelForAllChannels = currentLevel
            }
        }
    }
}

该AudioChannel结构定义了两个存储的类型属性以支持其功能。第一个,thresholdLevel定义音量可以采用的最大阈值。这个的常量值10所有AudioChannel实例拥有。如果音频信号的值高于10,则将限制为此阈值(如下所述)。

第二个类型属性是一个名为maxInputLevelForAllChannels的变量存储属性。这会跟踪任何 AudioChannel实例接收的最大输入值。它以初始值开始0。

AudioChannel结构还定义了一个名为currentLevel的存储实例属性,它表示通道的当前音量0到10。

currentLevel属性有一个didSet属性观察者来检查设置的currentLevel值。该观察者执行两项检查:

  • 如果currentLevel新值大于允许值thresholdLevel,则currentLevel属性观察者将限制为thresholdLevel。
  • 如果currentLevel的新值(在任何赋值之后)高于先前由任何 AudioChannel实例接收的任何值,则属性观察者将新currentLevel值存储在maxInputLevelForAllChannelstype属性中。

注意: 在这两个检查的第一个中,didSet观察者设置currentLevel为不同的值。但是,这不会导致再次调用观察者。

可以使用AudioChannel结构来创建两个新的音频通道叫leftChannel和rightChannel,代表立体声音响系统的音量:

var leftChannel = AudioChannel()
var rightChannel = AudioChannel()

如果currentLevel将左通道设置为7,则可以看到maxInputLevelForAllChannelstype属性更新为7:

leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
print(AudioChannel.maxInputLevelForAllChannels)

如果您尝试将右侧通道currentLevel设置为11,则可以看到右侧通道的currentLevel属性上限为最大值10,并且maxInputLevelForAllChannelstype属性更新为10:

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

推荐阅读更多精彩内容