第三章 类和结构体

第三章:Classes and Structs 类和结构体

到现在你已经了解了整型,字符串,数组和字典,但是仅有他们还是不够的。Swift和其他面向对象编程的语言一样,也提供了通过定义类来保存数据以及调用类里的方法。如果你熟悉Object-C,Java,c#等其他编程语言,那你肯定已经接触过class这个概念了。

Swift也允许你自定义结构体,简称结构。和其他如c一样的语言不同,Swift中的结构可以像类一样保存数据,调用方法。

靠,那类和结构还有毛的不同?这样的疑问对于理解他们的概念是非常重要的!在本章,你将利用一个使用了类和结构的应用来学习他们的创建以及部署过程。

这个新的应用将带你去硅谷寻找宝藏。说不定下一次你参加WWDC的时候,你就可以用这个app找到宝藏也说不定~~~

别yy了,赶紧学习吧!

Getting started - 开始

为了让你能直接学习了解,所以我已经帮你新建了一个项目,因为这一章的重点是类和结构,所以你没必要花时间去建一个操作项目。直接进入主题更快一些不是!

原书提供的资源代码都不可用了,所以我会重新都生成一遍项目(又是一个工作量,悲了个剧),在提供的资源文件夹中第三章中可查看。打开工程项目你会发现是很简单的一个项目,里面就是一个带有地图view的导航栏控制器。

运行代码可看到显示了中国区域的地图:
这里写图片描述

The class concept - 类的概念

如果你有过Object-C或者其他面向对象的编程经验,那你肯定对类十分的了解。但是为了着重区分类class和结构structs的差异,所以我们再来温习下类的概念吧!

在面向对象的编程中,你主要是通过类的实例或对象进行控制。对象有相关的数据以及操作这些数据的方法。一个对象的数据主要是数值或者字符串这样的原始数据,也可能包含有其他对象的引用。
一系列的对象通常可以建立成一个有着层级结构的模型。这涉及到生物分类学中的一个经典案例。比如说猫,青蛙,乌龟以及猫头鹰全是动物(主要是四足脊柱动物),他们有着他们相同的属性(年龄,体重,物种)但也有些自己独有的(猫的毛皮的颜色和质感,猫头鹰的翅膀)。

你可以在面向对象的语言中用层级结构的类来描述他们的关系。比如说他们有个拥有着共同属性的叫animal的类。不同的子类如猫,青蛙,乌龟以及猫头鹰可以继承自这个animal类。每个animal子类都可以表现出父类的行为,继承父类的方法和数据。

那么结构又有什么不同呢?在Swift中,类和结构都可以帮你模型化数据,他们都可以保存数据并且有着处理这些数据的方法,但是又各有特点。在这章你会发现他们的不同的地方,以便让你知道什么时候用类,什么时候用结构。(我勒个去,才要进入正题,看着这么一丢丢,翻译起来和嚼蜡一样…)

My first class - 我的第一个类

当前这个应用做的事情还很少,只是展示了你在地球的哪个区域。很明显你还需要知道one piece在哪疙瘩。

你在硅谷(改为成都好呢还是改为中国好,纠结)的宝藏有这以下几个特点:
1.HstoryTreasure:历史文物宝藏是包含了和年相关的历史遗迹
2.FactTreasure:真相宝藏包含了和宝藏相关的线索。
3.HQTreasure:宝藏基地是硅谷的公司总部(继续纠结中,用首都?还是成都?)

每个这些宝藏都有着和位置相关的信息。如果你想到了用类的层次结构来划分。恭喜你,你上道了!
你可以将上述描述的情况用下列对象图进行表示:
这里写图片描述
上面的图表展示了类的层次结构。每个矩形框都代表了一个类,里面包含了名字以及一些相关的数据。用这种方法可以看到,每个treasures(宝藏类)都有两个“what(和宝藏相关的信息)”和“location(宝藏的位置)”属性。

现在可以来操作看看了!

Creating the class - 创建类

在工程中添加一个新的类点击xcode的File\New\File… ,选择iOS\Source\Swift File ,点击next并命名为Treasure 。

你现在能看到一个空白的Swift文件生成。如果你是从Object-C转过来的,你可能就傻眼了。Swift不需要单独的一个header头文件,也不需要一个.m的实现文件。你也不需要用import导入你的代码。看上去是有点奇怪,但用久了肯定会觉得方便不少。

import让你可以访问其他库里面的类,结构以及方法等。新生成的文件第一行就为你导入了整个的Foundation 框架。

如果你是Object-C的开发员的话,一定对Foundation框架很熟悉。这个框架里包含了常用的对象,比如最基础的NSObject类等。

Swift在Foundation框架中提供了自己对类的基本实现,比如前面章节中提到的字符串,数组和字典。然而你会在本章以及本书中发现,import导入对Swift的开发仍然是非常重要的。

先敲些代码吧:

class Treasure {

    let what: Stringlet latitude: Double 
    let longitude: Double 
} 

这个基本的例子声明了一个类:在关键字class后面给你想要定义的类命名,然后在括号里定义你的类。你可以在类里用var也可以用let声明你的属性,就像在方法中声明局部变量一样。var和let的区别便是确保你的代码在上下文中是否是可修改的。

因为编译器无法推导出他的类型,所以你需要声明下,如果你需要初始化值得话,也可以直接在类型后面进行初始化。

小技巧:如果你要初始化值得话,那么你就没有必要明确的声明数据的类型,因为编译器可以自己推导出来。然而直接声明类型的好处在于你可以直接一眼扫过去便知道所有属性数据的类型了。
眼尖的读者可能已经发现所有的这些变量都是常数,而且都没有定义初始化值,怎么可以这样呢?不是必须初始化吗?

仔细一看,xcode其实已经报错!
这里写图片描述
错误提示你需要给你定义的类初始化,因为类里面的常数都没有初始化值。牢记,只要不是可选类型就一定要为属性初始化一个有效的值。

在这花括内,几个属性的下面添加以下代码:

init(what: String, latitude: Double, longitude: Double) { 
    self.what = what

    self.latitude = latitude

    self.longitude = longitude 
} 

这几行代码在Swift中代表着初始化,类似于Object-C中的init或者c++和java中的构造方法。

这应该是你Swift中敲的第一个函数,恭喜你咯~~

Swift的语法是要求非常一致的。在Swift的函数中参数的名字以及类型必须和你定义的所有变量一致。注意的是必须要有描述的类型,因为在函数参数的地方没有值让他推断这个参数应该是什么类型。

初始化只有一个工作,在类初始化实例时自身的属性必须有值。在这个类中,因为要给三个变量设值。而所有的类的值都是通过初始化传入的,所以类的对象必须要在初始化的时候就知道这三个值,并传入。这是Swift设计的另外一个安全机制。什么宝藏没有经纬度值呢,有脾气你举个例子出来!所以这三个变量是必须要有值滴。该守得规则还是要守滴。

这个类基本就做的差不多了,有个小地方可能需要再改进下。

A struct-ural improvement - 结构的改进

到目前为止,一切看上去都挺好,但美中不足的是经度和纬度应该是在一起的。他们应该是一个单元中包含的两个信息,就像宝藏信息应该是由“什么宝藏”和“在什么地方”组成一样。所以将经纬度包装在一起而不是保存成两个Double数据显然是更好。

struct GeoLocation {
    var latitude: Double 
    var longitude: Double 
} 

这就是结构方便的地方了。结构可以像类一样持有数据,也可以持有方法。但结构可以被当做是值的对象,什么都不用做只需要持有数据就行。(值的对象你可能还不太理解,后面会有例子讲解)

继续在项目中添加一个文件,点击File\New\File… ,选择iOS\Source\Swift File并点击下一步。并命名为GeoLocation 。

同样的一个空的Swift文件。添加代码如下:

struct GeoLocation { 
    var latitude: Double 
    var longitude: Double 
}

这可能让你想起类的定义。这里只有一点不一样。用的是关键词struct而不是class。这便是在Swift声明了一个结构,简单吧!!!

你定义了一个用来同时保存经度和纬度的结构。结构里面的属性通常用的是变量而不是常数。但是只要你愿意,你也可以用常数定义。

现在在你的Treasure类中部署新建的结构,打开Treasure.swift 并替换掉你定义的经度和纬度。代码如下:

let location: GeoLocation

注意下你并没有导入GeoLocation文件但依然可以用。这是因为在Swift中,应用中的每一个文件都会自动导入到你要用的另一个文件中。即使你写一个静态库或者框架也是如此,其他的文件都可以访问。

这让码农轻松不少不是!Object-C的开发员都知道在Object-C代码中光是用import都要用码不少代码。

你也应该注意到现在初始化的地方报错。这是因为你应用的经纬度刚刚已经移除了。修复下:

init(what: String, location: GeoLocation) { 
    self.what = what

    self.location = location 
}

所有的工作都做完了,而且明显现在展示地理位置的方式显得更高明不是。简单的用一个有意义的结构而不是两个不明所以的Double。

Reference types vs. value types - 引用类型VS值类型

正如你所见的,结构和类的定义是非常像的。回想下结构是怎么存储值的。两者间的区别并不抽象:类是引用类型,而结构是值类型。这意味着当你传递一个类,Swift实际传递的是类的引用给另外一个对象。当你传递一个结构时,Swift是复制内容给对象。下面我们来演示下。

Object-C的开发员可能会发现在Object-C中类和结构的展现的行为是一样的。

思考下下面的代码以及输出语句(在playground中试下):
这里写图片描述
这定义了都包含有一个变量值的结构和类。都先生成一个变量对象,然后再赋值给第二个变量对象,接着修改第二个对象中属性的值。

注意发现在结构中,仅仅第二个变量值发生了改变,而在类中两个变量值都发生了改变。这个例子很有代表性。当你将classA分配给classB,Swift用相同的引用将两个变量的真实指针赋予到相同的实例上。但是当你分配structA到StructB时,Swift仅仅是复制了他在结构中存在的值。

小技巧:在代码的执行底层,Swift拥有写入时复制的功能,能够聪明的知道只有到绝对需要的时候才开始复制结构的值。这也就是说structB=struct
并没有立即执行复制的功能。只有当你要改变他的值,在运行的时候才开始复制

下面的示例图进一步的说明结构复制值与类复制引用的区别:
这里写图片描述

虽然看上去差异很小,但是类和结构在构建一个常数let时的差异确是非常明显的。

回想下第一章关于var和let关键字定义的变量和常量。当一个实例比如变量,类和结构的操作是相同的。你可以修改他们的属性或重新给他们分配新的值。当一个实例是常数时,类和结构有着非常大的不同,如下所诉:
1.在常量类中,你可以修改类中的属性,但是不可以重新分配一个类给常量类
2.在常量结构中,你不可以修改结构的属性也不可以重新被分配值
上面说的有点抽象,瞧瞧下面的例子:
这里写图片描述

在Swift中常量结构完全不可进行修改!这也正是为什么数组和字典是结构而不是类。

Convenience initializers - 便利初始化

有时用一个简单的便利方法进行初始化也是不错的,在这个Treasure案例中,在初始化的时候直接赋值经纬度,让用户无需再在GeoLocation中初始化,显然更好些。

打开Treasure.swift 并将代码添加到初始化下面:

convenience init(what: String,latitude: Double, longitude: Double) 
{
  let location = GeoLocation(latitude: latitude,       longitude: longitude) 
    self.init(what: what, location: location) 
}

这便是所谓的便利初始化了,convenient关键字说明他还没有完整的初始化自己,而是推迟了本身的初始化。否则便称为指定初始化,即手动指定一个初始化方法。

Object-C的开发者可能对于指定初始化比较熟悉,这个机制在Object-C中运用了很长一段时间。Swift在正式的编译时候会执行,如果没有在初始化中初始化变量,则Swift直接抛出编译时错误。

你可能还有些奇怪,因为这个便利初始化中还创建了GeoLocation,但是你并没有在其结构中声明一个初始化。结构不需要初始化,Swift在创建他们时,为每一个参数都定义了一个按顺序排序的初始化方法。这使得结构的初始化出乎意料的简单和易懂。因为他们直接将数据描述封装在了结构体中。

Class inheritance - 类的继承

现在让我们来了解下类的继承。回想下可以知道Treasure 类有三个子类:HistoryTreasure, FactTreasure 和 HQTreasure.

打开Treasure.swift 并且将下面的代码加到文件的底部。

// 1 
class HistoryTreasure: Treasure { 
    let year: Int 
// 2 
    init(what: String, year: Int,latitude: Double, longitude: Double) 
{ 
    self.year = year
   let location = GeoLocation(latitude: latitude, longitude: longitude) 
    super.init(what: what, location: location) 
    } 
} 
// 3 
class FactTreasure: Treasure { 
    let fact: String 
    init(what: String, fact: String, latitude: Double, longitude: Double) 
{
  self.fact = fact
   let location = GeoLocation(latitude: latitude, longitude: longitude) 
    super.init(what: what, location: location) 
    } 
} 
// 4 
class HQTreasure: Treasure {    
    let company: String 
    init(company: String, latitude: Double, longitude: Double) { 
    self.company = company
 let location = GeoLocation(latitude: latitude, longitude: longitude) 
    super.init(what: company + "总部", location: location) 
    } 
} 

让我们分部分的来剖析下:
1.你可以在声明的时候表示类的继承关系,只需要在类名的冒号后面添加父类的名字就可以了。
2.HistoryTreasure有个和宝藏相关额外的信息-年,因此你必须指定一个初始化方法来初始化这个值。如果你没有指定初始化方法,则Swift会自动调用父类的初始化值,但是父类的初始化中没有year这个属性。

类的指定初始化必须指定用父类的初始化,不可以直接使用convenience 的初始化,这就有点尴尬了,你需要复制用来创建地理位置结构的代码。

如果你是Object-C开发员的话会发现调用父类的super.init()的位置有点奇怪。是放在方法的最后位置,因为在Swift中,初始化的工作是用来初始化这个类中所有声明的属性,然后交给父类。父类方法并不知道子类做了些什么,有什么,所以不能放在前面。

3.然后是声明FactTreasure和HQTreasure。他们和前面的一样,每个都有自己相关的的数据,所以都需要初始化方法来初始化值。

啧啧!你写了一个类,一个结构和一个完整的继承结构。你该感到自豪了(啊啊啊,自豪点在哪啊)。编译并运行下看看代码能不能正常运行。因为你还没有使用过你的新类和结构,所以应用程序的运行和刚刚相比并没有变化。接着来学习下一步。

Swift and MapKit - Swift和地图框架

你写了一个类来保存treasures宝藏,也写了一个保存有地址信息的结构。是时候将一些宝藏在地图中显示出来了。

打开ViewController.swift.里面就是一个包含了一个地图view的类。
先来了解下类中的代码:

override func viewDidLoad() { 
    super.viewDidLoad() 
} 

这是你见到的第一个覆盖函数的例子,控制器调用了一个叫viewDidLoad的方法然后在里面加载view。你可以在这个地方自定义你的view。

在Swift中要注意的是,如果你在类中要覆盖一个已经存在的方法,则一定要加上关键字override。这样无论是谁看到都能知道这个方法覆盖了父类中的方法。

关键字override能够帮助编译器检查你的方法是不是正确的。比如,如果你拼错了一个覆盖的方法名,则编译器提示错误,因为父类中不存在。同样的,如果你不知道是否父类有某个方法,自定义了一个viewDidLoad这个方法,则编译器会抛出错误告诉你这个方法存在了。

你应该也注意到了用关键字super来调用了父类的方法。
在@IBOutlet的代码后面声明如下的属性:

var treasures: [Treasure] = [] 

现在将宝藏显示在你的地图中,你将先初始化叫treasures的数组,将下面的代码添加到viewDidLoad 的下面

self.treasures = [

    HistoryTreasure(what: "Google总部", year: 1999, latitude: 37.44451, longitude:-122.163369), 
    HistoryTreasure(what: "Facebook总部", year: 2005, latitude: 37.444268, longitude:-122.163271),

    FactTreasure(what: "斯坦福大学", fact: "成立于1885年的利兰·斯坦福.", latitude: 37.427474, longitude: -122.169719), 
    FactTreasure(what: "莫斯科尼", fact: "自2003年以来WWDC的主办地.", latitude: 37.783083, longitude: -122.404025),

    HQTreasure(company: "Apple",latitude: 37.331741, longitude: -122.030333), 
    HQTreasure(company: "Facebook",latitude: 37.485955, longitude: -122.148555), 
    HQTreasure(company: "Google",latitude: 37.422, longitude: -122.084), 
] 

现在将硅谷的所有宝藏数据都放在了self.treasures数组中了。不要纠结为何应用导航栏写的是中国寻宝,但是坐标全是美国的。表在意细节╮(╯▽╰)╭

Class extensions and computed properties - 类的扩展和计算属性

现在你需要将这些宝藏地点在地图中展示出来,将“annotations”大头针在mapkit中标记出来。annotations需要遵循MKAnnotation协议。

打开Treasure.swift 并在第一行中添加导入代码:

import MapKit 

然后在定义类的代码下面添加扩展代码:

extension Treasure: MKAnnotation {
    var coordinate: CLLocationCoordinate2D {
         return self.location.coordinate 
    } 
    var title: String {
         return self.what 
    } 
} 

将代码复制进playground会报错,Swift2.0有了些许修改,报3个错
1.Treasure类遵循NSObjectProtocol
2.变量要前面用@objc声明
3.title的类型和协议中定义的类型不一样。所以修改代码如下
这里写图片描述

这就是类的扩展,允许你给类添加一些额外的方法。如果你有过Object-C的开发经验,你应该知道在Object-C中也是有扩展存在的。在Swift中,显著的区别在于不仅可以额外添加方法,还可以添加属性。正是因为如此,将代码放在扩展或是放在主类中似乎是没什么区别了,但是放在不同的扩展中对于代码的理解和维护却有很大的帮助。

你在扩展的方法中实现了MKAnnotation的协议。这个协议定义了你必须有两个属性:coordinate和title。你声明这两个属性但是和平常的声明看上去有些不一样:尽管他们都是用了var来定义。但是他们包含了一个和函数一样用于返回值的闭包方法。

他们是计算属性,不是函数。他们和一般的属性的不同在于他们的值是每次计算所得而不是依靠传入的实例变量。除了计算属性是每次执行相关联的代码来访问属性外,其他的和一般的属性使用相同。

目前,这个代码因为在访问一个并不存在的coordinate属性,所以不能编译,先不管。

Your first struct extension - 你的第一个扩展结构

打开GeoLocation.swift 并导入以下代码:

import MapKit 

然后在文件的最底部添加扩展代码

extension GeoLocation {
    var coordinate: CLLocationCoordinate2D { 
    return CLLocationCoordinate2D(latitude: self.latitude,longitude: self.longitude)
    } 
    var mapPoint: MKMapPoint {
 return MKMapPointForCoordinate(self.coordinate) 
    } 
}

就像类一样,结构也是可以扩展的。在上面代码中,你不需要像类Treasure那样声明任何协议。相反,这是个简单夸张独立的代码。好处非常明显:让你的逻辑代码以单元模块形式分开。在这个示例中,你将坐标和地图管理的代码分开了。

这里的两个计算属性返回了你声明的坐标(经度和纬度)和地图的位置(mapPoint)。运行代码。

一切正常,但是在不将Treasure声明继承自NSObject时,此时会报错。也就是说Swift1.0中,Treasure如果没有继承NSObject时,使用协议不会报错,但是,在运行编译的时候会报错。Swift2.0改进了,在静态编译,不运行代码的时候便提示你需要继承了。

Inheriting from NSObject - 继承自NSObject

这个错误是因为你使用的协议MKAnnotation继承自NSObjectProtocol。为了定义的一致性,所以你的Treasure也必须是要遵循NSObjectProtocol.

Object-C的开发员可能对NSObject非常熟悉。NSObject是Apple Object-C框架代码中,几乎所有对象的基类。NSObject是一个遵循了NSObject协议的类。为什么这个类即是协议又是类超出了咱们本书的范围。但是毋庸质疑的是,你知道Treasure是要遵循NSObjectProtocol协议的。

继承代码如下:

class Treasure: NSObject 

如果你使用MapKit,那么你将发现经常都会使用到NSObject。通过NSObject的继承,你可以无缝的让Swift类使用Object-C的类。是不是很神奇,很拽~~。

Pinning the map - 在地图上标记大头针

现在Treasure类遵循了MKAnnotation,你可以将他添加到mapView中了。打开ViewController.swift并在ViewDidLoad的结尾处添加以下代码:

self.mapView.delegate = self 
self.mapView.addAnnotations(self.treasures)

先别急着看xcode上提示你控制器要添加协议。第一行声明了当前控制器是地图view的代理。这第二行是将地图的注释坐标全部放入地图中。这个代理运行控制器去告诉地图如何展示这些宝藏点。

在文件底部添加类的扩展:

extension ViewController:MKMapViewDelegate{
    func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView?{
        let treasure = annotation as? Treasure

        if treasure != nil {
            var view = mapView.dequeueReusableAnnotationViewWithIdentifier("pin") as? MKPinAnnotationView

            if view == nil {
                view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "pin")
                view?.canShowCallout = true
                view?.animatesDrop = false
                view?.calloutOffset = CGPoint(x: -5,y: -5)
                view?.rightCalloutAccessoryView = UIButton(type: UIButtonType.DetailDisclosure)
            }else{
                view?.annotation = annotation
            }

            return view

        }
        return nil
    }
}

说明:
1.is:和右边的类型一样或者是右边类型的子类
2.as!:当左边类型是右边类型的父类时,可强转为右边的子类,不符合便报错
3.as?:和option可选类型一样,如果左边的父类强制转换为右边的子类失败返回nil

这声明了一个遵循了MKMapViewDelegate协议的控制器,以便他可以做地图view的代理,下面是代码的解释:
1.实现了mapView:viewForAnnotation方法。
2.如果annotation是Treasure类,那么在地图上显示的pin会重复复用标记了唯一标识符“pin”的view。程序会不断复用这个view,而不是新建一个新的view。如果你对UITableView比较熟悉的话,你便会明白这个复用的概念,和UITableViewCell的复用机制一样。
3.如果view为nil,则创建一个新的进行设置。
4.如果annotation存在,则直接修改view上的annotation。
5.最后返回annotationView

谢谢你能和我一起坚持到这,运行你的代码,然后移动地图到北美洲附近,放大地图,你就可以看到你标记的几个地点了。

这里写图片描述

所有大头针标记的地方都是宝藏哦~~~(这样子说话好有羞耻感…)

The reduce algorithm - 优化代码

你可能会感到有些丧气,当你打开应用的时候并不是直接展示宝藏的位置,还需要你挪动地图位置,然后放大缩小地图。在你开始寻宝前你还需要找到自己的位置,你说蛋疼不蛋疼。

这个问题很容易解决,打开ViewController.swift然后在viewDidLoad末尾处添加代码:

       // 1
        let rectToDisplay = self.treasures.reduce(MKMapRectNull) {
            (mapRect: MKMapRect, treasure: Treasure) -> MKMapRect in
            // 2
            let treasurePointRect = MKMapRect(origin: treasure.location.mapPoint,
                          size: MKMapSize(width: 0, height: 0))
            return MKMapRectUnion(mapRect, treasurePointRect)
            // 3
        }
            // 4
        self.mapView.setVisibleMapRect(rectToDisplay, edgePadding: UIEdgeInsetsMake(74, 10, 10, 10), animated: false)

信不信由你,就这五行代码就直接让所有的宝藏显示在你视野范围内的地图中了,那他是如何工作的呢:
1.该算法通过使用一个叫reduce的数组函数。reduce数组意味着函数数组的数组每个元素结合形成一个,最后返回一个返回值。在每个步骤中。下一个元素的数组传递减少了当前值的值。函数的返回值就变回了当前降低的值。在这种情况下,你看到的最后一个值是MKMapRectNull.
2.在每一步的减少中,你都是在计算只有一个单一宝藏地图的封闭矩形。
3.然后返回一个由目前整体矩形以及单一宝藏矩形形成的整体矩形。
4.递减完成时,地图矩形将会包含所有宝藏的地图矩形。换句话说,这个矩形就足以将每个宝藏都包含了。

然后设置的可见的地图矩形,你使用边缘间距以确保最近的地图针不会在导航栏下面或太靠近屏幕的边缘。(其实上面这个方法不用懂,毕竟本章的重点不在此)

不用手动拖动地图,放大地图了,是不是帅毙了!

Note:Reduce
是个典型的函数编程,通过从一个集合中返回迭代计算的值。你会在Swift中经常看到这样的编程代码。如果你对方法编程感兴趣的话,在第七章中有详细的介绍。

Polymorphism - 多态

当前所有的大头针都是同一种颜色。如果每个类型的宝藏类型可以用不同颜色来进行区分表示就好了。这一听就是多态的工作!

多态,即相同的方法会根据不同的子类有各自不同的反应机制,在这个实例中,很明显你需要一个返回颜色的方法,每个子类都可以根据自身条件返回对应的颜色。

打开Treasure.swift 并在Treasure类中实现:

func pinColor() -> MKPinAnnotationColor { 
    return MKPinAnnotationColor.Red 
} 

这是默认实现的方法,如果不覆盖这个方法则默认返回一个红色的大头针。

接着在HistoryTreasure的类中覆盖此方法

override func pinColor() -> MKPinAnnotationColor { 
    return MKPinAnnotationColor.Purple 
} 

再接着在HQTreasure 类中覆盖此方法

override func pinColor() -> MKPinAnnotationColor { 
    return MKPinAnnotationColor.Green 
} 

在FactTreasure中没有覆盖此方法,所以他返回一个默认的颜色。

接着打开ViewController.swift 并找到你在MKMapViewDelegate扩展中实现的方法mapView(_:viewForAnnotation:),在return view语句前加入代码:

view?.pinColor = (treasure?.pinColor())! 

这个方法调用了treasure中的pinColor()并返回设置的颜色。这个方法将根据每个不同的子类执行正确的pinColor()的方法。
编译并运行,看到界面如下:
这里写图片描述

是不是更好了些,地图上的大头针更好看了~~

Dynamic dispatch and final classes - 动态调用和最终类

多态性的方法需要在运行时查找方法。对于pinColor(),根据子类Treasure的实例来判断是使用覆盖的父类方法还是直接使用父类的方法。这种行为叫动态调用。

在Object-C中每个方法的调用都是动态调用。在大量使用动态调用的语言中,你甚至可以在运行的时候添加方法和类!这便让编译器无法在运行的时候做很多的优化处理。

在Swift中允许动态调用多态行为。Swift用的方法类似与C++,而不是Object-C的消息传递调用。他通过虚拟表或“vtables”(虚表)来动态调用。

在上面的例子中。当编译器调用一个在Treasure的变量类型的pinColor(),他知道要用虚表去查找(动态调用),因为Treasure的类的子类实现了pinColor().

但是如果编译器发现的是和HQTreasure这样子类的变量呢?在这个例子中,编译器依旧会用vtable动态调用来查找,因为谁知道这个类是不是添加有某个子类在哪个犄角旮旯。在这个应用中,HQTreasure没有子类。即使开发员知道这种情况,编译器也不会尝试去优化直接调用函数,而是依然尝试动态调用。为了优化,处理无用的调用,有方法给编译器一个提示HQTreasure永远不会有子类来解决这个问题。

可以用关键词final来声明一个类不可以拥有子类。任何尝试在final类中创建子类的编译器都会返回错误。这是一个有用的设计,同时也可以显著的提高性能。一旦你声明了一个final类,编译器就明确的知道要实现的方法是当前的实例里面的。比如,如果你让HQTreasure声明为final然后调用HQTreasure实例的方法pinColor(),编译器会知道要调用的是HQTreasure版本里的方法,不用再到子类层去寻找。

让我们来演练下这个测试。打开Treasure.swift并修改Treasure的三个子类为final。

final class HistoryTreasure: Treasure 
final class FactTreasure: Treasure 
final class HQTreasure: Treasure 

将类声明为final是一个很好的操作习惯。有助于编译器在你代码运行前的优化,嗯哼!让你的代码飞起来︿( ̄︶ ̄)︿。

Adding annotations - 添加地图标注

现在可以在地图上看到宝藏分布的地方,但是还什么都做不了。没有任何方式可以进一步了解详细的宝藏信息,所以让我们来给这个标记点添加些标注。

打开Treasure.swift 并在顶部Treasure类的上面的文件处添加协议

@objc protocol Alertable {
 func alert() -> UIAlertController 
}

这个协议是Treasure子类都需要遵循的。协议中有一个单独的方法alert。当用户调用时返回一个控制器类型的UIAlertController。

在最底部添加扩展:

extension HistoryTreasure: Alertable { 
        func alert() -> UIAlertController { 
        let alert = UIAlertController(
     title: "历史",
       message: "从 \(self.year):\n\(self.what)", preferredStyle:                       UIAlertControllerStyle.Alert) 
    return alert 
    } 
} 
extension FactTreasure: Alertable { 
        func alert() -> UIAlertController { 
        let alert = UIAlertController(
     title: "真相",
       message: "\(self.what):\n\(self.fact)", preferredStyle: UIAlertControllerStyle.Alert) 
    return alert
     } 
} 
extension HQTreasure: Alertable {
         func alert() -> UIAlertController { 
        let alert = UIAlertController(
     title: "总部",
       message: "\(self.company)的总部", preferredStyle: UIAlertControllerStyle.Alert) 
    return alert 
    } 
} 

每一个方法里都设置了treasure类型的标题和一些其他相关的信息。注意的是我们使用了字符串的嵌入方法!

现在打开ViewController.swift并在MKMapViewDelegate的扩展后面添加以下代码:

func mapView(mapView: MKMapView,annotationView view: MKAnnotationView, 
calloutAccessoryControlTapped control: UIControl) 
{
  if let treasure = view.annotation as? Treasure { 
    if let alertable = treasure as? Alertable { 
        let alert = alertable.alert() 
        alert.addAction( 
    UIAlertAction(
 title: "OK",
   style: UIAlertActionStyle.Default, handler: nil)) 
    self.presentViewController(alert, animated: true, completion: nil) 
        } 
    } 
} 

这个代理方法在用户点击大头针或者大头针顶部栏右边的感叹号时被调用。

在这个方法中,你给Treasure的每个view添加了注释,就像刚刚那样,然后你现在再次检查他是否符合Alertable的协议,以便你能获取到对话框alert。然后在这个对话框中添加一个“ok”按钮,最后展示出来。

运行程序并点击大头针,接着点击注释上面右边的详细按钮(感叹号)。观察这个treasure的对话框,UI应该和下面差不多:
这里写图片描述

啊呜~~~~!这就是宝藏的地址了!

Sorting an array - 排序数组

如果你的用户可以在发现一个宝藏后能立马发现接下来的一个最近的宝藏是不是很爽。要实现这个方法其实也很容易。
首先,打开GeoLocation.swift 并在GeoLocation结构中定义以下方法:

func distanceBetween(other: GeoLocation) -> Double {
    let locationA = CLLocation(latitude: self.latitude, longitude: self.longitude) 
    let locationB = CLLocation(latitude: other.latitude, longitude: other.longitude) 
    return locationA.distanceFromLocation(locationB) 
} 

这个添加在GeoLocation结构中的方法是用来计算自己和另外一个GeoLocation实例间的距离。他用的是地理的核心库CLLocation的方法来计算的,毕竟要计算地球上两点间的距离是非常难的。

注意的是结构可以和类一样持有方法。这是非常有用的,这与结构中只能有变量的如c语言是明显不一样的。因为c结构不能包含有函数,操作他们的行为通常需要将函数声明为全局。例如,当你在CGRect中的操作时,你需要CGRectUnion,CgRectDivide和CGRectGetMidX。这些功能混乱的布局在全局中,很难在CGRect中找到所有的功能操作。这些操作工作留给了开发者,需要让他们自己把所有需要内容都导入到头文件。所以Swift明显又高一筹。因为相关的方法都可以在内部结构中全找到。

现在打开ViewController.swift 并找到实现的方法

mapView:annotationView:calloutAccessoryControlTapped:

,在调用presentViewController: 前添加代码如下:

alert.addAction(UIAlertAction(
 title: "最近的",
  style: UIAlertActionStyle.Default) { 
    action in 
    // 1 
    var sortedTreasures = self.treasures 
    sortedTreasures.sortInPlace { 
    // 2 
    let distanceA = treasure.location.distanceBetween($0.location) 
    let distanceB = treasure.location.distanceBetween($1.location) 
    return distanceA < distanceB } 
    // 3 
    mapView.deselectAnnotation(treasure, animated: true) 
    mapView.selectAnnotation(sortedTreasures[1], animated: true) }) 

这在alert上另外再添加了一个方法。新的方法做了如下事情:(了解下就行,不需要非得懂)
1.你将要排列所有的宝藏信息,所以你创建了一个本地变量用于复制一份源数组信息。这个排序方法的闭包函数需要一个参数,并返回一个前面两个对象比对的布尔值。

2.接着,你计算当前宝藏与你排序的每一个宝藏信息的距离。注意这里使用了 0 1.这是在闭包函数中调用第一个和第二个参数的快速语法。在介绍闭包函数的章节中我们还有很多这样的例子!

你检查第一个地点的距离与第二个的距离,如果第一个小则返回true。通过这种方式,你的宝藏信息便会以到当前宝藏地点的最短到最长的顺序进行排序。

3.最后你点击后会取消当前的宝藏并会选择新的宝藏地点。你可能会问为什么会选择最近的地点时选择的是数组中的第二个元素,因为的第一个元素永远是自己。

编译并运行程序,选择一个大头针并点击调出信息展示,接着点击“Find Nearest”。这个app会寻找到接下来的最近的一个宝藏地点!

这里写图片描述

Equality and operator overload - 等号和运算符重载

用户发现了宝藏地点,但却没有记录保存他们是否来过。用一个简单的方法来记录,显示一个覆盖在地图上的标记用来记录用户到过地点的路线。如果发现用户去过了这个 地方,则这个应用弹出提示。

打开ViewController.swift 并在类的顶部添加属性,就添加在treasures数组属性的下面:

var foundLocations: [GeoLocation] = [] 
var polyline: MKPolyline!

第一个属性用来保存GeoLocation 结构的一系列信息,以便app用来记录跟踪用户发现的宝藏和顺序。

第二个属性保存MKPolyline。这是覆盖在地图上的点的集合信息。用户可以通过这个属性看到他们发现的每个宝藏的路径。这个属性的类型是隐式解包。也就是说,在用户发现任何宝藏信息前,可以用nil来表示。

接着,找到MKMapViewDelegate 扩展,并在底部添加代码如下:

    func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer{
        let polylineOverLay = overlay as? MKPolyline
        if polylineOverLay != nil {
            let renderer = MKPolylineRenderer(polyline:polylineOverLay!)
            renderer.strokeColor = UIColor.blueColor()
            return renderer
        }
        //原来的代码是可以直接返回nil,但是现在返回类型值必须存在,所以这里随意模拟一个值
        let renderer = MKPolylineRenderer(polyline: polylineOverLay!)
        return renderer
    }

这个方法用来告诉地图如何渲染给定的路线。通过使用MKPolyline来关联叫做MKPolylineRenderer的渲染器。

现在找到

mapView:annotationView:calloutAccessoryControlTapped:

就像你前面刚刚加的对话类型一样,在两个弹出的语句见添加代码:

alert.addAction(UIAlertAction(
title: "发现",
style: UIAlertActionStyle.Default) { action in 
self.markTreasureAsFound(treasure) })

这个方法即在用户发现了宝藏的时候触发。在viewController的类里面添加下面方法:

  func markTreasureAsFound(treasure: Treasure) { // 1
        if let index = self.foundLocations.indexOf(treasure.location)
            { // 2
            let alert = UIAlertController( title: "Oops!",message: "你已经来过这里(在第\(index + 1)步)! 再试一次!",
                                           preferredStyle: .Alert)
            alert.addAction(UIAlertAction(title: "OK",
                                            style: .Default,
                                            handler: nil))
            self.presentViewController(alert, animated: true, completion: nil)
        } else { // 3
            self.foundLocations.append(treasure.location)
            // 4
            if self.polyline != nil { self.mapView.removeOverlay(self.polyline)
            }
            // 5
            var coordinates = self.foundLocations.map { $0.coordinate }
            self.polyline = MKPolyline(coordinates: &coordinates,count: coordinates.count)
            self.mapView.addOverlay(self.polyline)
        }
    }

下面介绍了这个方法都做了些什么:(函数的具体实现不用特别了解)

1.首先,你先用indexOf方法判断locations数组中是否有宝藏信息。这个indexOf会遍历数组返回可选类型的信息,有值则是对应的索引,如果没有找到则返回nil。可选类型的这个特点很棒,就像你看到的那样,代码是不是特别易读。
2.如果在locations的数组中已经存在了location。那么则弹出对话框显示用户在这一步发现了宝藏。
3.如果locations数组中没有这个location,则将其添加到数组中。
4.然后,如果某条路线已经存在了,你需要将他从地图中移除。如果不移除的话,每次发现都会重新绘制一个新的线条在上面。
5.最后,你创建一个新的MKPolyline并添加到地图视图中。注意使用map函数的数组。这个函数为数组的每个元素都提供了创建一个新的数组结果的闭包函数。上面的实例使用了简短的闭包函数,因为Swift可以map函数前实现自动推断。数组的每个元素都传递到闭包函数直到变量为$0.

这个时候你还会发现在indexOf()使用的地方报错。猜猜为什么。这个方法需要扫描数组的每个元素,它使用相等操作符==进行匹配,你不能对类和结构体使用这个匹配符号。

接着来讲讲符号重载,先定义Equatable等号协议,因此,你需要让GeoLocation循着Equatable协议。
需要定义一个如下这种协议

protocol Equatable {
   func ==(lhs: Self, rhs: Self) -> Bool 
} 

你需要实现一个方法。但是这个方法名叫==。这看上去非常奇怪,如果你是Object-C开发员的话,可能会看到过类似的的isEqual。
这是另一个Swift有别于Object-C的地方,有一种被称为运算符重载的神奇用法。不是用一种特殊的方法让你进行相同判断,而是Swift让你重载==操作符进行相同判断。

打开GeoLocation.swift 并在最底部添加:

extension GeoLocation: Equatable { } 
func ==(lhs: GeoLocation, rhs: GeoLocation) -> Bool { 
    return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude 
} 

扩展的通常方法是声明协议的一致性,但是注意==函数没有在扩展内部。你必须将其声明为全局范围的运算符重载,因为他们不是部署在某一个类的方法,而是属于你使用==运算符的任何地方。他们只是与类相关联,关联两个类的实例参数用于比较。实现很简单:检查两个经度和纬度是否相同,并返回布尔值。
这里写图片描述

Access Control - 访问控制

目前,所有的方法和变量都被声明在类和结构中,默认是public。这意味着任何其他地方的代码都可以进行调用。通过访问控制修饰符,Swift提供了灵活的方法以便你访问每个属性,方法等。

下面是三个级别的访问控制符:

Public:谁都可以访问。
Internal:只有在相同的target(库或app)中的其他代码可以访问。这是默认的访问级别。
Private:只有同一个源文件的代码可以访问。

使用访问控制有助于代码维护。例如,您可以在你的类中使用很多的辅助方法,但你并不想公开暴露出来,因为这些不同的状态类应该对用户是隐藏的。这些可以被标记为private用于阻止用户使用这些方法。

internal的访问级别很适合在库中限制。通常你的类有的方法也都是不需要在其他的库中使用。如果这些方法被暴露在库外,则其他的使用者就可以看到内部的数据结构之类的。很明显你不想将这些库中的方法暴露在外。因为internal是默认级别的,如果你要访问外部的库,你可能需要用public来应用于对应的类,结构,枚举等,让其可以被任何类,结构等访问。

在应用程序中,internal访问级别也通常是你想使用的(方便,并且是默认的)。应用代码不会被别的库中的代码使用,所以他也不需要访问本身以外的任何东西。也就是说,单元测试意味着公众是可以使用的,单元测试通常是一个单独的目标,要访问任何隐藏的单元测试都需要先将其声明为public。

Object-C开发者乐于学习Swift,私有方法确实是私有方法。不会在运行的时候让你有任何的后门去访问不该访问的私有方法。

让我们看看在应用中的行动。

修改方法声明如下:

private func markTreasureAsFound(treasure: Treasure) 

编译并运行,每件事都和刚刚代码的运行情况一样。没错,确实没变化!

视图控制器下也有三个属性。这些都是本地状态值,所以不应该从外部访问。继续改变他们的声明如下:

private var treasures: [Treasure] = []
private var foundLocations: [GeoLocation] = [] 
private var polyline: MKPolyline! 

再次编译运行,依然正常工作,但是外界却再也无法联系到这些属性!

internal修饰符在应用中有些冗余,只有在多个应用中分享库时使用他才会有意义。然而,想象你创建一个TreasureHunt库,打包所有的代码,以便让另外一个应用可以显示这个相同的寻宝游戏app。这种情况下,你需要标记Treasure和GeoLocation为internal,以便他们不被其他应用调用。只有ViewController类可以被使用。这种情况下通常会重命名TreasureHuntViewController!

访问权限修饰符对于声明很有帮助,尽可能使用Private。这样你只需要公开核心的api使用对象。这样做可以减少更多的错误并提高代码的维护性。你重构私有方法也可以不用担心破坏了外部的api。恭喜你,这个应用你建立完了。祝你寻宝好运!

Where to go from here? - 接着干什么呢

在这一章中,你了解了不少类和结构的内容。你创建了你自己的类的层级并看到了如何扩展类和结构。你了解了类和结构之间的差异,以便你能正确的使用。你也学会了动态调用以及算术符的重载等。

此外,你还开发了一个很酷的涉及到硅谷的应用。这也许是你的第一个ios应用程序,如果是这样,你该给自己个大大的红花,然后站在胜利的舞台上。热泪盈眶,感谢国家,感谢爸妈!感动中国,感动你我!

虽然这一章给你讲解了很多的基础概念,但是这本书的其余部分还会有涉及到类和结构。你会在更多的真实用例中看到如何使用类和结构,在后面,你还会了解到如何通过使用泛型让编程更加强大。
经验丰富的Object-C开发员也会对如何让Swift与Object-C直接关联进行混编感兴趣,这些我们后面都会有涉及到。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值