Swift基础语法(七)对象

Swift基础语法文章汇总

本文主要讲述了属性、初始化器、方法、下标。虽然是以对象为例进行讲解,但大部分也可以使用在结构体、枚举中。当然也要清楚他们三者之前的区别。对于属性,Swift更具体的划分了存储属性和计算属性,并且可以更方便的为存储属性增加属性观察器。对于初始化器,需要着重理解Swift为实例的安全使用所进行的规范化,设定了两段式初始化和安全检查来确保初始化过程的安全,只有初始化完成后才可以进行实例的使用。方法的定义和实现没有特殊性。只不过Swift显式的增加了下标函数,可以更方便的操作存储属性

主要内容:

  1. 属性
  2. 初始化器
  3. 方法
  4. 下标

1、属性

Swift中的属性包括实例属性和类型属性,又分为存储属性和计算属性。存储属性可以类比OC的成员变量,计算属性可以类比OC的属性,但是它并没有成员变量

1.1 存储属性

存储属性相当于是OC的实例变量,没有setter和getter,它直接存储在实例的内存中,结构体、类可以定义实例存储属性,枚举不可以定义。
代码:

/*
 1、存储属性和计算属性
 */
func test1() {
    struct Circle {
        //存储属性
        var radius: Double
        //计算属性
        var diameter: Double {
            set {
                radius = newValue / 2
            }
            get {
                radius * 2
            }
        }
    }
    let circle = Circle(radius: 5)
    print(circle.radius)
    print(circle.diameter)
}
test1()

1.1.1 延迟存储属性

延迟存储属性lazy Stored Property,使用lazy修饰,在第一次使用属性的时候才会进行初始化
代码:

/*
 2、延迟存储属性
 */
func test2() {
    class PhotoView {
        //这是一个存储属性,直接将闭包表达式结果赋值Image
        lazy var image: Image = {
            let url = "https://image.baidu.com/xx.png"
            let data = Data(url: url)
            return Image(data: data)
        }()
    }
}

说明:

  1. 如果想要在使用属性的时候才去加载属性,那么就应该设置成lazy
  2. 比如这里的image加载,需要涉及网络加载,就应该使用lazy
  3. lazy因为是要在使用时才会执行,所以必须是var修饰,不能用let
  4. 如果多条线程同时第一次访问lazy属性,无法保证属性只被初始化一次

1.1.2 属性观察器

监听属性的修改
代码:

/*
 3、属性观察器
 */
func test3() {
    struct Circle {
        var radius: Double {
            willSet {
                print("willSet", newValue)
            }
            didSet {
                print("didSet", oldValue, radius)
            }
        }
        init() {
            self.radius = 1.0//不会触发观察器
            print("Circle init!")
        }
    }
    // Circle init!
    //willSet 2.0
    //didSet 1.0 2.0
    var cicle = Circle()
    cicle.radius = 2.0
}
test3()

说明:

  1. 观察器有两个方法,一个是willSet,一个是didSet
  2. 分别是在属性将要修改、属性修改完成的时候执行
  3. willSet会传递新值,默认叫newValue
  4. didSet会传递旧值,默认叫做oldValue
  5. 在初始化器中和属性定义时设置不会触发观察(这个也容易理解,这个没有观察的意义)
  6. willSet和didSet方法其实是包含在setter方法中的

1.2 计算属性

计算属性就相当于OC的属性,有getter方法就决定了是计算属性,它作为函数,不占用实例的内存,枚举、结构体、类都可以定义计算属性
代码:

//计算属性和存储属性的区别
struct Circle {
    //存储属性
    var radius: Double
    //计算属性
    var diameter: Double {
        set {
            radius = newValue / 2
        }
        get {
            radius * 2
        }
    }
}
    
let circle = Circle(radius: 5)
print(circle.radius)
print(circle.diameter)
    
//只读计算属性
struct Circle2 {
    //存储属性
    var radius: Double
    //计算属性
    var diameter: Double {
        get {
            radius * 2
        }
    }
}
    
//简写
struct Circle3 {
    //存储属性
    var radius: Double
    //计算属性
    var diameter: Double { radius * 2 }
}

说明:

  1. set传入的新值默认叫做newValue,也可以自定义
  2. 计算属性只能是var,不能用let修饰,这也容易理解,既然定义了计算属性说明肯定是要变化的
  3. 计算属性的本质就是函数
  4. 如果只有一个get方法,那么是只读计算属性,此时可以简写

枚举原始值rawValue的本质

代码:

//枚举的rawValue的本质就是只读计算属性
enum TestEnum: Int {
    case test1 = 1, test2 = 2, test3 = 3
    var rawValue: Int {
        switch self {
        case .test1:
            return 10
        case .test2:
            return 11
        case .test3:
            return 12
            //条件全部保证,就不需要增加default
            //            default:
            //                <#code#>
            //            }
        }
    }
}
print(TestEnum.test3.rawValue)//12

说明:

  1. 枚举原始值rawValue的本质就是:只读计算属性
  2. 此时枚举只存储序号,并没有存储1.2.3,他们的获取就是通过rawValue来计算的

验证:
查看执行TestEnum.test3.rawValue语句的汇编打印

验证.png

1.3 inout对于属性的传递

属性作为inout修饰的参数传递,他们的传递方式和普通的变量有一定的不同
代码:

/*
 5、属性使用inout的分析
 */
func test5() {
    //定义一个结构体
    struct Shape {
        var width : Int
        var side : Int {
            willSet {
                print("willSetSide", newValue)
            }
            didSet {
                print("didSetSide", oldValue, side)
            }
        }
        var girth: Int {
            set {
                width = newValue / side
                print("setGirth", newValue)
            }
            get {
                print("getGirth")
                return width * side
            }
        }
        func show() {
            print("width=\(width), side=\(side), girth=\(girth)")
        }
    }
    
    //定义一个函数,传入属性
    func test(_ num: inout Int) {
    num = 20
    }
    
    //传入函数
    var s = Shape(width: 10, side: 4)
    test(&s.width)//传入存储属性
    s.show()
    test(&s.side)//传入带观察器的存储属性
    s.show()
    test(&s.girth)//传入计算属性
    s.show()
}
test5()

说明:

  1. 首先可以看到这三种都可以作为inout修饰的参数传递,传递进地址值,之后修改
  2. 对于存储属性直接将属性的地址传入,在函数体内修改后外部的存储属性自然就改变了
  3. 计算属性使用inout,也是传递的地址,它会先调用getter方法将其放到一个栈空间中,将这个地址传递到函数中,在函数体内执行完成后,再调用setter方法
  4. 属性观察器的存储属性,在赋值时其实调用的是setter方法,willSet和didSet方法其实是包含在setter方法中的。它在作为输入输出参数的时候,也是会先将值赋到局部变量中,之后将局部变量的地址作为参数传递到函数中进行修改。修改后将这个局部变量通过调用set方法赋值到存储属性中

理解:

  1. inout修饰的参数,传入地址后,在函数体内会直接将数值赋值到这个地址中。
  2. 所以对于一般的存储属性可以直接将地址传进去。而对于属性观察器以及计算属性,对其设值需要调用一下set方法或者get方法,此时就不能直接把地址赋值进去,因为inout修饰的参数是直接将值赋值到地址空间上,不会调用set和get方法
  3. 需要在外界重新赋值一次。所以需要在外界先创建一个局部变量,将这个局部变量传递到函数中去执行,执行后,将修改的局部变量赋值到属性中就会触发set方法和get方法。

小结:

  1. inout的本质就是引用传递
  2. 存储属性直接将地址值传入
  3. 带有属性观察器的属性或者计算属性会先将属性值拷贝到一个空间,再将这个空间地址传到函数中进行修改,修改后再将这个空间的值赋值给属性

1.4 类型属性

类型属性只能通过类型访问,整个程序只有一份内存,可以看做是其他语言的类属性或者静态属性
代码:

//类型属性
struct Car {
    static var count: Int = 0
    init() {
        Car.count += 1
    }
}
let c1 = Car()
let c2 = Car()
print(Car.count)//3

说明:

  1. 通过static修饰的静态属性就是类型属性
  2. 在class中可以通过class修饰的属性也是类型属性,class只能修饰class的类型属性

注意:

  1. 类型属性必须设定初始值,因为类型不会创建实例,也就不会调用Init初始化器,所以也就不会在初始化器中设置,因此需要在属性中直接设置默认值
  2. 存储类型属性默认就是lazy,第一次使用的时候才会初始化
  3. 就算被多个线程同时访问,也是线程安全的
  4. 存储类型属性可以是let

单例模式案例:
代码:

//类型属性实现单例模式
class FileManager {
    //属性获取
    static let shared = {
        FileManager()
    }()
    //私有无法调用
    private init() { }
}
//两个对象完全一样
/*
 对象.(unknown context at $1000039a8).(unknown context at $100003a38).FileManager
 对象.(unknown context at $1000039a8).(unknown context at $100003a38).FileManager
 */
print(FileManager.shared)
print(FileManager.shared)

说明:

  1. static修饰说明是类型属性,因此保证只要一份
  2. static修饰说明是线程安全的
  3. let可以实现只赋值一次
  4. 这里不需要使用lazy修饰,就是默认懒加载
  5. 通过打印也可以看到两个对象是同一个

1.5 注意

  1. 在创建类或结构体的实例时,必须为所有的实例存储属性设置一个合适的初始值
    1. 因为在编译时会创建这个结构体或类的内存
    2. 可以在初始化器里设置,也可以直接在存储属性上分配一个默认属性值
  2. 如果多条线程同时第一次访问lazy属性,是线程不安全的
  3. 当结构体中包含一个延迟存储属性时,这个结构体变量只能用var修饰的时候才可以访问延迟属性,对于延迟存储属性,当使用这个属性时,就会给它赋值,而给它赋值时就会改变结构体的结构,所以结构体变量是不可以用let的
  4. 属性观察器、计算属性的功能也可以应用在全局变量、局部变量身上
  5. Swift的计算属性不会自动生成存储属性
  6. 如果想要直接存下来这个值,就用存储属性,如果是需要通过计算得到的值,那就用计算属性。其实就和OC的实例变量和属性一样
  7. 实例属性通过实例访问,类型属性通过类型访问
  8. 枚举类型不可以定义存储实例属性,但可以定义存储类型属性,计算属性

2、初始化器

初始化器在Swift中强制区分为指定初始化器和便捷初始化器,类、结构体、枚举都可以定义初始化器,初始化器的调用规则用以确保所有的初始化器都能够初始化所有的实例,还通过两段式初始化和安全检查确保初始化的安全。

2.1 初始化器的分类

指定初始化器
定义

//指定初始化器
init(parameters) {
    statements
}

说明:

  1. 每个类至少有一个指定初始化器,指定初始化器是类的主要初始化器,也可以有多个指定初始化器,但是一个类通常只有一个指定初始化器
  2. 类的指定初始化器用来作为默认初始化器
  3. 指定初始化器要初始化当前类的所有实例

便捷初始化器
定义

//便捷初始化器
convenience init(parameters) {
    statements
}

说明:

  1. 便捷初始化器需要使用convenience来修饰
  2. 便捷初始化器需要调用便捷初始化器或指定初始化器

2.2 调用规则

示意图:

调用规则.png

说明:

  1. 指定初始化器必须从它的直系父类调用指定初始化器(指定初始化器不能调用同类的指定初始化器,只能调父类的)
  2. 便捷初始化器必须从同一类中调用另一个初始化器(可以是便捷初始化器,也可以是指定初始化器,必须是同一类中,便捷初始化器不能调用父类的初始化器)
  3. 便捷初始化器最终必须调用一个指定初始化器,也就是说调用链的最后必须是一个指定初始化器

2.3 两段式初始化和安全检查

第一阶段初始化所有存储属性,第二阶段使用实例,通过两段式必须先给实例初始化,再使用实例可以让实例更加安全。安全检查就是检查是否遵守初始化器的规则

2.3.1 两段式初始化

第一阶段:

  1. 外层调用指定/便捷初始化器
  2. 分配内存给实例,但未初始化
  3. 指定初始化器先将当前类定义的存储属性都初始化(如果调用的是便捷初始化器,最后也会调用到本类的指定初始化器)
  4. 指定初始化器调用父类的初始化器,不断向上调用,形成初始化器链

第二阶段:
1、从顶部初始化器开始向下,链中的每一个指定初始化器都有机会进一步定制实例
2、初始化器现在能够使用self来定制实例(访问、修改它的属性,调用它的实例方法等等)
3、最终,链中任何便捷初始化器都有机会定制实例以及使用self

示例代码:

/*
 2、两段式初始化
 */
func test7() {
    class Person {
        var age: Int
        init(age: Int) {
            self.age = age
            self.age = 10
        }
    }
    
    class Student : Person {
        var score: Int
        init(age: Int, score: Int) {
            self.score = score
            super.init(age: age)
            self.score = 100
        }
        
        convenience init() {
            self.init(age: 0, score: 0)
        }
    }
    var student: Student = Student()
}
test7()

说明:

  1. 子类调用便捷初始化器来初始化,便捷初始化器会调用该类的指定初始化器
  2. 子类的初始化器先初始化本类的存储属性,再调用父类的初始化器,由下到上
  3. 子类和父类的初始化完成后,才可以进行个性化定制。
  4. 先进行父类的个性化定制,再进行子类的,右下到上

分析:

  1. 个性化定制是由上到下,而且要在初始化后执行的
  2. 先将自己的存储属性都初始化,之后再调用父类的初始化器,这样在初始化结束后就可以进行设值使用了
  3. 必须先初始自己的存储属性,再调用父类初始化器,就是为了在继承体系中的每个初始化器的最后都可以使用self,设值。否则的话父类初始化器的最后是不可以调self 的,因为此时子类的存储属性还没有初始化
  4. 子类初始化器、父类初始化器、使用属性这个顺序不能变
  5. 继承体系中所有的指定初始化器的末尾都可以使用属性

总结:

  1. 先在堆内创建空间,之后进行初始化,初始化完成后才可以使用该实例
  2. 创建空间后其值是以前空间残留的,因此后面必须先初始化才能使用,否则就会有安全问题
  3. 指定初始化器是纵向的,便捷初始化器是横向的
  4. 子类调用初始化器进行初始化的过程中,初始化是从下到上,定制实例是从上到下

2.3.2 安全检查

安全检查就是检查是否遵守初始化器的规则

检查内容:
1、指定初始化器必须保证在调用父类初始化器之前,其所在类定义的所有存储属性都要完成初始化
2、指定初始化器必须先调用父类初始化器,然后才能为继承的属性设置新值
3、便捷初始化器必须先调用同类中的其他初始化器,然后再为任意属性设置新值
4、初始化器在第一阶段初始化完成之前,不能调用任何实例方法,不能读取任何实例属性的值,也不能引用self
5、只有在第一阶段结束 后,才可以使用实例

2.4 初始化器的重写和继承

初始化器的指定初始化器和便捷初始化器在是重写和继承中是有区别的
规则:

  1. 默认情况下子类会继承父类的指定初始化器和便捷初始化器
  2. 但如果子类自己有指定初始化器,基于调用规则考虑,子类无法继承父类的初始化器。
    1. 因为子类指定初始化器必须先自己初始化再调用父类指定初始化器
  3. 子类可以重写父类的指定初始化器为指定初始化器或便捷初始化器
    1. 基于调用规则考虑,如果重写为便捷初始化器,函数体内依然要调用本类的指定初始化器
    2. 重写父类的指定初始化器为指定初始化器,也要加上自己的存储属性的初始化
    3. 因为这里的重写仅仅是重写,调用规则还是要考虑的
  4. 当子类不继承父类的初始化器时,只能重写父类的指定初始化器,无法重写父类的便捷初始化器
    1. 调用规则中知道子类可以调用父类的指定初始化器,但是不能调用父类的便捷初始化器
    2. 既然无法调用父类的便捷初始化器,那么重写也就无从谈起了
  5. 如果子类全部继承或重写了父类的指定初始化器,那么会继承父类的便捷初始化器。因为此时子类也有父类的指定初始化器了
  6. 子类有没有定义新属性与这些规则都没有关系,关系仅仅在于子类是否有指定初始化器

理解:

  1. 便捷初始化器能否继承依赖于子类与父类的指定初始化器是否一样
    1. 因为便捷初始化器来自于本类的指定初始化器
    2. 如果本类的初始化器与父类的一样 ,那么就可以继承
    3. 如果本类的初始化器与父类的不一样 ,那么就不可以继承
  2. 子类是否继承父类指定初始化器,依赖于子类是否自定义指定初始化器
    1. 因为调用规则来说子类的指定初始化器必须先初始化自己,再调用父类指定初始化器
  3. 子类都可以重写父类的指定初始化器为指定初始化器或便捷初始化器,但是不能重写父类的便捷初始化器
    1. 因为指定初始化器是纵向调用,便捷初始化器是横向调用
    2. 而且这里的重写也仅仅是普通的重写,调用规则仍然适用,调用规则来说不能调用父类的便捷初始化器

代码:

/*
 3、初始化器的重写和继承
 */
func test8() {
    class Person {
        var age: Int
        var name: String
        init(age: Int, name: String) {
            self.age = age
            self.name = name
        }
        init() {
            self.age = 0
            self.name = ""
        }
        convenience init(age: Int) {
            self.init(age: age, name: "")
        }
        convenience init(name: String) {
            self.init(age: 0, name: name)
        }
    }
    
    class Student : Person {
        //重写或继承父类的指定初始化器,子类也会继承父类的便捷初始化器
        override init(age: Int, name: String) {
            super.init(age: age, name: name)
        }
        override init() {
            super.init(age: 0, name: "")
        }
    }
    
    var stu = Student(age: 10, name: "WY")
    var stu1 = Student()
    var stu2 = Student(age: 20)
    var stu3 = Student(name: "wenyi")
}

说明:

  1. 可以将父类的指定初始化器重写为指定初始化器,也可以重写为便捷初始化器
  2. 只要是重写或继承父类的指定初始化器,那么子类就可以继承父类的便捷初始化器了

2.5 必需初始化器

用required修饰指定初始化器,其他所有子类必须实现该初始化器
代码:

/*
 4、必需初始化器
 */
func test9() {
    class Person {
        var age : Int
        required init() {
            self.age = 0
        }
        init(age: Int) {
            self.age = age
        }
    }
    
    class Student: Person {
        var no : Int
        //正常初始化器规则
        init(no: Int) {
            self.no = no
            super.init(age: 0)
        }
        //重写父类的required初始化器
        required init() {
            self.no = 0
            super.init()
        }
    }
}

说明:

  1. 必需初始化器只要加上required
  2. 该初始化器必须被其所有子类的对象所执行
  3. 实现方式包括继承或重写
  4. 如果子类重写了required初始化器,也必须加上required,不用加override

2.6 可失败初始化器

初始化器创建失败返回nil就叫做可失败初始化器,类、结构体、枚举中的都可以使用Init?来定义可失败初始化器

2.6.1 基本使用

代码:

class Person {
    var name: String
    init?(name: String){
        if name.isEmpty {
            return nil
        }
        self.name = name
    }
}

说明:

  1. 可以看到判断字符串为空,返回nil,也就是没有认为该实例初始化失败,该对象为nil
  2. 很明显,既然可以返回nil,那么可失败初始化器返回的是一个可选项

初始化器的使用

var person: Person? = Person()
var age = person?.age()
var name = person?.name

说明:
1、这里的person是可选项,因此我们去调用age()时不能直接使用person调用。
2、如果使用person!来调用,有可能出现person为nil的情况,此时nil调用age()直接崩溃
3、因此使用person?来调用,如果nil就不会调用age(),直接返回nil,如果不为nil才会调用age()
4、由此也可以看到age有可能被赋值为nil,因此age只能是个可选项。person?.age()得到的是一个可选项

隐式解包:

class Person {
    var name: String
    init!(name: String){
        if name.isEmpty {
            return nil
        }
        self.name = name
    }
    convenience init() {
        self.init(name: "")
    }
}

说明:

  1. 可失败初始化器返回的对象是可选项,因此在使用时必须要解包才能用
  2. 我们可以直接使用init!来隐式解包
    1. 但是通过实际调用发现这样写也需要解包才能使用
    2. 因此可以用在这里,便捷初始化器调用指定初始化器,此时隐式解包就起到作用了,不用手动解包
    3. 当然此时会有隐患,所以最好不要这样用

2.6.2 可选链

在调用链中如果有一个可选项,那么这调用链就是可选链,因为可选项可能为nil,那么其结果就有可能为nil,也就必须是个可选项了
代码:

/*
 6、可选链
 */
func test11() {
    class Car {
        var price = 0
    }
    class Dog {
        var weight = 0
    }
    class Person {
        var name: String = ""
        var dog: Dog = Dog()
        var car: Car? = Car()
        func age() -> Int { 18 }
        func eat() { print("Person eat") }
        subscript(index: Int) -> Int { index }
    }
    var person: Person? = Person()//拿到可选项对象
    var age1 = person!.age() // Int 自动解包,nil也会调用age()
    var age2 = person?.age() // Int? 自动解包,nil时直接返回nil
    var name = person?.name // String? 自动解包
    var index = person?[6] // Int? 获取下标,后面会讲到,本质也是一个函数
    
    var dog = person?.dog // Dog?
    var weight = person?.dog.weight // Int? 可选链,有一个为nil就直接返回nil
    var price = person?.car?.price // Int?
}

说明:

  1. 如果前边的可选项为nil,那么该实例不再调用方法、下标、属性,而是直接返回nil
  2. 如果可选项不为nil,该实例调用方法、下标、属性,结果会包装成可选项,这是因为这个变量有可能为nil,所以就只能为可选项了
  3. 如果结果本来就是可选项,将不再包装,实例调用Car会包装可选项,但是这里的Car得到的本来就是一个可选项,所以并不会再次包装了
  4. 可选项调用方法属性等得到的一定是一个可选项,而这个可选项可以再次调用方法属性等,这样形成的一个链就是可选链,这个链中只要有一处为nil,那么它就直接返回nil,不再执行了。

可选链的使用:

代码:

//数组调用
var scores = ["WY":[1,2,3],"wenyi":[4,5,6]]
scores["WY"]?[0] = 10
scores["wenyi"]?[2] += 10
scores["wenyiya"]?[0] = 10
    
//函数调用
var dict: [String : (Int, Int) -> Int] = {
    "sum" : (+),
    "diffenrence" : (-)
}
var result = dict["sum"]?(10,20)
    
//可选项赋值
var num1: Int? = 5
num1? = 10
var num2: Int? = nil
num2? = 10

说明:

  1. 数组调用中,先得到字典中的数组,再获取数组中的值,两次调用形成了可选链,如果传入的key值不存在,那么就会返回nil。而不再会执行[0]了
  2. 函数调用中,拿到sum函数,之后再执行sum函数
  3. (+)和(-)是其实是函数,这里编译器会识别出我们想要进行加法和减法运算
  4. 在可选链中,只要出现了一个可选项,那么最后得出的结果就是可选项,这个很容易理解,因为前一个值得到的是可选项,那么它就有可能是nil,它为nil,那么后面都不会执行,最终会返回nil。因此得到的最终结果就有可能为nil,所以必须是可选项
  5. 可选项赋值中,可以直接赋值给num1,也可以赋值给num1?,如果赋值给num1?,那么就是可选项,这样做有一个好处就是如果num1为nil,那么不会再执行,可以减少无效性能损耗

2.7 总结

注意:

  1. 不允许同时定义参数标签、参数个数、参数类型相同的可失败初始化器和非可失败初始化器,重载与是不是可失败的没有关系
  2. 可失败初始化器可以调用非可失败初始化器,非可失败初始化器调用可失败初始化器需要进行解包
  3. 如果初始化器调用一个可失败初始化器导致初始化失败,那么整个初始化过程都失败,并且之后的代码都停止执行。也就是说只要有一个为nil,编译器就不会执行后边的代码
  4. 可以用一个非可失败初始化器重写一个可失败初始化器,但反过来是不行的
  5. 在swift中没有返回值的函数调用,也是可以接收一个变量的,因为在Swift中Void其实是空元组,并非真正的空
  6. 父类的属性在它自己的初始化器中赋值不会触发属性观察器,但在子类的初始化器中赋值会触发属性观察器

总结:

  1. Swift对于初始化器的安全做了很多措施,包括强制分为指定初始化器和便捷初始化器、两段式初始化、安全检查
  2. 继承关系中所有的初始化器保证都能初始化所有实例
  3. 初始化的过程是从下到上的,实例使用的过程是从上到下的
  4. 同类的两个指定初始化器,是不能互相调的。指定初始化器是纵向的,便捷初始化器是横向的
  5. 必需初始化器强制让子类实现该初始化器,可以通过重写或继承
  6. 可失败初始化器返回的对象是可选项
  7. 可选链中只要有一个nil就返回nil

3、方法

3.1 简单定义

代码:

/*
 1、方法
 */
func test12() {
    //方法定义
    class Car {
        static var cout = 0
        init() {
            Car.cout += 1
        }
        static func getCount() -> Int { cout }
    }
    let c0 = Car()
    let c1 = Car()
    let c2 = Car()
    print(Car.getCount()) // 3
}

说明:

  1. 有类型方法和实例方法,类型方法通过static或class来定义的,如果是结构体或枚举不能用class定义
  2. 实例方法只能使用实例属性,类型方法既可以使用实例属性也可以使用类型属性
  3. 实例方法中的self表示当前实例,类型方法中的self表示当前实例或当前类型

3.2 关键字

3.2.1 mutating

结构体和枚举是值类型,默认情况下,值类型的属性不能被自身的实例方法修改,通过在func关键字前加mutating可以允许这种修改行为(类本身就可以直接进行修改的)
代码:

//关键字mutating
struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(deltaX: Double, deltaY: Double) {
        x += deltaX
        y += deltaY
        // self = Point(x: x + deltaX, y: y + deltaY)
    }
}

说明:

  1. 直接在func关键字前加mutating就可以了

3.2.2 @discardableResult

如果一个函数/方法有返回值,但是我们没有将其接收,调用时会报黄色警告,此时可以在func前面加上@discardableResult就可以消除警告
代码:

//关键字@discardableResult
struct Point2 {
    var x = 0.0, y = 0.0
    @discardableResult   mutating func moveX(deltaX: Double) -> Double {
        x += deltaX
        return x
    }
}
var p = Point2()
p.moveX(deltaX: 10)

4、下标

Swift提供了下标函数,使用subscript可以给任意类型增加下标功能,这里的下标就类似于数组的下标使用,但比数组的下标操作可以更加丰富。我们通过下标函数可以更方便的进行数据操作

4.1 基本使用

代码:

 class Point {
    var x = 0.0, y = 0.0
    //index就是传入的下标
    //在函数内对不同的下标进行不同操作
    //也可以只有get方法
    subscript(index: Int) -> Double {
        set {
            if index == 0 {
                x = newValue
            } else if index == 1 {
                y = newValue
            }
        }
        get {
            if index == 0 {
                return x
            } else if index == 1 {
                return y
            }
            return 0
        }
    }
}
var p = Point()
p[0] = 11.1
p[1] = 22.2
print(p.x) // 11.1
print(p.y) // 22.2
print(p[0]) // 11.1
print(p[1]) // 22.2

//设置参数标签
class Point2 {
    var x = 0.0, y = 0.0
    subscript(index i: Int) -> Double {
        if i == 0 {
            return x
        } else if i == 1 {
            return y
        }
        return 0
    }
}
    
//类方法的下标
class Sum {
    static subscript(v1: Int, v2: Int) -> Int {
        return v1 + v2
    }
}
print(Sum [10, 20]) // 30

说明:

  1. subscript语法类似于方法、计算属性,本质上也就是一个方法
  2. 这个subscript函数可以传入下标,在函数体内又可以通过传入的值来判断进行操作,只不过在调用这个函数时就可以通过下标来调用
  3. 需要通过set方法、get方法来实现,也可以没有set方法,只有get方法
  4. 在这个函数中也可以设置 参数标签,这样在设置下标的时候可以更加具体
  5. 下标方法可以是类型方法

4.2 类和结构体作为下标函数的返回的区别

4.2.1 类作为下标函数返回

代码:

//定义类
class Point {
    var x = 0, y = 0
}
class PointManager {
    var point = Point()
    subscript(index: Int) -> Point {
        get { point }
    }
}
    
var pm = PointManager()
pm[0].x = 11
pm[0].y = 22
// Point(x: 11, y: 22)
print(pm[0])
// Point(x: 11, y: 22)
print(pm.point)

说明:

  1. 如果返回的是class,那么即使只有get方法,也可以直接赋值。因为它是引用类型
  2. pm[0]就拿到了point结构体,此时可以直接赋值给结构体的存储属性

4.2.2 结构体作为下标函数返回

代码:

//定义结构体
struct Point {
    var x = 0, y = 0
}
class PointManager {
    var point = Point()
    subscript(index: Int) -> Point {
        set { point = newValue }
        get { point }
    }
}
    
var pm = PointManager()
pm[0].x = 11
pm[0].y = 22
// Point(x: 11, y: 22)
print(pm[0])
// Point(x: 11, y: 22)
print(

说明:

  1. 如果只有get方法,是不能给pm[0]赋值的
  2. 2、当有set方法时,此时的赋值就和下面的那条等价了

总结:

  1. 对于结构体来说,point作为值类型,pm[0]拿出来的point是拷贝出来的值,所以是无法修改内部的Point值的。所以只能使用set/get方法对内部的point来设置
  2. 对于类来说,在外面进行修改是可以修改下标函数的里对象的值的,所以是可以直接进行修改的

4.3 接收多个参数的下标

注意一下写法就可以,和二维数组一样
代码:

/*
 4、多维下标
 */
func test15() {
    class Grid {
        var data = [
            [0, 1, 2],
            [3, 4, 5],
            [6, 7, 8]
        ]
        subscript(row: Int, column: Int) -> Int {
            set {
                guard row >= 0 && row < 3 && column >= 0 && column < 3 else {
                    return
                }
                data [row][column] = newValue
            }
            get {
                guard row >= 0 && row < 3 && column >= 0 && column < 3 else {
                    return 0
                }
                return data [row][column]
            }
        }
    }
    
    let grid = Grid()
    grid[0, 1] = 77
    grid[1, 2] = 88
    grid[2, 0] = 99
    print(grid.data)
}

5、总结

  1. 属性可以分为存储属性、计算属性、还可以分为类属性和实例属性
  2. 存储属性占用实例内存,可以添加属性观察器,属性观察器本质是函数
  3. 计算属性不占用实例内存,本质是函数
  4. 初始化器分为指定初始化器和便捷初始化器
  5. 对于调用规则的理解,可以通俗的认为指定初始化器是类的默认初始化器,它可以在继承体系中沟通父类子类的属性,因此是纵向的。而便捷初始化器是为了调用时更加方便,因此它必须要调用默认的指定初始化器。
  6. 初始化器用来初始化实例,Swift为了确保初始化的安全提供了两段式初始化过程和安全检查来初始化
  7. 过程为:子类的便捷初始化器-> 子类的指定初始化器 -> 子类属性的初始化 -> 父类属性的初始化 -> 父类实例的使用 -> 子类实例的使用
  8. 方法没有特殊性,只是增加了下标函数

你可能感兴趣的:(Swift基础语法(七)对象)