Swift -- 02 函数、枚举

593 阅读10分钟

swift.webp

函数

1、函数的定义

1、带返回值函数

传递的形参(参数)只能是let类型;

func sumfunc(v1:Int,v2:Int)->Int
{
    return v1+v2
}
sumfunc(v1: 10, v2: 20)

// 返回值为Int类型

2、隐式返回

整个函数体,只有一个单一的表达式,函数可以使用隐式返回这个表达式
注意:函数体内如果有其他代码,不仅仅只有表达式(如:v1+v2),那么需要使用return返回

func sumfunc(v1:Int,v2:Int)->Int
{
    v1+v2
}
sumfunc(v1: 10, v2: 20)
// 返回值为Int类型

3、无返回值

func sayHello()->Void
{
    print("Hello")
}

func sayHello() ->()
{
    print("Hello")
}

func sayHello()
{
    print("Hello")
}
// 这三种方式,都可以表达无返回值函数

4、返回元组,实现多返回值

func calculate(v1:Int,v2:Int)->(sum:Int,difference:Int,average:Int)
{
    let sum = v1+v2;
    return (sum,v1-v2,sum >> 1) 
}
calculate(v1: 20, v2: 10)
// return值为:30,10,15
// >> 右移运算符,右移一个位置,值减半
// << 左移运算符,左移一个位置,值翻倍

2、函数的注释

/// 求和
///
/// 将2个数据相加,取和
///
/// - Parameter v1: 第一个参数
/// - Parameter v2: 第二个参数
/// - Returns 两数之和
///
/// - Note 传入两个整数
func sumfunc(v1:Int,v2:Int)->Int
{
    return v1+v2
}
sumfunc(v1: 10, v2: 20)

选中函数,按住option,就可以查看注释

iShot2022-04-19_18.03.51.jpg

3、参数标签、默认参数值

1、为了提高代码的可读性,Swift提供参数标签

func gotowork(at time:String)
{
    print("go to work at\(time)")
}
gotowork(at:"09:00")

可以使用_(下划线)省略参数标签

func sum(_ v1:Int, _ v2:Int)->Int
{
    v1 + v2
}
sum(10,20)

func sum1(_ v1:Int, _ v2:Int,v3:Int)->Int
{
    v1 + v2 + v3
}
sum1(10,20,v3:20)

注意⚠️:
如果所有形参都使用省略标签,那么在函数调用时,需要给所有形参赋值;如果只赋值1个值,系统将无法识别这个值要赋值给哪一个参数;

2、可以给参数设置默认值

func job(job:String="teacher")
{
    print("my job is \(job)")
}
job()
func sum(_ v1:Int=0, _ v2:Int=0,v3:Int)->Int
{
    v1 + v2 + v3
}
sum(10,v3:20)
如果其他参数有默认值,那么就是可选参数,可以给他传递值,也可以不传递,
比如上面的v2,我们可以选择不传入,那么它就会使用默认值;

4、可变参数

注意⚠️:
1、一个函数最多只能有一个可变参数;
2、紧跟在可变参数后面的参数,不能使用_(下划线)省略参数标签

func test(_ numbers:Int... , name:String, _ other:String)
{}

test(1,2,3,name:"QLY","笑笑")

//传入一个Int类型的可变参数numbers

5、输入输出参数 inout

inout定义一个输入输出参数,可以在函数内部修改外部实参的值;

var value = 50
print(value) // 此时value值为50`
func increment(_ value: inout Int, _ length: Int = 10) {
    value += length
}
increment(&value)
print(value) // 此时 value 值为60,成功改变了函数外部变量 value 的值`

注意⚠️:
1、可变参数不能标记为inout
2、inout参数不能有默认值;
3、inout参数只能传入可以被多次赋值的,也就是只能传入var(变量)类型的,不能传入let(常量)类型;
4、inout参数传递的本质是地址传递,属于引用传递,也就是引用类型;\

6、函数重载

可以定义多个名字相同的函数,但要求:
1、函数名相同;
2、参数个数不同 || 参数类型不同 || 参数标签不同;
例子:

func sum(v1:Int, v2:Int) -> Int{}

func sum(v1:Int, v2:Double) -> Int{} //参数类型不同

func sum(v1:Int,v2:Int,v3:Int) -> Int{}//参数个数不同

func sum(_ v1:Int,_ v2:Int) -> Int{}//参数标签不同

注意:⚠️
1、函数重载与返回值无关;
2、默认参数与函数重载一起使用容易产生二义性;如下图

iShot2022-04-20_00.08.45.jpg 3、可变参数、省略参数标签和函数重载一起使用,容易产生二义性,编译有可能报错

7、函数类型

函数类型形式参数类型返回值类型组成;

func test() {
}// 函数类型:()->Void 或者 () -> ()

func sum(v1:Int, v2:Int) -> Int
{
    v1+v2
}//函数类型:(Int,Int)-> Int

函数类型作为变量类型

func sum(v1:Int, v2:Int) -> Int
{//函数类型:(Int,Int)-> Int
    v1+v2
}
var fn:(Int,Int)->Intsum(v1:v2:)
fn(1,2)//结果为:3

函数类型作为函数参数

func sum(v1:Int, v2:Int) -> Int
{//函数类型:(Int,Int)-> Int
    v1+v2
}
func result(_ sum:(Int,Int)->Int,v1:Int,v2:Int)->Int
{
    return sum(v2, v1)
}
result(sum(v1: v2:), v1: 20,v2:30)

8、高阶函数

返回值是函数类型的函数,叫做高阶函数

func fnext(_ input:Int)->Int{
    input+1
}
func fprevious(_ input:Int)->Int
{
    input-1
}
func ff(_ bol:Bool)->(Int) -> Int
{
    bol ? fnext(_:):fprevious(_:)
}
ff(true)(3)
ff(false)(5)

嵌套函数 将函数定义在函数内部

func ff(_ bol:Bool)->(Int) -> Int
{
    func fnext(_ input:Int)->Int{
         input+1
    }
    func fprevious(_ input:Int)->Int
    {
        input-1
    }
    return bol ? fnext(_:):fprevious(_:)
}
ff(true)(3)
ff(false)(5)

枚举

1、枚举的基本用法

1、常规枚举:

enum Passwork{
    case number
    case other
}

enum Passwork{
    case number, other
}

使用:
var pass = Passwork.number

2、关联值枚举

enum Passwork{
    case number(Int,Int,Int,Int)
    case other
}

使用:
var pass1 = Passwork.number(9,8,7,6)
var pass2 = Passwork.number(19,82,17,36)

3、原始值枚举 枚举成员可以使用相同类型的默认值预先设值,这个默认值就叫:原始值

注意⚠️:原始值,不占用枚举变量的内存(往下有解释)

enum Section:Int{//隐式原始值枚举
    case number1 = 1
    case number2 = 2
    case number3 = 3
    case number4 = 4
}
var sec = Section.number1
print(sec) // 打印结果为:1

4、隐式原始值枚举

如果枚举原始值为Int、String类型,Swift会自动分配原始值

enum Section:Int{//隐式原始值枚举
    case number1,number2,number3,number4
}
//原始值分别为:0、1、2、3

2、MemoryLayout

可以使用MemoryLayout来获取数据类型占用的内存大小;
例子:

enum Passwork{
    case number(Int,Int,Int,Int)
    case other
}
var pass1 = Passwork.number(9,8,7,6)
var pass2 = Passwork.number(19,28,17,21)


//实际占用多少内存
MemoryLayout<Passwork>.size //结果为:33
//分配多少内存
MemoryLayout<Passwork>.stride //结果为:40
//内存对齐方式
MemoryLayout<Passwork>.alignment //结果为:8
enum Section:Int {
    case number1,number2,number3,number4
}
var section1 = Section.number1

//实际占用多少内存
MemoryLayout<Section>.size //结果为:1
//分配多少内存
MemoryLayout<Section>.stride //结果为:1
//内存对齐方式
MemoryLayout<Section>.alignment //结果为:1

两个例子,为什么内存结果差异这么大呢❓\color{red}{两个例子,为什么内存结果差异这么大呢❓}


Psaawork枚举是关联枚举,它的关联值是由外部传入,可以传入不同的值进去, 所以每一个枚举变量,都需要有自己的内存去存储这些值;
所以比如说pass1、pass2 这些变量,需有自己的内存去存储它对应的关联值;

所以关联值枚举的特点就是:将传递进来的关联值,直接存储到枚举变量内存里;所以枚举变量的大小,与关联值大小有关;

Section是隐式原始值枚举,它的值是固定的,值与枚举成员是绑定在一起的; 但它的值,并不会存储到枚举变量里(比如说section1这个变量);

如果像关联枚举那样存储到变量里,那么就会造成内存浪费;

举例子:
var section1 = Section.number1 // 值=1,Int类型 占 8字节

 var section2 = Section.number1 // 值=1,Int类型 占 8字节

 var section2 = Section.number1 // 值=1,Int类型 占 8字节

那么section1、section2、section3,取同一个number1,就占用了 24字节,但是 number1 的值都是固定的,不会由外界修改,却占用24字节,不是很浪费吗? 

所以编译器会针对这种类型的枚举做优化,它会给number1、number2、number3、 做一个标记,比如说:
number1 标记为 1;
number2 标记为 2;
number3 标记为 3;
所以number1 里存储的值是1,number2 里存储的值是2,number3 里存储的值是3;
如此section1、section2、section3、这三个枚举变量,内存各自占用 1 个字节;
如果拿到他们的值, 直接通过 section1.rawValue, 就会返回Int 8 字节的数据;

所以,原始值枚举,是不会将值存储到枚举变量里的;

枚举内存探索

定义一个关联枚举,使用MemoryLayout读取实际占用内存、系统分配内存、内存对其方式;
通过枚举地址,查看每一次关联枚举后其占用大小;

例子:

enum TestEnum{
    case test1(Int,Int,Int)
    case test2(Int,Int)
    case test3(Int)
    case test4(Bool)
    case test5
}
print(MemoryLayout<TestEnum>.size); // 实际占用大小:25
print(MemoryLayout<TestEnum>.stride);//系统分配大小:32
print(MemoryLayout<TestEnum>.alignment);//字节对齐方式:8
var t = TestEnum.test1(1, 2, 3)
print(Mems.ptr(ofVal: &t))
t = TestEnum.test2(4, 5)
t = TestEnum.test3(6)
t = TestEnum.test4(true)

结果:

var t = TestEnum.test1(1, 2, 3)

内存:
// 01 00 00 00 00 00 00 00 //可以看出,01 对应存储的数据,就是1;
// 02 00 00 00 00 00 00 00 //02 对应存储的数据,就是2;
// 03 00 00 00 00 00 00 00 //03 对应存储的数据,就是3;
// 00 00 00 00 00 00 00 00 
//那么这里?都是0,存储什么呢?如果不存储,为什么要开启8字节?
//不要说因为8字节内存对齐哦,因为上面存储到3,正好已经24字节了,满足8字节对齐
// 那么为什么这里还要继续开启8字节??? 我们后续讲解


t = TestEnum.test2(4, 5)

内存:
//04 00 00 00 00 00 00 00 //04 对应存储的数据,就是4;
//05 00 00 00 00 00 00 00 //05 对应存储的数据,就是5;
//00 00 00 00 00 00 00 00 //空
//01 00 00 00 00 00 00 00 //01 对应什么???为什么会出现01;我们后续讲解


t = TestEnum.test3(6)
内存:
//06 00 00 00 00 00 00 00 //06 对应存储的数据,就是6;
//00 00 00 00 00 00 00 00 //空
//00 00 00 00 00 00 00 00 //空
//02 00 00 00 00 00 00 00 //02 对应什么???为什么会出现02;我们后续讲解


t = TestEnum.test4(true)

内存:
//01 00 00 00 00 00 00 00 //01 对应布尔值true存储的数据
//00 00 00 00 00 00 00 00 //空
//00 00 00 00 00 00 00 00 //空
//03 00 00 00 00 00 00 00 //03 对应什么???


通过 MemoryLayout<TestEnum>.size,得出枚举实际占用25字节,
但是我们看到,传递进入到数据,最大也就是3字数字,也就24字节,那么多一个字节怎么来?

其实,每一个关联值枚举内存里,都有一个case 下标的内存标识:
第一个为:00;对应的下标为:0
第二个为:01;对应的下标为:1
第三个为:02;对应的下标为:2
第四个为:03;对应的下标为:3

如此用来区别不同的case;

这也就是为什么,占用25个字节;

最后一个字节,存储的是case的下标;

这也是为什么原始值枚举,占用内存只有 1 的原因 这里的内存,只是case下标的内存而已

知识补充

1、实参、形参

实参:在传入函数之前已有明确定义的参数;
形参:参数只在此函数内有效并可使用,函数外不需要有明确定义;

2、值类型、引用类型

在Swift中,有两种参数传递方式:值类型引用类型

值类型:传递的是参数的一个副本,这样在调用参数的过程中不会影响原始数据
引用类型:把参数本身引用(内存地址)传递过去,在调用的过程会影响原始数据