【Swift】:面向对象(一)

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


一、结构体


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、强制你调用一下当前类的主要构造方法,复用主要构造方法里代码的同时,也确保了每个存储属性都能被赋初始值
    // 便利构造方法通常用在对系统的类进行构造方法的扩展这种场景下,也就是一般都出现在extension里,便利构造方法需要在init前面加个convenience关键字,并且方法里需要显性地调用一下self.init
    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、对象属性

  • 存储属性

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

  • 计算属性

计算属性的本质其实就是一对setget方法(我们可以只实现get方法——只读,也可以同时实现setget方法——读写),它的值不存储在对象的内存中。枚举、结构体和类都可以定义计算属性。

举个例子:

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

        get { // get方法
            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

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

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

func getDiameter() -> Double {
    return radius * 2
}

2、类属性

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

  • 存储属性

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

  • 计算属性

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

举个例子:

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

3、lazy + 闭包和属性观察器

  • lazy + 闭包

lazy + 闭包类似于OC里的getter方法(懒加载),用它们定义的属性只有在第一次被用到时才会初始化。lazy + 闭包只能应用在var的、存储属性身上。

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

class Car {
    var name: String?
    var price: Double?
    
    init() {
        print("Car init")
    }
    
    func run() {
        print("Car run")
    }
}

class Person {
    // 不使用lazy
    var car: Car = {
        let car = Car()
        car.name = "奔驰"
        car.price = 35
        return car
    }()
    
    init() {
        print("Person init")
    }
    
    func travel() {
        self.car.run()
    }
}

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


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

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

class Car {
    var name: String?
    var price: Double?
    
    init() {
        print("Car init")
    }
    
    func run() {
        print("Car run")
    }
}

class Person {
    // 使用lazy
    lazy var car: Car = {
        let car = Car()
        car.name = "奔驰"
        car.price = 35
        return car
    }()
    
    init() {
        print("Person init")
    }
    
    func travel() {
        self.car.run()
    }
}

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


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

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

var person = Person()
person.travel()


// 控制台打印:
Person init
Car init
Car run
  • 属性观察器

属性观察器类似于OC里的setter方法,用来监听一个属性被修改掉,我们用新的数据去刷新UI这样的场景。属性观察器只能应用在非lazy的、var的、存储属性身上,这三个条件之外的其它属性都不能使用。

struct Circle {
    var radius: Double = 11 { // 这里radius还是个存储属性啊,只不过设置了属性观察器
        willSet {
            print("即将发生变化", "当前是\(radius)", "即将是\(newValue)") // 即将发生变化 当前是11.0 即将是12.0
        }
        
        // 实际中主要用didSet方法
        didSet {
            print("已经发生变化", "当前是\(radius)", "曾经是\(oldValue)") // 已经发生变化 当前是12.0 曾经是11.0
        }
    }
}

var circle = Circle()
circle.radius = 12

tips:

1️⃣计算属性、lazy+ 闭包和属性观察器的样子都是在属性后面有个大括号{},那怎么区分它们呢?其实在实际开发中,计算属性总是在属性后面直接就跟上{}了,而lazy+ 闭包和属性观察器一般都会在属性后面有个=,然后才跟上{}

2️⃣Swift里定义单例非常简单,就是一个类属性,只需要一句话:

class SingletonUtil {
    // 一句话定义单例,并且是线程安全的
    static let shared = SingletonUtil()
}


四、方法


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

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


补充


Swift里其实只有两种数据类型:值类型和引用类型,值类型的传递肯定就是深拷贝,引用类型的传递肯定就是浅拷贝。

把值类型的变量赋值给另一个let常量、var变量或函数的参数,是值传递、深拷贝。

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)

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

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容