Swift知识点(二)

Swift闭包与属性详解

6. 闭包表达式与闭包


看看Gemini的说法:

要理解这两个概念,我们得把心态放平:Swift 里的闭包(Closure)其实是一个大概念,而我们平时嘴边挂着的“闭包”,通常指的是这个大概念里的一种特例(即闭包表达式)。

我们把这两个概念拆开揉碎了讲。


1. “函数也是闭包” —— 这是真的吗?

是的。 在 Swift 中,函数(Function)实际上就是一种特殊的闭包。

你可以把“闭包”想象成一个父类,或者一个大的分类。在这个分类下,有三种形态:

形态名字捕获值的能力我们管它叫啥
全局函数有名字 (func xxx)不捕获也就是我们平时的 Function
嵌套函数有名字 (func xxx)能捕获 (捕获外层函数的值)Nested Function
闭包表达式没名字 ({ ... })能捕获 (捕获上下文的值)我们俗称的 Closure

所以,当你定义一个 func add(a: Int, b: Int) 时,你其实定义了一个“有名字、但不捕获任何外部变量”的闭包


2. 什么是“闭包表达式”?

闭包表达式 (Closure Expression) 指的是那一坨代码的写法

它是为了简洁而生的。如果不使用闭包表达式,我们要传一段逻辑给别人,必须得先定义一个函数:

繁琐写法(用函数):

func mySort(a: Int, b: Int) -> Bool {
    return a < b
}
// 传进去的是函数名
array.sorted(by: mySort)

简洁写法(用闭包表达式):
我不想起名字了,直接把逻辑塞进去!

// 这就是闭包表达式
// 格式:{ (参数) -> 返回值 in 代码 }
array.sorted(by: { (a: Int, b: Int) -> Bool in
    return a < b
})

更简洁写法(Swift 语法糖):

array.sorted(by: { $0 < $1 }) // 依然是闭包表达式,极致简化

总结: 闭包表达式就是轻量级、没名字、随写随用的内联代码块。


3. 什么是“捕获”?(闭包的灵魂)

为什么它叫“闭(Closure)”包?
因为它会把周围的变量**“封闭/包裹”**在自己的怀里,带走使用。

这是闭包最难理解、也最强大的地方。

举个栗子:计步器

想象你在写一个生成计步器的工厂函数。

func makeCounter() -> () -> Int {
    var totalSteps = 0 // 这是一个局部变量
    
    // 下面这个 func 是一个“嵌套函数”(也是闭包)
    func step() -> Int {
        totalSteps += 1 // 重点:它捕获了外面的 totalSteps!
        return totalSteps
    }
    
    return step // 返回这个函数
}

// 开始测试
let myCounter = makeCounter()

按照常理,makeCounter() 执行完后,totalSteps 这个局部变量应该被销毁(释放内存),对吧?

但是!
当你调用 myCounter() 时:

print(myCounter()) // 输出 1
print(myCounter()) // 输出 2
print(myCounter()) // 输出 3

它居然还记得 totalSteps 的值!

解释:
这就是闭包(Closure)的含义。
step 函数被返回并赋值给 myCounter 时,它不仅仅是把代码拷过去了,它还自带了一个背包
。它把它用到的外部变量 totalSteps 打包放进了背包里,一起带走了。
即使 makeCounter 函数已经结束了,那个 totalSteps 依然活在 myCounter 的背包里。

需要注意的是:
这个step函数是定义在makeCounter内部的,所以才有此捕获功能。
如果step函数定义在外面,就没有捕获功能了


4. 总结

  1. 函数也是闭包吗?

    • 对。全局函数是“没背包、有名字”的闭包;闭包表达式是“有背包、没名字”的闭包。
  2. 什么是闭包表达式?

    • 就是 { ... in ... } 这种写法。它是为了让你不用专门去起个函数名,直接原地写逻辑。
  3. 核心区别:

    • 普通的 C 语言函数指针只是一段代码的地址。
    • Swift 的闭包 = 代码 + 背包(捕获的环境变量)
此时你的理解应该是:

“哦,原来func是正规军,有名有姓;{}闭包表达式是特种兵,没名没姓,而且特种兵最牛的地方在于,它撤退的时候能把战场上的物资(变量)打包带走,下次还能接着用。”


闭包表达式(Closure Expression)

闭包表达式是一种在简短行内就能写完闭包的语法

也就是,闭包表达式,只是一种简洁、快速实现闭包的语法

Swift 的闭包表达式拥有简洁的风格,鼓励在常见场景中实现简洁,无累赘的语法
常见的优化包括:

  • 利用上下文推断形式参数和返回值的类型;
  • 单表达式的闭包可以隐式返回;
  • 简写实际参数名;
  • 尾随闭包语法。

函数的定义

  • 可以通过func定义一个函数
func sum(_ v1: Int, _ v2: Int) -> Int
{
	v1 + v2
}

可以理解,就是前面说的,函数也是闭包
闭包就是可以捕获上下文中的常量或变量

  • 也可以通过闭包表达式定义一个函数
    闭包表达式有如下的一般形式:
{ (parameters) -> (return type) in
    statements
}
等价于:
{
	(参数列表) -> 返回值类型 in
	函数体代码
}

闭包表达式语法能够使用常量形式参数、变量形式参数和输入输出形式参数,但不能提供默认值
可变形式参数也能使用,但需要在形式参数列表的最后面使用。
元组也可被用来作为形式参数和返回类型。

闭包的函数整体部分由关键字 in 导入,这个关键字表示:
闭包的形式参数类型和返回类型定义已经完成,并且闭包的函数体即将开始。

通常我们说的闭包更多指的是闭包表达式,也就是没有函数名称的代码块,因此也叫做匿名闭包。

var fn = {
	(v1: Int, v2: Int) -> Int in
	return v1 + v2
}
调用:fn(10, 20)

或者
{
	(v1: Int, v2: Int) -> Int in
	return v1 + v2
}(10, 20)
闭包表达式的简写
函数定义:
func exec(v1: Int, v2: Int, fn:(Int, Int) -> Int){
	print(fn(v1, v2))
}

调用方法一:
exec(v1: 10, v2: 20, fn:{
	(v1: Int, v2: Int) -> Int in
	return v1 + v2
})

由于类型可以被推断出来,因此,类型可以不写。
因为类型可以不写,返回值类型不写,那么->也可以省略
简写为:

调用方法二:

exec(v1: 10, v2: 20, fn:{
	v1, v2 in
	return v1 + v2
})

单个表达式闭包能够通过从它们的声明中删掉 return 关键字来隐式返回它们单个表达式的结果

调用方法三:

exec(v1: 10, v2: 20, fn:{
	v1, v2 in
	v1 + v2
})

在闭包里面,可以使用 $0、$1分别代表闭包表达式里面第1个参数和第2个参数,因此,可以简化为:

调用方法四:

exec(v1: 10, v2: 20, fn:{ $0 + $1 })

其实这次是省略了v1, v2 in。里面没有了参数列表,也就不需要in做区分,因此,前面可以省略

更简化的方法:

exec(v1: 10, v2: 20, fn: + )

简直不是人。。。

其他优化例子
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
reversedNames = names.sorted(by: { $0 > $1 } )

这里, $0 和 $1 分别是闭包的第一个和第二个 String 实际参数。

尾随闭包

如果函数的最后一个参数为闭包,则该闭包也被称为尾随闭包
如果你需要将一个很长的闭包表达式作为函数最后一个实际参数传递给函数且闭包表达式很长,使用尾随闭包将增强函数的可读性。
尾随闭包是一个被书写在函数形式参数的括号外面(后面)的闭包表达式,但它仍然是这个函数的实际参数。

  //一个带有尾随闭包的函数
  func someFunctionThatTakesAClosure(closure:() -> Void){
  }
  //函数的调用1,闭包作为参数传进去
  someFunctionThatTakesAClosure({
  })
  //函数的调用2,闭包放在括号外面
  someFunctionThatTakesAClosure() {
  }

还是上面的例子:
可简写为:
reversedNames = names.sorted() { $0 > $1 }
如果闭包表达式作为函数的唯一实际参数传入,而你又使用了尾随闭包的语法,那你就不需要在函数名后边写圆括号了:
reversedNames = names.sorted { $0 > $1 }

真是简写的没法了

闭包(Closure)

定义: 一个函数和它所捕获的变量\常量环境组合起来,称为闭包

函数的定义有两种方式:func和闭包表达式

  • 一个函数,一般指的是:定义在函数内部的函数(嵌套函数)
  • 一般它捕获的是外层函数的局部变量\常量
//定义Fn式一个函数:参数Int,返回值Int
typealias Fn = (Int) -> Int

//定义一个函数:函数名:getFn,参数:空,返回值类型Fn
func getFn() -> Fn{
    //局部变量
    var num = 0
    //内部方法
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    //getFn函数的返回值
    return plus
}//返回的plus和num形成了闭包(也就是plus+num就称为闭包)

var fn = getFn()
print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))
打印结果:
1
3
6
10

按说var fn = getFn()执行完毕后,局部变量num就被释放了。后续fn(1)此时再使用num,应该报错
但,其实是没错的,因为num已经被存储在堆空间了,因此没有被释放

并且,四次调用,访问的都是同一块堆上的内存空间num,因此,num的值被保留下来了(可以连加)

问:为啥num被存储到堆上面呢?

因为,返回出去的函数plus,里面用到了上层函数(getFn)的局部变量,就会分配堆空间,捕获该局部变量

并且,调用一次getFn()函数,就会分配一个单独的堆空间去存储num

是将num的值0,存储在堆空间。栈空间的num=0已经被回收了

var fn1 = getFn()
var fn2 = getFn()
print(fn1(1))
print(fn2(2))
print(fn1(3))
print(fn2(4))
打印:
1
2
4
6

可以把闭包想象成是一个类的实例对象

  • 内存在堆空间
  • 捕获的局部变量\常量就是对象的成员(存储属性)
  • 组成闭包的函数就是类内部定义的方法
//类
class Closure{
    var num = 0
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
}

var cs1 = Closure()
var cs2 = Closure()
print(cs1.plus(1))
print(cs1.plus(2))
print(cs2.plus(3))
print(cs2.plus(4))
打印:
1
3
3
7

闭包能够捕获和存储定义在其上下文中的任何常量和变量的引用,这也就是所谓的闭合并包裹那些常量和变量,因此被称为“闭包”

全局和内嵌函数,实际上是特殊的闭包。
闭包符合如下三种形式中的一种:

  • 全局函数是一个有名字但不会捕获任何值的闭包;(全局函数是一个闭包)
  • 内嵌函数是一个有名字且能从其上层函数捕获值的闭包;(内嵌函数也是一个闭包)
  • 闭包表达式是一个轻量级语法所写的可以捕获其上下文中常量或变量值的没有名字的闭包

闭包的形式有:

在这里插入图片描述

捕获值

一个闭包能够从上下文捕获已被定义的常量和变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍能够在其函数体内引用和修改这些值。

函数和闭包都是引用类型
无论你什么时候赋值一个函数或者闭包给常量或者变量,你实际上都是将常量和变量设置为对函数和闭包的引用

自动闭包

有一个如下的比较函数:如果v1>0,则返回v1,否则返回v2

func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
    return v1 > 0 ? v1 : v2
}

print(getFirstPositive(1, 2))
print(getFirstPositive(-1, 2))
打印:
1
2

当v2传入的是一个函数(参数为空,返回值为Int)的时候,即使v1>0,第二个参数也被调用了一次

func getNumber() -> Int {
    let a = 10
    let b = 11
    print("----")
    return a + b
}
print(getFirstPositive(1, getNumber()))
打印:
----
1

已经判断出v1 > 0了,其实后面的函数没必要执行

为了提高性能,可以将函数getFirstPositive的第二个参数修改为函数:

func getFirstPositive2(_ v1: Int, _ v2: () -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}

print(getFirstPositive2(1, {
    print("1111")
    return 10
}))
print(getFirstPositive2(-1, {
    print("22222")
    return 10
}))
打印:
1
22222
10

可以看出,当v1>0的时候,不会执行第二个参数里面的函数
调用的时候,可以简写调用:

print(getFirstPositive2(10, {20}))
print(getFirstPositive2(-10){20})
打印:
10
20

看起来不易读

因此,swift提供了 自动闭包

只有带 @autoclosure 的,才叫“自动闭包”。

func getFirstPositive3(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}

print(getFirstPositive3(1, 20))
print(getFirstPositive3(-1, 10))
打印:
1
10

如上所示,调用的时候,第二个参数闭包,传入的是一个Int值
其实,内部是将第二个参数20,修改为了{20}

语法糖

关于@autoclosure的注意点:

  • @autoclosure仅支持 () -> T格式的参数(无参,有返回值)
  • @autoclosure并非只支持最后一个参数
  • 空合并运算符 ?? 使用了@autoclosure技术
  • 有@autoclosure与无@autoclosure,也可以构成函数的重载

🎯 终极对比:延迟执行 & 颜值

类型代码长相 (颜值)执行时机 (功能)评价
普通传参func(value)立即执行无法延迟 (一来就干活,浪费性能)
普通闭包func({ value })延迟执行功能强大,但在调用处要写 {},略丑
自动闭包func(value)延迟执行🌟 既要又要:写法像普通传参一样漂亮,功能像闭包一样省性能

自动闭包的语法表现是把花括号去掉了,让调用看起来像传普通参数。 但它的核心本质是延迟求值(Lazy Evaluation)。它把一句表达式自动封装成闭包,这样函数内部就可以决定是否执行、或者何时执行这段代码。比如 Swift 的 ?? 运算符和 assert 就是利用这个特性来避免不必要的计算开销。

逃逸闭包

在 Swift 中,闭包是引用类型,当它被作为一个函数参数传递给一个函数时,这个函数会持有这个闭包的引用。大多数情况下,函数会立即执行闭包并返回,也就是说闭包的生命周期与函数的生命周期一致,我们称这样的闭包为非逃逸闭包。

但有一些情形下,函数在返回之后,闭包仍然被保留而未被执行,例如将闭包储存为函数外部的属性,或者在另外一个异步执行的闭包中被调用,我们称这样的闭包为逃逸闭包。

逃逸闭包必须在闭包参数名前标注 @escaping 关键字。这在使用闭包时会使你更清晰地理解闭包是如何和函数进行交互的。

func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    someArray.append(completionHandler)
}

在 this example example中, completionHandler 闭包参数有 @escaping 注解,意味着这个闭包是可逃逸的。因为闭包被加入到 someArray 数组中,并不会立即被执行,而是会在函数返回之后的某个时刻执行。

逃逸闭包在某些场景下非常有用,比如异步调用,延时调用,还有存储为全局变量或类的实例变量等场景。不过需要注意的一点是,由于逃逸闭包的生命周期可能超过函数本身,可能会引起 strong reference cycle 强引用环,不恰当的使用可能导致内存问题。为了解决这个问题,你需要在闭包中显式地使用 [unowned self] 或者 [weak self] 来避免强引用环。

闭包作为一个实际参数传递给一个函数,并且在函数返回后才被调用的时候,我们就说这个闭包从函数中逃逸了
在 Swift 中,我们需要在参数前加上 @escaping 关键字声明闭包是逃逸的。
如常见的异步操作就需要使用逃逸闭包。


简单一句话概括:
“逃逸”指的就是:这个闭包在函数执行完之后,还没死,逃到了函数外面去活了下来。

1. 直观场景:什么叫“逃跑”了?

我们对比两种情况:

情况 A:不逃逸 (Non-Escaping) —— 默认情况

比喻: 你叫个钟点工来打扫卫生。你盯着他干完活,然后给他钱,让他走人,你也关门睡觉。

  • 特点: 钟点工(闭包)的生命周期包含在你的这次“雇用过程”(函数)里。函数结束,闭包肯定也结束了。
  • 例子: map, filter, 以及我们刚才说的 assert
func doWork(closure: () -> Void) {
    print("1. 开始干活")
    closure() // 马上执行
    print("2. 干完活了")
}
// 函数执行完,closure 也就销毁了,没逃掉。
情况 B:逃逸 (@escaping)

比喻: 你去叫个快递员寄快递。你把东西给他,填个单子,他就走了。你回屋了(函数结束)。但是,送货这件事(闭包) 还没发生,可能过两天他才送达。

  • 特点: 快递员(闭包)逃出了你的视线(函数作用域),在未来的某个时间点才执行。
  • 例子: 网络请求回调、异步任务(GCD)、把闭包存到一个全局数组里。
// 定义一个数组,用来存闭包(这就是逃逸目的地)
var completionHandlers: [() -> Void] = []

// ⚠️ 如果不加 @escaping,这行代码会报错!
func storeClosure(handler: @escaping () -> Void) {
    print("1. 函数要把闭包存起来")
    
    // 关键动作:闭包被存到了函数外面的变量里!
    // 这意味着函数 storeClosure 跑完了,handler 还没死。
    completionHandlers.append(handler) 
    
    print("2. 函数跑完了")
}

2. 为什么要区分这个?(为了内存安全!)

你可能会问:“反正都是闭包,Swift 为什么非要我写个 @escaping 标记一下?这么麻烦。”

这是一个巨大的“警示牌”

对于不逃逸闭包 (Non-Escaping)

编译器知道这个闭包在函数结束前肯定会跑完。

  • 好处: 编译器可以做极致优化(甚至不需要在堆上分配内存),而且不用担心循环引用(因为它马上就死了)。
对于逃逸闭包 (@escaping)

编译器心里慌啊!它想:

“大哥,这个闭包被存起来了,可能过 10 分钟才跑。那时候这个函数所在的 View Controller 可能都已经被用户关掉了(dealloc)!”

所以,当你标记为 @escaping 时,Swift 强迫你做两件事:

  1. 必须显式管理内存:如果闭包里用到了 self,你必须写 self.xxx,或者用 [weak self]
  2. 提醒你检查循环引用:它是在大喊“注意!这里有雷!别让闭包把 self 强引用卡死!”

3. 代码实战:不加 @escaping 会怎样?

如果你尝试在一个需要逃逸的场景下(比如异步),不加这个关键字:

func performAsyncWork(completion: () -> Void) {
    // ❌ 编译报错:
    // "Escaping closure captures non-escaping parameter 'completion'"
    DispatchQueue.main.async {
        completion() 
    }
}

原因: DispatchQueue.main.async 本身就是个异步操作,它会把任务放到以后执行。你的 performAsyncWork 函数马上就返回了,但 completion 还要等着被调用。这不就“逃”了吗?

修正:

// ✅ 加上 @escaping 告诉编译器:别怕,我知道它要逃
func performAsyncWork(completion: @escaping () -> Void) {
    DispatchQueue.main.async {
        completion() 
    }
}

4. 逃逸闭包最大的坑:self 的循环引用

这是面试和实战中 100% 会遇到的问题。

class NetworkManager {
    var onFinish: (() -> Void)? // 用来存逃逸闭包
    
    func startRequest(handler: @escaping () -> Void) {
        self.onFinish = handler // 保存闭包(逃逸了)
    }
}

class ViewController {
    let manager = NetworkManager()
    var name = "Gemini"
    
    func doRequest() {
        // 调用
        manager.startRequest {
            // ⚠️ 危险!
            // 因为是逃逸闭包,它被 manager 存着。
            // 闭包里又引用了 self (ViewController)。
            // 结果:Manager -> 闭包 -> Self -> Manager
            // 形成闭环,内存泄露!
            print(self.name) 
        }
    }
}

Swift 的良苦用心:
如果你在逃逸闭包里不写 self.,编译器会报错。它强迫你写 self.name,就是为了让你看到 self 这个词的时候心里咯噔一下:“我是不是该用 [weak self] ?”

总结

  1. 什么是逃逸?
    闭包没有在函数内当场执行完,而是被存起来或者扔到异步线程里,活得比函数还长

  2. 怎么标识?
    在参数类型前面加 @escaping

  3. 为什么重要?

    • 非逃逸 = 安全、省内存(不用担心循环引用)。
    • 逃逸 = 危险(容易导致循环引用),必须小心处理 self

现在,你再去回顾所有的网络请求库(Alamofire)或者 GCD 的 API,你会发现它们的闭包参数前面全都有 @escaping

swift中闭包与oc中的block有什么区别与相同点?

闭包(Closure)在 Swift 中,与 Objective-C 中的 blocks 形式上有相似处,但在实现、语法以及使用上有很多不同之处。

相似点:

  1. 匿名函数块:都是一种能捕获和存储上下文环境的匿名函数块。可以当作参数在函数之间传递,并且可以在需要的时候调用。

  2. 变量捕获:两者都能在其内部访问和修改外部变量。

区别:

  1. 语法:Swift 的闭包语法更为简洁清晰,通过对参数和返回类型的尾随闭包语法,类型推断等特性,使得闭包的定义及使用变得更为简洁,提高了代码的可读性。

Objective-C 的 block 语法要求声明返回类型,而 Swift 的闭包则不需要声明(但可以声明,这取决于具体用途)。

例如,在 Swift 中,你可以这样声明一个闭包:

let swiftClosure = { (number: Int) -> String in
    return "Swift closure: \(number)"
}

在 Objective-C 中,你需要这样声明一个 block:

NSString *(^objcBlock)(NSInteger) = ^(NSInteger number) {
    return [NSString stringWithFormat:@"Objective-C block: %ld", number];
};
  1. 内存管理:Objective-C 的 block 在捕获外部变量时需要注意 __block 、 __weak 标识符的使用,否则很容易造成循环引用。而 Swift 的闭包提供了 [unowned self] 和 [weak self] 语法来避免循环引用。

  2. 可访问性:Swift 的闭包能访问其作用域内的所有常量和变量,这包括其他函数中的参数和变量,这点在 Objective-C 的 block 中不完全支持。

  3. 可修改可逃逸性:在 Swift 中,闭包默认为不可逃逸,必要时可以使用 @escaping 关键字修改。Objective-C 的 block 默认为可逃逸,而且不能修改。

  4. 运行时处理:Objective-C 的 blocks 在运行时声明,Swift 的闭包在编译时声明。

总结来说,虽然 Swift 中的闭包和 Objective-C 中的 blocks 在面向函数编程的功能上有诸多相似之处,但在语法清晰度、内存管理、作用域访问性、可逃逸性等方面,Swift 的闭包具有较大优势。这些优点使得闭包适应更多的编程场景,提高了代码的可读性和可维护性。


8. 属性

Swift中,跟实例相关的属性可以分为两大类:存储属性、计算属性

存储属性(Stored Property)

类似于成员变量
存储在实例的内存中
结构体、类可以定义存储属性
枚举不可以定义存储属性

在创建结构体或类的实例时,必须为所有的存储属性设置一个合适的初值
可以在初始化器里设置,也可以为属性分配一个默认值

struct Circle{
	//存储属性
	var radius: Double
	//计算属性
	var diameter: Double{
		set{
			radius = newValue / 2
		}
		get{
			radius * 2
		}
	}
}


var circle = Circle(radius: 5)
print(circle.radius)//5
print(circle.diameter)//10
//只需要设置radius的值,就可以通过计算属性中get方法确定diameter的值


circle.diameter = 12
print(circle.radius)//6
print(circle.diameter)//12
//通过修改计算属性diameter的set方法,修改了radius的方法
对存储属性的懒加载

lazy只能用于var变量,不能用于let常量

lazy的两种用法:
方法一:
lazy var str: String = "Hello"

方法二:使用闭包

lazy var names: NSArray = {
    let names = NSArray()
    print("只在首次访问输出")
    return names
}()
问:为何使用闭包可以做到懒加载?
class Person {
    var age: Int = 100
    //首先,lazy不能去掉,去掉了里面就不能写self了
    //因为写lazy,代表不是立马执行的,而是用到的时候才执行
    lazy var getAge: Int = {
        return self.age
    }()//()代表执行
    
    //上面等价于:
    //lazy var getAge: Int = self.age
    
    //重要的是,getAge不是闭包表达式,而是一个Int类型的值(即闭包里面的返回值)
    deinit {print("deinit")}
}

func test(){
    var p = Person()
    print(p.getAge)
}

test()

计算属性(Computed Property)

计算属性不是直接存储值,而是提供get和set方法
get:用来取值,封装取值的过程
set:用来设值,封装设值得过程

计算属性是用来间接修改其他属性值的

本质就是方法(函数)
不占用实例的内存
结构体、枚举、类都可以定义计算属性

上例中,set方法传入的新值默认叫做newValue
也可以自定义:

set(newName){
	radius = newName / 2
}

只读计算属性:只有get,没有set

struct Circle{
	//存储属性
	var radius: Double
	//计算属性
	var diameter: Double{
		get{
			radius * 2
		}
	}
}

可以简写:

struct Circle{
	//存储属性
	var radius: Double
	//计算属性
	var diameter: Double{
		radius * 2
	}
}

定义计算属性只能使用var,不能使用let
原因是,计算属性的值可能会发生变化(即使是只读计算属性)

属性监听器

计算属性本身就有监听,不需要考虑为其添加监听的事
对于存储属性,可以通过willSet和didSet来监听属性的改变
举个例子:
在这里插入图片描述
需要注意的是: willSet和didSet在初始化过程中是不会被调用的
也就是,在上面例子中,第11行初始化的时候不会调用willSet和didSet


9. 方法(Method)

定义在类、结构体、枚举内部的函数,被称为方法
方法又分为:实例方法和类型方法

  • 实例方法(Instance Methods):通过实例调用
  • 类型方法(Class Methods):通过类型调用。用staticclass关键字定义

在类型方法中调用self,self就代表类
在实例方法中调用self,self就代表实例

下标(subscrip)

使用subscrip可以给任意类型(枚举、结构体、类)增加下标功能
subscrip的语法类似实例方法、计算属性,其本质就是方法(函数)
举一个下标的例子:

class Point {
    var x = 0.0, y = 0.0
    //类似方法定义,只是没有func关键字,没有方法名
    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)
print(p.y)
print(p[0])
print(p[1])
打印:
11.1
22.2
11.1
22.2

使用subscrip后,就可以直接对对象像数组一样操作,取p[0]、p[1]了

subscrip定义的返回值类型,决定了get方法的返回值类型,也决定了newValue的值
首先,返回值类型与get出去的类型,肯定是保持一致的。

subscrip可以没有set方法,但是必须有get方法

取值嘛

subscrip如果只有get方法,则get可以省略(语法糖)

上述下标是对象方法
下标也可以写成类方法:

class Sum {
    static subscript(v1: Int, v2: Int) -> Int{
        return v1 + v2
    }
}
//下标访问,使用[],传两个参数,用,隔开即可
print(Sum[10, 20])//30

在 Swift 中,静态下标是一种语法糖,它让我们可以用中括号 [] 访问或设置类型的特定值,而不需要调用一个方法或访问一个属性。

继承(Inheritance)

  • 值类型不支持继承,只有类支持继承

  • 在oc中,NSObject、NSProxy是基类,其余都继承NSObject

  • 在swift中,只要是不继承其他类的,都称为基类

  • swift是单继承

有继承就有重写:

重写

子类可以重写父类的:下标、方法、属性,重写必须调用override
方法重写很正常,OC中常用的
下标的本质就是方法,因此也可以重写
属性,有计算属性,本质也是方法,因此,也可以重写

子类重写父类的方法,当使用子类调用该方法的时候,调用子类的方法
如果在执行子类方法的同时,还想让父类方法也执行,那么,需要调用super.methd()

实例方法、下标重写
class Animal {
    func speak(){
        print("Animal speak")
    }
    
    subscript(index: Int) -> Int {
        return index
    }
}

class Cat: Animal {
    override func speak() {
        super.speak()
        print("Cat speak")
    }
    //加override
    override subscript(index: Int) -> Int {
    	//调用父类的下标调用方法super[index]
        return super[index] + 1
    }
}

//多态:父类类型指向子类类型
//animal虽然是父,但其实子类Cat里面东西多(继承animal的东西+自己的东西)
let animal = Cat()
animal.speak()
print(animal[6])
打印:
Animal speak
Cat speak
7
类方法、下标重写
  • class修饰的类方法、下标,可以重写
  • static修饰的类方法、下标,不允许重写

这也是被class修饰的类方法、下标与被static修饰的类方法、下标的区别

属性重写
  • 子类可以将父类的属性(不管是存储属性,还是计算属性),重写为计算属性
  • 子类不可以将父类重写为存储属性
  • 只能重写var属性,不能重写let属性
  • 重写时,属性名、类型要一致
  • 子类重写后的属性权限 不能小于 父类属性的权限

父类只读 --> 重写后的子类 可以为读写属性
父类读写 --> 重写后的子类 必须为读写属性
不可以将一个继承来的读写属性重写为一个只读属性

存储属性、计算属性都可以重写
属性重写提供了:get、set、willSet、didSet四个关键字对属性进行重写

一个重写计算属性的例子:

import Foundation

class Car {
    var speed:Int {//计算属性
        set {
            print("car-set")
        }
        get {
            return 10
        }
    }
}

class Tank: Car {
    override var speed:Int {//计算属性
        set {
            print("tank-set")
        }
        get {
            return 1
        }
    }
}

let tank = Tank()
print(tank.speed)

结果:1

Tank继承Car,本来car的speed有一个初值10,现在tank的初值变为了1


看完计算属性的重写,我们再看下存储属性的重写:
在这里插入图片描述报错,那么加个override是否就可以了呢?

在这里插入图片描述依然报错
那么,究竟怎么重写父类的存储属性呢???

一个重写存储属性的例子:

import Foundation

class Car {
    var speed:Int = 10
}

class Tank: Car {
    override var speed:Int {
        get {
            return super.speed
        }
        set {
            print("tank - set")
        }
    }
}

let tank = Tank()
print(tank.speed)

结果: 10

从上面可以看出,重写属性,不管是计算属性还是存储属性,都需要借助get、set
也可以得出:存储属性重写为计算属性

例子2:
在这里插入图片描述

以上都是重新对象属性
对于类型属性:

  • class修饰的计算类型属性可以被子类重写

class不允许修饰存储属性,所以,上面只能是计算类型属性

  • static修饰的类型属性(存储、计算),不可以被子类重写
属性观察器

可以在子类中,为父类属性(除了只读计算属性、let属性)增加属性观察器(也就是willSet、didSet)

那么willSet、didSet如何使用呢?

在这里插入图片描述

final

如果方法、属性、下标被final修饰,则不允许被子类重写
如果类被final修饰,则不允许被继承


10. 多态原理、初始化、可选链

多态

let animal = Animal()
//父类指针,指向子类对象,这种称为多态
//animal虽然是父,但其实子类Cat里面东西多(继承animal的东西+自己的东西)
animal = Cat()

struck与class创建的值,调用方法时的区别

从汇编打断点来看:
struct建立的值,调用对应方法的时候,在编译期已经决定调用哪一个方法,是直接调用,调用性能快
class建立的值,调用对应方法的时候,在运行期才决定要调用哪一个方法,是动态调用,调用性能相比struct要弱一点

class对象(堆空间)里面存储的内容:

前8个字节存放:类型信息(类似OC中的isa),其本身是一个指针,指向的是另外一处堆空间,再这个堆空间里面,存有很多类型相关的信息:比如,要调用的方法地址

再8个字节存放:引用计数(retainCount)

剩余的存放:属性信息

遵循8的倍数原则

在这里插入图片描述

var dog1 = Dog()
var dog2 = Dog()

dog1与dog2的前8个字节,内存地址是一样的,也就是类型信息是一样的(只有一份)

从函数调用上,引申出一个问题:

Swift的派发机制

Swift 具有丰富的方法派发机制,这涉及到如何在运行时确定调用哪个方法。主要有以下几种机制:

  1. 直接派发(Direct Dispatch):此类方法调用发生在编译阶段,编译器会直接在调用处"内联"这个函数的实现。这种派发方式最快最高效,因为它避免了函数调用的开销。常见于结构体和枚举的方法派发。

  2. 虚表派发(Table Dispatch,常见于 v-table):这是面向对象语言中最常见的派发方式,Swift 的类方法默认使用这种方式。在这种机制中,如果有一个类 Animal,我们创建了一个子类 Dog 并重写了某个方法,例如 bark() ,在运行时如果子类调用 bark() ,此时会通过虚表找到子类 Dog 中 bark() 方法的地址去调用。

  3. 消息派发(Message Dispatch):在 Swift 中,这主要发生在和 Objective-C 交互的部分,例如使用了 dynamic 关键字或者 @objc 注解的方法。这种机制的弹性最大,但也是最慢的。例如:

@objc dynamic func myMethod() {
    print("This method uses Objective-C message dispatch.")
}

根据这些派发机制,有几个重要的注意点:

  • 尽可能使用直接派发。这是最快的,并且可以让编译器优化你的代码。只有当你需要使用类的多态特性时,才需要其他的派发机制。

  • 虚表派发能够提供面向对象编程的多态性,但是也增加了一定的开销。如果类或方法是 final 的,那么它们就可以使用直接派发,因为这些类和方法不能被重写。

  • 消息派发是最灵活的,但也是最慢的。一般来说,应避免使用,除非你需要和 Objective-C 的运行时进行交互,或使用一些动态特性。

举个具体的例子,让以上的派发机制更加形象化:

struct MyStruct {
    func method() {}  // 直接派发,因为是值类型
}

class MyClass {
    func method() {}  // 默认虚表派发,因为是类
}

final class MyFinalClass {
    func method() {}  // 直接派发,因为类是final
}

class MyDynamicClass {
    @objc dynamic func method() {}  // 消息派发,因为 @objc dynamic
}

初始化器

类、结构体、枚举都可以定义初始化器
有两种初始化器:

  • 指定初始化器(designated initializer)
  • 便捷初始化器(convenience initializer)

其他主要知识点:

  • 每个类至少有一个指定初始化器,指定初始化器是类的主要初始化器
  • 默认初始化器:类的指定初始化器
  • 类偏向于少量指定初始化器,一个类通常只有一个指定初始化器
  • 默认初始化器,指定初始化器。当自定义了带参数的指定初始化器后,编译器生成的不带参的初始化器就没了
  • 便捷初始化器,要最终调用指定初始化器。自定义便捷初始化器后,默认初始化器还在
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值