《Swift开发者必备Tips》读书笔记 一、Swift新元素

        这是一本比基础的Swift教程稍微深入一点的书籍,特别适合看过官方的教程之后的提高。总体来说,本书并不是很深入的书籍,适合刚接触Swift的读者,以及想要充实自己基础知识的读者。

一、Swift新元素

1.1 柯里化

        简单来说,柯里化就是把方法、函数的参数个数减少。柯里化主要用在函数式编程中,笔者认为其主要作用有以下几个:

  1. 把具有不同参数个数的函数、方法统一为只有一个(或相同参数个数)参数,简单来说就是:把原本不统一的类型给统一
  2. 创建一个函数模板(下面的示例代码进行了展示)
// 如果想要写一个给输入参数+2、+3、+4的函数,则需要重新写类似的函数
func addOne(num : Int)->Int {
  return num + 1
}

// 可以通过柯里化来解决此问题
func addNumber(_ adder: Int)->(Int)->Int {
  return {
    num in
    return num + adder
  }
}

let add1 = addNumber(1)
print(add1(2)) // 3
var add2 = addNumber(2)
print(add2(2)) // 4

        作者在此小节中讲的实际应用例子,笔者没有看懂,希望有识之士能够留言,助我解惑。总体来看,笔者的感觉是,作者在很早写了此书,然后没有随着语言的更新,及时更新书中的例子。

1.2 讲protocol的方法声明为mutating

        在协议中,某些方法可能会修改遵循此写一点对象本身。此时,如果协议的方法没有标明mutating,当遵循此协议的是类时,没有问题;但,当遵循此协议的对象是值类型时,则会编译不过。为了使协议具有通用性(在Swift中struct、enum都可以实现协议),所以推荐给协议的方法添加mutating关键字。

1.3 Sequence

        在Swift中使用for in语句、map等迭代性质的语法时,都需要对应的容器、对象实现Sequence协议。Sequence协议只支持——遵循协议的对象实现单次遍历。关于Sequence的详细内容,参见《Swift进阶》中的集合类型、集合类型协议两个章节,那里讲述的较深入、全面、详细。

1.4 元组

        在谈到Swift时,经常提到的值类型是struct、enum,其实元组也是值类型。元组可以简单理解为更加简单的结构体,其可提高代码的灵活性。

1.5 @autoclosure和??

        简单来说,某些情况下,我们想要写一个闭包,但是,写个闭包又比较费事,那么通过@autoclosure关键字,编译器就可以帮我们实现一个闭包,降低我们的工作量。

func logIfTrue(_ predicate: @autoclosure ()->Bool) {
  if predicate() {
    print("log")
  }
}

logIfTrue(2 > 1)

        自动闭包除了可以实现代码简单外,其还有一个重要作用:延迟调用。

var level: Int?
var startLevel = 1
var currentLevel = level ?? startLevel
// 此时,??运算符直接把startLevel赋值给currentLevel

func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) ->T ) {
  switch optional {
  case .Some(let value):
    return value
  case .None:
    return defaultValue()
  }
}

// 为什么??运算符的第二个参数要写一个自动闭包?????
// 如果不这么写的话,会导致调用??运算符时,默认值必须被计算,某些情况下,这会是一个耗时的操作
// 通过自动闭包实现了延迟调用,从而在用户不知不觉中提升了性能

1.5 @escaping

        某些情况下,作为参数传递到函数中的闭包,不是在此函数出栈之前就被执行了,而是在出栈之后被执行,此类闭包逃逸出了此函数的栈空间,为了标示此情况,需要明确标识出此种情况,所以使用@escaping函数来做标识。Swift编译器也会对此种情况进行检查,如果发现则会报语法错误。

1.6 Optional Chaining

        由于Swift引入了可选值,为了获取可选值包含的实际值,就需要解包等操作。某些情况下,此类解包代码太过于繁琐,通过可选链来有效避免此类问题。

 class Toy {
    let name: String
    init(name: String) {
      self.name = name
  }
}

class Pet {
    var toy: Toy?
}

class Child {
    var pet: Pet?
}

 if let toyName = xiaoming.pet?.toy?.name {
  // 太好了,小明既有宠物,而且宠物还正好有个玩具
}

1.7 操作符

        与 Objective-C 不同,Swift 支持重载操作符、新建运算符这样的特性。

struct Vector2D {
  var x = 0.0
  var y = 0.0
}

// 重载运算符
func +(left: Vector2D, right: Vector2D) -> Vector2D {
  return Vector2D(x: left.x + right.x, y: left.y + right.y)
}

// 新建运算符
 precedencegroup DotProductPrecedence {
    associativity: none
    higherThan: MultiplicationPrecedence
}
infix operator +*: DotProductPrecedence

func +* (left: Vector2D, right: Vector2D) -> Double {
  return left.x * right.x + left.y * right.y
}
  • precedencegroup
    定义了一个操作符优先级别。操作符优先级的定义和类型声明有些相似,一个操作符必需要属于某个特定的优先级。Swift 标准库中已经定义了一些常用的运算优先级组,比如加法 优先级 ( AdditionPrecedence ) 和乘法优先级 ( MultiplicationPrecedence ) 等,你可以在这里找 到完整的列表。如果没有适合你的运算符的优先级组,你就需要像我们在例子中做得这 样,自己指定结合律方式和优先级顺序了。
  • associativity
    定义了结合律,即如果多个同类的操作符顺序出现的计算顺序。比如常⻅的加法和减法都 是 left ,就是说多个加法同时出现时按照从左往右的顺序计算 (因为加法满足交换律,所 以这个顺序无所谓,但是减法的话计算顺序就很重要了)。点乘的结果是一个 Double ,不 再会和其他点乘结合使用,所以这里是 none 。
  • higherThan
    运算的优先级,点积运算是优先于乘法运算的。除了 higherThan ,也支持使用 lowerThan 来指定优先级低于某个其他组。
  • infix
    表示要定义的是一个中位操作符,即前后都是输入;其他的修饰子还包括 prefix 和 postfix ,不再赘述;
            Swift 的操作符是不能定义在局部域中的,因为至少会希望在能在全局范围使用你的操作符,否则操作符也就失去意义了。另外,来自不同 module 的操作符是有可能冲突的,这对于库开发者来说是需要特别注意的地方。像解决类型名冲突那样通过指定库名字来进行调用的。因此在重载或者自定义操作符时,应当尽 量将其作为其他某个方法的 "简便写法",而避免在其中实现大量逻辑或者提供独一无二的功能。

1.8 func 的参数修饰

        在Swift中参数默认是let的,如果想要修改参数则需要使用inout参数。

1.9 字面量表达

        所谓字面量,就是指像特定的数字,字符串或者是布尔值这样,能够直截了当地指出自己的类型并为变量进行赋值的值。这些是经常可能会用到的一些字面量协议(ExpressibleByArrayLiteral、ExpressibleByBooleanLiteral、ExpressibleByDictionaryLiteral、ExpressibleByFloatLiteral、ExpressibleByNilLiteral、ExpressibleByIntegerLiteral、ExpressibleByStringLiteral),类型实现此协议后,就可以通过这些字面量创建对应的类型。

 protocol ExpressibleByBooleanLiteral {
    typealias BooleanLiteralType
    /// Create an instance initialized to `value`.
    init(booleanLiteral value: BooleanLiteralType)
}

 /// The default type for an otherwise-unconstrained boolean literal
typealias BooleanLiteralType = Bool

enum MyBool: Int {
    case myTrue, myFalse
}
extension MyBool: ExpressibleByBooleanLiteral {
    init(booleanLiteral value: Bool) {
      self = value ? .myTrue : .myFalse
  }
}

let myTrue: MyBool = true
let myFalse: MyBool = false
myTrue.rawValue // 0
myFalse.rawValue // 1

1.9 下标

extension Array {
  subscript(input: [Int]) -> ArraySlice<Element> {
    get {
      var result = ArraySlice<Element>()
      for i in input {
        assert(i < self.count, "Index out of range")
        result.append(self[i]) }
        return result
    }
    set {
        for (index,i) in input.enumerated() {
          assert(i < self.count, "Index out of range")
          self[i] = newValue[index]
        }
      }
  }
}

var arr = [1,2,3,4,5]
arr[[0,2,3]] //[1,3,4]
arr[[0,2,3]] = [-1,-3,-4]
arr   //[-1,2,-3,-4,5]

1.10 方法嵌套

        方法嵌套,为一些较长的方法的重构提供了新的可能:这些拆分出来的方法又不能在其他地方使用,又没必要暴露出去,通过在当前方法内嵌套方法,则有效解决此问题。

1.11 命名空间

        OC不支持命名空间,所以只能通过前缀来近似解决,由此导致OC中标识符一般都比较长。在Swift中,通过module来支持命名空间,能够有效避免问题。其类似于C++中的namespace。

 // MyFramework.swift
// 这个文件存在于 MyFramework.framework 中 public class MyClass {
  public class func hello() {
    print("hello from framework")
  }
}
// MyApp.swift
// 这个文件存在于 app 的主 target 中
class MyClass {
  class func hello() {
    print("hello from app")
  }
}

 MyClass.hello()
// hello from app
MyFramework.MyClass.hello()
// hello from framework

1.12 typealias

        typealias是用来为已经存在的类型重新定义名字的,通过命名,可以使代码变得更加清晰。使用的语法也很简单,使用 typealias 关键字像使用普通的赋值语句一样,可以将某个已经存在的类型 赋值为新的名字。

typealias Location = CGPoint
typealias Distance = Double

class Person<T> {
}
typealias Worker = Person

// 以下代码编译错误
typealias Worker = Person<T> // Cannot find type 'T' in scope

// 通过以下方法修正
typealias Worker<T> = Person<T>
  
// 另外一种用法
protocol Cat { ... }
protocol Dog { ... }
typealias Pat = Cat & Dog

1.13 associatedtype

        associatedtype用于协议,使协议支持泛型。具有此关键字的协议不能用于类型,只能用于类型约束。

1.14 可变参数函数

        在Swift中,实现可变参数大大简单起来。

func myFunc(numbers: Int..., string: String) {
    numbers.forEach {
      for i in 0..<$0 {
      print("\(i + 1): \(string)")
    }
  }
}
myFunc(numbers: 1, 2, 3, string: "hello") // 输出:
// 1: hello
// 1: hello
// 2: hello
// 1: hello
// 2: hello
// 3: hello
  • 可变参数可以在函数列表的任意位置
  • 在同一个方法中只能有一个参数是可变的
  • 可变参数都必须是同一种类型的等

1.15 初始化方法顺序

        在某个类的子类中,初始化方法里语句的顺序并不是随意的,我们需要保证在当前子类实例的成员初始化完成后才能调用父类的初始化方法。
        一般来说,子类的初始化顺序是:

  • 设置子类自己需要初始化的参数
  • 调用父类的相应的初始化方法(可选的,如果不写,系统自动帮我们调用;如果有第三步的语句,则此语句不可省略)
  • 对父类中的需要改变的成员进行设定(可选的,如果不需要,则不用调用)

1.16 Designated,Convenience 和 Required

        一方面,Swift 强化了 designated 初始化方法的地位。 Swift 中不加修饰的 init 方法都需要在方法中保证所有非 Optional 的实例变量被赋值初始化,而在 子类中也强制 (显式或者隐式地) 调用 super 版本的 designated 初始化。
        与 designated 初始化方法对应的是在 init 前加上 convenience 关键字的初始化方法。这类方法 是 Swift 初始化方法中的 “二等公⺠”,只作为补充和提供使用上的方便。所有的 convenience 初始 化方法都必须调用同一个类中的 designated 初始化完成设置,另外 convenience 的初始化方法是 不能被子类重写或者是从子类中以 super 的方式被调用的。

  • 初始化路径必须保证对象完全初始化,这可以通过调用本类型的 designated 初始化方法来得 到保证;
  • 子类的 designated 初始化方法必须调用父类的 designated 方法,以保证父类也完成初始 化。
            对于某些我们希望子类中一定实现的 designated 初始化方法,我们可以通过添加 required 关键 字进行限制,强制子类对这个方法重写实现。这样做的最大的好处是可以保证依赖于某个 designated 初始化方法的 convenience 一直可以被使用。

备注
此处说明并不详细,其实实际情况比此处描述的要复杂,不过日常也不经常用到那么复杂的初始化放阿飞。如果需要连接,可以参考官方的《Swift编程语言》。

1.17 初始化返回 nil

  • 初始化方法没有必要使用return语句
  • 对于可能失败的初始化方法,可以显示return nil

1.18 static 和 class

  • static、class的作用相同,都是标明类型相关的属性、方法
  • 建议一直使用static,来避免class导致的,值类型不适用问题

1.19 多类型和容器

        通常情况下,Array、Dictionary、Set都是存储相同类型。但是,在某些特定情况下,集合类型也可以存储不同类型。

  • 可以通过存储Any、AnyObject来实现存储不同类型
  • 通过存储遵循某种协议的对象
// 以下是另外实现存储多种不同类型的方法
// 此种方法可以保留编译器的检查
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)
} }
// 输出: // 2
// Two // 6

1.20 default 参数

        Swift 的方法是支持默认参数的,也就是说在声明方法时,可以给某个参数指定一个默认使用的值。在调用该方法时要是传入了这个参数,则使用传入的值,如果缺少这个输入参数,那么直接使用设定的默认值进行调用。
        和其他很多语言的默认参数相比较,Swift 中的默认参数限制更少,并没有所谓 "默认参数之后不 能再出现无默认值的参数"这样的规则,举个例子,下面两种方法的声明在 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)
}

sayHello1(str2: " ", str3: "World")
sayHello2(str1: "Hello", str2: " ")
//输出都是 Hello World
 

        与上面不同的另外一种指定默认参数的方法如下面代码所示。default指定的默认值,在方法内部来给出。

func NSLocalizedString(key: String,
                                      tableName: String? = default,
                                      bundle: NSBundle = default,
                                      value: String = default,
                                      comment: String) -> String

1.21 正则表达式

        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
    }
}

let mailPattern = "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
let matcher: RegexHelper
do {
  matcher = try RegexHelper(mailPattern)
}

let maybeMailAddress = "[email protected]"
if matcher.match(maybeMailAddress) {
  print("有效的邮箱地址")
}
// 输出:
// 有效的邮箱地址


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
  }
}

 if "[email protected]" =~ "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$" {
  print("有效的邮箱地址")
}
// 输出:
// 有效的邮箱地址

1.22 模式匹配

        此处讲述的模式匹配比较简单,如果想要深入了解,可以参考如下文档。

  1. 模式匹配第一弹: switch, enums & where 子句
  2. 模式匹配第二弹:元组,range 和类型
  3. 模式匹配第三弹: 自定义的模式匹配和语法糖
  4. 模式匹配第四弹:if case,guard case,for case

1.23 ... 和 ..<

        范围操作符,一个支持[],左闭右闭,一个支持[),左闭右开。

1.24 AnyClass,元类型和 .self

在 Swift 中能够表示 “任意” 这个概念的除了 Any 和 AnyObject 以外,还有一个AnyClass 。 AnyClass 在 Swift 中被一个 typealias 所定义:

  typealias AnyClass = AnyObject.Type

        通过 AnyObject.Type 这种方式所得到是一个元类型 (Meta)。在声明时我们总是在类型的名称后面 加上 .Type ,比如 A.Type 代表的是 A 这个类型的类型。也就是说,我们可以声明一个元类型来 存储 A 这个类型本身,而在从 A 中取出其类型时,我们需要使用到 .self :

 class A {
}
let typeA: A.Type = A.self

        AnyClass 所表达的东⻄其实并没有什 么奇怪,就是任意类型本身

 class A {
}
let typeA: AnyClass = A.self
class A {
  class func method() {
    print("Hello")
  }
}

let typeA: A.Type = A.self typeA.method()
// 或者
let anyClass: AnyClass = A.self
(anyClass as! A.Type).method()

1.25 协议和类方法中的 Self

        Self指实现协议的自身。

1.26 动态类型和多方法

        Swift默认使用静态消息派发。

class Pet {}
class Cat: Pet {}
class Dog: Pet {}
func printPet(_ pet: Pet) { print("Pet")
}
func printPet(_ cat: Cat) { print("Meow")
}
func printPet(_ dog: Dog) { print("Bark")
}

printPet(Cat()) // Meow
printPet(Dog()) // Bark
printPet(Pet()) // Pet

func printThem(_ pet: Pet, _ cat: Cat) {
    printPet(pet)
    printPet(cat)
}
printThem(Dog(), Cat())
// 输出: // Pet // Meow

1.27 属性观察

        属性观察 (Property Observers) 是 Swift 中一个很特殊的特性,利用属性观察我们可以在当前类型 内监视对于属性的设定,并作出一些响应。Swift 中为我们提供了两个属性观察的方法,它们分别 是 willSet 和 didSet 。

class MyClass {
  var date: NSDate {
  willSet {
      let d = date
      print("即将将日期从 \(d) 设定至 \(newValue)")
    }
    didSet {
      print("已经将日期从 \(oldValue) 设定至 \(date)")
    }
  }

  init() {
    date = NSDate()
    }
 }

let foo = MyClass()
foo.date = foo.date.addingTimeInterval(10086)

// 输出
// 即将将日期从 2014-08-23 12:47:36 +0000 设定至 2014-08-23 15:35:42 +0000 // 已经将日期从 2014-08-23 12:47:36 +0000 设定至 2014-08-23 15:35:42 +0000

1.28 final

        final 关键字可以用在 class , func 或者 var 前面进行修饰,表示不允许对该内容进行继承或 者重写操作。
        给一段代码加上 final 就意味着编译器向你作出保证,这段代码不会再被修改;同时,这也意味 着你认为这段代码已经完备并且没有再被进行继承或重写的必要,因此这往往会是一个需要深思 熟虑的决定。

  • 类或者方法的功能确实已经完备了
  • 子类继承和修改是一件危险的事情
  • 为了父类中某些代码一定会被执行
  • 性能考虑

1.29 lazy 修饰符和 lazy 方法

        在 Swift 中我们使用在变量属性前加 lazy 关键字的方式来简单地指定延时加载。我们在使用 lazy 作为属性修饰符时,只能声明属性是变量。另外我们需要显式地指定属性类型,并使用一个可以对这个属性进行赋值的语句来在首次访问属性时运行。如果我们多次访问这 个实例的 str 属性的话,可以看到只有一次输出。

 class ClassA {
  lazy var str: String = {
    let str = "Hello" print("只在首次访问输出") return str
  }()
}

// 为了简化,我们如果不需要做什么额外工作的话,也可以对这个 lazy 的属性直接写赋值语句:
 lazy var str: String = "Hello"

        关于容器的lazy用法参见:Swift高阶函数解析

1.30 Reflection 和 Mirror

struct Person {
   let name: String
   let age: Int

    func test() -> () {
        print("sdfsd")
    }
}
let xiaoMing = Person(name: "XiaoMing", age: 16)
let r = Mirror(reflecting: xiaoMing) // r 是 MirrorType
print("xiaoMing 是 \(r.displayStyle!)")
print("属性个数:\(r.children.count)")
for child in r.children {
    print("属性名:\(String(describing: child.label)),值:\(child.value)")
}
// 输出:
// xiaoMing 是 Struct
// 属性个数:2
// 属性名:name,值:XiaoMing // 属性名:age,值:16

1.31 隐式解包 Optional

  var maybeObject: MyClass!

1.32 多重 Optional

var aNil: String? = nil
var anotherNil: String?? = aNil
var literalNil: String?? = nil

// 输出anotherNil
if anotherNil != nil {
  print("anotherNil") 
}
// 没有输出
if literalNil != nil {
  print("literalNil")
}

1.32 Optional Map

let num: Int? = 3
let result = num.map {
  $0 * 2
}
// result 为 {Some 6}

1.33 Protocol Extension

  • 如果类型推断得到的是实际的类型
    • 那么类型中的实现将被调用;如果类型中没有实现的话,那么协议扩展中的默认实现将被使用
  • 如果类型推断得到的是协议,而不是实际类型
    • 并且方法在协议中进行了定义,那么类型中的实现将被调用;如果类型中没有实现,那 么协议扩展中的默认实现被使用
    • 否则 (也就是方法没有在协议中定义),扩展中的默认实现将被调用

1.34 where 和模式匹配

//  switch 语句中,可以使用 where 来限定某些条件 case:
let name = ["王小二","张三","李四","王二小"]
name.forEach { switch $0 {
  case let x where x.hasPrefix("王"):
    print("\(x)是笔者本家")
  default:
    print("你好,\($0)")
  }
}
// 输出:
// 王小二是笔者本家 // 你好,张三
// 你好,李四
// 王二小是笔者本家

// 在 for 中我们也可以使用 where 来做类似的条件限定:

extension Sequence where Self.Iterator.Element : Comparable {     public func sorted() -> [Self.Iterator.Element]
}

1.35 indirect 和嵌套 enum

        参见《Swift进阶》中的枚举的介绍。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章