Swift:面向对象(一)

一、结构体
二、类
三、属性
四、方法


补充
 1、值类型和引用类型的赋值(深拷贝、浅拷贝)
 2、值类型和引用类型的let


一、结构体


1、定义结构体

// 定义结构体
struct Point {
    var x: Int
    var y: Int
}

// 用结构体的构造方法,创建结构体变量
var point = Point(x: 11, y: 12)

切记:定义结构体时,必须保证结构体的每个存储属性都有初始值,当然你可以在定义存储属性时就直接给它们赋初始值,也可以在稍后的构造方法里给它们赋初始值。

2、结构体的构造方法(支持重载)

其实我们每定义一个结构体,编译器就会自动帮我们生成相应的构造方法,至于会生成什么样的构造方法,那要看我们定义的结构体长什么样,但是你只需要记住一个宗旨就行了:结构体的构造方法,必须得保证结构体的每个存储属性都能被赋初始值。

比如我们定义了下面这样一个结构体:

struct Point {
    var x: Int
    var y: Int
}

编译器其实自动帮我们生成了这个结构体的构造方法:

init(x: Int, y: Int) {
    self.x = x
    self.y = y
}

当我们调用Point()函数创建结构体变量时,本质上就是在调用相应的构造方法。

var point = Point(x: 11, y: 11)

又比如我们定义了下面这样一个结构体:

struct Point {
    var x: Int = 11
    var y: Int = 11
}

编译器其实自动帮我们生成了这个结构体的构造方法:

init(x: Int, y: Int) {
    self.x = x
    self.y = y
}

init(x: Int) {
    self.x = x
}

init(y: Int) {
    self.y = y
}

init() {
    
}

当我们调用Point()函数创建结构体变量时,本质上就是在调用相应的构造方法。

var point1 = Point(x: 11, y: 12)
var point2 = Point(x: 13)
var point3 = Point(y: 14)
var point4 = Point()

当然我们也可以自定义结构体的构造方法,但是需要注意的是:一旦我们自定义了结构体的构造方法,编译器就不会自动帮我们生成构造方法了,一个也不会。

比如我们定义下面这样一个结构体,并且自定义了构造方法:

struct Point {
    var x: Int
    var y: Int
    
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

那么我们就只能调用如下Point()函数创建结构体变量,因为其它的构造方法都没有了。

var point = Point(x: 11, y: 11)

3、结构体变量的内存大小、内存布局

结构体变量的内存大小,和我们平常那样分析是一致的:成员的绝对大小、内存对齐导致的实际大小、分配时对齐参数导致的分配大小。

struct Point {
    var x: Int
    var y: Int
}

func test() {
    var point = Point(x: 11, y: 11) // 栈区变量
}

print(MemoryLayout<Point>.size) // 16
print(MemoryLayout<Point>.stride) // 16
print(MemoryLayout<Point>.alignment) // 8

结构体变量在不同的地方创建就对应着不同内存区域的一块连续内存。比如结构体变量是在函数内部创建的,它就对应着栈区的一块连续内存;在函数外部创建的,它就对应着全局区的一块连续内存;作为对象的一个属性创建的,它就对应着堆区的一块连续内存。上面的例子中,我们是定义了一个栈区的结构体变量,因此它的内部布局大概如下:


二、类


1、定义类

// 定义类
class Size {
    var width: Int
    var height: Int
    
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
}

// 用类的构造方法,创建引用变量和对象
var size = Size(width: 11, height: 11)

切记:定义类时,必须保证类的每个存储属性都有初始值,当然你可以在定义存储属性时就直接给它们赋初始值,也可以在稍后的构造方法里给它们赋初始值。

2、类的构造方法(支持重载)

和结构体不同的是,编译器不会轻易给类自动生成相应的构造方法,只有一种情况会生成,那就是我们在定义类的时候就给每个存储属性赋了初始值,编译器才会为我们自动生成一个不带参数的构造方法,像下面这样:

class Size {
    var width: Int = 11
    var height: Int = 11
}

var size = Size()

所以大多数情况下都是我们自定义类的构造方法,自定义构造方法的核心宗旨和结构体还是一样的:类的构造方法,必须得保证类的每个存储属性都能被赋初始值。同样的,一旦我们自定义了类的构造方法,编译器就不会自动帮我们生成构造方法了,一个也不会。

class Size {
    var width: Int
    var height: Int
    
    // 主要构造方法(又叫全能构造方法),要确保每个存储属性都能被赋初始值
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
    
    // 便捷构造方法(其实便捷构造方法可以通过给全能构造方法的参数设置默认值来替代)
    // 1、提供多个更加灵活的构造方法,供外界选择
    // 2、之所以选择定义便捷构造方法,而不是定义多个主要构造方法,是因为多个主要构造方法里可能有重复的代码,这样写更优雅
    // 3、强制你调用一下当前类的主要构造方法,复用主要构造方法里代码的同时,也确保了每个存储属性都能被赋初始值
    convenience init(width: Int) {
        // 内部必须得调用一下当前类的主要构造方法
        self.init(width: width, height: 0)
    }

    convenience init(height: Int) {
        self.init(width: 0, height: height)
    }

    convenience init() {
        self.init(width: 0, height: 0)
    }
}

var size = Size(width: 11, height: 11)

如果存在继承关系时,子类的构造方法里必须手动调用一下父类的构造方法,以确保从父类继承而来的存储属性能被正常赋初始值。

class Animal {
    var age: Int
    
    init(age: Int) {
        self.age = age
    }
}

class Dog: Animal {
    var weight: Int
    
    init(age: Int, weight: Int) {
        self.weight = weight
        
        super.init(age: age) // 手动调用一下父类的构造方法,不过要先初始化完自己的,再调用
    }
}

当对象销毁时,会触发类的deinit方法,我们称之为反构造方法,类似于OC的dealloc方法、C++的析构函数。

class Size {
    deinit {
        print("Size对象销毁了")
    }
}

3、对象的内存大小、内存布局

像结构体那样正常分析没有错,只不过对象的话,还得在前面加上类型信息的8个字节和引用计数的8个字节。

然后macOS、iOS下对象的内存大小总是16的倍数,不仅OC对象是这样哦,Swift对象也是这样,因为对象都是通过malloc函数去分配内存的,那个函数里就做了这样的强制规定。

class Size {
    var width: Int
    var height: Int
    
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
}

func test() {
    var size = Size(width: 11, height: 11) // 栈区变量,堆区对象
    
    print("Size对象:", Mems.size(ofRef: size)) // Size对象: 32
}
test()

print("size变量:", MemoryLayout<Size>.size) // size变量: 8
print("size变量:", MemoryLayout<Size>.stride) // size变量: 8
print("size变量:", MemoryLayout<Size>.alignment) // size变量: 8

因此上面例子中,Size对象的内存布局大概如下:


三、属性


1、实例属性

  • 存储属性

存储属性类似于成员变量这个概念,它的值存储在实例的内存中。结构体和类可以定义存储属性,枚举不可以定义存储属性。

  • 计算属性

计算属性的本质其实就是一对儿settergetter方法(当然我们可以选择只实现getter方法,也可以选择同时实现settergetter方法),它的值不存储在实例的内存中。枚举、结构体和类都可以定义计算属性。

举个例子:

struct Circle {
    // 存储属性,定义属性时必须声明数据类型
    var radius: Double
    
    // 计算属性,定义属性时必须声明数据类型
    var diameter: Double {
        set(diameter) { // setter方法
            radius = diameter / 2
        }

        get { // getter方法
            return radius * 2
        }
    }
}

var circle = Circle(radius: 11)
print(circle.radius) // 11.0
print(circle.diameter) // 22.0

circle.diameter = 12
print(circle.radius) // 6.0
print(circle.diameter) // 12.0

上面我们说“计算属性的本质其实就是一对儿settergetter方法”,所以计算属性diameter其实就等价于我们在结构体里面自己定义了这么两个方法:

func setDiameter(diameter: Double) {
    radius = diameter / 2
}

func getDiameter() -> Double {
    return radius * 2
}
  • 延迟存储属性:懒加载

我们可以用lazy关键字(懒加载)来定义一个延迟存储属性,延迟存储属性是指这个属性只有第一次被用到时才会初始化。

不使用lazy时,即便我们仅仅是用到Person对象,而没有用到它的属性——Car对象,那在创建Person对象时,Car对象也会被初始化。因为Swift要求在创建结构体或类的实例时,它的每个存储属性都必须得到初始化。

class Car {
    init() {
        print("Car init")
    }
    
    func run() {
        print("Car run")
    }
}

class Person {
    var car = Car()
    
    init() {
        print("Person init")
    }
    
    func travel() {
        self.car.run()
    }
}

// 创建Person对象
var person = Person()


// 控制台打印:
Car init
Person init

而如果我们使用了lazyCar对象在没有用到时就不会被初始化。

class Car {
    init() {
        print("Car init")
    }
    
    func run() {
        print("Car run")
    }
}

class Person {
    // 延迟存储属性
    lazy var car = Car()
    
    init() {
        print("Person init")
    }
    
    func travel() {
        self.car.run()
    }
}

var person = Person()


// 控制台打印:
Person init

只有它真正被用到时才会初始化。

var person = Person()
person.travel()


// 控制台打印:
Person init
Car init
Car run

重要重要重要!!!

实际开发中,我们在给一个结构体或者类定义属性时,一般有三种情况(类比OC):

  • 对于Bool、Int、Float、Double、String、枚举这种基本类型的数据,我们一般是在定义属性的时候就初始化它们,但是不写懒加载;
  • 对于Array、Dictionary、肯定存在的控件类的属性,我们一般是在定义属性的时候就初始化它们,但是要配合懒加载lazy
  • 对于那种有可能存在有可能不存在的控件,定义为可选项。例如新浪微博的首页的tableView和visitorView都应该定义成可选项,因为tableView只有在登录状态下才存在,而visitorView只有在非登录状态下才存在;
  • 对于闭包回调类的属性,因为没办法对它们进行初始化,而是在真正实现的时候才赋值给它们,所以定义为可选项;

2、类型属性

Swift除了可以定义实例属性外,还可以通过static关键字定义类型属性,即通过类来调用的属性。

  • 存储属性

类型存储属性其实就是个全局变量,存储在全局区,程序运行过程中只有1份内存,只不过普通全局变量直接通过变量名就可以访问,而它则需要通过特定的类型来访问,所以可以看作是加了一个访问权限。枚举、结构体和类都可以定义类型存储属性。

  • 计算属性

类型计算属性的本质也是一对儿settergetter方法。枚举、结构体和类都可以定义类型计算属性。

举个例子:

struct Circle {
    static var radius: Double = 11
    static var diameter: Double {
        set(diameter) {
            radius = diameter / 2
        }
        get {
            return radius * 2
        }
    }
}

print(Circle.radius) // 11.0
print(Circle.diameter) // 22.0

Circle.diameter = 12
print(Circle.radius) // 6.0
print(Circle.diameter) // 12.0
  • 延迟存储属性

类型存储属性默认就是lazy(懒加载)的。

3、属性观察器

属性观察器用来监听一个属性是否被修改,修改了就会触发相应的回调,类似于OC的KVO。但是属性观察器只能应用在非lazy的、var的、存储属性身上,这三个条件之外的其它属性都不能使用。

struct Circle {
    var radius: Double = 11 { // 这里radius还是个存储属性啊,只不过设置了属性观察器
        willSet {
            print("willSet", newValue)
        }
        
        didSet {
            print("didSet", oldValue, self.radius)
        }
    }
}

var circle = Circle()
// 触发willSet:willSet 12.0
// 触发didSet:didSet 11.0 12.0
circle.radius = 12


四、方法


枚举、结构体和类里面都可以定义函数,我们把定义在它们内部的函数称为方法。

enum Direction {
    case east
    case west
    case south
    case north
    
    func show() {
        print("direction is \(self)")
    }
}

var direction = Direction.east
direction.show() // direction is east
struct Point {
    var x: Int
    var y: Int
    
    func show() {
        print("x = \(self.x), y = \(self.y)")
    }
}

var point = Point(x: 11, y: 12)
point.show() // x = 11, y = 12
class Size {
    var width: Int
    var height: Int
    
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
    
    func show() {
        print("width = \(self.width), height = \(self.height)")
    }
}

var size = Size(width: 11, height: 12)
size.show() // width = 11, height = 12

方法当然也分为实例方法和类型方法:

class Car {
    static var count = 0
    
    init() {
        Car.count += 1
    }
    
    // 实例方法
    func run() {
        print("Car run")
    }
    
    // 类型方法
    static func getCount() {
        print("Car count:", self.count)
    }
}

var car1 = Car()
var car2 = Car()
var car3 = Car()

car1.run() // Car run
Car.getCount() // Car count: 3

枚举和结构体的实例方法内部,不允许修改它们自己的属性,如果想要修改需要加上mutating关键字。

enum StateSwitch {
    case low
    case middle
    case high
    
    mutating func next() {
        switch self {
        case .low:
            self = .middle
        case .middle:
            self = .high
        case .high:
            self = .low
        }
    }
}

struct Point {
    var x: Double = 0
    var y: Double = 0
    
    mutating func moveBy(deltaX: Double, deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}


补充


1、值类型和引用类型的赋值(深拷贝、浅拷贝)

把值类型的变量赋值给另一个letvar变量或函数的参数,是值拷贝、深拷贝。

struct Point {
    var x: Int
    var y: Int
}

var point1 = Point(x: 11, y: 12)
var point2 = point1 // 值拷贝、深拷贝

point2.x = 13
point2.y = 14

print(point1) // Point(x: 11, y: 12)
print(point2) // Point(x: 13, y: 14)

把引用类型的变量赋值给另一个letvar变量或函数的参数,是引用拷贝、浅拷贝。

class Size {
    var width: Int
    var height: Int
    
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
}

var size1 = Size(width: 11, height: 12)
var size2 = size1 // 引用拷贝、浅拷贝

size2.width = 13
size2.height = 14

print(size1.width, size1.height) // 13 14
print(size2.width, size2.height) // 13 14

2、值类型和引用类型的let

我们定义一个常量,是指该常量对应的那块内存里的数据不能被修改。

struct Point {
    var x: Int
    var y: Int
}

let point = Point(x: 11, y: 12)
point = Point(x: 13, y: 14) // 报错
point.x = 13 // 报错
point.y = 14 // 报错
class Size {
    var width: Int
    var height: Int
    
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
}

let size = Size(width: 11, height: 12)
size = Size(width: 13, height: 14) // 报错
size.width = 13 // 不报错
size.height = 14 // 不报错

推荐阅读更多精彩内容