美文网首页swift编程开发Swift编程Swift&Objective-C
初识Swift的枚举,结构体,和类

初识Swift的枚举,结构体,和类

作者: matrix_lab | 来源:发表于2016-08-13 21:05 被阅读106次

    本篇文章翻译自:Getting to Know Enums, Structs and Classes in Swift
    原作:ray fix on March 8, 2016


    在只有OC的日子里,我们只是封装类是不太能够满足我们的工作要求的。然而,在现代的iOS和Mac编程中,swift提供我们3种选择:枚举,结构体,和类。
    配合这协议,这些类型能够创造不可思议的事情。尽管这些类型有着相似的能力,但也有很大的不同。
    这篇tutorial的目的有三:

    • 带你体验枚举,结构体,和类的使用
    • 给你何时应该使用他们的直觉
    • 帮你理解他们各自是怎么工作的

    首先,这篇tutorial假设你已经有了一些swift的基础知识和面向对象的编程经验。

    全是关于类型

    swift三大卖点:安全,快速和简洁。
    安全意味着使用swift很难写出耗费内存和bugs隐藏很深的代码。swift让你的工作变得更加安全,因为它会在编译的时候提醒你产生了bug,而不是把你晾在哪儿直到运行时。
    而且,因为swift可以让你更清楚的表达你的意图,优化器可以让你的代码运行得更加轻盈,快速。
    因为建立在一些概念上,swift语言的核心是简单和高度正规化。尽管它有一些规则,但是你也可以使用它做一些amazing的事情。你能做到这些的关键是swift的类型系统:

    types

    尽管只有6个, swift的类型非常强大。没错,不像其他语言有很多内建的类型,swift只有6个。
    这些类型由4个命名类型:协议,枚举,结构体,类和2个复合类型:元组,函数组成。
    你可能会想到其他的基本类型---像 Bool, Int, UInt, Float, Double, Character, String, Array, Set, Dictionary, Optional等等。但是这些基础类型是由命名类型建立而来,可以看作是swift标准库的一部分。
    本篇tutorial重点是枚举,结构体和类组成的命名模型类型。

    可缩放矢量图形(SVG)的形状

    作为一个演示示例,你会创建一个安全,快速,简单的SVG图形渲染框架。
    SVG是基于XML的2D矢量图形格式。这个特点在1999年被W3C 确定为公共标准。

    开始

    File\New\Playground,创建一个playground,命名为Shapes, 并且设置为OS X平台(这个时候应该是MacOS了)。点击Next,选择保存路径,创建并保存文件。清空该文件,然后添加:

    import Foundation
    

    目标是渲染下面的东西:

    <!DOCTYPE html><html><body><svg width='250' height='250'><rect x='110.0' y='10.0' width='100.0' height='130.0' stroke='Teal' fill='Aqua' stroke-width='5' /><circle cx='80.0' cy='160.0' r='60.0' stroke='Red' fill='Yellow' stroke-width='5'  /></svg></body></html>
    
    

    相信我,如果用浏览器或者WebKit视图渲染,它会看起来好很多。

    你需要一个类型来代表颜色,SVG使用CSS3颜色类型,这种类型可以指定为一个名称,RGB或者 HSL。了解这些特征可以浏览:http://www.w3.org/TR/css3-color/.
    在SVG中定义一个色值的方式是把它指定为图形的一个属性。例如:fill = 'Gray'。swift中一种简单的方式是用字符串来表示色值, 如: let fill = "Gray"

    使用字符串虽然简单有效,但是有几个不可忽视的缺点:

    1. 很容易发生错误。任何字符串即使它不代表色值,也能编译通过,但是在运行时,显示的效果就不对了嘛。例如:"Gray"如果拼写成"e",将达不到预期效果,但可以编译通过。
    2. 自动补全不会帮你找到有效的颜色名称。
    3. 如果作为参数你传递一个色值,那么字符串作为一个颜色类型不会表现的那么明显,自解释性较差。

    枚举来帮忙

    使用自定义类型来解决这个问题。如果你是从Cocoa Touch转过来的,你可能会封装一个像UIColor的类。当然使用类来设计是可以的,但是swift提供了更多的选择来定义你的模型。
    我们还没有写任何东西,来想一想我们怎么用枚举来实现Color。
    你可能会这么实现:

     enum ColorName {
        case Black
        case Silver
        case Gray
        case White
        case Maroon
        case Red
    }
    

    上面的实现跟C语言的枚举很相似。然而,不像C语言的枚举,swift可以让你指定每一种Case的类型.
    指定为支持存储类型的枚举可以看做是RawRepresentable类型。因为它自动遵守了RawRepresentable协议。
    因此,你可以指定ColorName为String类型,并且为每一个case赋值,如下:

    ColorName: String {
        case Black = "Black"
        case Silver = "Silver"
        case Gray = "Gray"
        case White = "White"
        case Maroon = "Maroon"
        case Red = "Red"
    }
    

    然而,swift为String类型的枚举做了些特殊的工作。你不必指明每一种case等于啥,编译器会自动让字符串(指rawValue)跟名称一致。这意味着你只需要写上case名就可以了:

    enum ColorName: String {
        case Black 
        case Silver
        case Gray
        case White
        case Maroon
        case Red
    }
    
    

    你还可以使用逗号隔开每一种case来减少代码量,这时仅使用一个case关键字:

    enum ColorName: String {
       case Black, Silver, Gray, White, Maroon, Red
    }
    
    

    现在你有了第一个自定义类型,所有的好事从这里就开始了。例如:

    let fill = ColorName.Grey // 错误:拼写错误的颜色名称不能通过编译,很好! 
    let fill = ColorName.Gray //  正确的名称可以自动补全,编译通过🤗
    

    关联值

    用ColorName来命名颜色很好,但是你可能回想起CSS颜色不止这一种表示方式,还是:RGB, HSL等等。你怎么模型化它们呢?
    swift的枚举类型非常适合来模型化有多种可能的类型。例如:CSS颜色,每一种case可以与它的数据结伴出现。这些数据被称为关联值。
    使用枚举定义一个CSSColor:

    enum CSSColor {
        case Named(ColorName)
        case RGB(UInt8, UInt8, UInt8)
    }
    

    有了这个定义,CSSColor可以有2种状态

    1. 它可以是Named, 这种情况下,关联值是ColorName类型的值
    2. 也可以是RGB,这种情况下,关联值是3个UInt8类型(0-255)组成的元组,他们分别表示red, green, 和blue。

    注:为了简洁,示例中我们省略了RGBA, HSL和HSLA的场景。

    枚举的协议和方法

    如果你想要能够打印多个CSSColor的实例,像其他命名类型一样,可以遵守协议。你的类型可以通过遵守CustomStringConvertible协议来使用print语句。
    跟swift标准库协作的关键是遵守标准库的协议。为CSSColor扩展协议:

    extension CSSColor: CustomStringConvertible {
        var description: String {
            switch self {
            case .Named(let colorName):
                return colorName.rawValue
            case .RGB(let red, let green, let blue):
                return String(format: "#%02X%02X%02X", red, green, blue)
            }
        }
    }
    

    这使得CSSColor遵守CustomStringConvertible协议。这样就告诉swift,CSSColor可以被转换为字符串。我们通过实现description计算属性告诉它怎么转换。
    这个实现中,self要分情况去确定枚举类型是Named,还是RGB。在每一种case下,你可以转化为对应格式的字符串。Named case下返回字符串名称,而RGB case下,返回特定格式的red, green, blue的值。

    let color1 = CSSColor.Named(.Red)
    let color2 = CSSColor.RGB(0xaa, 0xaa, 0xaa)
    print("color1 = \(color1), color2 = \(color2)")
    

    不像只使用字符串来表示颜色,编译器会在编译时进行类型检查,且结果证明准确无误。

    枚举类型的构造器

    就像swift的类和结构体一样,你也可以为枚举类型添加构造器。例如:你可以用Gray来创建一个构造器。

    extension CSSColor {
        init(gray: UInt8) {
            self = .RGB(gray, gray, gray)
        }
    }
    

    playground中添加代码:

    let color3 = CSSColor(gray: 0xaa)
    print(color3)
    

    现在你可以很方便创建Gray颜色了。

    枚举类型的命名空间

    命名类型可以作为一个命名空间让代码组织良好,降低复杂度。你创建了ColorName和CSSColor,且ColorName只在CSSColor的上下文中用到。
    如果你把ColorName隐藏到CSSColor模块中,岂不是更完美?
    从playground中移除ColorName,取而代之的是:

    extension CSSColor { 
            enum ColorName : String { 
                  case Black, Silver, Gray, White, Maroon, Red, Purple, Fuchsia,   Green,Lime, Olive, Yellow, Navy, Blue, Teal, Aqua 
        }
    }
    

    这样就把ColorName移动到CSSColor的扩展中了。现在ColorName被雪藏,被定义成CSSColor的内部类型。

    swift一个重大特性是声明顺序通常是无所谓的。编译器会扫描的文件好几次,把顺序弄明白,而不像C/C++/OC需要前向声明(用到的类型必须之前声明过)。
    然而,如果你收到一个关于ColorName的一个错误提示说它是未声明的类型。移除上面的扩展,重新定义ColorName就是了。有时候,playground会对定义的顺序比较敏感,即使是真的没有关系。

    枚举可以被设置成不能初始化的纯粹命名空间。例如:你很快就会用到数学常量pi执行一些运算。当然,你可以使用Foundation的 M_PI宏,但是你最好定义自己的让工作尽可能轻便。(Arduino 微控制器,我们来了!😈)

    enum Math {
        static let pi = 3.1415926535897932384626433832795028841971694
    }
    

    因为Math中不包含任何case,且在扩展中添加case是非法的,那么Math将不能实例化。你永远不会误用Math为一个变量或者参数。
    声明pi为一个static常量,你不必实例化。当你需要pi值的时候,你仅需要使用Math.pi就好,而不用记住一大串数字。

    持股枚举(我要成股东了👻)

    swift的枚举类型比其他语言的枚举要强大的多,例如: C或OC。正如你所见,你可以扩展他们,创建构造器方法,提供命名空间,和封装一些操作。
    目前,你已经使用枚举模块化了CSS colors了。我们能这么做得益于我们理解了CSS colors,修整了W3C的规范。
    从一些常见的情形中挑选一个,使用枚举类型很合适,例如:一周的七天,硬币的正反面,状态机的状态。swift的Optional也是用枚举实现的,它是带有关联值的.None或者.Some的一种情况。
    换句话说,如果你想要CSSColor能够扩展到其他没有在W3C规范中定义的颜色空间模块中,枚举类型不是首先。好吧,这会把我们带到下一个swift命名类型---结构体

    结构体

    因为你想要SVG的使用者能够定义他们自己的图形,那么使用枚举来定义图形类型不是一个好的选择。
    新的枚举case之后不能添加到扩展中。那么重任就落到类和结构体身上了。
    swift标准库团队建议当你创建新的模型的时候,你首先应该使用协议设计接口,你想要你的图形是Drawable类型,所以添加以下代码到playground中:

    protocol Drawable {
        func draw(context: DrawingContext)
    }
    

    协议定义了图形为Drawable类型。它有一个作图方法可以用来画DrawingContext类型的图形。
    说到DrawingContext,当然它是另一个协议。

    protocol DrawingContext {
        func draw(circle: Circle)
        // 更多基础类型,马上到来
    } 
    

    DrawingContext知道怎么绘制纯粹的几何图形:圆形,矩形和其他基础类型。

    注意:我们没有指定作图技术具体是哪一种,但是你实现的时候,要把他们都考虑进去---可能是SVG, HTML5Canvas, Core Graphics, OpenGL, Metal等等。

    现在可以来定义一个遵守Drawable协议的圆形了。

    struct Circle: Drawable {
        var strokeWidth = 5
        var strokeColor = CSSColor.Named(.Red)
        var fillColor = CSSColor.Named(.Yellow)
        var center = (x: 80.0, y: 160.0)
        var radius = 60.0
        
        func draw(context: DrawingContext) {
            context.draw(self)
        }
    }
    

    在结构体有几个存储属性:

    • strokewidth: 描边宽度
    • strokeColor: 描边颜色
    • fillColor: 填充色
    • center: 圆心位置
    • radius: 圆半径

    结构体跟类的工作方式有很大不同,也许最大的差别:结构体是值类型,而类是引用类型。

    值类型 VS. 引用类型

    值类型是一个分离的,独立的实体。
    值类型比较突出的代表是整形,每一种编程语言中都有。如果你想了解值类型是怎么工作的,那么可以想一想整形是怎么做的。例如:

    Int:
    var a = 10
    var b = a
    a = 30 //b的值仍然是10
    a == b //false 
    
    Circle(结构体类型)
    var a = Circle()
    a.radius = 60.0
    var b = a
    a.radius = 1000.0 //b.radius仍然是60.0
    

    如果Circle是Class类型,那么它就具有了引用语意了。也就意味着它会引用共有的对象。

    Circle(使用class定义

    var a = Circle() //Class类型
    a.radius = 60.0
    var b = a
    a.radius = 1000.0 //b.radius现在变成了1000.0
    

    当你使用值类型创建一个对象时,拷贝就发生;当使用引用类型,变量会引用相同的对象。类和结构体在这个点有很大的不同。

    矩形模型

    添加如下代码到playground,创建矩形类型,完善做图库。

    struct Rectangle: Drawable {
        var strokeWidth = 5
        var strokeColor = CSSColor.Named(.Teal)
        var fillColor = CSSColor.Named(.Aqua)
        var origin = (x: 100.0, y: 10.0)
        var size = (width: 100.0, height: 130.0)
        
        func draw(context: DrawingContext) {
            context.draw(self)
        }
    }
    

    你需要更新DrawingContext协议,以便它能够知晓如何去绘制矩形。在playground中更新DrawingContext:

    protocol DrawingContext {
        func draw(circle: Circle)
        func draw(rectangle: Rectangle)
        // 更多基础类型马上到来 ...
    }
    

    Circle和Rectangle都遵守Drawable协议。他们都会去做DrawingContext协议规定的工作。
    现在是时候创建一个绘制SVG style的具体模型。

    final class SVGContext: DrawingContext {
        private var commands: [String] = []
        
        var width = 250
        var height = 250
        
     //1
        func draw(circle: Circle) {
            commands.append("<circle cx='\(circle.center.x)' cy='\(circle.center.y)\' r='\(circle.radius)' stroke='\(circle.strokeColor)' fill='\(circle.fillColor)' stroke-width='\(circle.strokeWidth)'  />")
        }
        
     //2
        func draw(rectangle: Rectangle) {
             commands.append("<rect x='\(rectangle.origin.x)' y='\(rectangle.origin.y)' width='\(rectangle.size.width)' height='\(rectangle.size.height)' stroke='\(rectangle.strokeColor)' fill='\(rectangle.fillColor)' stroke-width='\(rectangle.strokeWidth)' />")
        }
        
        
        var SVGString: String {
            var output = "<svg width='\(width)' height='\(height)'>"
            for command in commands {
                output += command
            }
            output += "</svg>"
            return output
        }
        
        var HTMLString: String {
            return "<!DOCTYPE html><html><body>" + SVGString + "</body></html>"
        }
        
    }
    

    SVGContext是一个类,包含了一个私有String类型数组commands。在section 1和2处,遵守DrawingContext协议,draw方法会添加正确的XML字符串来渲染图形。
    最后,你需要一个文档类型来装载这些Drawable实例:

    struct SVGDocument {
        var drawables: [Drawable] = []
        
        var HTMLString: String {
            let context = SVGContext()
            for drawable in drawables {
                drawable.draw(context)
            }
            return context.HTMLString
        }
        
        
        mutating func append(drawable: Drawable) {
            drawables.append(drawable)
        }
    }
    

    这里,HTMLString是SVGDocument的一个计算型属性,它会创建一个SVGContext实例,并从该实例中返回HTMLString。

    展示一下SVG

    我们终于可以画出一个SVG了,playground添加如下内容:

    var document = SVGDocument()
    let rectangle = Rectangle()
    document.append(rectangle)
    
    let circle = Circle()
    document.append(circle)
    
    let HTMLString = document.HTMLString
    print(HTMLString)
    

    创建一个默认的圆形和矩形,并把它们放到文件中,然后打印XML。

    让我们来瞧瞧SVG吧:

    import WebKit
    import XCPlayground
    let view = WKWebView(frame: CGRect(x: 0, y: 0, width: 250, height: 250))
    view.loadHTMLString(HTMLString, baseURL: nil)
    XCPlaygroundPage.currentPage.liveView = view
    

    这里需要playground做一些tricky工作---设置web view来显示SVG,按下Command-Option-Return在辅助编辑器中显示web view。



    这酸爽,你get到了吗?

    目前为止,你用结构体(值类型)和协议实现了Drawable模块。
    现在我们用类,这会要求你去定义一个基类和衍生类。传统的面向对象的处理图形问题的方法是把draw()方法写在基类里。
    即使现在你不用它,但是知道这种方法还是有用的。它会像这样:

    hierarchy

    代码层面,它看起来像这样 --- 这只是为了示范,不用把这些加到playground中:

    class Shape {
        var strokeWidth = 1
        var strokeColor = CSSColor.Named(.Black)
        var fillColor = CSSColor.Named(.Black)
        var origin = (x: 0.0, y: 0.0)
        func draw(context: DrawingContext) { fatalError("not implemented") }
    }
    
    class Circle : Shape {
        override init() {
            super.init()
            strokeWidth = 5
            strokeColor = CSSColor.Named(.Red)
            fillColor = CSSColor.Named(.Yellow)
            origin = (x: 80.0, y: 80.0)
        }
        
        var radius = 60.0
        override func draw(context: DrawingContext) {
            context.draw(self)
        }
    }
    
    class Rectangle : Shape {
        override init() {
            super.init()
            strokeWidth = 5
            strokeColor = CSSColor.Named(.Teal)
            fillColor = CSSColor.Named(.Aqua)
            origin = (x: 110.0, y: 10.0)
        }
        
        var size = (width: 100.0, height: 130.0)
        override func draw(context: DrawingContext) {
            context.draw(self)
        }
    }
    

    为了让面向对象编程更加安全,swift引入了override关键词。他要求编程者知道什么时候重载方法。它会防止重载已经存在的方法或者你认为恰当但实际不恰当的重载。当你在使用一个新版本的库并且理解其中的变化是很重要的事情。

    然而,这个面向对象的方法有几点不足:

    • 第一个问题:注意到基类中实现的draw()方法为了避免被误用,调用了fatallError()提醒衍生类应该重载这个方法。不幸的是,这个检查发生在运行时,而不是编译时。

    • 第二个问题:为了保证正确性,Circle和Rectangle类不得不处理基类的实例化方法。但是这会让实例化过程变得复杂。

    • 第三个问题: 随着需求的发展,基类很难维持基类的地位
      例如:你想要添加一个Line类型。为了能够跟已经存在的系统协同工作,它不得不衍生于Shape,这会事情看起来不正常。而且,你的Line类需要初始化基类的fillColor属性,对于一条线显然是没有意义的。

    line

    基于此,又可以重构你的层级关系使之变得更加合理。然而实际中,既要求修改基类,而又不影响到既存方法是不太可能的。而且通常一开始,很难修改正确。

    最后,类具有引用语意,这个之前已经讨论过。尽管ARC大多数时候会处理引用,但你也得十分小心不要引入循环引用,否则会发生内存泄露。如果你把相同的图形添加到数组中,且修改了其中一个的颜色值,另一些的色值也会变化,这真的让小伙伴们惊呆了。

    为什么还要用类呢?

    鉴于以上的不足,为什么我还要用类呢?
    首先,它们允许你采用已经非常成熟的Cococa和Cocoa Touch框架。另外,引用类型还是有很重要的用途的。例如:如果一个对象的拷贝操作很耗内存,那么把它封装成类不失为一个好的选择。
    那么,该怎么选择呢?区别值类型和引用类型是很有帮助的,关于这个话题,可以参考:Reference vs. Value Types in Swift.

    计算型属性

    所用的命名类型你都可以自定义setter和getter方法,而不必对应一个存储属性。
    如果你想要在Circle中添加一个直径的getter和setter方法,通过半径很容易实现。
    只需要添加如下代码:

    extension Circle {
        var diameter: Double {
            get {
                return 2 * radius
            }
            
            set {
                radius = newValue/2
            }
        }
    }
    

    这里基于半径实现了一个新的计算型属性。当你要获得直径时,它返回半径的2倍。当你设置直径时,它会设置半径为直径的1/2。
    但是通常情况下,你只会想要实现一个特别的getter方法。这种情况下,你不必写get{}语句块,直接写函数体即可。周长和面积就是很好的例子嘛。
    添加如下代码:

        var area: Double {
            return radius * radius * Math.pi
        }
        
        var perimeter: Double {
            return 2 * radius * Math.pi
        }
    

    不像类,结构体方法默认情况下不允许修改结构体,或者说改变存储型属性。但是你可以把方法声明成mutating。
    例如:

        func shift(x: Double, y: Double) {
            center.x += x
            center.y += y
        }
    

    我们试图给Circle定义一个shift()方法,它可以移动circle的位置。但是编译器会在两行赋值语句处抛出错误。

    ERROR: Left side of mutating operator has immutable type ‘Double'

    我们可以通过添加关键字mutating来解决问题:

        mutating func shift(x: Double, y: Double) {
            center.x += x
            center.y += y
        }
    

    这就告诉了编译器方法改变了结构体。

    逆向建模和类型约束

    swift有一个重要特性:逆向建模。它可以让你在不知晓源码的情况下,扩展类型的行为。
    这里有一个比较常见的例子:假设你正在使用SVG的代码,你想像Circle一样给Rectangle添加area和perimeter属性。那么,可以添加代码:

    extension Rectangle {
        var area: Double {
            return size.width * size.height
        }
        
        var perimeter: Double {
            return 2 * (size.width + size.height)
        }
    }
    

    之前,你向已经存在的模型添加了2个方法,现在,你将这些方法整合到一个正式的新协议。

    protocol ClosedShapeType {
        var area: Double { get }
        var perimeter: Double { get }
    }
    

    接下来,你要通过添加如下代码,告知Circle和Rectangle遵守这个协议:

    extension Circle: ClosedShapeType {}
    extension Rectangle: ClosedShapeType {}
    

    你可以定义一个函数,例如:计算一个数组中元素的周长的总和(当然这些数组元素要遵守ClosedShapeType协议, 元素可以是结构体,枚举,类)。

    func totalPerimeter(shapes: [ClosedShapeType]) -> Double {
        return shapes.reduce(0){$0 + $1.perimeter}
    }
    totalPerimeter([circle, rectangle])
    

    这里使用了reduce来计算直径的总和。你可以在An Introduction to Functional Programming.了解更多它是如何工作的。

    延伸阅读

    完整的playground在这里获得.
    在这篇tutorial, 你了解到了枚举,结构体,和类 --- swift的命名模型类型
    三者相似点:封装,初始化方法,都可以有计算型属性,遵守协议,都可以逆向建模。
    然而,它们也有不同点:

    枚举是值类型,由一组case构成,每一种case可以有不同的关联值。枚举类型的值对应着定义中的一个case。它们不能有存储属性。

    结构体,像枚举类型一样也是值类型,但是可以有存储属性。

    类,像结构体一样有存储属性。它们也可以构建层级结构,在层级中重载属性和方法。鉴于此,显式的基类的初始化方法是必须的。不像结构体和枚举类型,类是引用类型,即共享、语义。

    获得更多信息,可以移步之前提到过的系列文章第二部分,Reference vs. Value Types in Swift.
    希望你享受本次swift命名模型类型的旋风之旅。如果你乐于接受挑战,可以考虑建立一个SVG渲染库的完整版本。你已经有了一个很好的开始。看好你哟😜, 欢迎交流。

    相关文章

      网友评论

      • 绍清_shao:哥们翻译了多长时间啊,很强势啊:+1:

      本文标题:初识Swift的枚举,结构体,和类

      本文链接:https://www.haomeiwen.com/subject/fobssttx.html