许多编程语言都有一个共同的特点,有一个叫枚举的类型enum 。枚举是将相似的数据组合在一起。例如,在Cocoa中处理文本的对齐问题时,你可能会看到如NSTextAlignment.Center之类的枚举值。NStextAlignment枚举可以让你用较为轻松且易读的名字来标记不同的类型,就像Center或Left。
swfit的枚举类型不仅仅是一个枚举。相对于像传统的枚举有离散的值,更像是一个类或结构。在Swift中,枚举可以关联相关的方法甚至是构造函数!
在这一章中,你将使用playground来学习并练习如何将枚举和switch语句运用在你的app中。switch语句和枚举在一起使用非常强大。你将看到这两个基本功能组合在一起时是如何强大的编程技术。
腾空下大脑,准备下了解枚举吧!
枚举是多种语言里的语句块,开发人员可以定义一组值来一起构成一个类型。
卡片游戏用来举例子蛮合适的。一个卡片游戏的app可能会用到一个枚举来描述下列几种情况:梅花,方块,黑桃,红桃:
enum Suit {
case Heart
case Club
case Diamond
case Spade
}
在Swift中定义枚举的方式和c或oc是非常像的。让我们看看在Swift中是如何使用枚举的。
新建一个playground并将下面的代码填入其中:
enum Shape {
case Rectangle
case Square
case Triangle
case Circle
声明了一个叫Shape的枚举,里面有四种状态,每种状态都是一个枚举值。
既然你已经定义了枚举,现在你就可以将这个枚举作为是一种类型进行使用了。后面紧接着添加代码:
var aShape = Shape.Triangle
注意你引用这个值时使用的语法:Shape是枚举的名字,Triangle是枚举里面的类型。
如果你定义了变量的类型,则你可以直接从你和定义的类型名相同的枚举中获取值,就像下面这样:
var aShape: Shape = .Triangle
声明了这个类型是Shape后,你可以直接修改原来变量的值,如下:
aShape = .Square
如果你是oc开发者,你可能知道每个枚举值都可以分配一个值。Swift也提供了这个相同的功能,你还会看到Swift的枚举比oc的更强。
修改Shape的定义如下:
enum Shape: Int {
case Rectangle
case Square
case Triangle
case Circle
}
你肯定会不由自主的想Shape是否是继承了Int类型,毕竟语法和类以及结构的继承太像了。在枚举中,这个语法表示枚举持有的原始值是Int类型。
现在你的枚举有了原始值的类型,枚举也有两个方法:rawValue和rawValue:。在playground中添加如下两行:
var triangle = Shape.Triangle
triangle.rawValue
rawValue返回和提供的枚举值相关联的原始值,查看playground的右边栏,你可以看到Triangle的原始值是2:
正如你猜的那样,原始值是从0开始的,Rectangle是0,Square是1,Triangle是2,Circle是3.
与此相反的是rawValue:将原始值转换为对应的枚举值,继续往后添加代码:
var square = Shape(rawValue: 1)
你可能也注意到右边栏的输出有点不一样了,你会看到返回的枚举值是可选类型。因为并不是每个整数都有着对应的枚举值,所以要返回一个可选类型。
在后面继续添加如下代码:
var notAShape = Shape(rawValue: 100)
注意到右边栏显示对应的值是nil。
如果你愿意,你也可以给每个枚举都定义一个特殊的值,而不是让Swift自动给他们赋值。修改其中一个枚举值如下:
case Rectangle = 10
注意下现在Triangle有个原始值是12了。当你的枚举是个整数时,里面的每个枚举值都可以不用特意去分配值,Swift会自动根据前一个枚举值的原始值加1赋值。
使用原始值的一个主要原因是你可以序列化也可以反序列化枚举的值。举个例子,枚举的类型只是一个符号,并不能像数字或字符串那样在在服务器请求json。如果你给每个枚举值都定义了值,你就可以在服务器上进行处理了。然后服务器和app就可以进行交互,可以将值直接映射到枚举值上了。
枚举值的类型并不是仅限于只可以使用Int,可以是任何的数据类型,例如Float和Double,他甚至可以是String。
挑战:修改Shape的原始值类型为Sting。立马你就会注意到编译器报错。这是因为如果你的类型不是Int(或者文字可转换为Int),则你就必须为每一个枚举值设置原始值。
下面举个例子:
enum Shape: String {
case Rectangle = "Rectangle"
你需要为你的每个枚举值都设置一个值。以及改变rawValue:方法调用时的参数类型。
继续往后添加代码:
enum Ratios: Float {
case pi = 3.141
case tau = 6.283
case phi
}
编译器报错,如果枚举不是integer类型时,需要为所有的枚举赋一个原始值。整数值可以自动在前一个枚举值的原始值的基础上加1.
当枚举和Switch成对出现时,使用非常的安全。最关键的是你可以使用switch语句去测试匹配一个枚举值。
在Swift中,switch的语句比其他语言多了一些使用技巧,现在来看一下。
新建一个playground,并添加代码:
enum Shape {
case Rectangle
case Square
case Triangle
case Circle
}
这个和前面创建的简单枚举一样。接着,在代码后面继续添加:
var aShape = Shape.Rectangle
switch(aShape) {
case .Rectangle:
print("This is a rectangle")
}
如果你是oc开发员,则对这个语法一定很熟悉。声明了一个switch并去匹配aShape的枚举值。
但是此处编译器报错提示你还没有完整的列出所有不同枚举值的匹配情况。
在Swift中的switch必须要十分的详尽,也就是说你需要处理枚举的每一个匹配情况。这是一种安全机制,因为我们经常会因为疏忽大意而漏写了某个匹配情况造成bug,尤其是在匹配枚举值的时候!在其他语言中,你可以只添加一个枚举值而switch会跳过不匹配的情况,在Swift中则会抛出错误进行提示。
修改为完整的switch如下:
switch(aShape) {
case .Rectangle:
print("This is a rectangle")
case .Square:
print("This is a square")
default: break
}
添加了一个匹配Square的情况,同时也添加了一个default的匹配情况用于处理其他没有进行匹配的情况。你会看到调试显示“This is a rectangle ”,匹配了aShape的值。
现在的这个语句块非常的详尽,因为他分别独立的处理了Rectangle,Square以及其他没有匹配情况的值。
如果你对其他语句中的switch语句熟悉的话,你可能已经发现这个switch中的前两句没有break。你可能会认为控制器在匹配了Rectangle后,还会继续往下匹配。在其他语言如oc中确实如此,他们都有贯通的情况,依靠break来跳出switch,如果没有则会继续往下匹配不同的状态。
然而Swift不会有贯通的情况。没有贯通的情况可以消除因为疏忽忘写break造成的bug。所以我得谢谢你咯<( ̄3 ̄)> ,Swift!
在上面的代码中default里面必须要写一个break,因为不能在case里放一个空的代码,这是唯一需要用到break的时候。
Swift也可以实现匹配多个情况达到和贯通一样的效果,修改代码如下:
case .Rectangle, .Square:
print("This is a quadrilateral")
case .Circle:
print("This is a circle")
default: break
}
正如你所见的,Swift一次性匹配多个情况的方法非常的简单。使用逗号(不用单独的写一行)来分割不同的枚举值,不仅读起来更加简洁,而且摒除了忘记写break造成的bug。
本章的剩下内容你还会了解到更多的和switch语句有关的姿势。
到目前为止,你都可能认为Swift的枚举和其他语言的枚举并没有什么不同的,现在就来为大家展示下不同之处。
Swift允许你为每一个枚举值都分配一个或多个值,叫关联值。这个特点让枚举在Swift中异常强大。
现在就来看个例子,在新的playground中添加如下代码:
enum Shape {
case Rectangle(Float, Float)
case Square(Float)
case Triangle(Float, Float)
case Circle(Float)
}
和前面一样,你声明了一个叫Shape的枚举。然而这一次你给每个枚举值都声明了一个关联值。关联值中的括号里指明了他们的类型。
在这个例子中,你使用相关值来保存不同形状的大小。两个浮点数表示长方形的宽和高,正方形的边长,三角形的底和高以及圆的半径。
现在你可以在生成枚举值时添加相关值。playground添加代码:
var rectangle = Shape.Rectangle(5, 10)
生成了一个宽是5,高是10的新的Rectangle值。注意,这很难说清在这种情况下每个参数是什么,第一个参数是宽还是高?为了提示,你可以给每个参数命名,如下:
enum Shape {
case Rectangle(width: Float, height: Float)
case Square(Float)
case Triangle(base: Float, height: Float)
case Circle(Float)
}
在添加了名字后,前面对变量rectangle变量的声明就必须修改了:
var rectangle = Shape.Rectangle(width: 5, height: 10)
这看起来有点像类和结构了!在这点上,你可能会想尝试用rectangle.width方式去访问相关的值。
结果不足为奇,你并不能像结构和类访问属性那样直接访问关联值。关联值和原始值不同,原始值提供了一种用其他方式展示枚举内容的方式,例如使用interger时可以序列化或反序列化每个枚举的值。另一点不同的是,你只能在switch语句中访问到枚举的关联值。
让我们看看他是如何工作的,在playground的底部继续添加代码:
switch (rectangle) {
case .Rectangle(let width, let height):
print("Rectangle: \(width) x \(height)")
default:
print("Other shape")
}
控制台输出:
上面的代码和普通的switch看似并没有什么不同,除了在case中添加了对应的相关值中绑定了参数宽和高。
但是等等,这里还有更多和相关值有关的技巧。switch语句不仅可以读取枚举的值。修改代码:
switch (rectangle) {
case .Rectangle(let width, let height) where width <= 10:
print("Narrow rectangle: \(width) x \(height)")
case .Rectangle(let width, let height):
print("Wide rectangle: \(width) x \(height)")
default:
print("Other shape")
}
现在switch语句中有两个匹配Rectangle的情况。注意下第一个匹配,添加了一个where子句用于匹配特定的Rectangle值-宽度不大于10.
因为你的rectangle的宽度是5,所以你将看到输出Narrow rectangle。
你也可以在where 的子句后面再做些其他事情,比如,修改下代码:
case .Rectangle(let width, let height) where width == height:
print("Square: \(width) x \(height)")
case .Square(let side):
print("Square: \(side) x \(side)")
你用了一个长宽相同来匹配一个正方形。干的漂亮!
不过请注意,case的顺序很重要,来看看为啥,将Rectangle的匹配移动到switch的最顶部:
switch (rectangle) {
case .Rectangle(let width, let height):
print("Wide rectangle: \(width) x \(height)")
case .Rectangle(let width, let height) where width == height:
print("Square: \(width) x \(height)")
case .Square(let side):
print("Square: \(side) x \(side)")
case .Rectangle(let width, let height) where width <= 10:
print("Narrow rectangle: \(width) x \(height)")
default:
print("Other shape")
}
现在控制台输出:
这是你期望的吗?宽度小于10那行没有输出呢?为什么匹配的不是另一个Rectangle。这说明switch语句只会使用第一个相匹配的情况。
挑战:写一下其他的值来匹配Square,Triangle和Circle。你的匹配情况应该如下:
1.Triangle的高大于10
2.Triangle的高是底的两倍
3.Circle的半径小于5
在Swift中,枚举在很多方面都和类与结构体很像。他有和结构一样相同的值语义,这也就是说他通过值传递到函数中与结构一样是复制。枚举也可以有实例方法,就像类和结构一样。
新建playground并添加代码:
enum Shape {
case Rectangle(width: Float, height: Float)
case Square(Float)
case Triangle(base: Float, height: Float)
case Circle(Float)
}
接着在在枚举中添加方法:
func area() -> Float {
switch(self) {
case .Rectangle(let width, let height):
return width * height
case .Square(let side):
return side * side
case .Triangle(let base, let height):
return 0.5 * base * height
case .Circle(let radius):
return Float(M_PI) * powf(radius, 2)
}
}
现在你应该相信为什么我会说Swift的枚举比oc强很多了吧!
这里添加了一个方法用来获取Shape枚举下的不同形状的面积。自己本身会确认到底使用哪一个公式来进行计算,底部继续添加代码:
var circle = Shape.Circle(5)
circle.area()
你可以在右边栏中看到计算结果:
Perfect!你现在可以计算这些形状的面积了。当然,你还有许多的其他类型的方法可以添加到Shape中。
挑战:添加一个计算周长的方法在Shape枚举中。他应该能计算形状所有边的长的总和。
你甚至可以在枚举中实现初始化,在枚举声明代码的最底部继续添加:
init(_ rect: CGRect) {
let width = Float(CGRectGetWidth(rect))
let height = Float(CGRectGetHeight(rect))
if width == height {
self = Square(width)
} else {
self = Rectangle(width: width, height: height)
}
}
允许Shape的构造器使用CGRect。在初始化中,你分配值给自己,而不是返回一个值。在这个例子中,如果CGRect的边长相同,则方法生成一个正方形Square,否则创建一个矩形Rectangle。
在playground的最底部添加代码:
var shape = Shape(CGRect(x: 0, y: 0, width: 5, height: 10)) shape.area()
利用参数CGRect使用一个新的构造器生成了一个Shape,看一下右边的输入内容,正如你所期待的那样,计算的面积值为50.0。
枚举的初始化必须为self分配个内容,否则编译器报错,比如:
init(_ string: String) {
switch(string) {
case "rectangle":
self = Rectangle(width: 5, height: 10)
case "square":
self = Square(5)
case "triangle":
self = Triangle(base: 5, height: 10)
case "circle":
self = Circle(5)
default:
break
}
}
尽管上面的初始化中基本所有情况都覆盖了,但是switch语句中default里没有给self分配内容,所以依然编译不通过。
这样的初始化用一个工厂方法更好,在枚举代码底部添加:
static func fromString(string: String) -> Shape? {
switch(string) {
case "rectangle":
return Rectangle(width: 5, height: 10)
case "square":
return Square(5)
case "triangle":
return Triangle(base: 5, height: 10)
case "circle":
return Circle(5)
default:
return nil
}
}
该方法是静态的,意味着他属于类型本身而不是任何的实例。
该方法使用了一个string并返回相关的Shape值。使用可选类型作为返回值,允许返回nil,以防传入的string匹配不到任何shape情况。
现在在最底部添加测试代码:
if let anotherShape = Shape.fromString("rectangle") {
anotherShape.area()
}
这里使用了一个新的工厂方法通过字符串来生成了一个shape。因为返回的是一个可选值,所以他需要用解包。在这里,使用的是if语句来打开绑定的内容给一个变量。
Swift的可选类型是一个枚举,这可能会让你有点吃惊。他的实现基本和下面一样(具体的细节删除掉了)
enum Optional
case None
case Some(T)
init()
init(_ some: T)
static func convertFromNilLiteral() -> T?
}
第一个case是None,也就是可选类型中的nil。第二个case是Some,即可选类型中包含的值。
这里他使用了泛型允许其可选类型可以包含任何类型的值。同样的对Some使用了关联值。
可选的类型也表明,枚举可以实现协议,就像其他完整类型的类和结构。
NiLiteraConvertible的协议允许你使用可选的nil-编译器自动将其转换为一个调用convertFromNiLiteral的方法类型。
这些都说明了Swift中枚举的强大,远超过在oc中和他同名的枚举。
JSON parsing using enums &#8211; 用枚举来解析JSON
上面的内容听起来可能很抽象。Shapes是有趣的,但不是大多数应用都会使用到shapes。你已经看了一些枚举和可选类型一起使用的例子。他们是很强大,但是事实上你很少能看到枚举用法。
让我们来看一下另一个现实生活中的例子。JSON解析在应用中是非常常见的。在Swift中,他是类型安全的,但相对的是繁琐的JSON解析响应的数据。你会得到大量的嵌套,让我们来看看。
创建一个新的playground,然后添加代码:
let json = "{\"success\":true,\"data\":{\"numbers\":[1,2,3,4,5],\"animal\":\" dog\"}}"
if let jsOnData= (json as NSString).dataUsingEncoding(NSUTF8StringEncoding)
{
let parsed: AnyObject? = try NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions(rawValue: 0))
// Actual JSON parsing section
if let parsed = parsed as? [String:AnyObject] {
if let success = parsed["success"] as? NSNumber {
if success.boolValue == true {
if let data = parsed["data"] as? NSDictionary {
if let numbers = data["numbers"] as? NSArray {
print(numbers)
}
if let animal = data["animal"] as? NSString {
print(animal)
}
}
}
}
}
}
满满的一屏幕代码是个什么鬼!这里使用了NSDictionary,NSArray等的。这么复杂的写法,白瞎了Swift的安全特性。在Swift中,这并不是唯一解析JSON的方法,但大多数方法总是最终被嵌套的相当复杂。
相反,你可以使用枚举让其变得简单。在第四章“泛型”中,在你不知道的时候就使用了JSON解析。Flickr类需要解析从FlickrAPI过来的信息。你可能点进去看过,也可能没有。现在让我们来看看他是怎么工作的吧。
在这一章的资源文件中找到这个文件。打开叫JSON.swift的文件来看一下。他是个枚举!这使得json的解析明显的变得更加的容易。让我们看看他是怎么工作的。
首先看看枚举的定义:
enum JSONValue {
case JSONObject([String:JSONValue])
case JSONArray([JSONValue])
case JSONNumber(NSNumber)
case JSONNull
}
这是解决这个问题的核心部分。每个在JSON中的值都可以是一个对象,一个数组,字符串,数字,布尔值或null。一个字典的对象需要一个字符串和JSONValue,一个数组是一个JSON值的数组。这完美的描述了一个枚举的关联值。so,这就是我们接下来要做的。
通过这种方式,你可以用一个json对象来描述枚举下每一个JSONValue值。
大多数的JSON解析需要用string关键字来读取。在之前的例子中,data和success值是从外部读取到的。这里可以使用下标subscript。下面是在枚举中的下标定义:
subscript(key: String) -> JSONValue? {
get {
switch self {
case .JSONObject(let value):
return value[key]
default: return nil
}
}
}
注意这个下标语法返回的是一个可选类型的JSONValue。也就是说如果这个对象不存在则返回nil。同样的,如果JSONValue不是个对象,同样也以nil返回。如果你查看下JSON.swift 文件那么你将看到一个整数参数的下标函数。和这个下标函数十分相似,但是是用来读数组的值。
你还能够从对象和数组中获得更多的JSONValues,但怎么从这个value中获取到实际的字符串,Bool等对象呢?需要通过如下的属性计算获取:
var string: String? {
switch self {
case .JSONString(let value):
return value
default:
return nil
}
}
就像这个下标语法,属性返回一个可选类型。如果JSONValue是一个JSONString类型则应该是个字符串,所以返回他的关联值,否则返回一个nil。其他的匹配类型也是同样的使用这种属性计算方法访问。
最后,还需要一个方法将读到的对象读入到JSONValue中。用来将字典,数组或其他任何类型的对象表示到JSONValue的枚举中。就像这样:
static func fromObject(object: AnyObject) -> JSONValue? {
switch object {
case let value as NSString:
return JSONValue.JSONString(value)
case let value as NSNumber:
return JSONValue.JSONNumber(value)
case let value as NSNull:
return JSONValue.JSONNull
case let value as NSDictionary:
var jsonObject: [String:JSONValue] = [:]
for (k: AnyObject, v: AnyObject) in value {
if let k = k as? NSString {
if let v = JSONValue.fromObject(v) {
jsonObject[k] = v
} else {
return nil
}
}
}
return JSONValue.JSONObject(jsonObject)
case let value as NSArray:
var jsonArray: [JSONValue] = [] for v in value {
if let v = JSONValue.fromObject(v) {
jsonArray.append(v)
} else {
return nil
}
}
return JSONValue.JSONArray(jsonArray)
default:
return nil
}
}
该方法使用了NSJSONSerialization,返回了如NSArray和NSDictionary等基础对象。方法检测了对象的类型并返回一个相关的JSONValue实例。
现在就让我将JSON.swift的文件运用练习下!将这个文件中的代码全部复制粘贴到一个playground中。你必须使用复制粘贴,因为playground不能引用其他的文件。
现在在后面继续添加代码:
if let jsOnData=(json as NSString).dataUsingEncoding(NSUTF8StringEncoding)
{
if let parsed: AnyObject = NSJSONSerialization.JSONObjectWithData( jsonData,
options: NSJSONReadingOptions.fromRaw(0)!, error: nil)
{
if let jsOnParsed= JSONValue.fromObject(parsed) {
// Actual JSON parsing section
if jsonParsed["success"]?.bool == true {
if let numbers = jsonParsed["data"]?["numbers"]?.array {
print(numbers)
}
if let animal = jsonParsed["data"]?["animal"]?.string {
print(animal)
}
}
}
}
}
顶部有一个额外的if语句,因为JSONValue.fromObject()返回的是一个可选类型所以在这里需要使用到解包。然而,实际上这里的JSON解析部分已经从5层嵌套变为了现在的2层。
注意这里使用了可选链来解锁可选类型。例如,如果当在解析number时“data”这个key不存在,则这个表达式会返回一个nil且”numbers“key键不会调用。
同样的如果“animal”key存在但不是一个字符串则if语句会返回nil不会执行print方法。
想必前面的代码你保留住了Swift的安全特性,还有什么比这更重要的?!我想现在你也会同意枚举的强大了吧。所以你应该在编程的时候充分的利用好他。当你有一个可以组一组预定义不同东西的类型如JSON时,这个枚举实在是太实用了。
在这一章中,你创建了一个简单的枚举并实现了枚举额外的内容原始值和关联值。
你还使用了switch语句来检查枚举的值。此外,你还看到了在Swift中,枚举远超其他传统编程语言如oc的优点,例如详尽的检查以及先进的匹配模式。
最后,你看到了如何将枚举关联到方法和初始化构造器中,通过研究发现可选类型也是一个枚举!
你可能会发现你在Swift中会频繁的使用枚举,因为相比在oc等语言中,Swift的枚举可以让你做更多的事情。
将你所学的技巧付诸实践,并利用枚举完善你的应用程序!