Swift 轻松掌握喵神高阶心法 V
ShoshanaSes
8年前
<p>过去的一个月将一个H5项目从0-1给搞出来了,说实话Vue,React, Angular, 三足鼎立的态势下,选择组件化的Vue.js是明智之举~ 上完UAT就能够有时间继续我的Swift3.0的学习啦,哈哈,好久没有敲Swift竟然感觉到有些生疏。。</p> <h3><strong>Tip18 Designated,Convenience 和 Required</strong></h3> <p>我们在深入初始化方法之前,不妨先再想想 Swift 中的初始化想要达到一种怎样的目的。</p> <p>其实就是安全。在 Objective-C 中,init 方法是非常不安全的:没有人能保证 init 只被调用一次,也没有人保证在初始化方法调用以后实例的各个变量都完成初始化,甚至如果在初始化里使用属性进行设置的话,还可能会造成各种问题,虽然 Apple 也明确说明了不应该在 init 中使用属性来访问,但是这并不是编译器强制的,因此还是会有很多开发者犯这样的错误。</p> <p>所以 Swift 有了超级严格的初始化方法。一方面,Swift 强化了 designated 初始化方法的地位。Swift 中不加修饰的 init 方法都需要在方法中保证所有非 Optional 的实例变量被赋值初始化,而在子类中也强制 (显式或者隐式地) 调用 super 版本的 designated 初始化,所以无论如何走何种路径,被初始化的对象总是可以完成完整的初始化的。</p> <pre> <code class="language-swift">class ClassA { let numA: Int init(num: Int) { numA = num } } class ClassB: ClassA { let numB: Int override init(num: Int) { numB = num + 1 super.init(num: num) } }</code></pre> <p>在上面的示例代码中,注意在 init 里我们可以对 let 的实例常量进行赋值,这是初始化方法的重要特点。在 Swift 中 let 声明的值是常量,无法被写入赋值,这对于构建线程安全的 API 十分有用。而因为 Swift 的 init 只可能被调用一次,因此在 init 中我们可以为常量进行赋值,而不会引起任何线程安全的问题。</p> <p>与 designated 初始化方法对应的是在 init 前加上 convenience 关键字的初始化方法。这类方法是 Swift 初始化方法中的 “二等公民”,只作为补充和提供使用上的方便。所有的 convenience 初始化方法都必须调用同一个类中的 designated 初始化完成设置,另外 convenience 的初始化方法是不能被子类重写或者是从子类中以 super 的方式被调用的。</p> <pre> <code class="language-swift">class ClassA { let numA: Int init(num: Int) { numA = num } convenience init(bigNum: Bool) { self.init(num: bigNum ? 10000 : 1) } } class ClassB: ClassA { let numB: Int override init(num: Int) { numB = num + 1 super.init(num: num) } }</code></pre> <p>只要在子类中实现重写了父类 convenience 方法所需要的 init 方法的话,我们在子类中就也可以使用父类的 convenience 初始化方法了。比如在上面的代码中,我们在 ClassB 里实现了 init(num: Int) 的重写。这样,即使在 ClassB 中没有 bigNum 版本的 convenience init(bigNum: Bool),我们仍然还是可以用这个方法来完成子类初始化:</p> <pre> <code class="language-swift">let anObj = ClassB(bigNum: true) anObj.numA //10000 anObj.numB //10001</code></pre> <p>因此进行一下总结,可以看到初始化方法永远遵循以下两个原则:</p> <ol> <li>初始化路径必须保证对象完全初始化,这可以通过调用本类型的 designated 初始化方法来得到保证;</li> <li>子类的 designated 初始化方法必须调用父类的 designated 方法,以保证父类也完成初始化。</li> </ol> <p>对于某些我们希望子类中一定实现的 designated 初始化方法,我们可以通过添加 required 关键字进行限制,强制子类对这个方法重写实现。这样做的最大的好处是可以保证依赖于某个 designated 初始化方法的 convenience 一直可以被使用。一个现成的例子就是上面的 init(bigNum: Bool):如果我们希望这个初始化方法对于子类一定可用,那么应当将 init(num: Int) 声明为必须,这样我们在子类中调用 init(bigNum: Bool) 时就始终能够找到一条完全初始化的路径了:</p> <pre> <code class="language-swift">class ClassA { let numA: Int required init(num:Int) { numA = num } convenience init(bigNum: Bool) { self.init(num: bigNum ? 10000 : 1) } } class ClassB:ClassA { let numB: Int required init(num: Int) { numB = num + 1 super.init(num: num) } }</code></pre> <p>另外需要说明的是,其实不仅仅是对 designated 初始化方法,对于 convenience 的初始化方法,我们也可以加上 required 以确保子类对其进行实现。这在要求子类不直接使用父类中的 convenience 初始化方法时会非常有帮助。</p> <h3><strong>Tip19 初始化返回nil</strong></h3> <p>在 Objective-C 中,init 方法除了返回 self 以外,其实和一个普通的实例方法并没有太大区别。如果你喜欢的话,甚至可以多次进行调用,这都没有限制。一般来说,我们还会在初始化失败 (比如输入不满足要求无法正确初始化) 的时候返回 nil 来通知调用者这次初始化没有正确完成。</p> <p>但是,在 Swift 中默认情况下初始化方法是不能写 return 语句来返回值的,也就是说我们没有机会初始化一个 Optional 的值。一个很典型的例子就是初始化一个 url。在 Objective-C 中,如果我们使用一个错误的字符串来初始化一个 NSURL 对象时,返回会是 nil 代表初始化失败。所以下面这种 "防止度娘吞链接" 式的字符串 (注意两个 t 之间的空格和中文的句号) 的话,也是可以正常编译和运行的,只是结果是个 nil:</p> <pre> <code class="language-swift">NSURL *url = [[NSURL alloc] initWithString:@"https://github.com/coderZsq"]; NSLog(@"%@",url); //输出(null)</code></pre> <p>但是在Swift中情况就不那么乐观了, -initWithString: 在Swift中对应的是一个convenience init 方法: init(string URLString:String!).上面的Objective-C代码在Swift中等效为:</p> <pre> <code class="language-swift">let url = NSURL(string: "https://github.com/coderZsq") print(url)</code></pre> <p>init 方法在Swift1.1中发生了很大的变化, 为了将来龙去脉讲述清楚, 我们先来看看在Swift1.0下的表现:</p> <p>Swift1.0及之前</p> <p>如果在Swift1.0的环境下尝试运行上面代码的话, 我们会得到一个EXC_BAD_INSTRUCTION ,这说明触发了Swift内部的断言, 这个初始化方法不接受这样的输入. 一个常见的解决方法是使用工厂模式, 也就是写一个类方法来生成和返回实例, 或者在失败的时候返回nil. Swift的NSURL就做了这样的处理:</p> <p>class func URLWithString(URLString:String!)->Self!</p> <p>使用的时候:</p> <pre> <code class="language-swift">let url = NSURL.URLWithString("https://github.com/coderZsq") print(url) //输出nil</code></pre> <p>不过虽然可以用这种方式来和原来一样返回nil, 但是这也算事一种折中. 在可能的情况下, 我们还是应该倾向于尽量减少出现Optional的可能性,这样更有助于代码的简化.</p> <p>如果你确实想使用初始化方法而不愿意用工厂函数的话, 也可以考虑用一个Optional量来储存结果</p> <p>这样你就可以处理初始化失败了, 不过相应的代价是代码复杂度的增加</p> <pre> <code class="language-swift">let url: NSURL? = NSURL(string: "https://github.com/coderZsq") // nil</code></pre> <p>Swift1.1及之后</p> <p>虽然在默认情况下不能在init中返回nil, 但是通过上面的例子我们可以看到Apple自家的API还是有这个能力的.</p> <p>好消息是在Swift1.1中Apple已经为我们加上初始化方法中返回nil的能力. 我们可以在init声明时在其后加上一个 ? 或者 ! 来表示初始化失败可能返回nil. 比如为Int添加一个接收String作为参数的初始化方法. 我们希望在方法中对中文和英文的数据进行解析, 并输出Int结果, 对其解析并初始化的时候, 就可能遇到初始化失败的情况:</p> <pre> <code class="language-swift">extension Int { init?(fromString: String) { self = 0 var digit = fromString.characters.count - 1 for c in fromString.characters { var number = 0 if let n = Int(String(c)) { number = n } else { switch c { case "一": number = 1 case "二": number = 2 case "三": number = 3 case "四": number = 4 case "五": number = 5 case "六": number = 6 case "七": number = 7 case "八": number = 8 case "九": number = 9 case "零": number = 0 default: return nil } } self = self + number * Int(pow(10, Double(digit))) digit = digit - 1 } } } let number1 = Int(fromString: "12") //12 let number2 = Int(fromString: "三二五") //325 let number3 = Int(fromString: "七9八") //798 let number4 = Int(fromString: "吃了吗") //nil let number5 = Int(fromString: "la4n") //nil</code></pre> <p>所有的结果都将是Int?类型, 通过Optional Binding, 我们就能知道初始化是否成功, 并安全地使用它们了. 我们在这类初始化方法中还可以对self进行赋值, 也算是init方法里的特权之一.</p> <p>同时像上面例子中的NSURL.URLWithString 这样的工厂方法, 在Swift1.1中已经不再需要. 为了简化API和安全, Apple已经被标记为不可用了并无法编译. 而对应地, 可能返回nil的init方法都加上了?标记:</p> <p>convenince init?(String URLString: String)</p> <p>在新版本的Swift中, 对于可能初始化失败的情况, 我们应该始终使用可返回nil的初始化方法, 而不是类型工厂方法.</p> <h3><strong>Tip20 static 和 class</strong></h3> <p>Swift中表示"类型范围作用域"这一概念有两个不同的关键字, 它们分别是static和class. 这两个关键字确实都表达了这个意思, 但是在其他的一些语言, 包括Objective-C中, 我们并不会特别地区分类变量/类方法和静态变量/静态函数.但是在Swift的早期版本中, 这两个关键字确实不能用混的.</p> <p>在非 class 的类型上下文中, 我们统一使用static来表述类型作用域. 这包括在enum和struct中表述类型方法和类型属性时. 在这两个值类型中, 我们可以在类型范围内声明并使用存储属性, 计算属性和方法.static适用的场景有这些:</p> <pre> <code class="language-swift">struct Point { let x: Double let y: Double //存储属性 static let zero = Point(x: 0, y: 0) //计算属性 static var ones: [Point] { return [Point(x: 1,y: 1), Point(x: -1,y: 1), Point(x: 1,y: -1), Point(x: -1,y: -1)] } //类型方法 static func add(p1: Point,p2: Point) -> Point { return Point(x: p1.x + p2.x, y: p1.y + p2.y) } }</code></pre> <p>enum 的情况与这个十分类似, 就不再列举了.</p> <p>class 关键字相比起来就明白许多, 是专门用在class类型的上下文中的, 可以用来修饰类方法以及类的计算属性. 但是有一个例外, class中现在是不能出现class的存储属性的, 我们如果写类似这样的代码的话:</p> <pre> <code class="language-swift">class MyClass { class var bar: Bar? }</code></pre> <p>编译时会得到一个错误:</p> <p>class varibales not yet supported</p> <p>在Swift 1.2 及之后, 我们可以在class中使用static来声明一个类作用域的变量. 也即:</p> <pre> <code class="language-swift">class MyClass { static var bar: Bar? }</code></pre> <p>的写法是合法的. 有了这个特性之后, 像单例的写法就可以回归我们所习惯的方式了.</p> <p>有一个比较特殊的是protocol. 在Swift中class, struct和enum都是可以实现某个protocol的. 那么如果我们想在protocol里定义一个类型域上的方法或者计算属性的话, 应该用哪个关键字呢? 答案是使用static进行定义. 在使用的时候, struct或enum中仍然使用static, 而在class里我们既可以使用class关键字, 也可以用static, 它们的结果是相同的:</p> <pre> <code class="language-swift">protocol MyProtocol { static func foo() -> String } struct MyStruct: MyProtocol { static func foo() -> String { return "MyStruct" } } enum MyEnum: MyProtocol { static func foo() -> String { return "MyEnum" } } class MyClass: MyProtocol { //在class中可以使用class class func foo() -> String { return "MyClass.foo()" } //也可以使用static static func bar() -> String { return "MyClass.bar()" } }</code></pre> <p>在Swift1.2之前protocol中使用的是class作为关键字, 但着确实是不合逻辑的. Swift1.2和2.0分两次对此进行了改进. 现在只需要记住结论, 在任何时候使用static应该都是没有问题的.</p> <h3><strong>Tip21 多类型和容器</strong></h3> <p>Swift中常用的原生容器类型有三种, 他们分别是Array, Dictionay和Set:</p> <pre> <code class="language-swift">struct Array<Element> : RandomAccessCollection, MutableCollection { //... } struct Dictionary<Key : Hashable, Value> : Collection, ExpressibleByDictionaryLiteral { //... } struct Set<Element : Hashable> : SetAlgebra, Hashable, Collection, ExpressibleByArrayLiteral { //... }</code></pre> <p>它们都是泛型的, 也就是说我们在一个集合中只能放同一个类型的元素. 比如:</p> <pre> <code class="language-swift">let numbers = [1,2,3,4,5] // numbers的类型是[Int] let strings = ["hello","world"] // string的类型是[String]</code></pre> <p>如果我们要吧不相关的类型放到同一个容器类型中的话, 需要做一些转换的工作:</p> <pre> <code class="language-swift">// Any类型可以隐式转换 let mixed: [Any] = [1, "two", 3] // 转换为[NSObject] let objectArray = [1 as NSObject, "two" as NSObject, 3 as NSObject]</code></pre> <p>这样的转换会造成部分信息的损失, 我们从容器中取值时只能得到信息完全丢失后的结果, 在使用时还需要进行一次类型转换. 这其实是在无其他可选方案后的最差选择: 因为使用这样的转换的话, 编译器就不能再给我们提供警告信息了. 我们可以随意地将任意对象添加进容器, 也可以将容器中取出的值转换为任意类型, 这是一件十分危险的事情:</p> <pre> <code class="language-swift">let any = mixed[0] // Any类型 let nsObject = objectArray[0] // NSObject类型</code></pre> <p>其实我们注意到, Any其实不是具体的某个类型. 因此就是说其实在容器类型泛型的帮助下, 我们不仅可以在容器中添加同一具体类型的对象, 也可以添加实现了同一协议的类型的对象. 绝大多数情况下, 我们想要放入一个容器中的元素或多或少会有某些共同点, 这就使得用协议来规定容器类型会很有用. 比如上面的例子如果我们希望的是打印出容器内的元素的description, 可能我们更倾向于将数组声明为[CustomStringConvertible]的:</p> <pre> <code class="language-swift">let mixed: [CustomStringConvertible] = [1, "two", 3] for obj in mixed { print(obj.description) }</code></pre> <p>这种方法虽然也损失了一部分类型信息, 但是对于Any或者AnyObject还是改善很多, 在对于对象中存在某种共同特性的情况下无疑是最方便的. 另一种做法是使用enum可以带有值的特点, 将类型信息封装到特定的enum中. 下面的代码封装了Int或者String类型:</p> <pre> <code class="language-swift">enum IntOrString { case IntValue(Int) case StringValue(String) } let mixed = [IntOrString.IntValue(1), IntOrString.StringValue("two"), IntOrString.IntValue(3)] for value in mixed { switch value { case let .IntValue(i): print(i * 2) case let .StringValue(s): print(s.capitalized) } }</code></pre> <p>通过这种方法, 我们完整地在编译时保留了不同类型的信息. 为了方便, 我们甚至可以进一步为IntOrString使用字面量转换的方法编写简单的获取方式, 但那时另外一个故事了.</p> <h3><strong>Tip22 default 参数</strong></h3> <p>Swift的方法是支持默认参数的, 也就是说在声明方法时, 可以给某个参数指定一个默认使用的值. 在调用该方法时要是传入了这个参数, 则使用传入的值. 如果缺少这个输入参数, 那么直接使用设定的默认值进行调用. 可以说这是Objective社区盼了好多年的一个特性了, Objective-C由于语法的特点几乎无法再不大幅改动的情况下很好地实现默认参数.</p> <p>和其他很多语言的默认参数相比较, Swift中的默认参数限制更少, 并没有所谓"默认参数之后不能再出现无默认值的参数"这样的规则, 举个例子, 下面两种方法的声明在Swift里都是合法可用的:</p> <pre> <code class="language-swift">func sayHello1(str1: String = "Hello", str2: String, str3: String) { print(str1 + str2 + str3) } func sayHello2(str1: String, str2: String, str3: String = "World") { print(str1 + str2 + str3) }</code></pre> <p>其他不少语言只能使用后面一种写法, 将默认参数作为方法的最后一个参数.</p> <p>在调用的时候, 我们如果想要使用默认值的话, 只要不传入相应的值就可以了. 下面这样的调用将得到同样的结果:</p> <pre> <code class="language-swift">sayHello1(str2: " ", str3: "World") sayHello2(str1: "Hello", str2: " ") // 输出都是Hello World</code></pre> <p>这两个调用都省略了带有默认值的参数, sayHello1中str1是默认的"Hello", 而sayHello中的str3是默认的"World".</p> <p>另外如果喜欢Cmd + 单击点来点去到处看的朋友可能会注意到NSLocalizedString这个常用方法的签名现在是:</p> <pre> <code class="language-swift">func NSLocalizedString(_ key: String, tableName: String? = default, bundle: Bundle = default, value: String = default, comment: String) -> String</code></pre> <p>默认参数写的是defualt, 这是含有默认参数的方法所产生的Swift的调用接口. 当我们指定一个编译时就能确定的常量来作为默认参数的取值时, 这个取值时隐藏在方法实现内部, 而不应该暴露给其他部分. 与NSLocalizedString很相似的还有Swift中的各类断言:</p> <pre> <code class="language-swift">func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)</code></pre> <h3><strong>Tip23 正则表达式</strong></h3> <p>作为一门先进的编程语言, Swift可以说吸收了众多其他先进语言的优点, 但是有一点确实让人略微失望的, 就是Swift至今为止并没有在语言层面上支持正则表达式.</p> <p>大概是因为其实app开发并不像Perl或者Ruby那样的语言需要处理很多文字匹配的问题, Cocoa开发者确实不是特别依赖正则表达式. 但是并不排除有希望使用正则表达式的场景, 我们是否能像其他语言一样, 使用比如 =~ 这样的符号来进行正则匹配呢?</p> <p>最容易想到也是最容易实现的当然是自定义 =~ 运算符. 在Cocoa中我们可以使用NSRegularExpression 来做正则匹配, 那么其实我们为它写一个包装并不是什么太困难的事情. 因为做的字符串正则匹配, 所以 =~ 左右两边都是字符串. 我们可以先写一个接受正则表达式的字符串, 以此生成NSRegularExpression对象. 然后使用该对象李艾匹配输入字符串, 并返回结果告诉调用者匹配是否成功. 一个最简单的实现可能是下面这样的:</p> <pre> <code class="language-swift">struct RegexHelper { let regex: NSRegularExpression init(_ pattern: String) throws { try regex = NSRegularExpression(pattern: pattern, options: .caseInsensitive) } func match(_ input: String) -> Bool { let matches = regex.matches(in: input, options: [], range: NSMakeRange(0, input.utf16.count)) return matches.count > 0 } }</code></pre> <p>在使用的时候, 比如我们想要匹配一个邮箱地址, 我们可以这样来使用:</p> <pre> <code class="language-swift">let mailPattern = "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$" let matcher: RegexHelper do { matcher = try RegexHelper(mailPattern) } let maybeMailAddress = "https://github.com/coderZsq" if matcher.match(maybeMailAddress) { print("有效的邮箱地址") } // 输出: // 有效的邮箱地址”</code></pre> <p>现在我们有了方便的封装, 接下来就让我们实现 =~ 吧. 这里只给出结果了, 关于如何实现操作符和重载操作符的内容, 可以参考操作符一节的内容.</p> <pre> <code class="language-swift">precedencegroup MatchPrecedence { associativity: none higherThan: DefaultPrecedence } infix operator =~: MatchPrecedence func =~ (lhs: String, rhs: String) -> Bool { do { return try RegexHelper(rhs).match(lhs) } catch _ { return false } }</code></pre> <p>这下我们就可以使用类似于其他语言的正则匹配的方法了:</p> <pre> <code class="language-swift">if "https://github.com/coderZsq" =~ "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"{ print("有效的邮箱地址") }</code></pre> <p>Swift1.0版本主要会专注于成为一个非常适合制作app的语言, 而现在看来Apple和Chris也有野心将Swift带到更广阔的平台去. 那时候可能会在语言层面加上正则表达式是支持, 到时候这篇tip可能也就没有意义了, 不会我个人还是非常期盼那一天早些到来.</p> <p> </p> <p>来自:http://www.jianshu.com/p/dab6fd2ed215</p> <p> </p>