swift 的基础知识

1:?? 的作用

对可选类型的值进行解包,如果解包后的数据为nil,就用提供的缺省值
如:let url = responModel.url ?? ""

2.Optional(可选型) 是用什么实现的

enum Optional : _Reflectable, NilLiteralConvertible { 
  case None 
  case Some(T)
}

在这个定义中,对 T 没有任何限制,也就是说,我们是可以在 Optional 中装入任意东西的,甚至也包括 Optional 对象自身。打个形象的比方,如果我们把 Optional 比作一个盒子,实际具体的 String 或者 Int 这样的值比作糖果的话,当我们打开一个盒子 (unwrap) 时,可能的结果会有三个 -- 空气,糖果,或者另一个盒子

3.接口和类方法中的 SELF

我们在看一些接口的定义时,可能会注意到出现了首字母大写的 Self 出现在类型的位置上

protocol IntervalType {
    //...

    /// Return `rhs` clamped to `self`.  The bounds of the result, even
    /// if it is empty, are always within the bounds of `self`
    func clamp(intervalToClamp: Self) -> Self

    //...
}

比如上面这个 IntervalType 的接口定义了一个方法,接受实现该接口的自身的类型,并返回一个同样的类型。

这么定义是因为接口其实本身是没有自己的上下文类型信息的,在声明接口的时候,我们并不知道最后究竟会是什么样的类型来实现这个接口,Swift 中也不能在接口中定义泛型进行限制。而在声明接口时,我们希望在接口中使用的类型就是实现这个接口本身的类型的话,就需要使用 Self 进行指代。

但是在这种情况下,Self 不仅指代的是实现该接口的类型本身,也包括了这个类型的子类。从概念上来说,Self 十分简单,但是实际实现一个这样的方法却稍微要转个弯。为了说明这个问题,我们假设要实现一个 Copyable 的接口,满足这个接口的类型需要返回一个和接受方法调用的实例相同的拷贝。一开始我们可能考虑的接口是这样的:

protocol Copyable {
    func copy() -> Self
}

这是很直接明了的,它应该做的是创建一个和接受这个方法的对象同样的东西,然后将其返回,返回的类型不应该发生改变,所以写为 Self。然后开始尝试实现一个 MyClass 来满足这个接口:

class MyClass: Copyable {

    var num = 1

    func copy() -> Self {
        // TODO: 返回什么?
        // return
    }
}

我们一开始的时候可能会写类似这样的代码:

这是错误代码

func copy() -> Self {
    let result = MyClass()
    result.num = num
    return result
}

但是显然类型是有问题的,因为该方法要求返回一个抽象的、表示当前类型的 Self,但是我们却返回了它的真实类型 MyClass,这导致了无法编译。也许你会尝试把方法声明中的 Self 改为 MyClass,这样声明就和实际返回一致了,但是很快你会发现这样的话,实现的方法又和接口中的定义不一样了,依然不能编译。
为了解决这个问题,我们在这里需要的是通过一个和当前上下文 (也就是和 MyClass) 无关的,又能够指代当前类型的方式进行初始化。希望你还能记得我们在对象类型中所提到的 dynamicType,这里我们就可以使用它来做初始化,以保证方法与当前类型上下文无关,这样不论是 MyClass 还是它的子类,都可以正确地返回合适的类型满足 Self 的要求:

func copy() -> Self {
    let result = self.dynamicType.init()
    result.num = num
    return result
}

但是很不幸,单单是这样还是无法通过编译,编译器提示我们如果想要构建一个 Self 类型的对象的话,需要有 required 关键字修饰的初始化方法,这是因为 Swift 必须保证当前类和其子类都能响应这个 init 方法。另一个解决的方案是在当前类类的声明前添加 final 关键字,告诉编译器我们不再会有子类来继承这个类型。在这个例子中,我们选择添加上 requiredinit 方法。最后,MyClass 类型是这样的:

class MyClass: Copyable {

    var num = 1

    func copy() -> Self {
        let result = self.dynamicType.init()
        result.num = num
        return result
    }

    required init() {

    }
}

我们可以通过测试来验证一下行为的正确性:

let object = MyClass()
object.num = 100

let newObject = object.copy()
object.num = 1

print(object.num)     // 1
print(newObject.num)  // 100

而对于 MyClass 的子类,copy() 方法也能正确地返回子类的经过拷贝的对象了。

另一个可以使用 Self 的地方是在类方法中,使用起来也十分相似,核心就在于保证子类也能返回恰当的类型。

4. Swift中的Protocol

什么是Protocol?

Protocol是Swift中的一种自定义类型,可以使用protocol定义某种约定,而不是某一种类型,一般用于表示某种类型的共性。

Protocol 用法

定义一个protocol

protocol PersonProtocol {
    func getName()
    func getSex()
}

某个class、struct或者enum要遵守这种约定的话,需要实现约定的方法

struct Person: PersonProtocol {
    func getName() {
         print("MelodyZhy")
    }
    func getSex() {
         print("boy")
    }
}
protocol中的约定方法,当方法中有参数时是不能有默认值的
protocol中也可以定义属性,但必须明确指定该属性支持的操作:只读(get)或者是可读写(get set)
protocol PersonProtocol {
    // 我们也可以在protocol中定义属性
    // ️必须明确指定该属性支持的操作:只读(get)或者是可读写(get set)
    var height: Int { get set }
    func getName()
    func getSex()
    // protocol中的约定方法,当方法中有参数时是不能有默认值的
    // ❌ Default argument not permitted in a protocol method
    // func getAge(age: Int = 18)
    func getAge(age: Int)
}
虽然height在protocol中是一个computed property,但在遵守该约定的类型中可以简单的定义成一个stored property
当protocol中定义了一个只读属性,其实我们也可以在遵守该约定的类型中完成该属性的可读可写
protocol PersonProtocol {
    var height: Int { get set }
    var weight: Int { get }
    func getName()
    func getSex()
    func getAge(age: Int)
}

struct Person: PersonProtocol {
    var height = 178
    var weight = 120
    func getName() {
         print("MelodyZhy")
    }
    func getSex() {
         print("boy")
    }
    func getAge(age: Int) {
        print("age = \(age)")
    }
}

var person = Person()
person.height   // 178
person.height = 180
person.height  // 180

person.weight // 120
// 可以更改(但在以前版本的Swift中是不能直接这样更改的
// 需要借助一个内部的stored property
// 然后把这个属性设计成一个computed property实现 get 和 set 方法)
person.weight = 130 // 130
// 当我们把person从Person转换成PersonProtocol时 他就是只读的了
// ❌Cannot assign to property: 'weight' is a get-only property
(person as PersonProtocol).weight = 120
如何定义可选的protocol属性或者方法?
@objc protocol PersonProtocol {
    optional var height: Int { get set }
    optional var weight: Int { get }
    optional func getName()
    optional func getSex()
    optional func getAge(age: Int)
}

class Person: PersonProtocol {
    // 如果想提供可选的约定方法或者属性那么只能定义@objc的protocol
    // 并且这种约定只能class能遵守
}
protocol可以继承,当然struct、class、enum都可以同时遵守多个约定
// 例如:
protocol PersonProtocol {
    var height: Int { get set }
    var weight: Int { get }
    func getName()
    func getSex()
    func getAge(age: Int)
}
protocol Engineer: PersonProtocol {
    var good: Bool { get }
}
protocol Animal {
}
struct Person: Engineer, Animal {
    // 省略了该实现约定的方法和属性
}

protocol extension

protocol extension 最关键的一点就是能在 protocol extension 方法中获取 protocol 的属性,因为Swift编译器知道任何一个遵守 protocol 的自定义类型,一定会定义这个 protocol 约定的各种属性,既然这样我们就可以在 protocol extension 中添加默认的实现了。这也是为什么会有 protocol oriented programming 这个概念,但这时候肯定会有人说我通过面对对象的编程方式也可以实现,但为什么要用遵守 protocol 的方法呢,这个要等到了解 extension 中的 type constraints 后解释...

先看一个通过 protocol extension 添加默认实现的代码例子

// 定义一个人属性的 protocol
protocol PersonProperty {
    var height: Int { get } // cm
    var weight: Double { get } // kg
    // 判断体重是否合格的函数
    func isStandard() -> Bool
}
extension PersonProperty {
    // 给 protocol 添加默认的实现
    func isStandard() -> Bool {
        return self.weight == Double((height - 100)) * 0.9
    }
    // 给 protocol 添加默认属性
    var isPerfectHeight: Bool {
        return self.height == 178
    }
}
struct Person: PersonProperty {
    var height: Int
    var weight: Double
    // 如果自定义类型里面创建了遵守的 protocol 中的方法
    // 那么他将覆盖 protocol 中的方法
//    func isStandard() -> Bool {
//        return true
//    }
}
// 创建遵守 PersonProperty 的自定义类型
let p = Person(height: 178, weight: 61.5)
// 那么 p 这个自定义类型 天生就有判断这个人身高体重是否合格的方法
p.isStandard() // false
// 同样天生具有判断是否是 Perfect Height 的属性
p.isPerfectHeight // true

protocol extension 中的 type constraints

这相当于给 protocol extension 中的默认实现添加限定条件,写法如下

// 运动因素的 protocol
protocol SportsFactors {
    // 运动量
    var sportQuantity: Double { get }
}

// 下面这种写法就用到了 extension 中的 type constraints
// 意思是 只有同时遵守了 SportsFactors 和 PersonProperty 时
// 才使 PersonProperty 获得扩展 并提供带有 sportQuantity 属性的 isStandard 方法
extension PersonProperty where Self: SportsFactors {
    func isStandard() -> Bool {
        // 随意写的算法 不要在意
        return self.weight == Double((height - 100)) * 0.9 - self.sportQuantity
    }
}

protocol oriented programming 的优点

1、首先继承是 class 专有的,所以它不能用来扩展其他类型,但 protocol 是没有这种局限性的
2、试想一下,上面的代码你用面对对象的编程方式的话可能你就需要多一个运动量的属性,同时也要修改 isStandard 函数,一切看起来特别自然,随着后续需求的更改可能会有更多因素影响是否是合格的体重,那么这时候你就会在不知不觉中将你代码的耦合度成倍提高,其实对于这个类来说,他完全不需要知道是否是合格体重的计算细节,所以我们完全可以把这些类型无关的细节从类型定义上移出去,用一个 protocol 封装好这些细节,然后让其成为这个类型的一种修饰,这就是POP的核心思想。
3、当有多种因素制约是否是合格体重时,我们可以用多个 protocol 来对该类型进行修饰,每一种修饰的相关细节,我们都在对应的 protocol extension 中单独的封装起来,这样就大大降低了代码的耦合度,同时代码的可维护性也得到了相应的提高。swift标准库中大部分都是用这种思想构建的。**

5.Protocol Oriented Programming 面向协议编程

面向协议编程中,Protocol 实际上就是 DIP 中的抽象接口。通过之前的讲解,采用面向协议的方式进行编程,即是对依赖反转原则 DIP 的践行,在一定程度上降低代码的耦合性,避免耦合性过高带来的问题。下面通过一个具体实例简单讲解一下:
首先是高层次结构的实现,创建EmmettBrown的类,然后声明了一个需求(travelInTime方法)。

// 高层次实现 - EmmettBrown
final class EmmettBrown {
    private let timeMachine: TimeTraveling
    init(timeMachine: TimeTraveling) {
        self.timeMachine = timeMachine
    }
    func travelInTime(time: TimeInterval) -> String {
        return timeMachine.travelInTime(time: time)
    }
}

采用 Protocol 定义抽象接口 travelInTime,低层次的实现将需要依赖这个接口。

// 抽象接口 - 时光旅行
protocol TimeTraveling {
    func travelInTime(time: TimeInterval) -> String
}

最后是低层次实现,创建DeLorean类,通过遵循TimeTraveling协议,完成TravelInTime抽象接口的具体实现。

// 低层次实现 - DeLorean
final class DeLorean: TimeTraveling {
    func travelInTime(time: TimeInterval) -> String {
        return "Used Flux Capacitor and travelled in time by: \(time)s"
    }
}

使用的时候只需要创建相关类即可调用其方法。

// 使用方式
let timeMachine = DeLorean()
let mastermind = EmmettBrown(timeMachine: timeMachine)
mastermind.travelInTime(time: -3600 * 8760)

Delegate - 利用 Protocol 解耦

委托(Delegate)是一种设计模式,表示将一个对象的部分功能转交给另一个对象。委托模式可以用来响应特定的动作,或者接收外部数据源提供的数据,而无需关心外部数据源的类型。部分情况下,Delegate 比起自上而下的继承具有更松的耦合程度,有效的减少代码的复杂程度。

那么 Deleagte 和 Protocol 之间是什么关系呢?在 Swift 中,Delegate 就是基于 Protocol 实现的,定义 Protocol 来封装那些需要被委托的功能,这样就能确保遵循协议的类型能提供这些功能。

Protocol 是 Swift 的语言特性之一,而 Delegate 是利用了 Protocol 来达到解耦的目的。

Delegate 使用实例:
//定义一个委托
protocol CustomButtonDelegate: AnyObject{
    func CustomButtonDidClick()
}
 
class ACustomButton: UIView {
    ...
    weak var delegate: ButtonDelegate?
    func didClick() {
        delegate?.CustomButtonDidClick()
    }
}

// 遵循委托的类
class ViewController: UIViewController, CustomButtonDelegate {
    let view = ACustomButton()
    override func viewDidLoad() {
        super.viewDidLoad()
        ...
        view.delegate = self
    }
    func CustomButtonDidClick() {
        print("Delegation works!")
    }
}

代码说明

如前所述,Delegate 的原理其实很简单。ViewController会将 ACustomButtondelegate 设置为自己,同时自己遵循、实现了 CustomButtonDelegate 协议中的方法。这样在后者调用 didClick 方法的时候会调用 CustomButtonDidClick 方法,从而触发前者中对应的方法,从而打印出 Delegation works!

循环引用

我们注意到,在声明委托时,我们使用了 weak 关键字。目的是在于避免循环引用。ViewController拥有 view,而 view.delegate 又强引用了ViewController,如果不将其中一个强引用设置为弱引用,就会造成循环引用的问题。

AnyObject

定义委托时,我们让 protocol 继承自 AnyObject。这是由于,在 Swift 中,这表示这一个协议只能被应用于 class(而不是 struct 和 enum)。

实际上,如果让 protocol 不继承自任何东西,那也是可以的,这样定义的 Delegate 就可以被应用于 class 以及 structenum。由于 Delegate 代表的是遵循了该协议的实例,所以当 Delegate 被应用于 class 时,它就是 Reference type,需要考虑循环引用的问题,因此就必须要用 weak 关键字。

但是这样的问题在于,当 Delegate 被应用于structenum 时,它是 Value type,不需要考虑循环引用的问题,也不能被使用 weak关键字。所以当Delegate未限定只能用于 classXcode 就会对 weak 关键字报错:'weak' may only be applied to class and class-bound protocol types

delegate只能用于类中

定义协议的格式
编写协议的格式:
protocol 协议名字 : 基协议 {  //当然也可以不遵守基协议
    //方法的声明
}
例:定义一个买票的协议
protocol buyTicketProtocol {
    func buyTicket() -> Void
}
Tips:
如果一个协议继承了基协议NSObjectProtocol,那么遵守这个协议的类也必须要继承NSObject这个类
遵守协议的格式

一个类若要遵守一个协议,只需要在自己所继承的父类后面写上要遵守的协议名并以逗号","隔开,如果这个类无需继承,那么直接在冒号后面写上协议的名字就好

遵守协议的格式:
class Person : NSObject,SportProtocol{}
例:定义一个会买票的黄牛类
class Tout : buyTicketProtocol {  //无继承类遵守协议
    func buyTicket() {
        print("here's your ticket")
    }
}
Tips:
Swift中的基协为NSObjectProtocol,这与OC中的基协议(NSObject)有些不同

协议的继承

  • 上面有提到,当我们自定义一个协议的时候可以选择让这个协议继承自NSObjectProtocol,不单单如此,自定义的协议也可以遵守另外一个协议哦,基本格式如下:
protocol showTicketNumberProtocol {  //展示票号
    func showTicketNumber() -> Void
}
protocol buyTicketProtocol : showTicketNumberProtocol  {  //买票
    func buyTicket() -> Void
}
  • 如果一个类遵循了一个含有继承的协议,那么这个类就必须实现这个协议链中所有的必须实现的函数,否则编译报错
class Tout : buyTicketProtocol  { 
    func buyTicket() {
        print("here's your ticket")
    }
    func showTicketNumber() {  //必须实现buyTicketProtocol所继承的"父"协议中的函数
        print("123456")
    }
}
Tips:
上面提到的"如果一个协议继承了基协议NSObjectProtocol,那么遵守这个协议的类也必须要继承NSObject这个类"
这是因为我们需要NSObject这个父类来帮我们实现NSObjectProtocol中定义的函数,否则编译器会以"没有实现NSObjectProtocol中的函数为由而报错
协议中可选实现的函数

为了保证Swift语言的严谨性,不建议在协议中定义可选实现的函数,不过不建议不代表不能嘛,我们可以利用OC特性来实现在Swift协议中定义可选实现函数

  • 创建带有OC特性的协议
@objc  //表示一下代码含有OC特性
protocol showTicketNumberProtocol {
    optional func showTicketNumber() -> Void  //optional修饰的函数为可选择实现(或不实现)的函数
}
  • 遵守带有OC特性的协议
class Tout : showTicketNumberProtocol  {
    //终于,下面这个函数可以不实现,并且不会报错了
    @objc func showTicketNumber() {  //由于showTicketNumberProtocol含有OC特性,于是这个协议中所有的函数在实现之前都要有@objc来修饰
        print("123456")
    }
}

6.Structure 和Class 的差别

相同之处

1.拥有自己的属性(property)及方法(function)
2.可以自订建构子(initializer)

不同之处
  1. Structure无法继承
  2. Structure拥有成员建构子(Memberwise Initializer)
  3. Structure是Value Type , Class是Reference Type
  4. Structure不能直接修改内部属性,如果要修改必须加上mutating

大致意思就是说,虽然结构体和枚举可以定义自己的方法,但是默认情况下,实例方法中是不可以修改值类型的属性。
举个简单的例子,假如定义一个点结构体,该结构体有一个修改点位置的实例方法:

struct Point {
  var x = 0, y = 0
  
  func moveXBy(x:Int,yBy y:Int) {
    self.x += x
    // Cannot invoke '+=' with an argument list of type '(Int, Int)'
    self.y += y
    // Cannot invoke '+=' with an argument list of type '(Int, Int)'
  }
}

编译器抛出错误,说明确实不能在实例方法中修改属性值。

为了能够在实例方法中修改属性值,可以在方法定义前添加关键字 mutating

struct Point {
  var x = 0, y = 0
  mutating func moveXBy(x:Int,yBy y:Int) {
    self.x += x
    self.y += y
  }
}
var p = Point(x: 5, y: 5)
p.moveXBy(3, yBy: 3)

另外,在值类型的实例方法中,也可以直接修改self属性值。

enum TriStateSwitch {
  case Off, Low, High
  mutating func next() {
    switch self {
    case Off:
      self = Low
    case Low:
      self = High
    case High:
      self = Off
    }
  }
}
var ovenLight = TriStateSwitch.Low
ovenLight.next()
// ovenLight is now equal to .High
ovenLight.next()
// ovenLight is now equal to .Off”

TriStateSwitch枚举定义了一个三个状态的开关,在next实例方法中动态改变self属性的值。

当然,在引用类型中(即class)中的方法默认情况下就可以修改属性值,不存在以上问题。

7. 值类型和引用类型的区别

由于 Swift 中的 struct 为值类型,class 为引用类型,因此文中以这两种类型为代表来具体阐述

stack & heap

内存(RAM)中有两个区域,栈区(stack)和堆区(heap)。在 Swift 中,值类型,存放在栈区;引用类型,存放在堆区。

class RectClass {
    var height = 0.0
    var width = 0.0
}

struct RectStruct {
    var height = 0.0
    var width = 0.0
}

var rectCls = RectClass()
var rectStrct = RectStruct()

值类型(Value Type)

值类型,即每个实例保持一份数据拷贝。

在 Swift 中,典型的有 structenum,以及 tuple 都是值类型。而平时使用的 IntDoubleFloatStringArrayDictionarySet 其实都是用结构体实现的,也是值类型。
Swift 中,值类型的赋值为深拷贝(Deep Copy),值语义(Value Semantics)即新对象和源对象是独立的,当改变新对象的属性,源对象不会受到影响,反之同理。
在 Swift 中,双等号(== &!=)可以用来比较变量存储的内容是否一致,如果要让我们的struct 类型支持该符号,则必须遵守 Equatable协议。

extension CoordinateStruct: Equatable {
    static func ==(left: CoordinateStruct, right: CoordinateStruct) -> Bool {
        return (left.x == right.x && left.y == right.y)
    }
}

if coordA != coordB {
    print("coordA != coordB")
}

引用类型(Reference Type)

引用类型,即所有实例共享一份数据拷贝。

在 Swift 中,class 和闭包是引用类型。引用类型的赋值是浅拷贝(Shallow Copy),引用语义(Reference Semantics)即新对象和源对象的变量名不同,但其引用(指向的内存空间)是一样的,因此当使用新对象操作其内部数据时,源对象的内部数据也会受到影响。

class Dog {
    var height = 0.0
    var weight = 0.0
}

var dogA = Dog()
var dogB = dogA

dogA.height = 50.0
print("dogA.height -> \(dogA.height)")
print("dogB.height -> \(dogB.height)")

// dogA.height -> 50.0
// dogB.height -> 50.0

如果声明一个引用类型的常量,那么就意味着该常量的引用不能改变(即不能被同类型变量赋值),但指向的内存中所存储的变量是可以改变的。
在 Swift 中,三等号(=== &!==)可以用来比较引用类型的引用(即指向的内存地址)是否一致。也可以在遵守 Equatable 协议后,使用双等号(==&!=)用来比较变量的内容是否一致。

函数传参

在 Swift 中,函数的参数默认为常量,即在函数体内只能访问参数,而不能修改参数值。具体来说:
1.值类型作为参数传入时,函数体内部不能修改其值
2.引用类型作为参数传入时,函数体内部不能修改其指向的内存地址,但是可以修改其内部的变量值

值类型

当值类型的变量作为参数被传入函数时,相当于创建了新的常量并初始化为传入的变量值,该参数的作用域及生命周期仅存在于函数体内。
当引用类型的变量作为参数被传入函数时,相当于创建了新的常量并初始化为传入的变量引用,当函数体内操作参数指向的数据,函数体外也受到了影响

inout

inout是 Swift 中的关键字,可以放置于参数类型前,冒号之后。使用 inout之后,函数体内部可以直接更改参数值,而且改变会保留。

func swap(resSct: inout ResolutionStruct) {
    withUnsafePointer(to: &resSct) { print("During calling: \($0)") }
    let temp = resSct.height
    resSct.height = resSct.width
    resSct.width = temp
}

var iPhone6ResoStruct = ResolutionStruct(height: 1334, width: 750)
print(iPhone6ResoStruct)
withUnsafePointer(to: &iPhone6ResoStruct) { print("Before calling: \($0)") }
swap(resSct: &iPhone6ResoStruct)
print(iPhone6ResoStruct)
withUnsafePointer(to: &iPhone6ResoStruct) { print("After calling: \($0)") }

小结:值类型变量作为参数传入函数,外界和函数参数的内存地址一致,函数内对参数的更改得到了保留。
需要注意的是:
1.使用 inout 关键字的函数,在调用时需要在该参数前加上 & 符号
2.inout 参数在传入时必须为变量,不能为常量或字面量(literal)
3.inout 参数不能有默认值,不能为可变参数
4.inout 参数不等同于函数返回值,是一种使参数的作用域超出函数体的方式
5.多个inout 参数不能同时传入同一个变量,因为拷入拷出的顺序不定,那么最终值也不能确定

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

struct Rectangle {
    var width = 0.0
    var height = 0.0
    var origin = Point()
    
    var center: Point {
        get {
            print("center GETTER call")
            return Point(x: origin.x + width / 2,
                         y: origin.y + height / 2)
        }
        
        set {
            print("center SETTER call")
            origin.x = newValue.x - width / 2
            origin.y = newValue.y - height / 2
        }
    }
    
    func reset(center: inout Point) {
        center.x = 0.0
        center.y = 0.0
    }
    
}

var rect = Rectangle(width: 100, height: 100, origin: Point(x: -100, y: -100))
print(rect.center)
rect.reset(center: &rect.center)
print(rect.center)

// center GETTER call
// Point(x: -50.0, y: -50.0)

// center GETTER call
// center SETTER call

// center GETTER call
// Point(x: 0.0, y: 0.0)

inout参数的传递过程:

  1. 当函数被调用时,参数值被拷贝
  2. 在函数体内,被拷贝的参数修改
  3. 函数返回时,被拷贝的参数值被赋值给原有的变量

8.Swift 构造过程

构造器
语法
init()
{
    // 实例化后执行的代码
}
默认属性值

我们可以在构造器中为存储型属性设置初始值;同样,也可以在属性声明时为其设置默认值。

struct rectangle {
    // 设置默认值
    var length = 6
    var breadth = 12
}
var area = rectangle()
print("矩形的面积为 \(area.length*area.breadth)")
构造参数

你可以在定义构造器 init() 时提供构造参数,如下所示:

struct Rectangle {
    var length: Double
    var breadth: Double
    var area: Double
    
    init(fromLength length: Double, fromBreadth breadth: Double) {
        self.length = length
        self.breadth = breadth
        area = length * breadth
    }
    
    init(fromLeng leng: Double, fromBread bread: Double) {
        self.length = leng
        self.breadth = bread
        area = leng * bread
    }
}

let ar = Rectangle(fromLength: 6, fromBreadth: 12)
print("面积为: \(ar.area)")

let are = Rectangle(fromLeng: 36, fromBread: 12)
print("面积为: \(are.area)")
内部和外部参数名

跟函数和方法参数相同,构造参数也存在一个在构造器内部使用的参数名字和一个在调用构造器时使用的外部参数名字。

然而,构造器并不像函数和方法那样在括号前有一个可辨别的名字。所以在调用构造器时,主要通过构造器中的参数名和类型来确定需要调用的构造器。

如果你在定义构造器时没有提供参数的外部名字,Swift 会为每个构造器的参数自动生成一个跟内部名字相同的外部名。

struct Color {
    let red, green, blue: Double
    init(red: Double, green: Double, blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
    init(white: Double) {
        red   = white
        green = white
        blue  = white
    }
}

// 创建一个新的Color实例,通过三种颜色的外部参数名来传值,并调用构造器
let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)

print("red 值为: \(magenta.red)")
print("green 值为: \(magenta.green)")
print("blue 值为: \(magenta.blue)")

// 创建一个新的Color实例,通过三种颜色的外部参数名来传值,并调用构造器
let halfGray = Color(white: 0.5)
print("red 值为: \(halfGray.red)")
print("green 值为: \(halfGray.green)")
print("blue 值为: \(halfGray.blue)")

以上程序执行输出结果为:

red 值为: 1.0
green 值为: 0.0
blue 值为: 1.0
red 值为: 0.5
green 值为: 0.5
blue 值为: 0.5
没有外部名称参数

如果你不希望为构造器的某个参数提供外部名字,你可以使用下划线_来显示描述它的外部名。

struct Rectangle {
    var length: Double
    
    init(frombreadth breadth: Double) {
        length = breadth * 10
    }
    
    init(frombre bre: Double) {
        length = bre * 30
    }
    //不提供外部名字
    init(_ area: Double) {
        length = area
    }
}

// 调用不提供外部名字
let rectarea = Rectangle(180.0)
print("面积为: \(rectarea.length)")

// 调用不提供外部名字
let rearea = Rectangle(370.0)
print("面积为: \(rearea.length)")

// 调用不提供外部名字
let recarea = Rectangle(110.0)
print("面积为: \(recarea.length)")
可选属性类型

如果你定制的类型包含一个逻辑上允许取值为空的存储型属性,你都需要将它定义为可选类型optional type(可选属性类型)。

当存储属性声明为可选时,将自动初始化为空 nil。

struct Rectangle {
    var length: Double?
    
    init(frombreadth breadth: Double) {
        length = breadth * 10
    }
    
    init(frombre bre: Double) {
        length = bre * 30
    }
    
    init(_ area: Double) {
        length = area
    }
}

let rectarea = Rectangle(180.0)
print("面积为:\(rectarea.length)")

let rearea = Rectangle(370.0)
print("面积为:\(rearea.length)")

let recarea = Rectangle(110.0)
print("面积为:\(recarea.length)")

以上程序执行输出结果为:

面积为:Optional(180.0)
面积为:Optional(370.0)
面积为:Optional(110.0)
构造过程中修改常量属性

只要在构造过程结束前常量的值能确定,你可以在构造过程中的任意时间点修改常量属性的值。

对某个类实例来说,它的常量属性只能在定义它的类的构造过程中修改;不能在子类中修改。

尽管 length 属性现在是常量,我们仍然可以在其类的构造器中设置它的值:

struct Rectangle {
    let length: Double?
    
    init(frombreadth breadth: Double) {
        length = breadth * 10
    }
    
    init(frombre bre: Double) {
        length = bre * 30
    }
    
    init(_ area: Double) {
        length = area
    }
}

let rectarea = Rectangle(180.0)
print("面积为:\(rectarea.length)")

let rearea = Rectangle(370.0)
print("面积为:\(rearea.length)")

let recarea = Rectangle(110.0)
print("面积为:\(recarea.length)")
类的继承和构造过程

Swift 提供了两种类型的类构造器来确保所有类实例中存储型属性都能获得初始值,它们分别是指定构造器和便利构造器。

指定构造器实例
class mainClass {
    var no1 : Int // 局部存储变量
    init(no1 : Int) {
        self.no1 = no1 // 初始化
    }
}
class subClass : mainClass {
    var no2 : Int // 新的子类存储变量
    init(no1 : Int, no2 : Int) {
        self.no2 = no2 // 初始化
        super.init(no1:no1) // 初始化超类
    }
}

let res = mainClass(no1: 10)
let res2 = subClass(no1: 10, no2: 20)

print("res 为: \(res.no1)")
print("res2 为: \(res2.no1)")
print("res2 为: \(res2.no2)")
便利构造器实例
class mainClass {
    var no1 : Int // 局部存储变量
    init(no1 : Int) {
        self.no1 = no1 // 初始化
    }
}

class subClass : mainClass {
    var no2 : Int
    init(no1 : Int, no2 : Int) {
        self.no2 = no2
        super.init(no1:no1)
    }
    // 便利方法只需要一个参数
    override convenience init(no1: Int)  {
        self.init(no1:no1, no2:0)
    }
}
let res = mainClass(no1: 20)
let res2 = subClass(no1: 30, no2: 50)

print("res 为: \(res.no1)")
print("res2 为: \(res2.no1)")
print("res2 为: \(res2.no2)")
构造器的继承和重载

Swift 中的子类不会默认继承父类的构造器。

父类的构造器仅在确定和安全的情况下被继承。

当你重写一个父类指定构造器时,你需要写override修饰符。

class SuperClass {
    var corners = 4
    var description: String {
        return "\(corners) 边"
    }
}
let rectangle = SuperClass()
print("矩形: \(rectangle.description)")

class SubClass: SuperClass {
    override init() {  //重载构造器
        super.init()
        corners = 5
    }
}

let subClass = SubClass()
print("五角型: \(subClass.description)")

9.Swift中的元组(Tuples)

创建元组

在Swift中创建元组的方式很简单,它的语法有点类似数组,但是需要把方括号替换为圆括号:

let firstHighScore = ("Mary", 9001)

与数组不同的是,元组中的元素可以是任意类型。上面代码中firstHighScore元组就包含一个String类型的元素和一个Int类型的元素。
另外,在创建元组时你还可以给元组中的元素命名:

let secondHighScore = (name: "James", score: 4096)

这样可以让我们在使用元组的时候明确的指定某个元素,非常有用。在后面的文章中大家可以看到给元素命名的好处。

以上就是创建元组的两种方式,非常简单和简洁。你不需要像创建struct一样写出它的结构和内部属性,也不需要像创建class一样要写初始化方法。你只需要把你想用的、任何类型的值放在圆括号内,用逗号隔开即可。如果你愿意你还可以给每个元素命名,提高元组使用效率。

从元组中读元素

从元组中读取元素有几种方式,但一般我们会选择最适合当前应用场景的方式,并且确保选择的方式是在当前情况下最简单的一种。

元组元素没有命名

如果我们没有给元组的元素命名,我们可以用点语法,通过定义好的元组变量或常量获取它的第1个到第n个元素:

let firstHighScore = ("Mary", 9001)
firstHighScore.0            // Mary
firstHighScore.1        // 9001

如果你觉得上述这种方法会造成语义的不明确,那么我们还可以将元组赋值给一个带有元素名称的元组(元素名称个数要对应):


let (firstName, firstScore) = firstHighScore
firstName       // Mary
firstScore      // 9001

如果你只想读取firstHighScore元组中的分数,那么你可以这样写:

let (_, firstScore) = firstHighScore
firstScore      // 9001
元组元素有命名

如果我们已经给元组中的元素命名了名称,那么我们可以这样写:

let secondName = secondHighScore.name
let secondScore = secondHighScore.score
secondName      // James
secondScore     // 4096
将元组作为函数返回值

我们可以将元组作为函数的返回值,下面这个函数的返回值就是我们之前定义过的secondHighScore元组:

func getAHighScore() -> (name: String, score: Int)
{
    let theName = "Patricia"
    let theScore = 3894
    
    return (theName, theScore)
}

为什么说上述函数的返回值是secondHighScore元组呢?因为getAHighScore函数返回的元组元素个数、元素名称、元素类型均和secondHighScore相同。

其实将元组作为函数的返回值时也可以不必对元素进行命名,只要你明白每个元素代表的含义即可:

func getAHighScore() -> (String, Int)
{
    let theName = "Patricia"
    let theScore = 3894
    
    return (theName, theScore)
}

如果你不确定返回的元组一定不为nil,那么你可以返回一个可选的元组类型:

func maybeGetHighScore() -> (String, Int)?
{
    return nil
}

因为是可选的元组类型,所以当返回的元组不为nil时,你需要对元组进行解包:

if let possibleScore = maybeGetHighScore()
{
    possibleScore.0
    possibleScore.1
}
else
{
    println("Nothing Here")
}

注意:当你定义了一个没有返回值的函数时,其实该函数是返回一个空的元组()。

元组的访问级别

元组的访问级别取决于它包含的元素。比如元组里的元素都是private级别的,那么该元组也是private级别的。但这里有一个遵循最小的原则,也就是说如果一个元组中有两个元素,一个为private级别,另一个为public级别,那么该元组遵循最小原则,它的访问级别为private。

元组是值类型

关于值类型和引用类型的知识这里不再累赘,我们通过一个代码示例来看看元组是哪种类型:

var someScore = ("John", 55)
 
var anotherScore = someScore
anotherScore.0 = "Robert"
 
 
println(anotherScore.0)  //Outputs:  "Robert"
println(someScore.0)     //Outputs:  "John"

通过上述的代码示例可以看出,我把someScore元组赋值给了anotherScore,然后修改了anotherScore的第1个元素的值,最后分别打印了someScore和anotherScore第1个元素的值。someScore元组第一个元素的值为Robert,而anotherScore元组第一个元素的值仍然为John。由此可见元组是值类型。

你可能感兴趣的:(swift 的基础知识)