Category Archives: 基础课程Course

基础课程主要包含Swift语言的官方及第三方的培训课程/文档,以讲述Swift的基础,概念等知识点。

Swift中文教程(13)继承

转载自letsswift.com

一个类可以从另外一个类中继承方法,属性或者其它的一些特性。当一个类继承于另外一个类时,这个继承的类叫子类,被继承的类叫父类。继承是Swift中类区别于其它类型的一个基本特征。

Swift中的类可以调用父类的方法,使用父类的属性和下标,还可以根据需要使用重写方法或者属性来重新定义和修改他们的一些特性。Swift可以帮助你检查重写的方法和父类的方法定义是相符的。

类还可以为它继承的属性添加观察者,这样可以能够让它在一个属性变化的时候得到通知。属性观察者可以被添加给任何属性,不管它之前是存储属性还是计算属性。

1、定义一个基类

任何一个不继承于其它类的类被称作基类

注意:Swift的类不是从一个全局基类继承而来。在你编写代码的时,只要是在类的定义中没有继承自父类的类都是基类。

下面的例子定义了一个叫Vehicle的基类。基类包含两个所有交通工具通用的属性numberOfWheels和maxPassengers。这两个属性被一个叫description的方法使用,通过返回一个String描述来作为这个交通工具的特征:

1234567891011
classVehicle{
    var numberOfWheels:Int
    var maxPassengers:Int
    func description()->String{
        return"\(numberOfWheels) wheels; up to \(maxPassengers) passengers"
    }
    init(){
        numberOfWheels =0
        maxPassengers =1
    }}

这个交通工具类Vehicle还定义了一个构造函数来设置它的属性。构造函数更多的解释在Initialization一章,但是为了说明子类如何修改继承的属性,这里需要简要解释一下什么叫构造函数。

通过构造函数可以创建一个类型的实例。尽管构造函数不是方法,但是它们在编码的时候使用了非常相似的语法。构造函数通过确保所有实例的属性都是有效的来创建一个新的实例。

构造函数最简单的形式是使用init关键词的一个类似方法的函数,并且没有任何参数:

123
init(){
    // perform some initialization here}

使用构造函数语法TypeName和空的两个小括号来完成一个Vehicle实例的创建:

1
let someVehicle =Vehicle()

Vehicle的构造函数为属性设置了一些初始值(numberOfWheels = 0 然后 maxPassengers = 1)。

Vehicle类定义的是一个通用的交通工具特性,它本身没有太多意义,所以就需要冲定义它的一些属性或者方法来让它具有实际的意义。

2、产生子类

产生子类就是根据一个已有的类产生新类的过程。子类继承了父类的一些可以修改的特性。还可以为子类添加一些新的特性。

为了表明一个类是继承自一个父类,需要将父类的名称写在子类的后面,并且用冒号分隔:

123
classSomeClass:SomeSuperclass{
    // class definition goes here}

下面的例子定义了一种特定叫Bicycle的交通工具。这个新类是基于已有的类Vehicle产生的。书写方式是在类名Bicycle后加冒号加父类Vehicle名。

可以理解为:

定义一个新的类叫Bicycle,它继承了Vehicle的特性:

123456
classBicycle:Vehicle{
    init(){
        super.init()
        numberOfWheels =2
    }}

Bicycle是Vehicle的子类,Vehicle是Bicycle的父类。Bicycle类继承了Vehicle所有的特征,比如maxPassengers和numberOfWheels属性。你还可以为Bicycle类添加心的属性。

Bicycle类也定义了构造函数,在这个构造函数中调用了父类的构造函数super.init(),这样可以确保在Bicycle修改他们之前,父类已经初始化了。

注意:跟Objective-C不同的是,Swift中的构造函数没有默认继承。更多信息可以参考Initializer Inheritance and Overriding这一章节。

maxPassengers属性在继承自父类的时候已经被初始化了,对于Bicycle来说是正确的,因此不需要再做更改。然后numberOfWheels是不对的,所以被替换成了2.

不仅属性是继承于Vehicle的,Bicycle还继承了父类的方法。如果你创建一个实例,然后调用了已经继承的description方法,可以得到该交通工具的描述并且看到它的属性已经被修改:

123
let bicycle =Bicycle()
println("Bicycle: \(bicycle.description())")// Bicycle: 2 wheels; up to 1 passengers

子类本身也可以作为父类被再次继承:

123456
classTandem:Bicycle{
    init(){
        super.init()
        maxPassengers =2
    }}

上面的例子创建了Bicycle的子类,叫做tandem,也就可以两个人一起骑的自行车。所以Tandem没有修改numberOfWheels属性,只是更新了maxPassengers属性。

注意:子类只能够在构造的时候修改变量的属性,不能修改常量的属性。

创建一个Tandem的实例,然后调用description方法检查属性是否被正确修改:

123
let tandem =Tandem()
println("Tandem: \(tandem.description())")// Tandem: 2 wheels; up to 2 passengers

注意到description方法也被Tandem继承了。

3、重写方法

子类可以提供由父类继承来的实例方法,类方法,实例属性或者下标的个性化实现。这个特性被称为重写。

重写一个由继承而来的方法需要在方法定义前标注override关键词。通过这样的操作可以确保你所要修改的这个方法确实是继承而来的,而不会出现重写错误。错误的重写会造成一些不可预知的错误,所以如果如果不标记override关键词的话,就会被在代码编译时报错。

override关键词还能够让Swift编译器检查该类的父类是否有相符的方法,以确保你的重写是可用的,正确的。

访问父类方法,属性和下标

当在重写子类继承自父类的方法,属性或者下标的时候,需要用到一部分父类已有的实现。比如你可以重定义已知的一个实现或者在继承的变量中存储一个修改的值。

适当的时候,可以通过使用super前缀来访问父类的方法,属性或者下标:

叫someMethod的重写方法可以在实现的时候通过super.someMethod()调用父类的someMethod方法。

叫someProperty的重写属性可以在重写实现getter或者setter的时候通过super.someProperty调用父类的someProperty。

叫someIndex的重写下标可以在实现下标的时候通过super[someIndex]来访问父类的下标。

复写方法

你可以在你的子类中实现定制的继承于父类的实例方法或者类方法。

下面的例子演示的就是一个叫Car的Vehicle子类,重写了继承自Vehicle的description方法。

123456789101112
classCar:Vehicle{
    var speed:Double=0.0
    init(){
        super.init()
        maxPassengers =5
        numberOfWheels =4
    }
    override func description()->String{
        returnsuper.description()+"; "
            +"traveling at \(speed) mph"
    }}

Car中定义了一个新的Double类型的存储属性speed。这个属性默认值是0.0,意思是每小时0英里。Car还有一个自定义的构造函数,设置了最大乘客数为5,轮子数量是4.

Car重写了继承的description方法,并在方法名description前标注了override关键词。

在description中并没有给出了一个全新的描述实现,还是通过super.description使用了Vehicle提供的部分描述语句,然后加上了自己定义的一些属性,如当前速度。

如果你创建一个Car的实例,然后调用description方法,会发现描述语句变成了这样:

123
let car =Car()
println("Car: \(car.description())")// Car: 4 wheels; up to 5 passengers; traveling at 0.0 mph

复写属性

你还可以提供继承自父类的实例属性或者类属性的个性化getter和setter方法,或者是添加属性观察者来实现重写的属性可以观察到继承属性的变动。

重写属性的Getters和Setters

不管在源类中继承的这个属性是存储属性还是计算属性,你都可以提供一个定制的getter或者setter方法来重写这个继承属性。子类一般不会知道这个继承的属性本来是存储属性还是计算属性,但是它知道这个属性有特定的名字和类型。在重写的时候需要指明属性的类型和名字,好让编译器可以检查你的重写是否与父类的属性相符。

你可以将一个只读的属性通过提那家getter和setter继承为可读写的,但是反之不可。

注意:如果你为一个重写属性提供了setter方法,那么也需要提供getter方法。如果你不想在getter中修改继承的属性的值,可以在getter中使用super.someProperty即可,在下面SpeedLimitedCar例子中也是这样。

下面的例子定义了一个新类SpeedLimitedCar,是Car的一个子类。这个类表示一个显示在40码一下的车辆。通过重写继承的speed属性来实现:

12345678910
classSpeedLimitedCar:Car{
    overridevar speed:Double  {
    get{
        returnsuper.speed
    }
    set{
        super.speed =min(newValue,40.0)
    }
    }}

每当你要设置speed属性的时候,setter都会检查新值是否比40大,二者中较小的值会被设置给SpeedLimitedCar。

如果你尝试为speed设置超过40的值,description的输出依然还是40:

1234
let limitedCar =SpeedLimitedCar()
limitedCar.speed =60.0
println("SpeedLimitedCar: \(limitedCar.description())")// SpeedLimitedCar: 4 wheels; up to 5 passengers; traveling at 40.0 mph

重写属性观察者

你可以使用属性重写为继承的属性添加观察者。这种做法可以让你无论这个属性之前是如何实现的,在继承的这个属性变化的时候都能得到提醒。更多相关的信息可以参考Property Observers这章。

注意:不能为继承的常量存储属性或者是只读计算属性添加观察者。这些属性值是不能被修改的,因此不适合在重写实现时添加willSet或者didSet方法。

注意:不能同时定义重写setter和重写属性观察者,如果想要观察属性值的变化,并且又为该属性给出了定制的setter,那只需要在setter中直接获得属性值的变化就行了。

下面的代码演示的是一个新类AutomaticCar,也是Car的一个子类。这个类表明一个拥有自动变速箱的汽车,可以根据现在的速度自动选择档位,并在description中输出当前档位:

1234567891011
classAutomaticCar:Car{
    var gear =1
    overridevar speed:Double{
    didSet {
        gear =Int(speed /10.0)+1
    }
    }
    override func description()->String{
        returnsuper.description()+" in gear \(gear)"
    }}

这样就可以实现,每次你设置speed的值的时候,didSet方法都会被调用,来看档位是否需要变化。gear是由speed除以10加1计算得来,所以当速度为35的时候,gear档位为4:

1234
let automatic =AutomaticCar()
automatic.speed =35.0
println("AutomaticCar: \(automatic.description())")// AutomaticCar: 4 wheels; up to 5 passengers; traveling at 35.0 mph in gear 4

4、禁止重写

你可以通过标记final关键词来禁止重写一个类的方法,属性或者下标。在定义的关键词前面标注@final属性即可。

在子类中任何尝试重写父类的final方法,属性或者下标的行为都会在编译时报错。同样在扩展中为类添加的方法,属性或者下标也可以被标记为final。

还可以在类关键词class前使用@final标记一整个类为final(@final class)。任何子类尝试继承这个父类时都会在编译时报错。

感谢翻译小组成员:李起攀(微博)、若晨(微博)、YAO、粽子、山有木兮木有枝、渺-Bessie、墨离、矮人王、CXH、Tiger大顾(微博)
个人转载请注明出处和原始链接http://letsswift.com/2014/06/inheritance,商业转载请联系我们~ 感谢您对我们工作的支持~

Swift与Objective-C API交互(三)

Swift类型兼容性

定义一个继承自NSObject或者其他Objective-C的类,它自动与Objective-C兼容。如果你不需要将Swift对象导入Objective-C代码的话,没必要关注类型的兼容性。但是如果在Swift中定义的类不是Objective-C类的子类,在Objective-C中使用的时候,需要用@objc进行说明。

@objc使得Swift的API可以在Objective-C和它的运行时中使用。当使用@IBOutlet@IBAction或者@NSManaged等属性时,自动添加@objc属性。

@objc还可以用来指定Swift中的属性或方法在Objective-C中的名字,比如Swift支持Unicode名字,包括使用中文等Objective-C不兼容的字符。还有给Swift中定义的函数指定一个Selectorde名字。

@objc(Squirrel)
class 长沙戴维营教育 {
    @objc(hideNuts:inTree:)
    func 欢迎光临(Int, 姓名: String) {
        /* ... */
    }
}

@objc(<#name#>)属性作用在Swift的类上时,这个类在Objective-C的使用不受命名空间的限制。同样,在Swift中解归档Objective-C归档的对象时,由于归档对象中存放有类名,因此需要在Swift中用@objc<#name>说明Objective-C的类名。

Objective-C选择器(Selector)

Objective-C的选择器是方法的一个引用。在Swift中对应的是Selector结构体。使用字符串字面量可以构建一个选择器对象,如let mySelector: Selector = "tappedButton:"。由于字符串字面常量可以自动转换为选择器对象,因此可以在任何需要传递选择器的地方使用字符串字面常量。

import UIKit
class MyViewController: UIViewController {
    let myButton = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))

    init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!)
    {
        super.init(nibName: nibName, bundle: nibBundle)
        myButton.targetForAction("tappedButton:", withSender: self)
    }

    func tappedButton(sender: UIButton!) {
        println("tapped button")
    }
}
提示
performSelector:以及相关的调用选择器的方法没有被引入到Swfit中来,因为它们不是完全安全的。

如果Swift类继承自Objective-C的类,则它里面的方法和属性都能够作为Objective-C的选择器使用。而如果不是Objective-C的子类,需要使用@objc属性修饰,这个在前面的Swift类型兼容性中有描述。

Swift与Objective-C API交互(二)

兼容id类型

Swift包含一个叫AnyObject的协议,与Objective-C中的id类似,可以表示任意类型的对象。AnyObject协议允许你在利用无类型对象的灵活性的同时保持类型安全。
你可以给AnyObject类型的变量赋任意的值:

var myObject: AnyObject = NSData()

可以直接方法AnyObject类型对象的任意属性和方法,而不需要进行强制类型转换。

let dateDescription = myObject.description
let timeSinceNow = myObject.timeIntervalSinceNow

由于AnyObject类型的变量的类型需要到运行时才能确定,因此可能会导致不安全的代码。特别是你可以访问一个不存在的属性或者方法,它只是在运行时报错。

myObject.characterAtIndex(5)
//crash, myObject doesn't respond to that method

在进行类型转换的时候,不一定转换成功,因此Swift返回的是一个Optional值。你可以检查是否转换成功。

let userDefaults = NSUserDefaults.standardUserDefaults()
let lastRefreshDate: AnyObject? = userDefault.objectForKey("LastRefreshDate")
if let date = lastRefreshDate as? NSDate {
    println("\(date.timeIntervalSinceReferenceDate)")
}

如果你能够确定对象的类型,并且不为nil,可以使用as操作符进行强制转换。

let myDate = lastRefreshDate as NSDate
let timeInterval = myDate.timeIntervalSinceReferenceDate

nil对象

Objective-C中使用nil来表示引用一个空对象(null)。Swift中所有的值都不会为nil。如果需要表示一个缺失的值,可以使用Optional。

由于Objective-C不能确保所有值都非空,因此Swift将Objective-C中引入的方法的参数和返回值都用Optional表示。在使用Objective-C对象之前,应该检查它们是否存在。

扩展

Swift的扩展与Objective-C的类别有点类似。扩展能够为已有类、结构体、枚举等增加行为。

下面是给UIBezierPath添加扩展:

extension UIBezierPath {
    class func bezierPathWithTriangle(length: Float, origin: CGPoint) -> UIBezierPath {
        let squareRoot = Float(sqrt(3))
        let altitude = (squareRoot * length) / 2
        let myPath = UIBezierPath()

        myPath.moveToPoint(orgin)
        myPath.addLineToPoint(CGPoint(length, origin.x))
        myPath.addLineToPoint(CGPoint(length / 2, altitude))
        myPath.closePath()

        return myPath
    }
}

可以使用扩展增加属性(包括类属性或静态属性)。不过这些属性只能是通过计算得来,而不能进行存储。下面给CGRect添加一个area属性:

extension CGRect {
    var area: CGFloat {
        return width * height
    }
}

let rect = CGRect(x: 0.0, y: 0.0, width: 10.0, height: 50.0)
let area = rect.area
//area: CGFloat = 500.0

使用扩展可以在不创建子类的情况下让现有的类响应某个协议。需要注意的是,扩展不能覆盖已有的方法和属性。

闭包

Objective-C中的Block 被自动导入为Swift的闭包。例如:

void (^completionBlock)(NSData *, NSError *) = ^(NSData *data, NSError *error) {
    /* ... */
}

在Swift 中对应为:

let completionBlock: (NSData, NSError) -> void = {
    data, error in /* ... */
}

Swift的闭包和Objective-C中的Block是兼容的,可以在Swift中给Objective-C的方法传递闭包来代替Block对象。

闭包和Block对象有一点不同,里面的变量是可变的,也就是说与__block修饰的变量行为相同。

对象比较

Swift中有两种对象比较的方式。第一种是相等(==(equality),用来比较两个对象的内容是否相同。第二种是恒等(===(identity),比较两个变量或者常量是否引用同一个对象。

NSObject只能比较是否引用了同一个对象(恒等),如果要比较内容是否相同,应该实现isEqual:方法。

Swift与Objective-C API交互(一)

Swift和Objective-C可以进行互操作,也就是说可以在Objective-C项目中使用Swift代码,反过来也可以。当然,这种互操作之间最重要的是可以在Swift中调用Objective-C的接口,毕竟目前绝大部分接口都是通过Objective-C提供的。

初始化

在Swift中实例化一个Objective-C的类时,可以用Swift语法调用它的一个初始化器。Objective-C的初始化方法被分割成关键字。例如“initWith”变成“init”,而剩下的部分作为方法的参数名。

Objective-C的代码:

UITableView *myTableView = [[UITableView alloc] initWithFrame: CGRectZero style: UITableViewStyleGrouped];

对应的Swift代码为:

let myTableView: UITableView = UITableView(frame: CGRectZero, style: .Grouped)

在Swift中不需要调用alloc方法,它会自动处理对象的创建功能。注意:Swift不会显式的调用init方法。

定义变量或者常量的时候,可以省略它的类型,Swift会自动识别。

let myTextField = UITextField(frame: CGRect(0.0, 0.0, 200.0, 40.0))

为了方便起见,Objective-C的工厂方法被映射为Swift中的初始化器。例如:

UIColor *color = [UIColor colorWithRed: 0.5 green: 0.0 blue: 0.5 alpha: 1.0];

转换为Swift:

let color = UIColor(red: 0.5, green: 0.0, blue: 0.5, alpha: 1.0)

属性访问

在Objective-C和Swift中访问属性都是使用点操作符。

myTextField.textColor = UIColor.darkGrayColor()
myTextField.text = "Hello world"
if myTextField.editing {
    myTextField.editing = false
}

访问和设置属性的时候不需要使用圆括号,上面darkGrayColor之所以有括号,是因为调用的是UIColor的类方法,而不是属性。

Objective-C中不需要参数并且有一个返回值的方法,可以被当作隐含的获取方法(getter),从而使用点操作符。但是在Swift中点操作符只能访问Objective-C中使用@property定义的属性。

方法调用

在Swift中使用点操作符调用Objective-C中的方法。

Objective-C的方法在Swift中调用的时候,它的第一部分成为Swift的基本方法出现在括号之前。然后函数的第一个参数没有名字,剩下的部分作为Swift函数对应的参数名称。
Objective-C语法:

[myTableView insertSubview: mySubview atIndex: 2];

Swift代码:

myTableView.insertSubview(mySubview atIndex: 2)

调用无参的方法:

myTableView.layoutIfNeeded()

Swift中文教程(12)下标

转载自letsswift.com

类,结构和枚举类型都可以通过定义下标来访问一组或者一个序列中的成员元素。通过下标索引就可以方便地检索和设置相应的值,而不需要其他的额外操作。比如你可以通过someArray[index]来访问数组中的元素,或者someDictionary[key]来对字典进行索引。

你可以为一个类型定义多个下标,以及适当的下标重载用来根据传递给下标的索引来设置相应的值。下标不仅可以定义为一维的,还可以根据需要定义为多维的,多个参数的。

1、下标语法

下标可以让你通过实例名后加中括号内一个或多个数值的形式检索一个元素。语法和方法语法和属性语法类似,通过使用subscript关键定义,一个或多个输入参数以及一个返回值。不同于实例方法的是,下标可以是可读写的或者只读的。这种行为通过一个getter和setter语句联通,就像是计算属性一样。

12345678
subscript(index:Int)->Int{
    get{
        // return an appropriate subscript value here
    }
    set(newValue){
        // perform a suitable setting action here
    }}

newValue的类型和下标返回的类型一样。和计算属性一样,你可以选择不指定setter的参数,因为当你不指定的时候,默认参数newValue会被提供给setter。

和计算属性一样,只读下标可以不需要get关键词:

123
subscript(index:Int)->Int{
    // return an appropriate subscript value here}

下面是一个只读下标的实现,定义了一个TimesTable结构来表示一个整数的倍数表:

123456789
structTimesTable{
    let multiplier:Int
    subscript(index:Int)->Int{
        return multiplier * index
    }}
let threeTimesTable =TimesTable(multiplier:3)
println("six times three is \(threeTimesTable[6])")// prints "six times three is 18"

在这个例子中,实例TimesTable被创建为3倍数表,这是通过在初始化的时候为multiplier参数传入的数值3设置的。

注意:

倍数表是根据特定的数学规则设置的,所以不应该为threeTimeTable[someIndex]元素设置一个新值,所以TimesTable的下标定义为只读。

 

2、下标的使用

下标的具体含义由使用它时的上下文来确定。下标主要用来作为集合,列表和序列的元素快捷方式。你可以自由的为你的类或者结构定义你所需要的下标。

比如说,Swift中字典类型实现的下标是设置和检索字典实例中的值。可以通过分别给出下标中的关键词和值来设置多个值,也可以通过下标来设置单个字典的值:

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

上面的例子中定义了一个变量numberOfLegs,然后通过键值对初始化。numberOfLegs的类型是字典类型Dictionary<String, Int>。在字典创建之后,例子使用了下标赋值方法添加了一个类型为字符串的键”bird”和Int值2到字典中。

更多关于字典的下标可以参考:访问和修改字典这一章节

注意:

Swift中字典类型实现的键值对下标是可选类型。对于numberOfLges字典来说,返回的值是Int?,也就是可选Int值。字典的这种使用可选类型下标的方式说明不是所有的键都有对应的值。同样也可以通过给键赋值nil来删除这个键。

 

3、下标选项

下标可以接收任意数量的参数,参数的类型也可以各异。下标还可以返回任何类型的值。下标可以使用变量参数或者可变参数,但是不能够使用输入输出参数或者提供默认参数的值。

类或者结构可以根据需要实现各种下标方式,可以在需要的时候使用合适的下标通过中括号中的参数返回需要的值。这种多下标的定义被称作下标重载。

当然,最常见的下标用法是单个参数,也可以定义多个参数的下标。下面的例子演示了一个矩阵Matrix结构,它含有二维的Double值。矩阵结构的下标包括两个整形参数:

12345678910111213141516171819202122
structMatrix{
    let rows:Int, columns:Int
    var grid:Double[]
    init(rows:Int, columns:Int){
        self.rows = rows
        self.columns = columns
        grid =Array(count: rows * columns, repeatedValue:0.0)
    }
    func indexIsValidForRow(row:Int, column:Int)->Bool{
        return row >=0&& row < rows && column >=0&& column < columns
    }
    subscript(row:Int, column:Int)->Double{
        get{
            assert(indexIsValidForRow(row, column: column),"Index out of range")
            return grid[(row * columns)+ column]
        }
        set{
            assert(indexIsValidForRow(row, column: column),"Index out of range")
            grid[(row * columns)+ column]= newValue
        }
    }}

矩阵Matrix提供了一个初始化方法,使用两个参数rows和columns,然后建立了一个数组来存储类型为Double的值rows*columns。每个矩阵中的位置都被设置了一个初始值0.0。通过传递初始值0.0和数组长度给数组初始化方法完成上述操作。数组的初始化方法在:创建和初始化数组中有更详细的叙述。

你可以传递两个参数row和column来完成Matrix的初始化:

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

上面的初始化操作创建了一个两行两列的矩阵Matrix实例。这个矩阵实例的grid数组看起来是平坦的,但是实际上是矩阵从左上到右下的一维存储形式。

image 矩阵中的值可以通过使用包含row和column以及逗号的下标来设置:

12
matrix[0,1]=1.5
matrix[1,0]=3.2

这两个语句调用了下标的setter方法为右上和左下角的两个元素分别赋值1.5和3.2

image

矩阵下标的getter和setter方法都包括了一个断言语句来检查下标row和column是否有效。通过indexIsValid方法来判断row和column是否在矩阵的范围内:

123
func indexIsValidForRow(row:Int, column:Int)->Bool{
    return row >=0&& row < rows && column >=0&& column < columns
}

如果访问的矩阵越界的时候,断言就会被触发:

12
let someValue = matrix[2,2]// this triggers an assert, because [2, 2] is outside of the matrix bounds

感谢翻译小组成员:李起攀(微博)、若晨(微博)、YAO、粽子、山有木兮木有枝、渺-Bessie、墨离、矮人王、CXH、Tiger大顾(微博)
个人转载请注明出处和原始链接,商业转载请联系我们~ 感谢您对我们工作的支持~

Swift中文教程(11)方法

转载自letsswift.com

方法是关联到一个特定类型的函数。类、结构、枚举所有可以定义实例方法,封装特定任务和功能处理给定类型的一个实例。类、结构、枚举类型还可以定义方法,相关的类型本身。类型方法类似于objective – c类方法。

结构和枚举可以定义方法swift与C和objective – C是一个重大的区别。在objective – c中,类是唯一类型可以定义方法。在swift,你可以选择是否要定义一个类,结构,或枚举,还有你定义方法类型的灵活性创造。

1、实例方法

实例方法是属于一个特定的类,结构或枚举实例的功能。他们支持这些实例的功能,无论是通过提供方法来访问和修改实例属性,或提供的功能与实例的目的。实例方法具有完全相同的语法功能,如功能描述
你所属的类型的打开和关闭括号内写一个实例方法。一个实例方法具有隐式访问所有其他实例方法和该类型的属性。一个实例方法只能在它所属的类的特定实例调用,它不能访问一个不存在的实例。
这里,定义了一个简单的计数器类,它可以用来计数一个动作发生的次数的示例:

123456789101112
classCounter{varcount=0
func increment(){count++}
func incrementBy(amount:Int){count+= amount
}
func reset(){count=0}}

counter类可以定义三个实例方法:
增量递增计数器1。
incrementBy(amount:Int)由指定的整数金额递增计数器。
重置将计数器的值重置为零。
计数类也声明了一个变量属性,统计,跟踪当前的计数器值。
你调用实例方法具有相同点语法的属性

12345678
let counter =Counter()// the initial counter value is 0
counter.increment()// the counter's value is now 1
counter.incrementBy(5)// the counter's value is now 6
counter.reset()// the counter's value is now 0

本地和外部参数名称的方法

函数参数可以有一个本地名称(在函数体内使用)和外部名称(在调用函数时使用),所述外部参数名称。方法参数也是如此,因为方法与类型相关的函数。然而,本地名称和外部名称的默认行为是不同的函数和方法。

方法在Swift非常类似于objective – c的同行。在objective – c中,一个方法的名称在Swift通常是指使用preposition等方法的第一个参数,,或者,就像在incrementBy方法从前面的counter类的例子。使用可以被解读为一个判断的方法叫做preposition。Swift使这个方法建立命名约定易于编写通过使用一个不同的默认方法。

具体来说,Swift给第一个参数名称方法默认本地参数名称,并给出第二和后续的参数名称默认本地和外部参数名称。这个约定可以在熟悉的objective – c中调用到,并使得表达方法调用而不需要符合你的参数名称。

考虑这个替代版本的counter类,它定义了一个更复杂的形式的incrementBy方法:

123456
classCounter{varcount:Int=0
func incrementBy(amount:Int, numberOfTimes:Int){count+= amount * numberOfTimes
}}

这有两个parameters-amount和numberOfTimes incrementBy方法。默认情况下,Swift将amount视为本地名称,但将numberOfTimes视为本地和外部名称。您调用的方法如下:

123
let counter =Counter()
counter.incrementBy(5, numberOfTimes:3)// counter value is now 15

你不需要定义一个外部参数名称为第一个参数值,因为它是明确的函数名incrementBy。然而,第二个参数是由外部参数名称进行限定。
这种默认行为有效的外部方法,如果你有numberOfTimes参数之前写了一个hash符号(#):

123
func incrementBy(amount:Int,#numberOfTimes: Int) {count+= amount * numberOfTimes
}

上面描述的默认行为在Swift写入相同的方法定义,语法类似于objective – c,可以更方便地被调用。

  修改外部参数名称的行为方法

有时是有用的提供一个外部方法的第一个参数的参数名称,即使这不是默认行为。你自己可以添加一个显式的外部名称,或者你可以用一个散列前缀的名字的第一个参数标志使用本地名称作为外部的名字。
相反,如果你不想为第二个提供外部名称或后续参数的方法,覆盖默认行为通过使用下划线字符(_)作为一个明确的外部参数名称参数。

  Self属性

一个类型的每个实例都有所谓的一个隐含self属性,它是完全等同于该实例本身。您可以使用这个隐含的self属性来引用当前实例中它自己的实例方法。

在上面的例子中,增量方法也可以写成这样:

123
func increment(){self.count++}

在实践中,你不需要写self,这在你的代码会非常频繁。如果你没有明确写self,Swift假设你是指当前实例的属性或方法,每当你使用一个方法中一个已知的属性或方法名。这个假设是证明了里边三个实例方法的计数器使用count(rather than self.count)的。
主要的例外发生在一个实例方法的参数名称相同的名称作为该实例的属性。在这种情况下,参数名称的优先,有必要参考属性更多合格的方式。您可以使用隐式的自我属性的参数名和属性名来区分。
如果一个方法参数叫x,还有一个实例属性也叫x,在Swift中可以自动对两个x消除歧义,不会混淆。

1234567891011
structPoint{var x =0.0, y =0.0
func isToTheRightOfX(x:Double)->Bool{returnself.x > x
}}
let somePoint =Point(x:4.0, y:5.0)if somePoint.isToTheRightOfX(1.0){
println("This point is to the right of the line where x == 1.0")}// prints "This point is to the right of the line where x == 1.0"

如果没有self前缀,Swift将假定x的两种用法称为X的方法参数

  修改值类型的实例方法

结构和枚举值类型。默认情况下,一个值类型的属性不能修改它的实例方法
然而,如果您需要修改的属性结构或枚举在一个特定的方法,你可以选择该方法的变化行为。但任何更改都会使它得编写的方法结束时回到原来的结构。当该方法结束时还可以分配一个完全新的实例对其隐含的self属性,而这个新的实例将取代现有的。
你可以选择这个行为之前将变异的关键字嵌入函数关键字的方法:

1234567891011
structPoint{var x =0.0, y =0.0
mutating func moveByX(deltaX:Double, y deltaY:Double){
x += deltaX
y += deltaY
}}var somePoint =Point(x:1.0, y:1.0)
somePoint.moveByX(2.0, y:3.0)
println("The point is now at (\(somePoint.x), \(somePoint.y))")// prints "The point is now at (3.0, 4.0)"

Point结构上面定义了一个变异moveByX方法,它通过一定量移动一个Point实例。而不是返回一个新的起点,这种方法实际上会修改在其上调用点。该变异包含被添加到它的定义,使其能够修改其属性。
请注意,您不能调用变异方法结构类型的常数,因为它的属性不能改变,即使它们是可变的特性,如在固定结构实例存储的属性描述:

123
let fixedPoint =Point(x:3.0, y:3.0)
fixedPoint.moveByX(2.0, y:3.0)// this will report an error

   分配中的self变异方法
变异的方法可以分配一个全新的实例隐含的self属性。上面所示的点的例子也可以写成下面的方式来代替:

123456
structPoint{var x =0.0, y =0.0
mutating func moveByX(deltaX:Double, y deltaY:Double){self=Point(x: x + deltaX, y: y + deltaY)}}

此版本的突变moveByX方法创建一个全新的结构,它的x和y值被设置到目标位置。调用该方法的结果和早期版本是完全一样的

变异的方法枚举可以设置self参数是从同一个枚举不同的成员

123456789101112131415161718
enumTriStateSwitch{caseOff,Low,High
mutating func next(){switchself{caseOff:self=LowcaseLow:self=HighcaseHigh:self=Off}}}var ovenLight =TriStateSwitch.Low
ovenLight.next()// ovenLight is now equal to .High
ovenLight.next()// ovenLight is now equal to .Off

这个例子定义了一个三态开关枚举。三种不同的功率状态之间的切换周期(关,低,高)

2、类型方法

如上所述,实例方法的方法要求一个特定类型的实例。您还可以定义该类型自身的方法,这种方法被称为type方法,您显示的type方法直接在类结构体里面用class func开头 ,对于枚举和结构来说,类型方法是用static func开头。
请注意;
在objective – c中,您可以定义type-level方法仅为objective – c类。在Swift可以为所有类定义type-level方法,结构,和枚举。每种方法的显示局限于它所支持的类型。
类型方法调用dot syntax,就像实例方法。但是,您调用的是类型的方法,而不是该类型的一个实例。这里是你如何调用一个类调用SomeClass的一个类型的方法:

123456
classSomeClass{class func someTypeMethod(){// type method implementation goes here}}SomeClass.someTypeMethod()

在类型方法的主体,隐含的self属性是指类型本身,而不是该类型的一个实例。对于结构体和枚举,这意味着你可以使用自助静态属性和静态方法的参数消除歧义,就像你做的实例属性和实例方法的参数。
更普遍的是,你一个类型的方法体中使用任何不合格的方法和属性名称会参考其他 type-level方法和属性。 一种方法可以调用另一个类的方法与其他方法的名称,而不需要与类型名称前缀了。同样,结构和枚举类型的方法可以使用静态属性的名称,没有类型名称前缀访问静态属性。
下面的例子定义了一个名为LevelTracker结构,它通过游戏的不同层次或阶段跟踪球员的进步。这是一个单人游戏,但可以存储的信息为一个单一的设备上的多个玩家。
所有的游戏的水平(除了一级)当游戏第一次玩。每当玩家完成一个级别,该级别解锁设备上的所有玩家。LevelTracker结构使用静态属性和方法来跟踪哪些级别的比赛已经解锁。它还跟踪当前个别球员水平

1234567891011121314151617181920
structLevelTracker{staticvar highestUnlockedLevel =1static func unlockLevel(level:Int){if level > highestUnlockedLevel {
highestUnlockedLevel = level
}}static func levelIsUnlocked(level:Int)->Bool{return level <= highestUnlockedLevel
}var currentLevel =1
mutating func advanceToLevel(level:Int)->Bool{ifLevelTracker.levelIsUnlocked(level){
currentLevel = level
returntrue}else{returnfalse}}}

该LevelTracker结构跟踪任何玩家解锁的最高水平。这个值是存储在一个名为highestUnlockedLevel的静态属性。

LevelTracker还定义了两种类型的功能与highestUnlockedLevel,首先是一种叫做unlockLevel功能,每当一个新的水平解锁都会用来更新highestUnlockedLevel,第二个是levelIsUnlocked功能,如果一个特定的水平数已经解锁,就会返回ture。注意,这些类型的方法可以访问highestUnlockedLevel静态属性但是你需要把它写成LevelTracker.highestUnlockedLevel)。

除了它的静态属性和类型的方法,LevelTracker通过游戏追踪每个玩家的进度。它使用被称为currentLevel实例属性来跟踪玩家级别。
为了帮助管理urrentLevel属性,advanceToLevel LevelTracker定义一个实例方法。这种方法更新currentLevel之前,用来检查是否要求新的水平已经解除锁定。该advanceToLevel方法返回一个布尔值来指示它是否能够设置currentLevel。
该LevelTracker结构使用Player类,如下所示,跟踪和更新单个球员的进步:

1234567891011
classPlayer{var tracker =LevelTracker()
let playerName:String
func completedLevel(level:Int){LevelTracker.unlockLevel(level +1)
tracker.advanceToLevel(level +1)}
init(name:String){
playerName = name
}}

Player类创建LevelTracker的一个新实例来跟踪球员的进步。它也提供了一个名为completedLevel方法,每当玩家到达一个特定的级别,这种方法就会解锁一个新的级别和进度并把玩家移到下一个级别。(advanceToLevel返回的布尔值将被忽略,因为已知被调用LevelTracker.unlockLevel。)
您可以创建一个新球员Player 的实例,看看当玩家完成一个级别会发生什么:

1234
var player =Player(name:"Argyrios")
player.completedLevel(1)
println("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")// prints "highest unlocked level is now 2"

如果你创建第二个球员,你想尝试移动到尚未被游戏解锁的级别,就会出现当前级别失败

1234567
player =Player(name:"Beto")if player.tracker.advanceToLevel(6){
println("player is now on level 6")}else{
println("level 6 has not yet been unlocked")}// prints "level 6 has not yet been unlocked"

 

感谢翻译小组成员:李起攀(微博)、若晨(微博)、YAO、粽子、山有木兮木有枝、渺-Bessie、墨离、矮人王、CXH、Tiger大顾(微博)
个人转载请注明出处和原始链接,商业转载请联系我们~ 感谢您对我们工作的支持~

Swift中文教程(十)属性

转载自letsswift.com

属性是描述特定类、结构或者枚举的值。存储属性作为实例的一部分存储常量与变量的值,而计算属性计算他们的值(不只是存储)。计算属性存在于类、结构与枚举中。存储属性仅仅只在类与结构中。

属性通常与特定类型实例联系在一起。但属性也可以与类型本身联系在一起,这样的属性称之为类型属性。

另外,可以定义属性观察者来处理属性值发生改变的情况,这样你就可以对用户操作做出反应。属性观察者可以被加在自己定义的存储属性之上,也可以在从父类继承的子类属性之上。

1、存储属性
最简单的情形,作为特定类或结构实例的一部分,存储属性存储着常量或者变量的值。存储属性可分为变量存储属性(关键字var描述)和常量存储属性(关键字let描述)。

当定义存储属性时,你可以提供一个默认值,这些在“默认属性值”描述。在初始化过程中你也可以设置或改变存储属性的初值。这个准则对常量存储属性也同样适用(在“初始化过程中改变常量属性”描述)

下面的例子定义了一个叫FixedLengthRange的结构,它描述了一个一定范围内的整数值,当创建这个结构时,范围长度是不可以被改变的:

1
2
3
4
5
6
7
8
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被初始化之后它的值是不可以被改变的

常量结构实例的存储属性
如果你创建一个结构实例,并将其赋给一个常量,这个实例中的属性将不可以被改变,即使他们被声明为变量属性

1
2
3
4
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// this range represents integer values 0, 1, 2, and 3
rangeOfFourItems.firstValue = 6
// this will report an error, even thought firstValue is a variable property

因为rangeOfFourItems是一个常量(let),即便firstValue是一个变量属性,它的值也是不可以被改变的

这样的特性是因为结构是值类型。当一个值类型实例作为常量而存在,它的所有属性也作为常量而存在。

而这个特性对类并不适用,因为类是引用类型。如果你将引用类型的实例赋值给常量,依然能够改变实例的变量属性。

Lazy Stored Properties(懒惰存储属性?)
懒惰存储属性是当它第一次被使用时才进行初值计算。通过在属性声明前加上@lazy来标识一个懒惰存储属性。

注意
必须声明懒惰存储属性为变量属性(通过var),因为它的初始值直到实例初始化完成之后才被检索。常量属性在实例初始化完成之前就应该被赋值,因此常量属性不能够被声明为懒惰存储属性。

当属性初始值因为外部原因,在实例初始化完成之前不能够确定时,就要定义成懒惰存储属性。当属性初始值需要复杂或高代价的设置,在它需要时才被赋值时,懒惰存储属性就派上用场了。

下面的例子使用懒惰存储属性来防止类中不必要的初始化操作。它定义了类DataImporter和类DataManager:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 += "Some data"
manager.data += "Some more data"
// the DataImporter instance for the importer property has not yet been created

类DataManager有一个称为data的存储属性,它被初始化为一个空的String数组。虽然DataManager定义的其它部分并没有写出来,但可以看出DataManager的目的是管理String数据并为其提供访问接口。

DataManager类的部分功能是从文件中引用数据。这个功能是由DataImporter类提供的,这个类需要一定的时间来初始化,因为它的实例需要打开文件并见内容读到内存中。

因为DataManager实例可能并不需要立即管理从文件中引用的数据,所以在DataManager实例被创建时,并不需要马上就创建一个新的DataImporter实例。这就使得当DataImporter实例在需要时才被创建理所当然起来。

因为被声明为@lazy属性,DataImporter的实例importer只有在当它在第一次被访问时才被创建。例如它的fileName属性需要被访问时:

1
2
3
println(manager.importer.fileName)
// the DataImporter instance for the importer property has now been created
// prints "data.txt

存储属性与实例变量
如果你使用过Objective-C,你应该知道它提供两种方式来存储作为类实例一部分的值与引用。除了属性,你可以使用实例变量作为属性值的后备存储

Swift使用一个单一属性声明来统一这些概念。一个Swift属性没有与之相符的实例变量,并且属性的后备存储也不能直接访问。这防止了在不通上下文中访问值的混淆,并且简化属性声明成为一个单一的、最终的语句。关于属性的所有信息-包含名称、类型和内存管理等-作为类型定义的一部分而定义。

2、计算属性
除了存储属性,类、结构和枚举能够定义计算属性。计算属性并不存储值,它提供getter和可选的setter来间接地获取和设置其它的属性和值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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)
println("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// prints "square.origin is now at (10.0, 10.0)"

这个例子定义了三个处理几何图形的结构:
Point包含一个(x,y)坐标
Size包含宽度width和高度height
Rect定义了一个长方形,包含原点和大小size
Rect结构包含一个称之为center的计算属性。Rect当前中心点的坐标可以通过origin和size属性得来,所以并不需要显式地存储中心点的值。取而代之的是,Rect定义一个称为center的计算属性,它包含一个get和一个set方法,通过它们来操作长方形的中心点,就像它是一个真正的存储属性一样。

例子中定义了一个名为square的Rect变量,它的中心点初始化为(0, 0),高度和宽度初始化为10,由以下图形中的蓝色正方形部分。

变量square的center属性通过点操作符访问,它会调用center的getter方法。不同于直接返回一个存在的值,getter方法要通过计算才能返回长方形的中心点的值(point)。以上的例子中,getter方法返回中心点(5,5)。

然后center属性被设置成新的值(15,15),这样就把这个正方形向右向上移动到了途中黄色部分所表示的新的位置。通过调用setter方法来设置center,改变origin中坐标x和y的值,将正方形移动到新的位置。

image

setter声明的简略写法
如果计算属性的setter方法没有将被设置的值定义一个名称,将会默认地使用newValue这个名称来代替。下面的例子采用了这样一种特性,定义了Rect结构的新版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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方法,通过点操作符,可以放回属性值,但是不能修改它的值。
注意
应该使用var关键字将计算属性-包含只读计算属性-定义成变量属性,因为它们的值并不是固定的。let关键字只被常量属性说使用,以表明一旦被设置它们的值就是不可改变的了

通过移除get关键字和它的大括号,可以简化只读计算属性的定义:

1
2
3
4
5
6
7
8
9
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)
println("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// prints "the volume of fourByFiveByTwo is 40.0

这个例子定义了一个三维长方体结构Cuboid,包含了长宽高三个属性,和一个表示长方体容积的只读计算属性volume。volume值是不可被设置的,因为它直接由长宽高三个属性计算而来。通过提供这样一个只读计算属性,Cuboid使外部用户能够访问到其当前的容积值。

3、属性观察者
属性观察者观察属性值的改变并对此做出响应。当设置属性的值时,属性观察者就被调用,即使当新值同原值相同时也会被调用。

除了懒惰存储属性,你可以为任何存储属性加上属性观察者定义。另外,通过重写子类属性,也可以继承属性(存储或计算)加上属性观察者定义。属性重写在“重写”章节定义。

注意
不必为未重写的计算属性定义属性观察者,因为可以通过它的setter方法直接对值的改变做出响应

定义属性的观察者时,你可以单独或同时使用下面的方法:
willSet:设置值前被调用
didSet:设置值后立刻被调用

当实现willSet观察者时,新的属性值作为常量参数被传递。你可以为这个参数起一个名字,如果不的话,这个参数就默认地被命名成newValue。

在实现didSet观察者时也是一样,只不过传递的产量参数表示的是旧的属性值。

注意:
属性初始化时,willset和didSet并不会被调用。只有在初始化上下文之外,当设置属性值时才被调用

下面是一个willSet和didSet用法的实例。定义了一个类StepCounter,用来统计人走路时的步数。它可以从计步器或其它计数器上获取输入数据,对日常联系锻炼的步数进行追踪。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
println("About to set totalSteps to \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue  {
println("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声明了一个Int类型的、含有willSet和didSet观察者的存储属性totalSteps。当这个属性被赋予新值时,willSet和didSet将会被调用,即使新值和旧值是相同的。

例子中的willSet观察者为参数起了个新的名字newTotalSteps,它简单地打印了即将被设置的值。

当totalSteps值被更新时,didSet观察者被调用,它比较totalSteps的新值和旧值,如果新值比旧值大,就打印所增加的步数。didSet并没有为旧值参数命名,在本例中,将会使用默认的名字oldValue来表示旧的值。

注意
如果通过didSet来设置属性的值,即使属性值刚刚被设置过,起作用的也将会是didSet,即新值是didSet设置的值

4、全局和局部变量
以上所写的关于计算与观察属性值的特性同样适用于全局和局部变量。全局变量是在任何函数、方法、闭包、类型上下文外部定义的变量,而局部变量是在函数、方法、闭包中定义的变量。

前面章节所遇到过的全局、局部变量都是存储变量。和存储属性一样,存储变量为特定类型提供存储空间并且可以被访问

但是,你可以在全局或局部范围定义计算变量和存储变量观察者。计算变量并不存储值,只用来计算特定值,它的定义方式与计算属性一样。

注意
全局常量和变量通常是延迟计算的,跟懒惰存储属性一样,但是不需要加上@lazy。而局部常量与变量不是延迟计算的。

5、类型属性
实例属性是特定类型实例的属性。当创建一个类型的实例时,这个实例有自己的属性值的集合,这将它与其它实例区分开来。

也可以定义属于类型本身的属性,即使创建再多的这个类的实例,这个属性也不属于任何一个,它只属于类型本身,这样的属性就称为类型属性。

类型属性适用于定义那些特定类型实例所通用的属性,例如一个可以被所有实例使用的常量属性(就像c中的静态常量),或者变量属性(c中的静态变量)。

可以为值类型(结构、枚举)定义存储类型属性和计算类型属性。对类而言,只能够定义计算类型属性。

值类型的存储类型属性可以是常量也可以是变量。而计算类型属性通常声明成变量属性,类似于计算实例属性

注意
不想存储实例属性,你需要给存储类型属性一个初始值。因为类型本身在初始化时不能为存储类型属性设置值

类型属性句法
在C和Objective-C中,定义静态常量、变量和全局静态变量一样。但是在swift中,类型属性的定义要放在类型定义中进行,在类型定义的大括号中,显示地声明它在类型中的作用域。

对值类型而言,定义类型属性使用static关键字,而定义类类型的类型属性使用class关键字。下面的例子展示了存储和计算类型属性的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
// return an Int value here
}
}
enum SomeEnumeration {
static var storedTypeProperty = "Some value."     static var computedTypeProperty: Int {     // return an Int value here
}
}
class SomeClass {
class var computedTypeProperty: Int {
// return an Int value here
}
}

注意
上面的例子是针对只读计算类型属性而言的,不过你也可以像计算实例属性一样定义可读可写的计算类型属性

查询与设置类型属性
像实例属性一样,类型属性通过点操作符来查询与设置。但是类型属性的查询与设置是针对类型而言的,并不是针对类型的实例。例如:

1
2
3
4
5
6
7
println(SomeClass.computedTypeProperty)
// prints "42"
println(SomeStructure.storedTypeProperty)
// prints "Some value."
SomeStructure.storedTypeProperty = "Another value."
println(SomeStructure.storedTypeProperty)
// prints "Another value.

下面的例子在一个结构中使用两个存储类型属性来展示一组声音通道的音频等级表。每个通道使用0到10来表示声音的等级。

从下面的图表中可以看出,使用了两组声音通道来表示一个立体声音频等级表。当一个通道的等级为0时,所有的灯都不会亮,当等级为10时,所有的灯都会亮。下面的图中,左边的通道表示声音等级为9,右边的为7

image

上述的声音通道由以下的AudioChannel结构实例来表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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定义了音频所能达到的最高等级,对所有的AudoChannel实例而言,是个值为10的常量。当一个声音信号的值超过10时,会被截断为其阈值10。

第二个类型属性是一个变量存储属性maxInputLevelForAllChannels。它保存了当前所有AudioChannel实例中所接受到声音的最高等级,它被初始化为0。

结构还定义了一个存储实例属性currentLevel,表示当前的通道声音等级。这个属性使用didSet属性观察者来检测currentLevel的改变。这个观察者执行两道检查:
如果currentlevel的新值比阈值thresholdLevel大,currentLevel将被设置成thresholdLevel
如果currentLevel的新值比所有AudioChannel实例之前接受到的最大声音等级还要大,那么maxInputLevelForAllChannles将会被设置成cueentLevel大值。

注意
第一道检查中,didSet为currentLevel设置了新值。这并不会造成观察者再次被调用

可以创建两个AudioChannel实例,leftChannel和rightChannel,来表示一个立体声系统:

1
2
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()

如果设置左通道的currentLevel为7,它的类型属性maxInputLevelForAllChannels将更新成为7:

1
2
3
4
5
6
7
8
9
10
11
12
leftChannel.currentLevel = 7
println(leftChannel.currentLevel)
// prints "7"
println(AudioChannel.maxInputLevelForAllChannels)
// prints "7”
 
如果像设置右通道的currentlevel为11,它的值将被截短成为10,而且maxInputLevelForAllChannels的值也将更新为10:
“rightChannel.currentLevel = 11
println(rightChannel.currentLevel)
// prints "10"
println(AudioChannel.maxInputLevelForAllChannels)
// prints "10"

感谢翻译小组成员:李起攀(微博)、若晨(微博)、YAO、粽子、山有木兮木有枝、渺-Bessie、墨离、矮人王、CXH、Tiger大顾(微博)
个人转载请注明出处,商业转载请联系我们~ 感谢您对我们工作的支持~

Swift中文教程(九)类与结构

转载自letsswift.com

类与结构是编程人员在代码中会经常用到的代码块。在类与结构中可以像定义常量,变量和函数一样,定义相关的属性和方法以此来实现各种功能。

和其它的编程语言不太相同的是,Swift不需要单独创建接口或者实现文件来使用类或者结构。Swift中的类或者结构可以在单文件中直接定义,一旦定义完成后,就能够被直接其它代码使用。

注意:一个类的实例一般被视作一个对象,但是在Swift中,类与结构更像是一个函数方法,在后续的章节中更多地是讲述类和结构的功能性。

1、类和结构的异同

类和结构有一些相似的地方,它们都可以:

定义一些可以赋值的属性;

定义具有功能性的方法

定义下标,使用下标语法

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

在原实现方法上的可扩展性

根据协议提供某一特定类别的基本功能

更多内容可以阅读:属性,方法,下标,初始化,扩展和协议等章节

类还有一些结构不具备的特性:

类的继承性

对类实例实时的类型转换

析构一个类的实例使之释放空间

引用计数,一个类实例可以有多个引用

更多内容可以阅读:继承,类型转换,初始化自动引用计数

注意:结构每次在代码中传递时都是复制了一整个,所以不要使用引用计数

 

定义语法

类和结构拥有相似的定义语法,使用class关键词定义一个类,struct关键词定义结构。每个定义都由一对大括号包含:

1
2
3
4
5
6
class SomeClass {
// class definition goes here
}
struct SomeStructure {
// structure definition goes here
}

注意:在定义类和结构时,一般使用UpperCamelCase命名法来定义类和结构的名称,比如SomeClass和SomeStructure,这样也符合Swift其它类型的标准。而给属性和方法命名时,一般时候lowerCamelCase命名法,比如frameRate和incrementCount等。
下面是一个结构和一个类的定义示例:

1
2
3
4
5
6
7
8
9
10
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = falsevar
frameRate = 0.0
var name: String?
}

上面的例子首先定义了一个叫Resolution的结构,用来描述一个像素显示的分辨率,它有两个属性分别叫width和height。这两个属性被默认定义为Int类型,初始化为0.

之后定义了一个叫VideoMode的类,为视频显示的显示方式。这个类有四个属性,第一个属性resolution本身又是一个结构,然后是另外两个属性。最后一个属性用到了可选字符串类型String?,表示这个属性可以存在,或者不存在为nil。

类和结构的实例

上面的两个定义仅仅是定义了结构Resolution和类VideoMode的整体样式,它们本身不是一个特定的分辨率或者显示方式,这时候就需要实例化这个结构和类。

实例化的语法相似:

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

类和结构都使用实例语法来完成实例化。最简单的实例语法就是用两个括号()完成。在这种情况下定义的实例中的属性都会完成默认初始化。更多内容可以参考初始化一章。

访问属性

使用.语法就可以方便地访问一个实例的属性。在.语法中,在实例名之后加上(.)再加上属性名即可,不需要空格:

1
2
println("The width of someResolution is \(someResolution.width)")
// prints "The width of someResolution is 0"

在这个例子中,someResolution.width表示someResolution的width属性,返回了它的初始值0

也可以使用.语法连续地获取属性的属性,比如VideoMode中resolution属性的width属性

1
2
println("The width of someVideoMode is \(someVideoMode.resolution.width)")
// prints "The width of someVideoMode is 0"

使用这种方法不仅可以访问,也可以赋值:

1
2
3
someVideoMode.resolution.width = 1280
println("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// prints "The width of someVideoMode is now 1280"

注意:和Objective-C不同,Swift能够直接设置一个结构属性的子属性,就像上面这个例子一样。

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

每个结构都有一个成员初始化方法,可以在初始化的时候通过使用属性名称来指定每一个属性的初始值:

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

但是和结构不同,类实例不能够使用成员初始化方法,在初始化一章有专门的介绍。

 

2、结构和枚举类型是数值类型

数值类型是说当它被赋值给一个常量或者变量,或者作为参数传递给函数时,是完整地复制了一个新的数值,而不是仅仅改变了引用对象。

事实上读到这里你已经在前面几章见过数值类型了,所有Swift中的基础类型-整型,浮点型,布尔类型,字符串,数组和字典都是数值类型。它们也都是由结构来实现的。

在Swift中所有的结构和枚举类型都是数值类型。这意味这你实例化的每个结构和枚举,其包含的所有属性,都会在代码中传递的时候被完整复制。

下面的这个例子可以说明这个特性:

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

声明了一个常量hd,是Resolution的实例化,宽度是1920,高度是1080,然后声明了一个变量cinema,和hd相同。这个时候表明,cinema和hd是两个实例,虽然他们的宽度都是1920,高度都是1080。

如果把cinema的宽度更改为2048,hd的宽度不会变化,依然是1920

1
2
3
4
5
cinema.width = 2048
println("cinema is now \(cinema.width) pixels wide")
// prints "cinema is now 2048 pixels wide"
println("hd is still \(hd.width) pixels wide")
// prints "hd is still 1920 pixels wide"

这表明当hd被赋值给cinema时,是完整地复制了一个全新的Resolution结构给cinema,所以当cinema的属性被修改时,hd的属性不会变化。

下面的例子演示的是枚举类型:

1
2
3
4
5
6
7
8
9
10
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"

尽管经过几次赋值,rememberedDirection依然没有变化,这是因为在每一次赋值过程中,都是将数值类型完整地复制了过来。

 

3、类是引用类型

和数值类型不同引用类型不会复制整个实例,当它被赋值给另外一个常量或者变量的时候,而是会建立一个和已有的实例相关的引用来表示它。

下面是引用的示例,VideoMode被定义为一个类:

1
2
3
4
5
let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0

分别将这个实例tenEighty的四个属性初始化,然后tenEighty被赋值给了另外一个叫alsoTenEighty的常量,然后alsoTenEighty的frameRate被修改了

1
2
let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0

由于类是一个引用类型,所以tenEighty和alsoTenEighty实际上是同一个实例,仅仅只是使用了不同的名称而已,我们通过检查frameRate可以证明这个问题:

1
2
println("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// prints "The frameRate property of tenEighty is now 30.0"

注意到tenEighty和alsoTenEighty是被定义为常量的,而不是变量。但是我们还是可以改变他们的属性值,这是因为它们本身实际上没有改变,它们并没有保存这个VideoMode的实例,仅仅只是引用了一个VideoMode实例,而我们修改的也是它们引用的实例中的属性。

特征操作

因为类是引用类型,那么就可能存在多个常量或者变量只想同一个类的实例(这对于数值类型的结构和枚举是不成立的)。

可以通过如下两个操作来判断两个常量或者变量是否引用的是同一个类的实例:

相同的实例(===)

不同的实例(!==)

使用这些操作可以检查:

1
2
3
4
if tenEighty === alsoTenEighty {
println("tenEighty and alsoTenEighty refer to the same Resolution instance.")
}
// prints "tenEighty and alsoTenEighty refer to the same Resolution instance."

注意是相同的实例判断使用三个连续的等号,这和相等(两个等号)是不同的

实例相同表示的是两个变量或者常量所引用的是同一个类的实例

相等是指两个实例在数值上的相等,或者相同。

当你定义一个类的时候,就需要说明什么样的时候是两个类相等,什么时候是两个类不相等。更多内容可以从相等操作一章中获得。

指针

如果你有C,C++或者Objective-C的编程经验,你一定知道在这些语言中使用指针来引用一个内存地址。Swift中引用一个实例的常量或变量跟C中的指针类似,但是不是一个直接指向内存地址的指针,也不需要使用*记号表示你正在定义一个引用。Swift中引用和其它变量,常量的定义方法相同。

4、如何选择使用类还是结构

在代码中可以选择类或者结构来实现你所需要的代码块,完成相应的功能。但是结构实例传递的是值,而类实例传递的是引用。那么对于不同的任务,应该考虑到数据结构和功能的需求不同,从而选择不同的实例。

一般来说,下面的一个或多个条件满足时,应当选择创建一个结构:

结构主要是用来封装一些简单的数据值

当赋值或者传递的时候更希望这些封装的数据被赋值,而不是被引用过去

所有被结构存储的属性本身也是数值类型

结构不需要被另外一个类型继承或者完成其它行为

一些比较好的使用结构的例子:

一个几何形状的尺寸,可能包括宽度,高度或者其它属性,每个属性都是Double类型的

一个序列的对应关系,可能包括开始start和长度length属性,每个属性都是Int类型的

3D坐标系中的一个点,包括x,y和z坐标,都是Double类型

在其它情况下,类会是更好的选择。也就是说一般情况下,自定义的一些数据结构一般都会被定义为类。

 

5、集合类型的赋值和复制操作

Swift中,数组Array和字典Dictionary是用结构来实现的,但是数组与字典和其它结构在进行赋值或者作为参数传递给函数的时候有一些不同。

并且数组和字典的这些操作,又与Foundation中的NSArray和NSDictionary不同,它们是用类来实现的。

注意:下面的小节将会介绍数组,字典,字符串等的复制操作。这些复制操作看起来都已经发生,但是Swift只会在确实需要复制的时候才会完整复制,从而达到最优的性能。

字典的赋值和复制操作

每次将一个字典Dictionary类型赋值给一个常量或者变量,或者作为参数传递给函数时,字典会在赋值或者函数调用时才会被复制。这个过程在上面的小节:结构和枚举是数值类型中描述了。

如果字典中的键值是数值类型(结构或者枚举),它们在赋值的时候会同时被复制。相反,如果是引用类型(类或者函数),引用本身将会被复制,而不是类实例或者函数本身。字典的这种复制方式和结构相同。

下面的例子演示的是一个叫ages的字典,存储了一些人名和年龄的对应关系,当赋值给copiedAges的时候,里面的数值同时被完整复制。当改变复制了的数值的时候,原有的数值不会变化,如下例子:

1
2
var ages = ["Peter": 23, "Wei": 35, "Anish": 65, "Katya": 19]
var copiedAges = ages

这个字典的键是字符串String类型,值是Int类型,都是数值类型,那么在赋值的时候都会被完整复制。

1
2
3
copiedAges["Peter"] = 24
println(ages["Peter"])
// prints "23"

数组的赋值和复制操作

和字典Dictionary类型比起来,数组Array的赋值和复制操作就更加复杂。Array类型和C语言中的类似,仅仅只会在需要的时候才会完整复制数组的值。

如果将一个数组赋值给一个常量或者变量,或者作为一个参数传递给函数,复制在赋值和函数调用的时候并不会发生。这两个数组将会共享一个元素序列,如果你修改了其中一个,另外一个也将会改变。

对于数组来说,复制只会在你进行了一个可能会修改数组长度操作时才会发生。包括拼接,添加或者移除元素等等。当复制实际发生的时候,才会像字典的赋值和复制操作一样。

下面的例子演示了数组的赋值操作:

1
2
3
var a = [1, 2, 3]
var b = a
var c = a

数组a被赋值给了b和c,然后输出相同的下标会发现:

1
2
3
4
5
6
println(a[0])
// 1
println(b[0])
// 1
println(c[0])
// 1

如果改变a中的某个值,会发现b和c中的数值也会跟着改变,因为赋值操作没有改变数组的长度:

1
2
3
4
5
6
7
a[0] = 42
println(a[0])
// 42
println(b[0])
// 42
println(c[0])
// 42

但是,如果在a中添加一个新的元素,那么就改变了数组的长度,这个时候就会发生实际的复制操作。如果再改变a中元素的值,b和c中的元素将不会发生改变:

1
2
3
4
5
6
7
8
a.append(4)
a[0] = 777
println(a[0])
// 777
println(b[0])
// 42
println(c[0])
// 42

设置数组是唯一的

如果可以在对数组进行修改前,将它设置为唯一的就最好了。我们可以通过使用unshare方法来将数组自行拷贝出来,成为一个唯一的实体。

如果多个变量引用了同一个数组,可以使用unshare方法来完成一次“独立”

b.unshare()

这时候如果再修改b的值,c的值也不会再受影响

1
2
3
4
5
6
7
b[0] = -105
println(a[0])
// 777
println(b[0])
// -105
println(c[0])
// 42

检查两个数组时候共用了相同的元素

使用实例相等操作符来判断两个数组是否共用了元素(===和!===)

下面这个例子演示的就是判断是否共用元素:

1
2
3
4
5
6
if b === c {
println("b and c still share the same array elements.")
} else {
println("b and c now refer to two independent sets of array elements.")
}
// prints "b and c now refer to two independent sets of array elements."

也可以使用这个操作来判断两个子数组是否有共用的元素:

1
2
3
4
5
6
if b[0...1] === b[0...1] {
println("These two subarrays share the same elements.")
} else {
println("These two subarrays do not share the same elements.")
}
// prints "These two subarrays share the same elements."

强制数组拷贝

通过调用数组的copy方法来完成强制拷贝。这个方法将会完整复制一个数组到新的数组中。

下面的例子中这个叫names的数组会被完整拷贝到copiedNames中去。

1
2
var names = ["Mohsen", "Hilary", "Justyn", "Amy", "Rich", "Graham", "Vic"]
var copiedNames = names.copy()

通过改变copiedNames的值可以验证,数组已经被完整拷贝,不会影响到之前的数组:

1
2
3
copiedNames[0] = "Mo"
println(names[0])
// prints "Mohsen"

注意:如果你不确定你需要的数组是否是独立的,那么仅仅使用unshare就可以了。而copy方法不管当前是不是独立的,都会完整拷贝一次,哪怕这个数组已经是unshare的了。

 

感谢翻译小组成员:李起攀(微博)、若晨(微博)、YAO、粽子、山有木兮木有枝、渺-Bessie、墨离、矮人王、CXH、Tiger大顾(微博)
个人转载请注明出处,商业转载请联系我们~ 感谢您对我们工作的支持~

Swift中文教程(八)枚举类型

转载自letsswift.com

枚举定义了一个常用的具有相关性的一组数据,并在你的代码中以一个安全的方式使用它们。
如果你熟悉C语言,你就会知道,C语言中的枚举指定相关名称为一组整数值。在Swift中枚举更为灵活,不必为枚举的每个成员提供一个值。如果一个值(被称为“原始”的值)被提供给每个枚举成员,则该值可以是一个字符串,一个字符,或者任何整数或浮点类型的值。
另外,枚举成员可以指定任何类型,每个成员都可以存储的不同的相关值,就像其他语言中使用集合或变体。你还可以定义一组通用的相关成员为一个枚举,每一种都有不同的一组与它相关的适当类型的值的一部分。
在Swift中枚举类型是最重要的类型。它采用了很多以前只有类才具有的特性,如计算性能,以提供有关枚举的当前值的更多信息,方法和实例方法提供的功能相关的枚举表示的值传统上支持的许多功能。枚举也可以定义初始化,以提供一个初始成员值;可以在原有基础上扩展扩大它们的功能;并使用协议来提供标准功能。
欲了解更多有关这些功能,请参见Properties, Methods, Initialization, Extensions, Protocols

1、枚举语法

使用枚举enum关键词并把他们的整个定义在一对大括号内:

1
2
3
enum SomeEnumeration {
    // enumeration definition goes here
}

下面是一个指南针的四个点一个例子:

1
2
3
4
5
6
enum CompassPoint {
    case North
    case South
    case East
    case West
}

在枚举中定义的值(如North,South,East和West)是枚举的成员值(或成员)。这个例子里case关键字表示成员值一条新的分支将被定义。

Note
不像C和Objective-C,Swift枚举成员在创建时不分配默认整数值。在上面的例子CompassPoints中North,South,Eath,West不等于隐含0,1,2和3,而是一种与CompassPoint明确被定义的类型却各不相同的值。

多个成员的值可以出现在一行上,用逗号分隔:

1
2
3
enum Planet {
    case Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
}

每个枚举定义中定义了一个全新的类型。像其他Swift的类型,它们的名称(如CompassPoint和Planet)应为大写字母。给枚举类型单数而不是复数的名字,这样理解起来更加容易如:

1
var directionToHead = CompassPoint.West

使用directionToHead的类型时,用CompassPoint的一个可能值初始化的推断。一旦directionToHead被声明为一个CompassPoint,您可以将其设置为使用更短的.语法而不用再书写枚举CompassPoint值本身:

1
directionToHead = .East

directionToHead的类型是已知的,所以你可以在设定它的值时,不写该类型。使用类型明确的枚举值可以让代码具有更好的可读性。

2、匹配枚举值与switch语句

你可以使用单个枚举值匹配switch语句:

1
2
3
4
5
6
7
8
9
10
11
12
directionToHead = .South
switch directionToHead {
case .North:
    println("Lots of planets have a north")
case .South:
    println("Watch out for penguins")
case .East:
    println("Where the sun rises")
case .West:
    println("Where the skies are blue")
}
// prints "Watch out for penguins"

你可以理解这段代码:

“考虑directionToHead的价值。当它等于North,打印“Lots of planets have a north”。当它等于South,打印“Watch out for penguins”等等。

正如控制流所描述,Switch语句考虑枚举的成员,如果省略了West时,这段代码无法编译,因为它没有考虑CompassPoint成员的完整性。Switch语句要求全面性确保枚举成员,避免不小心漏掉情况发生。

当它不需要为每一个枚举成员都匹配的情况下,你可以提供一个默认default分支来涵盖未明确提到的任何成员:

1
2
3
4
5
6
7
8
let somePlanet = Planet.Earth
switch somePlanet {
case .Earth:
    println("Mostly harmless")
default:
    println("Not a safe place for humans")
}
// prints "Mostly harmless"

3、关联值

在上一节中的示例延时了一个枚举的成员是如何被定义(分类)的。你可以为Planet.Earth设置一个常量或变量,然后在代码中检查这个值。但是,它有时是有用的才能存储其它类型的关联值除了这些成员的值。这让你随着成员值存储额外的自定义信息,并允许在你的代码中来使用该信息。

Swift的枚举类型可以由一些数据类型相关的组成,如果需要的话,这些数据类型可以是各不相同的。枚举的这种特性跟其它语言中的奇异集合,标签集合或者变体相似

例如,假设一个库存跟踪系统需要由两种不同类型的条形码来跟踪产品。有些产品上标有UPC-A代码格式,它使用数字0到9的一维条码,每一个条码都有一个“数字系统”的数字,后跟十“标识符”的数字。最后一位是“检查”位,以验证代码已被正确扫描:

image 其他产品都贴有二维条码QR码格式,它可以使用任何的ISO8859-1字符,并可以编码字符串,最多2,953个字符:

image

这将是方便的库存跟踪系统能够存储UPC-A条码作为三个整数的元组,和QR代码的条形码的任何长度的字符串。

在Swift中可以使用一个枚举来定义两种类型的产品条形码,结构可以是这样的:

1
2
3
4
enum Barcode {
    case UPCA(Int, Int, Int)
    case QRCode(String)
}

这可以被理解为:

“定义一个名为条形码枚举类型,它可以是UPC-A的任一值类型的关联值(Int,Int,Int),或QRCode的一个类型为String的关联值。”

这个定义不提供任何实际的Int或String值,它只是定义了条形码常量和变量当等于Barcode.UPCA或Barcode.QRCode关联值的类型的时候的存储形式。

然后可以使用任何一种类型来创建新的条码:

1
var productBarcode = Barcode.UPCA(8, 85909_51226, 3)

此示例创建一个名为productBarcode新的变量,并与相关联的元组值赋给它Barcode.UPCA的值(8,8590951226,3)。提供的“标识符”值都有整数加下划线的文字,85909_51226,使其更易于阅读的条形码。

同一产品可以分配不同类型的条形码:

1
productBarcode = .QRCode("ABCDEFGHIJKLMNOP")

在这一点上,原来Barcode.UPCA和其整数值被新的Barcode.QRCode及其字符串值代替。_条形码的常量和变量可以存储任何一个_UPCA或QRCode的(连同其关联值),但它们只能存储其中之一在任何指定时间。

不同的条码类型像以前一样可以使用一个switch语句来检查,但是这一次相关的值可以被提取作为switch语句的一部分。您提取每个相关值作为常数(let前缀)或变量(var前缀)不同的情况下,在switch语句的case代码内使用:

1
2
3
4
5
6
7
switch productBarcode {
case .UPCA(let numberSystem, let identifier, let check):
    println("UPC-A with value of \(numberSystem), \(identifier), \(check).")
case .QRCode(let productCode):
    println("QR code with value of \(productCode).")
}
// prints "QR code with value of ABCDEFGHIJKLMNOP."

如果所有的枚举成员的关联值的提取为常数,或者当所有被提取为变量,为了简洁起见,可以放置一个var,或let标注在成员名称前:

1
2
3
4
5
6
7
switch productBarcode {
case let .UPCA(numberSystem, identifier, check):
    println("UPC-A with value of \(numberSystem), \(identifier), \(check).")
case let .QRCode(productCode):
    println("QR code with value of \(productCode).")
}
// prints "QR code with value of ABCDEFGHIJKLMNOP."

4、原始值

在关联值的条形码的例子演示了一个枚举的成员如何能声明它们存储不同类型的关联值。作为替代关联值,枚举成员可以拿出预先填入缺省值(称为原始值),从而具有相同的类型。

这里是一个存储原始的ASCII值命名枚举成员的一个例子:

1
2
3
4
5
enum ASCIIControlCharacter: Character {
    case Tab = "\t"
    case LineFeed = "\n"
    case CarriageReturn = "\r"
}

在这里,原始值被定义为字符类型的枚举叫做ASCIIControlCharacter,并设置了一些比较常见的ASCII控制字符。字符值的字符串和字符的描述。

注意,原始值是不相同关联值。原始值设置为预填充的值时,应先在你的代码中定义枚举,像上述三个ASCII码。对于一个特定的枚举成员的原始值始终是相同的。当你创建一个基于枚举的常量或变量的新成员的关联值设置,每次当你这样做的时候可以是不同的。

原始值可以是字符串,字符,或任何整数或浮点数类型。每个原始值必须在它的枚举中唯一声明。当整数被用于原始值,如果其他​​枚举成员没有值时,它们自动递增。

下面列举的是一个细化的早期Planet枚举,使用原始整数值来表示每个Planet的太阳系的顺序:

1
2
3
enum Planet: Int {
    case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
}

自动递增意味着Planet.Venus具有2的原始值,依此类推。

访问其toRaw方法枚举成员的原始值:

1
2
let earthsOrder = Planet.Earth.toRaw()
// earthsOrder is 3

使用枚举的fromRaw方法来试图找到一个特定的原始值枚举成员。这个例子识别Uranus的位置通过原始值为7:

1
2
let possiblePlanet = Planet.fromRaw(7)
// possiblePlanet is of type Planet? and equals Planet.Uranus

然而,并非所有可能的Int值都会找到一个匹配的星球。正因如此,该fromRaw方法返回一个可选的枚举成员。在上面的例子中,是possiblePlanet类型Planet?或“可选的Planet”。

如果你试图找到一个Planet为9的位置,通过fromRaw返回可选的Planet值将是无:

1
2
3
4
5
6
7
8
9
10
11
12
let positionToFind = 9
if let somePlanet = Planet.fromRaw(positionToFind) {
    switch somePlanet {
    case .Earth:
        println("Mostly harmless")
    default:
        println("Not a safe place for humans")
    }
} else {
    println("There isn't a planet at position \(positionToFind)")
}
// prints "There isn't a planet at position 9"

这个范例使用somePlanet= Planet.fromRaw(9)来尝试访问可选集合Planet,在可选Planet集合中设置检索条件somePlanet,在原始值为9的情况下,不能检索到位置为9的星球,所有else分支被执行。

 

感谢翻译小组成员:李起攀(微博)、若晨(微博)、YAO、粽子、山有木兮木有枝、渺-Bessie、墨离、矮人王、CXH、Tiger大顾(微博)
个人转载请注明出处,商业转载请联系我们~ 感谢您对我们工作的支持~

Swift中文教程(七)闭包

转载自letsswift.com

闭包(Closures)是独立的函数代码块,能在代码中传递及使用。Swift中的闭包与C和Objective-C中的代码块及其它编程语言中的匿名函数相似。
闭包可以在上下文的范围内捕获、存储任何被定义的常量和变量引用。因这些常量和变量的封闭性,而命名为“闭包(Closures)”。Swift能够对所有你所能捕获到的引用进行内存管理。

NOTE
假如你对“捕获(capturing)”不熟悉,请不要担心,具体可以参考Capturing Values(捕获值)。

全局函数和嵌套函数已在 Functions(函数)中介绍过,实际上这些都是特殊的闭包函数
全局函数都是闭包,特点是有函数名但没有捕获任何值。
嵌套函数都是闭包,特点是有函数名,并且可以在它封闭的函数中捕获值。
闭包表达式都是闭包,特点是没有函数名,可以使用轻量的语法在它所围绕的上下文中捕获值。
Swift的闭包表达式有着干净,清晰的风格,并常见情况下对于鼓励简短、整洁的语法做出优化。这些优化包括:
推理参数及返回值类型源自上下文
隐式返回源于单一表达式闭包
简约参数名
尾随闭包语法

1、闭包表达式
嵌套函数已经在Nested Functions(嵌套函数)中有所介绍,是种方便命名和定义自包含代码块的一种方式,然而,有时候在编写简短函数式的构造器时非常有用,它不需要完整的函数声明及函数名,尤其是在你需要调用一个或多个参数的函数时。
闭包表达式是一种编写内联闭包的方式,它简洁、紧凑。闭包表达式提供了数种语义优化,为的是以最简单的形式编程而不需要大量的声明或意图。以下以同一个sort函数进行几次改进,每次函数都更加简洁,以此说明闭包表达式的优化。

Sort函数
Swift的标准函数库提供了一个名为sort的函数,它通过基于输出类型排序的闭包函数,给已知类型的数组数据的值排序。一旦完成排序工作,会返回一个同先前数组相同大小,相同数据类型,并且的新数组,并且这个数组的元素都在正确排好序的位置上。
The closure expression examples below use the sort function to sort an array of String values in reverse alphabetical order. Here’s the initial array to be sorted:
以下的闭包表达式通过sort函数将String值按字母顺序进行排序作说明,这是待排序的初始化数组。

let names = [“Chris”, “Alex”, “Ewa”, “Barry”, “Daniella”]
sort函数需要两个参数:
一个已知值类型的数组
一个接收两个参数的闭包函数,这两个参数的数据类型都同于数组元素。并且
返回一个Bool表明是否第一个参数应排在第二个参数前或后。

这个例子是一组排序的字符串值,因此需要排序的封闭类型的函数(字符串,字符串)-> Bool。

One way to provide the sorting closure is to write a normal function of the correct type, and to pass it in as the sort function’s second parameter:

func backwards(s1: String, s2: String) -> Bool {
return s1 > s2
}
var reversed = sort(names, backwards)
// reversed is equal to [“Ewa”, “Daniella”, “Chris”, “Barry”, “Alex”]
If the first string (s1) is greater than the second string (s2), the backwards function will return true, indicating that s1 should appear before s2 in the sorted array. For characters in strings, “greater than” means “appears later in the alphabet than”. This means that the letter “B” is “greater than” the letter “A”, and the string “Tom” is greater than the string “Tim”. This gives a reverse alphabetical sort, with “Barry” being placed before “Alex”, and so on.
However, this is a rather long-winded way to write what is essentially a single-expression function (a > b). In this example, it would be preferable to write the sorting closure inline, using closure expression syntax.

Closure Expression Syntax
Closure expression syntax has the following general form:

{ (parameters) -> return type in
statements
}
Closure expression syntax can use constant parameters, variable parameters, and inout parameters. Default values cannot be provided. Variadic parameters can be used if you name the variadic parameter and place it last in the parameter list. Tuples can also be used as parameter types and return types.
The example below shows a closure expression version of the backwards function from earlier:

reversed = sort(names, { (s1: String, s2: String) -> Bool in
return s1 > s2
})
Note that the declaration of parameters and return type for this inline closure is identical to the declaration from the backwards function. In both cases, it is written as (s1: String, s2: String) -> Bool. However, for the inline closure expression, the parameters and return type are written inside the curly braces, not outside of them.
The start of the closure’s body is introduced by the in keyword. This keyword indicates that the definition of the closure’s parameters and return type has finished, and the body of the closure is about to begin.
Because the body of the closure is so short, it can even be written on a single line:

reversed = sort(names, { (s1: String, s2: String) -> Bool in return s1 > s2 } )
This illustrates that the overall call to the sort function has remained the same. A pair of parentheses still wrap the entire set of arguments for the function. However, one of those arguments is now an inline closure.

Inferring Type From Context
Because the sorting closure is passed as an argument to a function, Swift can infer the types of its parameters and the type of the value it returns from the type of the sort function’s second parameter. This parameter is expecting a function of type (String, String) -> Bool. This means that the String, String, and Bool types do not need to be written as part of the closure expression’s definition. Because all of the types can be inferred, the return arrow (->) and the parentheses around the names of the parameters can also be omitted:

reversed = sort(names, { s1, s2 in return s1 > s2 } )
It is always possible to infer parameter types and return type when passing a closure to a function as an inline closure expression. As a result, you rarely need to write an inline closure in its fullest form.
Nonetheless, you can make the types explicit if you wish, and doing so is encouraged if it avoids ambiguity for readers of your code. In the case of the sort function, the purpose of the closure is clear from the fact that sorting is taking place, and it is safe for a reader to assume that the closure is likely to be working withString values, because it is assisting with the sorting of an array of strings.

Implicit Returns from Single-Expression Closures
Single-expression closures can implicitly return the result of their single expression by omitting the returnkeyword from their declaration, as in this version of the previous example:

reversed = sort(names, { s1, s2 in s1 > s2 } )
Here, the function type of the sort function’s second argument makes it clear that a Bool value must be returned by the closure. Because the closure’s body contains a single expression (s1 > s2) that returns aBool value, there is no ambiguity, and the return keyword can be omitted.

Shorthand Argument Names
Swift automatically provides shorthand argument names to inline closures, which can be used to refer to the values of the closure’s arguments by the names $0, $1, $2, and so on.
If you use these shorthand argument names within your closure expression, you can omit the closure’s argument list from its definition, and the number and type of the shorthand argument names will be inferred from the expected function type. The in keyword can also be omitted, because the closure expression is made up entirely of its body:

reversed = sort(names, { $0 > $1 } )
Here, $0 and $1 refer to the closure’s first and second String arguments.

Operator Functions
There’s actually an even shorter way to write the closure expression above. Swift’s String type defines its string-specific implementation of the greater-than operator (>) as a function that has two parameters of typeString, and returns a value of type Bool. This exactly matches the function type needed for the sortfunction’s second parameter. Therefore, you can simply pass in the greater-than operator, and Swift will infer that you want to use its string-specific implementation:

reversed = sort(names, >)
For more about operator functions, see Operator Functions.

2、Trailing Closures
If you need to pass a closure expression to a function as the function’s final argument and the closure expression is long, it can be useful to write it as a trailing closure instead. A trailing closure is a closure expression that is written outside of (and after) the parentheses of the function call it supports:
•    func someFunctionThatTakesAClosure(closure: () -> ()) {
•    // function body goes here
•    }

•    // here’s how you call this function without using a trailing closure:

•    someFunctionThatTakesAClosure({
•    // closure’s body goes here
•    })

•    // here’s how you call this function with a trailing closure instead:

•    someFunctionThatTakesAClosure() {
•    // trailing closure’s body goes here
•    }
NOTE
If a closure expression is provided as the function’s only argument and you provide that expression as a trailing closure, you do not need to write a pair of parentheses () after the function’s name when you call the function.
The string-sorting closure from the Closure Expression Syntax section above can be written outside of thesort function’s parentheses as a trailing closure:
•    reversed = sort(names) { $0 > $1 }
Trailing closures are most useful when the closure is sufficiently long that it is not possible to write it inline on a single line. As an example, Swift’s Array type has a map method which takes a closure expression as its single argument. The closure is called once for each item in the array, and returns an alternative mapped value (possibly of some other type) for that item. The nature of the mapping and the type of the returned value is left up to the closure to specify.
After applying the provided closure to each array element, the map method returns a new array containing all of the new mapped values, in the same order as their corresponding values in the original array.
Here’s how you can use the map method with a trailing closure to convert an array of Int values into an array of String values. The array [16, 58, 510] is used to create the new array [“OneSix”, “FiveEight”, “FiveOneZero”]:
•    let digitNames = [
•    0: “Zero”, 1: “One”, 2: “Two”, 3: “Three”, 4: “Four”,
•    5: “Five”, 6: “Six”, 7: “Seven”, 8: “Eight”, 9: “Nine”
•    ]
•    let numbers = [16, 58, 510]
The code above creates a dictionary of mappings between the integer digits and English-language versions of their names. It also defines an array of integers, ready to be converted into strings.
You can now use the numbers array to create an array of String values, by passing a closure expression to the array’s map method as a trailing closure. Note that the call to numbers.map does not need to include any parentheses after map, because the map method has only one parameter, and that parameter is provided as a trailing closure:
•    let strings = numbers.map {
•    (var number) -> String in
•    var output = “”
•    while number > 0 {
•    output = digitNames[number % 10]! + output
•    number /= 10
•    }
•    return output
•    }
•    // strings is inferred to be of type String[]
•    // its value is [“OneSix”, “FiveEight”, “FiveOneZero”]
The map function calls the closure expression once for each item in the array. You do not need to specify the type of the closure’s input parameter, number, because the type can be inferred from the values in the array to be mapped.
In this example, the closure’s number parameter is defined as a variable parameter, as described inConstant and Variable Parameters, so that the parameter’s value can be modified within the closure body, rather than declaring a new local variable and assigning the passed number value to it. The closure expression also specifies a return type of String, to indicate the type that will be stored in the mapped output array.
The closure expression builds a string called output each time it is called. It calculates the last digit of numberby using the remainder operator (number % 10), and uses this digit to look up an appropriate string in thedigitNames dictionary.
NOTE
The call to the digitNames dictionary’s subscript is followed by an exclamation mark (!), because dictionary subscripts return an optional value to indicate that the dictionary lookup can fail if the key does not exist. In the example above, it is guaranteed that number % 10 will always be a valid subscript key for the digitNames dictionary, and so an exclamation mark is used to force-unwrap the String value stored in the subscript’s optional return value.
The string retrieved from the digitNames dictionary is added to the front of output, effectively building a string version of the number in reverse. (The expression number % 10 gives a value of 6 for 16, 8 for 58, and0 for 510.)
The number variable is then divided by 10. Because it is an integer, it is rounded down during the division, so16 becomes 1, 58 becomes 5, and 510 becomes 51.
The process is repeated until number /= 10 is equal to 0, at which point the output string is returned by the closure, and is added to the output array by the map function.
The use of trailing closure syntax in the example above neatly encapsulates the closure’s functionality immediately after the function that closure supports, without needing to wrap the entire closure within themap function’s outer parentheses.

 

3、获取值
闭包可以在其定义的范围内捕捉(引用/得到)常量和变量,闭包可以引用和修改这些值,即使定义的常量和变量已经不复存在了依然可以修改和引用。牛逼吧、

在Swift中最简单形式是一个嵌套函数,写在另一个函数的方法里面。嵌套函数可以捕获任何外部函数的参数,也可以捕获任何常量和变量在外部函数的定义。

看下面这个例子,一个函数方法为makeIncrementor、这是一个嵌套函数,在这个函数体内嵌套了另一个函数方法:incrementor,在这个incrementor函数体内有两个参数: runningTotal和amount,实际运作时传进所需的两个参数后,incrementor函数每次被调用时都会返回一个runningTotal值提供给外部的makeIncrementor使用:

func makeIncrementor(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementor() -> Int {
runningTotal += amount
return runningTotal
}
return incrementor
}

而函数makeincrementor的返回类型值我们可以通过函数名后面的()-> int得知返回的是一个Int类型的值。如需想学习了解更多地函数返回类型,可以参考: Function Types as Return Types.(超链接跳转)

我们可以看见makeincrementor这个函数体内首先定义了一个整型变量:runningtotal,初始值为 0 ,而incrementor()函数最终运行的出来的返回值会赋值给这个整型变量。

makeincrementor函数()中向外部抛出了一个forIncrement参数供外部穿参进来、一旦有值进入函数体内会被函数实例化替代为amount,而amount会被传递进内嵌的incrementor函数体中与整型常量runningTotal相加得到一个新的runningTotal并返回。而我们这个主函数要返回的值是Int类型,runningTotal直接作为最终值被返回出去、makeincrementor函数()执行完毕。

makeincrementor函数()在其内部又定义了一个新的函数体incrementor,作用就是将外部传递过来的值amount 传进incrementor函数中与整形常量runningTotal相加得到一个新的runningTotal,

单独的看incrementor函数、你会发现这个函数不寻常:
func incrementor() -> Int {
runningTotal += amount
return runningTotal
}

因为incrementor函数没有任何的参数,但是在它的函数方法体内却指向runningTotal和amount,显而易见、这是incrementor函数获取了外部函数的值amount,incrementor不能去修改它但是却可以和体内的runningTotal相加得出新的runningTotal值返回出去。

不过,由于runningtotal每次被调用时都会相加改变一次实际值,相应地incrementor函数被调用时会去加载最新的runningtotal值,而不再是第一次舒适化的0.并且需要保证每次runningTotal的值在makeIncrementor函数体内不会丢失直到函数完全加载完毕。要能确保在函数体内下一次引用时上一次的值依然还在。

注意
Swift中需要明确知道什么时候该引用什么时候该赋值,在incrementor函数中你不需要注解amount 和runningTotal。Swift还负责处理当函数不在需要runningTotal的时候,内存应该如何去管理。

这里有一个例子makeIncrementor函数:
let incrementByTen = makeIncrementor(forIncrement: 10)

4、引用类型闭包
在上面的例子中,incrementBySeven和incrementByTen是常量,但是这些常量在闭包的状态下依然可以被修改。为何?很简单,因为函数和闭包是引用类型。
当你指定一个函数或一个闭包常量/变量时、实际上是在设置该常量或变量是否为一个引用函数。在上面的例子中,它是闭合的选择,incrementByTen指的是恒定的,而不是封闭件本身的内容。
这也意味着,如果你分配一个封闭两种不同的常量或变量,这两个常量或变量将引用同一个闭包:
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50

感谢翻译小组成员:李起攀(微博)、若晨(微博)、YAO、粽子、山有木兮木有枝、渺-Bessie、墨离、矮人王、CXH、Tiger大顾(微博)
个人转载请注明出处,商业转载请联系我们~ 感谢您对我们工作的支持~