Swift知识点
简介
记录一下在swift学习过程中的知识点,方便查找、阅读、记忆,比较零碎
Swift与OC相比的优缺点
Swift相对OC有以下优势:
1.更加安全的语言;
OC(Objective-C)是一种动态语言,它的类型检查和错误检查都是在运行时完成的。这意味着在编译时,编译器无法检查类型错误和其他常见的编程错误。这样就可能导致运行时错误,从而影响应用程序的稳定性和安全性。
Swift是一种静态类型语言,这意味着编译器可以在编译时检查类型错误和其他常见的编程错误。这样可以减少运行时错误,从而提高应用程序的稳定性和安全性。此外,Swift还提供了一些安全性功能,例如可选类型、空类型和错误处理等,这些功能可以帮助开发人员更好地处理可能出现的错误和异常情况。
2.更加简洁的语法;
Swift的语法相对简洁,使用起来更加方便
3.更加高效的性能;
Objective-C的性能相对较低,这是由于它是一种动态语言,需要在运行时进行类型检查和消息传递。这些操作会导致额外的开销,并影响应用程序的性能。
Swift的性能相对较高,这是由于它是一种静态类型语言,可以在编译时优化代码。此外,Swift还提供了一些性能优化功能,例如内联函数和高级优化等,可以进一步提高应用程序的性能。
4.更加强大的特性。
Swift具有许多Objective-C所不具备的特性,例如结构体、泛型和函数式编程等。这些特性可以帮助开发人员更好地组织和管理代码,并提高应用程序的可重用性和可维护性。
Swift还具有许多其他特性,例如闭包、元组、类型推断和扩展等。这些特性可以帮助开发人员更加轻松地编写代码,并提高应用程序的质量和性能。
Swift编程语言有什么缺点
-
编译时间较长
Swift是一种静态类型语言,需要进行类型检查和编译优化,因此编译时间相对较长。对于大型项目来说,编译时间可能会很长,这会影响开发人员的效率和生产力。 -
不支持动态库
Swift不支持动态库,这意味着在使用Swift编写的库时,需要将代码静态链接到应用程序中。这可能会导致一些问题,例如增加应用程序的大小和内存使用量,以及降低代码的可重用性和可维护性。
1. 基础语法
main函数
Swift不用编写main函数,Swift将全局范围内的首句可执行代码作为程序入口
但,其实Swift是有代码入口的,是在:
AppDelegate上面有个默认的@UIApplicationMain标记,当做程序的入口,并且自动设置AppDelegate为App的代理
或者也可以自定义main函数入口,操作:
新建一个main.swift文件
自定义Application
代码尾部可以省略;
多句代码写到同一行,还是得用;
分开
常量、变量
let(let: 允许)修饰常量
var(variable:可变的)修饰变量
var a = 10
编译器会自动推算出a的类型为Int
带参数的打印:
var a = 10
print("ssss --- \(a)")
- 常量、变量在初始化之前,都不能使用
- let修饰的常量不要求在编译期确定值,但使用前必须赋值一次,且只能赋一次
标识符
标识符(比如:常量名、变量名、函数名)几乎可以使用任何字符
标识符不能以数字开头,不能包含:空白字符、制表符、箭头等特殊字符
数据结构分为:数据类型、函数、继承、协议、扩展
常见数据类型
数据类型分为:值类型(value type)、引用类型(reference type)
-
值类型分为:枚举(enum)、结构体(struct)
其中,结构体包括:Bool、Int、Float、Double、Character、String、Array、Dictionary、Set -
引用类型:类(class)
查看Swift中的Int定义:
会发现,public struct Int
,确实是struct类型
问:OC中的基本数据类型(比如int)为何在swift中被定义为结构体?
这是因为,封装成struct后,里面会封装一些方法,供原来的基本类型去使用,便于操作
而有关被定义成struct,也并非一定比基础数据类型所占用的空间大,其内部也做了响应的优化
比如可直接使用:
print("sss\(Int32.min)")
print("sss\(Int32.max)")
sss-2147483648
sss2147483647
/// On 32-bit platforms, `Int` is the same size as `Int32`, and
/// on 64-bit platforms, `Int` is the same size as `Int64`.
根据这张图显示,在Swift中,Int32的大小,跟基础数据类型所占大小一样,都是4字节
元组(tuple)
let http404Error = (404, "Not Found")
http404Error.0
http404Error.1
结果:
404
Not Found
这就是一个元组,以及调用元组内容
元组的作用是:
可以存放不同的类型
可以使得函数同时返回多个值(直接返回元组)
2. 流程控制和函数
if-else
if后面可以省略小括号
if后面只能放Bool类型
while
repeat-while
就相当于do-while
swift去掉了自增++,自减- -运算符
for
闭区间运算符:a…b 等价于 a <= x <= b
半开区间运算符:a…<b 等价于 a <= x < b
switch
- switch中,不仅可以比较整数,也可以比较字符串
- switch中,每一个case后面必须有可执行的语句
- switch中,不要求每个case语句都加break,没有break也不会穿透
- switch中,如果想穿透,使用fallthrough
- switch中,如果想多个case执行同一个代码,可以使用
case 1, 2, 3:
- switch中,case不仅可以是具体的值,还可以是某一个范围
- switch中,case不仅可以是范围,还可以是元组
- switch中,必须保证所有情况都被处理。也就是要不有default,如果没* 有default那么case可以保证所有值都处理了。简单点default必写
函数
函数中,void等价于空元组:void === ()
函数的形式参数名和实际参数标签
又称为:局部参数名和外部参数名
形式参数名 等价于 局部参数名
实际参数名 等价于 外部参数名
形参
形式参数名:定义函数的时候,用到的参数名
即:函数的实现内部使用
func sample(number: Int) {
println(number)
}
实参
实际参数标签:调用函数的时候,里面用到参数名
一般局部参数名前指定外部参数名,中间以空格分隔,外部参数名用于在函数调用时传递给函数的参数。
//firstArg、secondArg 实参;a、b形参
func pow(firstArg a: Int, secondArg b: Int) -> Int {
}
默认情况下,实际参数标签不写的话,就使用形式参数名作为实际参数标签
如果你为一个形式参数提供了实际参数标签,那么这个实际参数就必须在调用函数的时候使用标签。
如果对于函数的形式参数不想使用实际参数标签的话,可以利用下划线( _ )来为这个形式参数代替显式的实际参数标签。
注意
如果你提供了外部参数名,那么函数在被调用时,必须使用外部参数名。
带默认值的参数
在参数后面赋值,则是带默认值的参数,在传值的时候可以不传
形参后面,如果有默认值,则在调用的时候可以省略不传
inout
inout:修改外部参数
inout修饰的参数,不可以有默认值
inout的本质即是地址传递
inout参数只能传入可以被多次赋值的
var number = 10
changeNumber(numbers: &number)
print(number)
func changeNumber(numbers: inout Int)
{
numbers = 100
print(numbers)
}
函数可以作为参数、返回值
函数作为返回值
写法是在函数的返回箭头( ->)后立即写一个完整的函数类型
可变参数:(入参参数个数不确定)
- 一个函数最多只有一个可变参数
- 紧跟在可变参数后面的参数不能省略参数标签(即,可变参数可以不是最后一个参数)
func sum(_ numbers: Int...) -> Int
{
var total = 0
for number in numbers{
total += number
}
return total
}
print(sum(1,2,3,4,5,6))
函数重载(Function Overload)
函数名相同
参数个数不同 || 参数类型不同 || 参数标签不同
返回值与函数重载无关(只有返回值不同,不是函数重载)
函数重写
overload,继承
内联函数(Inline Function)
功能:将函数展开成函数体
效果:由于函数开辟、回收等操作,会有时间、空间的影响,因此,如果直接将函数展开成函数体,即(直接执行函数体,没有函数),则会节省程序运行的时间、空间
如果开启了编译器优化(Release模式默认会开启优化),编译器会自动将某些函数变成内联函数
问:开启编译器优化后,所以函数都会变成内联函数吗?
答:否
编译器会区分,如果函数体里面包含以下内容,则不会被转变为内联函数:
- 比较多的内容
- 包含递归调用
- 包含动态派发(OC中的动态绑定)
举个简单的例子:父类、子类有同一个方法
test()
,则var p:Person = Student()
那么,此时调用p.test()
,这种情况就属于动态派发(只有执行的时候才知道调用的哪一个函数)
never
//永远不会被内联(即使开启了编译器优化)
@inline(never) func test()
{
print("test")
}
__always
//开启编码器优化后,即使代码很长,也会被内联(递归调用函数、动态派发的函数除外)
@inline(__always) func test2()
{
print("test")
}
3. 枚举、可选项
枚举不仅可以放相同类型,也可以放不同类型
比如,成绩,既可以是Int数值类型,也可以是字符A\B\C等级类型
即,关联值(Associated Values) :枚举的成员值跟其他类型关联存储在一起
enum Score {
case point(Int)
case grade(Character)
}
var score = Score.point(98)
print("111- \(score)");
score = .grade("A")
print("222- \(score)");
或者:
enum Date {
case digit(year: Int, month: Int, day: Int)
case string(String)
}
var date = Date.digit(year: 2024, month: 03, day: 02)
date = .string("2024-03-02")
原始值(Raw Values)
枚举成员可以使用相同类型的默认值预先关联,这个默认值叫做:原始值
//冒号并不是继承,而是代表原始值的类型
enum Grade: String{
case perfect = "A"
case great = "B"
case good = "C"
case bad = "D"
}
如果枚举的原始值类型是string或者Int,那么swift会给它自动分配原始值
string类型的原始值,就是它自己本身的字符串
Int类型的原始值,从0开始
如果Int类型有指定值(比如100),那么后面没有赋值的值会进行+1赋值操作
递归枚举
即,枚举A里面的元素,里面用到了枚举A类型
此时,需要在枚举前面加上indirect修饰,或者在枚举元素前加indirect修饰,不然报错
//必须加indirect,不然编译不通过
indirect enum ArithExpr {
case number(Int)
case sum(ArithExpr, ArithExpr)
case difference(ArithExpr, ArithExpr)
}
或者
enum ArithExpr {
case number(Int)
indirect case sum(ArithExpr, ArithExpr)
indirect case difference(ArithExpr, ArithExpr)
}
原始值是不会存储到枚举对象里面(结果就是,一个原始值,则枚举对象占用内存大小一直是1)
关联值会将其存储到枚举对象里面(结果就是,有4个int类型的关联值,则枚举对象占用内存大小为4*8)
可选类型
可选类型的使用场合:当一个值可能存在,也可能不存在的时候,就使用可选类型
Int与Int?是两种不同的类型
Int?是对Int的 一层包装
Int?里面包含Int和nil
nil只能用于可选类型
var num: Int? = nil ✅
var num: Int? = 10 ✅
var numValue: Int = num ❎,将大值赋值给小值
var b: Int
print("bbb --- \(b)")
上面代码书写有问题,会编译不过去报错
var b: Int
不等价于
var b: Int = 0
而
var b: Int?
等价于
var b: Int? = nil
var optionalInteger: Int?
//<>泛型,声明了一个类型为可选 Int 的变量 optionalInteger,它的值可以是 Int 类型的一个数,或者是 nil ,表示缺失值。
var optionalInteger: Optional<Int>
这两种写法是一样的,即Int? === Optional<Int>
前面的这个问号,其实就是 Optional<Type>
的简写
为什么要有可选类型?
在没有可选类型的语言中,缺失的值通常由 null
或 nil
表示。这种方法有个问题:任何对象都可以赋值为 null
,这可能导致运行时错误,因为你的代码可能尝试访问一个 null
对象的方法或属性。Swift 的可选类型解决了这个问题,它使得你可以明确地表达一个值是有还是无,从而使代码更安全、更清晰。
优势
- 安全性:可选类型强制你处理值缺失的情况,减少了
nil
检查的遗漏,这样可以避免运行时错误。 - 清晰性:通过显式地标记那些可能为
nil
的地方,代码的可读性和维护性得到了提升。 - 集成:Swift 的标准库和编译器对可选类型提供了强大的支持,如可选绑定(optional binding)、可选链(optional chaining)和空合运算符(nil-coalescing operator),这些都使得与可选类型的交互更加便捷。
底层实现原理
在底层,可选类型是使用泛型枚举实现的。Optional
是一个泛型枚举,有两个成员:none
和 some(Wrapped)
,其中 Wrapped
表示包装的实际值的类型。none
成员表示没有值,相当于其他语言中的 null
或 nil
。some(Wrapped)
成员持有实际的值。
enum Optional<Wrapped> {
case none
case some(Wrapped)
}
这种实现方式使得 Swift 能够严格控制可选类型的使用,只有显式解包(例如,使用 !
或者通过可选绑定等安全方式)才能访问其值,从而确保了代码的安全性。
var optionalInt: Int? = 10
这里的 Int?
是 Optional<Int>
的简写,表示“这是一个 Int
类型,但它可能没有值”。
强制解包
在可选类型的后面加个!,就可以把可选类型(包装)的值取出来,赋值给具体类型
注意:如果可选类型(包装)的值不存在,仍然进行强制解包,会报错
if语句可以判断可选类型是否有值
可选绑定
import Foundation
if let num = Int("123") {
print(num)
}else {
print("no numValue")
}
可选绑定num也可以使用var
if或者while的判断条件如果有多个,并且里面有可选绑定,则多个判断条件中间使用", “分开,而不是使用”&&"
举个例子:
//遍历数组,将遇到的正数都加起来,如果遇到负数或者非数字,则停止遍历
var array = ["12", "21", "-20", "abc"]
var index = 0
var sum = 0
//中间多个判断项是用,拼接起来的
while let num = Int(array[index]), num>0 {
sum += num
index += 1
}
print(sum)
空合并运算符 ??
a ?? b
a 是可选项
b 可选项或非可选项
b 跟 a的存储类型必须相同
作用:
如果a 不为nil,则返回a
如果a为nil,则返回b
如果b不是可选项,返回a时,会自动解包
a ?? b
等价于
if(a != nil){
if(b是可选项){
return a//可选项
}else{//b不是可选项
return a!//解包后的值
}
}else{
return b
}
guard语句
guard 条件 else{
//do something
退出当前作用域//return、break、continue、throw error
}
- 当guard语句的条件为true的时候,跳过guard语句,执行后面代码
- 当guard语句的条件为false的时候,就会执行大括号里面的代码
5. 结构体和类
结构体
大部分的自定义数据结构都是类,比如Bool、Int、Double、String、Array、Dictionary等常见类型
- 所有的结构体,都有一个编译器自动生成的初始化器(initializer,初始化方法、构造器、构造方法)
结构体的初始化目的是:保证结构体里面所有的存储属性都有初始值
一旦在定义结构体时,自定义了初始化器,编译器就不再自动生成其他初始化器
结构体自定义初始化器 -> 不再自动生成初始化器
类
编译器没有为类自动生成可以传入成员值的初始化器
编译器会自动生成 无参的初始化器
这也就要求,class在定义的时候必须有初始化值,而struct定义的时候不需要(因为可以在初始化的时候再赋值)
- 成员(x, y)的初始化是在初始化器中完成的
结构体和类的相同点和不同点
如图所示
结构体属于值类型,则结构体里面的成员属性是存储在栈区域的
类属于引用类型,则至于size本身存储在栈空间,class里面的成员属性存储在堆空间
并且,结构体里面没有额外数据。而类里面除了原有的属性,还有其他数据:指向类型信息、引用计数
结构体和类都有的功能:
定义属性、方法、下标
定义初始化器
扩展extension、协议protocol
类有,而结构体没有的:
类有继承,结构体没有继承
反初始化器
引用计数(这个肯定是了)
-
String、Array、Dictionary虽然被定义为结构体,但是,并不是每次拷贝的时候都是真的拷贝,swift对其进行了优化,在值改变的时候,才进行真正的拷贝
-
在Swift标准库中,为了提升性能,String、Array、Dictionary、Set,采取了Copy On Write技术。比如:只有在”写“操作时,才会真正执行拷贝操作
-
在Mac、iOS中的malloc函数分配的内存大小总是16的倍数
-
一般把定义在enum、struct、class内部的函数,称为方法
枚举、结构体、类都可以定义方法
问:swift中,结构体与类的区别?哪一个更安全
Chat-GPT回答:
在 Swift 中,类和结构体都是复杂的数据类型,可以定义属性和对象,但是它们之间也有很多重要的差别:
1. 值类型与引用类型
首先,结构体是值类型,类是引用类型。这是两者之间最主要的区别。
当一个值类型被赋值给一个新的值或者被传递给一个函数时,它会被复制。在 Swift 中,所有基本类型(如Int, Float, Bool, String等)以及集合类型(如Array, Dictionary, Set等)都是值类型,并且它们背后都是以结构体的形式实现的。
而引用类型在赋值到变量、常量或者传递给一个函数时,只是传递了一个引用,所以多个变量可以引用同一个实例。
2. 继承
类支持继承,这意味着一个类可以继承另一个类的特性,如方法和属性。结构体则不支持继承。
3. 析构函数
类可以有析构函数,它允许类实例释放占用的资源。结构体没有析构函数。
强制型转化(runtime type)
类支持强制型转化(runtime type),你可以检查和解释一个类实例的运行时类型。结构体并不支持。
问:结构体、类哪一个更安全?
谈到安全问题,“安全”这个词可能需要更具体的上下文。但是,从值传递和引用传递角度讲,结构体可能被认为更“安全”,因为他们在传递和赋值时是复制的。所以,如果你修改了一个结构体的副本,原始版结构体不会受到影响。这避免了意外更改了原始数据的情况。然而,类可以共享同一个实例,修改一个引用,其他所有引用也会被影响,在某些情况下,这可能是你不想看到的。
正确使用的话,类和结构体都可以写出安全、高效的代码。你应根据你的特定需求和上下文情况来选择使用它们。
有关值类型、引用类型的知识点
在 Swift 中,数据类型主要分为两种:值类型(value type)和引用类型(reference type),它们有关键的区别。
值类型
值类型的特点是每个实例持有一份唯一的数据副本。当你把一个值类型的变量赋值给另一个变量,或者把它作为一个函数的参数,系统会复制一份新的数据。基于这种特性,值类型通常被认为相对更加的安全,因为你无需担心对其中一份数据的修改会影响其他引用同样数据的实例。
在 Swift 中,以下类型都是值类型:结构体(struct
),枚举(enum
),元组(Tuple),以及所有的基本数据类型(如Int
,Double
,Bool
,String
等)和集合类型(如Array
,Dictionary
,Set
等)。
例如,你创建了一个结构体的实例,并且把它赋值给另一个变量,那么当你修改这个新变量时,原始实例不会受到影响,因为系统实际上创建了一个全新的实例,而不是传递了一个引用。
对于数组和字典等集合类型,Swift 实现了一种叫做“写时复制”(copy-on-write)的技术,即只有在你试图修改这个集合的数据时,系统才会复制一份新的数据,这种技术既保证了代码的效率,又保留了值类型的特性。
引用类型
与值类型相对的是引用类型,引用类型的实例在内存中以一个引用的方式存在。当你把一个引用类型的实例赋值给一个新的变量或者常数时,新的变量或者常数和原本的实例都引用相同的内存空间,也就是说它们共享同一份数据。如果你通过其中任何一个的引用改变了那份数据,其他的所有引用都能看到这个改变。
在 Swift 中,类(class
)是一种引用类型的典型代表。对于引用类型的数据,Swift 使用了自动引用计数(ARC)进行内存管理。
例如,如果你创建了一个类的实例,并且把它赋值给另一个变量,那么这两个变量实际上引用的是同一个实例。如果你修改了其中任何一个变量所引用的实例的数据,另一个变量所引用的实例的数据也会发生改变。
结论
值类型和引用类型各有优劣,选择哪一种完全取决于你的具体需求。一般情况下,如果你需要确保实例的每一处修改都会产生新的唯一实例,那么可以使用值类型。如果你需要多个引用共享同一份数据,并且可以观察到数据的改变,那么可以使用引用类型。
Swift 对于值类型和引用类型的处理非常灵活,你可以根据你的需要灵活选择。记住,选择正确的类型可以让你的代码更加简洁,易读,易于管理。