Swift 四、Enum & Optional

Enum & Optional.png

一、Enum

1.1 枚举的基本用法

Swift语言中使用enum关键字来进行枚举的创建。

///创建一个姓氏枚举类型
enum Surname {
    case 赵
    case 钱
    case 孙
    case 李
}

上面的代码创建了一个姓氏枚举类型,这个枚举类型中定义了4个枚举值,分别是赵、钱、孙、李,上面的写法将4个枚举值分别在4个case语句中定义,开发者也可以在1个case子句中完成多个枚举值的定义,如

///创建一个姓氏枚举类型
enum Surname {
    case 赵, 钱,孙,李
}

1.2 枚举的原始值和相关值

枚举的原始值特性可以将枚举值与另一种数据类型进行绑定,相关值则可以为枚举值关联一些其他数据。通过相关值,开发者可以实现复杂的枚举类型

1.2.1 枚举的原始值

swift语言中的枚举支持开发者声明一个原始值类型,并将某个已经存在的类型的值与枚举值进行绑定,枚举指定原始值类型的语法与继承的语法有些类似,示例代码如下:

///为枚举类型指定一个原始值类型
enum CharEnum: Character {
    ///通过赋值的方式为枚举值设置一个原始值
    case a = "a"
    case b = "b"
    case c = "c"
    case d = "d"
}

如果开发者要指定枚举的原始值类型为Int类型,那么可以只设置第一个枚举值的原始值,其后的枚举值的原始值会在第一个枚举值原始值的基础上依次递增,示例如下:

enum IntEnum: Int {
    ///第一个枚举值的原始值设置为1
    case a = 1
    ///默认原始值为2
    case b
    ///默认原始值为3
    case c
    ///默认原始值为4
    case d
}

通过枚举类型中的rawValue属性来获取枚举的原始值,示例如下:

enum CharEnum: Character {
    ///通过赋值的方式为枚举值设置一个原始值
    case a = "a"
    case b = "b"
    case c = "c"
    case d = "d"
}

var char = CharEnum.a
var value = char.rawValue
print(value)

隐式RawValue分配是建立在swift的类型推断机制上的

enum DayOfWeek: String {
    case mon, tue, wed, thu, fri = "Hello World", sat, sun
}
print(DayOfWeek.mon.rawValue)
print(DayOfWeek.fri.rawValue)
print(DayOfWeek.sat.rawValue)

lldb打印输出

mon
Hello World
sat

我们当前的系统已经默认给我们的每一个枚举值分配了一个字符串,而这个字符串其实跟我们枚举成员值的字符串是一致的。那么它到底是怎么做到的哪,我们在SIL文件看一下。

enum DayOfWeek : String {
  case mon, tue, wed, thu, fri, sat, sun
  init?(rawValue: String)
  typealias RawValue = String
  var rawValue: String { get }
}
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer>>):
  alloc_global @$s4main1xSSvp                     // id: %2
  %3 = global_addr @$s4main1xSSvp : $*String      // user: %8
  %4 = metatype $@thin DayOfWeek.Type
  %5 = enum $DayOfWeek, #DayOfWeek.mon!enumelt    // user: %7
  // function_ref DayOfWeek.rawValue.getter
  %6 = function_ref @$s4main9DayOfWeekO8rawValueSSvg : $@convention(method) (DayOfWeek) -> @owned String // user: %7
  %7 = apply %6(%5) : $@convention(method) (DayOfWeek) -> @owned String // user: %8
  store %7 to %3 : $*String                       // id: %8
  %9 = integer_literal $Builtin.Int32, 0          // user: %10
  %10 = struct $Int32 (%9 : $Builtin.Int32)       // user: %11
  return %10 : $Int32                             // id: %11
} 

image.png

这里就是在访问我们rawValuegetter方法。

sil hidden @$s4main9DayOfWeekO8rawValueSSvg : $@convention(method) (DayOfWeek) -> @owned String {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $DayOfWeek):
  debug_value %0 : $DayOfWeek, let, name "self", argno 1 // id: %1
  switch_enum %0 : $DayOfWeek, case #DayOfWeek.mon!enumelt: bb1, case #DayOfWeek.tue!enumelt: bb2, case #DayOfWeek.wed!enumelt: bb3, case #DayOfWeek.thu!enumelt: bb4, case #DayOfWeek.fri!enumelt: bb5, case #DayOfWeek.sat!enumelt: bb6, case #DayOfWeek.sun!enumelt: bb7 // id: %2

switch_enum %0 就是我们传进来的当前枚举成员值mon,接下来就是一个模式匹配,匹配我们当前的成员值。匹配上了swift case #DayOfWeek.mon!enumelt: bb1代码块。

bb1:                                              // Preds: bb0
  %3 = string_literal utf8 "mon"                  // user: %8

那么这个字符串 "mon" 是从哪里得到的哪?这样的字符串其实就是一个字符串常量,而字符串常量存储在哪里哪,我们把Mach-o文件拖到MachoView应用来查看一下。


image.png

在枚举变量初始化时,开发者可以使用枚举类型加点语法的方式,如果这个枚举有指定的原始值,也可以通过枚举值的原始值来完成枚举实例的构造,示例如下:

enum IntEnum: Int {
    ///第一个枚举值的原始值设置为1
    case a = 1
    ///默认原始值为2
    case b
    ///默认原始值为3
    case c
    ///默认原始值为4
    case d
}

///通过原始值构造枚举变量a
var intEnum = IntEnum(rawValue: 1)

需要注意,通过原始值进行枚举实例的构造时,是有可能构造失败的,因为开发者传入的原始值不一定会对应某一个枚举值。因此,这个方法实际上返回的是一个Optional类型的可选值,如果构造失败,则会返回nil。

1.2.1 枚举的相关值

在定义枚举值的时候,开发者可以为其设置一个参数列表,这个参数列表被称为枚举的相关值。示例如下:

///定义形状枚举
enum Shape {
    ///圆形,设置圆心和半径为相关值
    case circle(center: (Double, Double), radius: Double)
    ///矩形,设置中心,宽高为相关值
    case rect(center: (Double, Double), width: Double, height: Double)
    ///三角形,设置三个顶点为相关值
    case triangle(point1: (Double, Double), point2: (Double, Double), point3: (Double, Double))
}

在创建相关值枚举的时候,开发者需要提供参数列表中所需要的参数,示例如下:

省略......
///创建圆形,圆心为(0, 0),半径为3
var circle = Shape.circle(center: (0, 0), radius: 3)
///创建矩形,中心点为(1, 1), 宽度为10,高度为15
var rect = Shape.rect(center: (1, 1), width: 10, height: 15)
///创建三角形,顶点为(2, 2), (3, 3), (2, 5)
var triangle = Shape.triangle(point1: (2, 2), point2: (3, 3), point3: (2, 5))

1.3 模式匹配

在switch-case结构语句中,匹配到枚举后,可以通过参数捕获的方式来获取枚举实例的相关值,这里捕获到的相关值参数可以在开发者的代码中使用,示例如下:

///定义形状枚举
enum Shape {
    ///圆形,设置圆心和半径为相关值
    case circle(center: (Double, Double), radius: Double)
    ///矩形,设置中心,宽高为相关值
    case rect(center: (Double, Double), width: Double, height: Double)
    ///三角形,设置三个顶点为相关值
    case triangle(point1: (Double, Double), point2: (Double, Double), point3: (Double, Double))
}

///创建圆形,圆心为(0, 0),半径为3
var circle = Shape.circle(center: (0, 0), radius: 3)
///创建矩形,中心点为(1, 1), 宽度为10,高度为15
var rect = Shape.rect(center: (1, 1), width: 10, height: 15)
///创建三角形,顶点为(2, 2), (3, 3), (2, 5)
var triangle = Shape.triangle(point1: (2, 2), point2: (3, 3), point3: (2, 5))

func ShapeFunc(param: Shape) {
    switch param {
    case let .circle(center, radius):
        print("此圆的圆心为:\(center),半径为:\(radius)")
    case let .rect(center, width, height):
        print("此矩形的中心为:\(center),宽为:\(width),高为:\(height)")
    case let .triangle(point1, point2, point3):
        print("此三角形的3个顶点为:\(point1),\(point2),\(point3)")
    }
}

ShapeFunc(param: circle)
ShapeFunc(param: rect)
ShapeFunc(param: triangle)

1.4 枚举的大小

接下来我们来讨论一下枚举占用的内存大小,这里我们区分几种不同的情况,首先第一种就是 No-payload enums

///UInt8
/// 最多可存储256个case
enum Week {
    
    case MONDAY
    
    case TUEDAY
    
    case WEDDAY
    
    case THUDAY
    
    case FRIDAY
    
    case SATDAY
    
    case SUNDAY
    
}

print(MemoryLayout.size)

打印结果是1字节。
可以看到这里我们测试出来的不管是 size 还是 stride 都是 1 ,这个地方我们也很好理解,当前的 enum 有几个 case ? 是不是 8 个啊,在 Swift 中进行枚举布局的时候一直是尝试使用最少的空间来存储 enum ,对于当前的 case 数量来说, UInt8能够表示 256 cases ,也就意味着如果一个默认枚举类型且没有关联值的 case 少于 256 ,当前枚举类型的大小都是 1 字节。

image.png

通过上面的打印我们可以直观的看到,当前变量 a , b , c这三个变量存储的内容分别 是 00, 01, 02 这和我们上面说的布局理解是一致的。
No-payload enums 的布局比较简单,我们也比较好理解,接下来我们来理解一下 Single- payload enums 的内存布局, 字面意思就是只有一个负载的 enum, 比如下面这个例子:

enum ZGEnum {
    
    case test_one(Bool)
    case test_two
    case test_three
    case test_four
    
}

print(MemoryLayout.size)
print(MemoryLayout.stride)

大家猜一下当前的这个案例, enum 在内存中的大小是多少?

1
1

如果我把当前的案例换一下,换成如下的案例,那么当前 enum 占用的大小又是多少?

enum ZGEnum {
    
    case test_one(Int)
    case test_two
    case test_three
    case test_four
    
}

print(MemoryLayout.size)
print(MemoryLayout.stride)

结果是

9
16

这里我们就产生了疑问了,为什么都是单个负载,但是当前占用的大小却不一致?
注意, Swift 中的 enum 中的 Single-payload enums 会使用负载类型中的额外空间来记录没有负载的 case 值。这句话该怎么理解?首先 Bool 类型1字节,也就是 UInt8 ,所以当前能表达 256 个 case的情况,对于 Bool类型来说,只需要使用低位的 0, 1 这两种情况,其 他剩余的空间就可以用来表示没有负载的 case 值。

enum ZGEnum {
    
    case test_one(Bool)
    case test_two
    case test_three
    case test_four
    
}

var a = ZGEnum.test_one(false)
var b = ZGEnum.test_one(true)
var c = ZGEnum.test_two
var d = ZGEnum.test_three
var f = ZGEnum.test_four

枚举单负载.png

可以看到,不同的 case 值确实是按照我们在开始得出来的那个结论进行布局的。

enum ZGEnum {
    
    case test_one(Int)
    case test_two
    case test_three
    case test_four
    
}

print(MemoryLayout.size)
print(MemoryLayout.stride)

var a = ZGEnum.test_one(10)
var b = ZGEnum.test_one(20)
var c = ZGEnum.test_two
var d = ZGEnum.test_three
var f = ZGEnum.test_four
枚举单负载Int类型.png

对于 Int 类型的负载来说,其实系统是没有办法推算当前的负载所要使用的位数,也就意味着当前 Int 类型的负载是没有额外的剩余空间的,这个时候我们就需要额外开辟内存空间来去存储我们的 case 值,也就是 8 + 1 = 9 字节。

上面说完了 Single-payload enums , 接下来我们说第三种情况 Mutil-payload enums , 有多个负载的情况产生时,当前的 enum是如何进行布局的哪?

enum ZGEnum {
    
    case test_one(Bool)
    case test_two(Bool)
    case test_three
    case test_four
    
}

print(MemoryLayout.size)
print(MemoryLayout.stride)

var a = ZGEnum.test_one(false)
var b = ZGEnum.test_two(true)
var c = ZGEnum.test_three
var d = ZGEnum.test_three
var f = ZGEnum.test_four
1
1

上面这个例子中,我们有两个 Bool 类型的负载,这个时候我们打印当前的 enum 大小 发现其大小仍然为 1,这个时候我们来看一下内存当中的存储情况。

枚举双负载Bool类型.png

这里我们可以看到当前内存存储的分别是 00 41 80 80 81 00 , 这里在存储当前的 case 时候会使用到 common spare bits,什么意思?其实在上一个案例我们也讲过了,首先 bool 类型需要 1 字节,也就是 8 位。


接下来我们来看一下 00 41 80 80 81 00 分别代表的是什么?首先 0, 4, 8 这里我们叫 做 tag value0, 1 这里我们就做tag index
当前一般来说,我们有多个负载的枚举时,当前枚举类型的大小取决于当前最大关联值的大小。

1.5 递归枚举

递归枚举其实就是使用递归的方式来进行数据描述。使用indirect关键字修饰的枚举值表示这个枚举值是可递归的,即此枚举值中的相关值可以使用其枚举类型本身。

enum Expression {
    case num(num: Int)
    indirect case add(num1: Expression, num2: Expression)
    indirect case sub(num1: Expression, num2: Expression)
    indirect case mul(num1: Expression, num2: Expression)
    indirect case div(num1: Expression, num2: Expression)
}

使用Expression枚举来描述复合表达式( (5 + 5) * 2 - 8) / 2的代码如下:

///单值 5
var num5 = Expression.num(num: 5)
/// 表达式 5 + 5
var exp1 = Expression.add(num1: num5, num2: num5)
///单值 2
var num2 = Expression.num(num: 2)
///表达式 (5 + 5) * 2
var exp2 = Expression.mul(num1: exp1, num2: num2)
///单值 8
var num8 = Expression.num(num: 8)

///表达式 (5 + 5) * 2 - 8
var exp3 = Expression.sub(num1: exp2, num2: num8)

///表达式 ((5 + 5) * 2 - 8) / 2
var expFinal = Expression.div(num1: exp3, num2: num2)

我们可以为这个四则表达式枚举类型Expression实现一个函数来进行运算,在开发中将描述与运算结合,能够编写出十分优美的代码。处理递归枚举通常会采用递归函数,函数方法实现示例如下:

func expressionFunc(num: Expression) -> Int {
    switch num {
    case let .num(num):
        return num
    case let .add(num1, num2):
        return expressionFunc(num: num1) + expressionFunc(num: num2)
    case let .sub(num1, num2):
        return expressionFunc(num: num1) - expressionFunc(num: num2)
    case let .mul(num1, num2):
        return expressionFunc(num: num1) * expressionFunc(num: num2)
    case let .div(num1, num2):
        return expressionFunc(num: num1) / expressionFunc(num: num2)
        
    }
}
///((5 + 5) * 2 - 8) / 2 打印结果为6
expressionFunc(num: expFinal)

关于递归枚举还有一点需要注意,如果一个枚举中所有的枚举值都是可递归的,开发者可以直接将整个枚举类型声明为可递归的,示例如下:

indirect enum Expression {
    case num(num: Int)
    case add(num1: Expression, num2: Expression)
    case sub(num1: Expression, num2: Expression)
    case mul(num1: Expression, num2: Expression)
    case div(num1: Expression, num2: Expression)
}

二、Optional

2.1 认识可选值

之前我们在写代码的过程中早就接触过可选值,比如我们在代码这样定义:

class ZGTeacher {
    var age: Int?
}

当前的 age 我们就称之为可选值,那对于 Optional 的本质是什么?我们直接跳转到源码,打开 Optional.swift 文件。

public enum Optional: ExpressibleByNilLiteral {
  
  case none

  case some(Wrapped)
}

从源码可以得知,Optional 的本质是枚举,那么我们也可以仿照系统的实现制作一个自己的 Optional。
比如给定任意一个自然数,如果当前自然数是偶数返回,否则为 nil,我们应该怎么表达这个案例。

enum MyOptional {
    case none
    case some(Value)
}

func getOddValue(_ value: Int) -> MyOptional {
    if value % 2 == 0 {
        return .some(value)
    } else {
        return .none
    }
}

这个时候给定一个数组,我们想删除数组中所有的偶数

截屏2022-01-14 11.49.10.png

这个时候编译器就会检查我们当前的 value 会发现他的类型和系统编译器期望的类型不符,这 个时候我们就能使用 MyOptional来限制语法的安全性。
于此同时我们通过 enum 的模式匹配来取出对应的值。

for index in array {
    let value = getOddValue(index)
    switch value {

    case .some(let v):
        array.remove(at: array.firstIndex(of: v)!)
    case .none :
        print("value not exit")
    }
}

如果我们把上述的返回值更换一下,其实就和系统的 Optional使用无疑。

func getOddValue(_ value: Int) -> Int {
    if value % 2 == 0 {
        return .some(value)
    } else {
        return .none
    }
}

这样我们其实是利用当前编译器的类型检查来达到语法书写层面的安全性。
当然如果每一个可选值都用模式匹配的方式来获取值在代码书写上就比较繁琐,我们还可以使 用 if let 的方式来进行可选值绑定。

case .some(let v):
        array.remove(at: array.firstIndex(of: v)!)

除了使用 if let 来处理可选值之外,我们还可以使用 gurad let 来简化我们的代码。
gurad letif let 刚好相反,gurad let 守护一定有值。如果没有,直接返回。 通常判断是否有值之后,会做具体的逻辑实现,通常代码多 如果用 if let t 凭空多了一层分支, gurad let 是降低分支层次的办法。

2.2 可选链

我们都知道在OC 中我们给一个 nil 对象发送消息什么也不会发生, Swift 中我们是没有办法向一个 nil 对象直接发送消息的,但是借助可选链可以达到类似的效果。我们看下面两段代码:

let str: String? = "abc"
let upperStr = str?.uppercased() // Optional<"ABC">
var str1: String?
let upperStr1 = str1?.uppercased() // nil

我们再来看下面这段代码输出什么?

let str: String? = "zhang"
let upperStr = str?.uppercased().lowercased()

同样的可选链对于下标和函数调用也适用。

var closure: ((Int) -> ())?
/// closure 为 nil 不执行
closure?(1)

let dict = ["one": 1, "two": 2]
///Optional(1)
var one = dict["one"]
///nil
var three = dict["three"]
print(one, three)

2.3 ?? 运算符 (空合并运算符)

( a ?? b ) 将对可选类型 a 进行空判断,如果 a 包含一个值就进行解包,否则就返回 一个默认值 b

  • 表达式 a 必须是 Optional 类型
  • 默认值 b 的类型必须要和 a 存储值的类型保持一致。
var q: Int? = 8
var value: Int
value = q ?? 0

print(value)

2.4 运算符重载与自定义

2.4.1 运算符重载

读者在认识重载运算符之前,首先应该清楚重载的概念。重载的概念最初是针对函数的,对同一个函数名设置不同的参数类型以实现不同的功能被称为函数的重载。
下面我们自定义一个圆形的类,通过重载加号运算符+来实现对圆形类实例的相加操作。
设计圆形类如下,其中有两个属性,分别表示圆形半径与圆心:

class Circle {
    var center: (Double, Double)
    var radius: Double
    init(center: (Double, Double), radius: Double) {
        self.center = center
        self.radius = radius
    }
}

定义两个Circle实例进行相加操作时应执行这样的运算:两个Circle实例相加返回一个新的Circle实例,并且这个新的Circle实例的圆心为两个操作数Circle实例半径的和,重载加法运算符如下:

func +(param1: Circle, param2: Circle) -> Circle {
    return Circle(center: param1.center, radius: param1.radius + param2.radius)
}

比如在开发中我们定义了一个二维向量,这个时候我们想对两个向量进行基本的操作,那么我们就可以通过重载运算符来达到我们的目的。

struct Vector {
    let x: Int
    let y: Int
}

extension Vector {
    static func + (fistVector: Vector, secondVector: Vector) -> Vector {
        return Vector(x: fistVector.x + secondVector.x, y: fistVector.y + secondVector.y)
    }
    
    static prefix func - (vector: Vector) -> Vector {
            return Vector(x: -vector.x, y: -vector.y)
    }
    
    static func - (fistVector: Vector, secondVector: Vector) -> Vector {
            return fistVector + -secondVector
    }

}

var x = Vector(x: 10, y: 20)
var y = Vector(x: 20, y: 30)
var z = x + y

print(z)
var u = -z
print(u)

输出打印

Vector(x: 30, y: 50)
Vector(x: -30, y: -50)

2.4.2 自定义运算符

开发者可以自定义不存在的运算符来实现特殊的需求。
自定义运算符分为两个步骤,首先开发者需要对要定义的运算符进行声明。在声明运算符的结构中,prefix的作用是声明运算符的类型,可以使用prefix关键字将其声明为前缀运算符,使用infix关键字将其声明为中缀运算符,使用postfix关键字将其声明为后运算符。
在进行运算符的实现时,后缀和前缀运算符只能有一个参数,参数在func关键字前需要表明要实现的运算符类型,而中缀运算符需要有两个参数且func关键字前不需要额外标明,示例如下:

///自定义前缀运算符
prefix operator ++

///进行自定义运算符实现
prefix func ++(num: Int) -> Int {
    return num + 1
}

var a = ++5
///将返回6
print(a)

///自定义中缀运算符
infix operator ++

func ++(num1: Int, num2: Int) -> Int {
    return num1 * num1 + num2 * num2
}
var b = 5 ++ 4
///将返回41
print(b)
///自定义后缀运算符
postfix operator ++

postfix func ++(num: Int) -> Int {
    return num + num
}

var c = 5++
///将返回10
print(c)

提示
前缀运算符是指只有一个操作数且在使用运算符进行运算时,运算符需要出现在操作数的前面;
中缀运算符需要有两个操作数,且在进行运算时运算符需要出现在两个操作数的中间;
后缀运算符只能有一个操作数,在运算时后缀运算符需要出现在操作数的后面。

2.4.3 运算符的优先级与结合性

任何运算符都有默认的优先级,开发者自定义的运算符也是如此,优先级越高的运算符越优先执行。对于结合性而言,由于前缀运算符与后缀运算符都只有一个操作数,因此它只对中缀运算符有意义。
在重载运算符操作中,并不会改变原运算符的结合性和优先级,但对于自定义运算符,开发者可以设置其结合性与优先级,示例如下:

precedencegroup customGroup {
    higherThan: AdditionPrecedence///优先级比加法运算符的优先级高
    lowerThan: MultiplicationPrecedence///优先级比乘法运算符的优先级低
    assignment: true///设置执行可选链操作时的优先级
    associativity: left///定义结合性
}
///定义中缀运算符,其属性由customGroup描述
infix operator +++: customGroup

当系统内置的优先级组不能满足我们的要求时,即可使用precedencegroup关键字来自定义优先级组。

2.5 隐式解析可选类型

隐式解析可选类型是可选类型的一种,使用的过程中和非可选类型无异。它们之间唯一的区别是,隐式解析可选类型是你告诉Swift 编译器,我在运行时访问时,值不会为 nil。

var age: Int?
var age1: Int!
age = nil
age1 = nil
截屏2022-01-14 15.55.32.png

其实日常开发中我们比较常见这种隐士解析可选类型。


截屏2022-01-14 15.56.47.png

IBOutlet类型是Xcode强制为可选类型的,因为它不是在初始化时赋值的,而是在加载视图的时候。你可以把它设置为普通可选类型,但是如果这个视图加载正确,它是不会为空的。

2.6 与可选值有关的高阶函数

  • map : 这个方法接受一个闭包,如果可选值有内容则调用这个闭包进行转换。
var dict = ["one": "1", "two": "2"]
let result = dict["one"].map{ Int($0) }/// Optional(Optional(1))

上面的代码中我们从字典中取出字符串”1”,并将其转换为Int类型,但因为String转换成 Int不一定能成功,所以返回的是Int?类型,而且字典通过键不一定能取得到值,所以map 返回的也是一个Optional,所以最后上述代码result的类型为Int??类型。
那么如果要把我们的双重可选展平开来,这个时候我们就需要使用到

  • flatMap: 可以把结果展平成为单个可选值
var dict = ["one": "1", "two": "2"]
let result = dict["one"].flatMap{ Int($0) } /// Optional(1)
  • 注意,这个方法是作用在Optioanl的方法,而不是作用在Sequence上的。
  • 作用在Sequence上的flatMap方法在Swift4.1中被更名为compactMap,该方法可以将序列中的nil过滤出去。
let array = ["1", "2", "3", nil]
let result = array.compactMap{ $0 } // ["1", "2", "3"]

let array1 = ["1", "2", "3", "four"]
let result1 = array1.compactMap{ Int($0) } // [1, 2, 3]

2.7 元类型、AnyClass、Self (self)

  • AnyObject: 代表任意类的 instance,类的类型,仅遵守类的协议。
class ZGTeacher {
    var age = 18
}

var t = ZGTeacher()
var t1: AnyObject = t
var t2: AnyObject = ZGTeacher.self
  • T.self: T是实例对象,当前T.self返回的就是他本身;如果 T 是类,当前 T.self 返回的就是元类型。
class ZGTeacher {
    var age = 18
}

var t = ZGTeacher()

var t1 = t.self
var t2 = t.self.self
var t3 = ZGTeacher.self

lldb打印一下

(lldb) po t1


(lldb) po t2


(lldb) po t3
ZGTest.ZGTeacher
class ZGTeacher {
    var age = 18
}

var t = ZGTeacher()

var t1 = t.self

var t2 = t.self.self

var t3 = ZGTeacher.self


通过打印我们可以得知,当前t3存储的是我们的metadata,也就是元类型。

  • self:
class ZGTeacher {
    var age = 18
    func test() {
        ///当前实例对象
        print(self)
    }
    static func test1() {
        ///self 是 ZGTeacher 这个类型本身
        print(self)
    }
}
  • Self: Self 类型不是特定类型,⽽是让您⽅便地引⽤当前类型,⽽⽆需重复或知道该类型的名称。 在协议声明或协议成员声明中,Self 类型是指最终符合协议的类型。可作为⽅法的返回类型, 作为只读下标的返回类型,作为只读计算属性的类型。
class ZGTeacher {
    static let age = 18
    func test() -> Self {
        ///当前实例对象
        return self
    }
}

class ZGPerson {
    static let age = 0
    let age1 = age
    var age2 = age
    lazy var age3 = Self.age
}

protocol MyProtocol {
    func get() -> Self
}
  • Any: 代表任意类的实例,包括 funcation 类型或者 Optional 类型。

    截屏2022-01-14 17.27.57.png

  • AnyClass: 代表任意实例的类型。

class ZGTeacher {
    var age = 18
}
var t: AnyClass = ZGTeacher.self

你可能感兴趣的:(Swift 四、Enum & Optional)