Swift:基本语法

一、常量与变量
二、数据类型
三、特有的运算符
四、流程控制语句


一、常量与变量


常量是指第一次赋值后,不能再次赋值的量。变量是指第一次赋值后,还能再次赋值的量。

Swift用let关键字来定义一个常量,用var关键字来定义一个变量,而且也只能用它俩来定义,这就意味着用letvar定义的量可以指向任意数据类型的数据,编译器能自动推断出量的数据类型。

// 常量
let a = 11 // 编译器能自动推断出a为Int类型
//a = 1111 // 不能再次赋值

// 变量
var b = 12 // 编译器能自动推断出a为Int类型
b = 1212 // 还能再次赋值

那实际开发中letvar怎么选择?

因为let是不可变的,编程更加安全,所以我们在实际开发中总是优先把一个量定义为let,当发现某处这个量需要变化时才把它改成var

当然我们在定义一个量时也可以指定它的数据类型。

let a: Int = 11
var b: Int = 12

那实际开发中指定数据类型和类型推断怎么选择?

实际开发中,我们极少使用指定数据类型,通常都是使用类型推断:

  • 结构体和类定义属性时必须指定数据类型;
  • 函数的参数必须指定数据类型;
  • Swift调用OC的构造方法返回instancetype的时候,我们最好指定一下变量的数据类型,这样可以避免后面的解包;
  • 其余不用指定数据类型。


二、数据类型



第一类:Bool、Int、Float、Double、String


1、基本使用
func demo() -> Void {
    
    let bool: Bool = true
    print(bool) // true
    print(type(of: bool)) // Bool
    
    let int: Int = 11
    print(int) // 11
    print(type(of: int)) // Int
    
    let float: Float = 11.11
    print(float) // 11.11
    print(type(of: float)) // Float
    
    let double: Double = 11.12
    print(double) // 11.12
    print(type(of: double)) // Double
    
    // 定义字符串
    let string = "Hello World 你好世界"
    print(string) // Hello World 你好世界
    print(type(of: string)) // String
    
    // 获取字符串的长度
    print(string.count) // 16
    // lengthOfBytes(编码):返回字符串指定编码所占的字节数
    print(string.lengthOfBytes(using: String.Encoding.utf8)) // 24(UTF8编码,一个字母占1个字节,一个汉字占3个字节)
    
    // 截取字符串:建议用NSString做中转
    let subStr = string.prefix(2) // 截取前两个字符
    print(subStr) // He
    let subStr1 = string.suffix(2) // 截取前两个字符
    print(subStr1) // 世界
    let startIndex = string.index(string.startIndex, offsetBy: 2)
    let endIndex = string.index(string.endIndex, offsetBy: -1)
    let subStr2 = string[startIndex...endIndex] // 区间截取
    print(subStr2) // llo World 你好世界
    
    // 拼接字符串
    let name = "张三"
    let age = 18
    let height = 180
    print("\(name) \(age) \(height)")

    // 字符串的格式化
    let h = 8
    let m = 29
    let s = 9
    // 简单格式
    let dateStr = "\(h):\(m):\(s)"
    print(dateStr) // 8:29:9
    // 复杂格式
    let dateStr1 = String(format: "%02d:%02d:%02d", h, m, s)
    print(dateStr1) // 08:29:09
    
    // 字符串分割成数组
    let arr = string.split(separator: " ")
    print(arr) // ["Hello", "World", "你好世界"]
}
2、StringIntDouble之间的转换:
func demo() -> Void {
    let str = "11"
    let int = Int(str)! // 字符串转Int
    print(int)
    let double = Double(str)! // 字符串转Double
    print(double)
    
    let int1 = 11
    let str1 = "\(int1)" // 数值转字符串(方式一)
    print(str1) // 11
    print(type(of: str1)) // String
    let double1 = 11.1234
    let str2 = String(format: "%.2f", double1) // 数值转字符串(方式二)
    print(str2) // 11.12
    print(type(of: str2)) // String
}


第二类:Array<T>([T])、Dictionary<T1, T2>([T1 : T2])


1、数组的三操作
func demo() -> Void {
    // 1、定义:Swift中使用 [] 定义数组
    let arr = [1, 1, 2]
    print(arr) // [1, 1, 2]
    print(type(of: arr)) // Array<Int>(类型推断出arr为Array<Int>类型,即泛型数组。Array<Int>类型和[Int]类型是一样的,[Int]是语法糖)
    
    // 2、遍历:传统for循环在Swift里被取消了,所有的遍历都是以for-in的形式出现
    // 2.1 for-in模拟传统for循环(实际开发很少用)
    for index in 0..<arr.count {
        print(arr[index]) // 1 1 2
    }
    
    // 2.2 for-in直接拿数据内元素
    for item in arr {
        print(item) // 1 1 2
    }
    // 2.3 enum block遍历:同时拿下标和元素(元祖接收)
    for (index, item) in arr.enumerated() {
        print(index, item)
        // 0 1
        // 1 1
        // 2 2
    }
    // 2.4 反向遍历数组,for-in直接拿数据内元素
    for item in arr.reversed() {
        print(item) // 2 1 1
    }
    // 2.5 反向遍历数组,enum block遍历:同时拿下标和元素(元祖接收):必须先enumerated,后reversed
    for (index, item) in arr.enumerated().reversed() {
        print(index, item)
        // 2 2
        // 1 1
        // 0 1
    }
    
    // 3、增删改:Swift里用let定义的数组是不可变的,用var定义的数组是可变的
    // 定义一个空数组:Swift里的数组其实都是泛型数组,所以我们在定义空数组时,必须指定数组内要存储数据的数据类型;而且此处相当于调用的是构造方法,所以也要写一下(),试想一下OC里的定义空的可变数组,不也是调用了mutableCopy方法嘛[@[] mutableCopy]
    var arr1 = [Int]()
    print(arr1) // []
    print(type(of: arr1)) // Array<Int>
    
    // 增
    arr1.append(100)
    arr1.append(101)
    arr1.append(contentsOf: [102, 103])
    print(arr1) // [100, 101, 102, 103]
    
    // 删
    arr1.removeFirst()
    print(arr1) // [101, 102, 103]
    arr1.removeLast()
    print(arr1) // [101, 102]
    arr1.remove(at: 1)
    print(arr1) // [101]
    arr1.removeAll()
    print(arr1) // []
    
    // 改
    arr1.append(100)
    arr1[0] = 1000
    print(arr1) // [1000]
}
2、字典的三操作
func demo() -> Void {
    
    // 1、定义:Swift中还是使用 [] 定义字典
    // 泛型字典:Dictionary<String, Any>和[String: Any]都是指当前字典的类型,它俩是一样的,[String: Any]是语法糖
    let dict: Dictionary<String, Any> = ["1": 1, "2": "2"]
    print(dict) // ["1": 1, "2": "2"]
    print(type(of: dict)) // Dictionary<String, Any>(类型推断出dict为Dictionary<String, Any>类型,即泛型字典。Dictionary<String, Any>类型和[String : Any]类型是一样的,[String : Any]是语法糖)
    
    // 2、遍历:enum block遍历:同时拿下标和元素(元祖接收)
    for (key, value) in dict {
        print(key, value)
        // 1 1
        // 2 2
    }

    // 3、增删改:Swift里用let定义的字典是不可变的,用var定义的字典是可变的
    // 定义一个空字典:Swift里的字典其实都是泛型字典,所以我们在定义空字典时,必须指定字典内要存储数据的数据类型;而且此处相当于调用的是构造方法,所以也要写一下(),试想一下OC里的定义空的可变字典,不也是调用了mutableCopy方法嘛[@{} mutableCopy]
    var dict1 = [String : Any]()
    print(dict1) // [:]
    print(type(of: dict1)) // [String : Any](实际开发中,我们绝大多数情都是定义这种类型的泛型字典)

    // 增
    dict1["3"] = 3
    dict1["4"] = "4"
    dict1["5"] = 5
    print(dict1) // ["4": "4", "5": 5, "3": 3]

    // 删
    dict1.removeValue(forKey: "3")
    print(dict1) // ["4": "4", "5": 5]
    dict1.removeAll()
    print(dict1) // [:]
    
    // 改
    dict1["1"] = 1
    dict1["1"] = 1000
    print(dict1) // ["1": 1000]
}
3、as强转
  • Swift中除了NSString <->StringNSArray <->ArrayNSDictionary <->Dictionary外,绝大多数的as强转都需要加?!
  • 如果as前面的类型是可选的就用as?来强转,因为as后面是?,所以强转之后还是一个可选项类型;
  • 如果as前面的类型不是可选的就用as!,因为as后面是!,所以强转之后就直接是一个平常数据类型了;

将父类转化为子类的时候需要强转(例如弹出tableViewCell那种场景)、从服务端请求回来的数据想调用Array或者Dictionary的方法调不到时可先强转一下,Swift里的StringArrayDictionary想转换为OC里的时需要强转。

例:用as强转将请求到的数据强转为ArrayMap

func demo() -> Void {
    
    /*
     比如我们从服务端请求到了订单的数据为
    
     datas: [
        {
            "name" : "张三",
            "age" : 18
        },
        {
            "name" : "张三",
            "age" : 18
        }
     ]
     */

    // as:类似于OC中的强制转换(NSArray *)、(NSDictionary *)
    let dataArr = datas as [[String : Any]] // 强转为Array
    let dict = dataArr[0] as [String : Any] // 强转为Dictionary
}


第三类:函数


1、函数的定义

Swift用func关键字来定义一个函数,定义的格式为:

func 函数名(函数的参数) -> 函数的返回值 {
    函数的执行体
}

举个例子:

// 无参无返回值,函数fn1的类型为:() -> Void
func fn1() -> Void {
    print(23)
}

// 无参有返回值,函数fn2的类型为:() -> Int
func fn2() -> Int {
    return 23
}

// 有参无返回值,函数fn3的类型为:(Int) -> Void
func fn3(v: Int) -> Void {
    print(v)
}

// 有参有返回值,函数fn4的类型为:(Int, Int) -> Void
func fn4(v1: Int, v2: Int) -> Int {
    return v1 + v2
}
2、函数的声明

Swift里没有函数的声明这一步,所以我们直接定义和调用就可以了,但是要保证函数定义发生在函数调用之前。

3、函数的调用
fn1() // 23

print(fn2()) // 23

fn3(v: 23) // 23

print(fn(v1: 11, v2: 12)) // 23
4、函数的参数,一些额外的东西
  • 函数定义时,必须指定参数的类型。
func fn(v: Int) -> Void {
    print(v)
}
  • 函数的参数可以设置默认值,这一点在写构造方法时有明显优势,写一个万能构造方法(即参数最多的构造方法)时给默认参数就可以了,就不必像OC里那样再写一系列的其它初始化方法调用万能构造方法
func sum(num1: Int = 1, num2: Int = 2) -> Int {
    return num1 + num2
}

print(sum(num1: 10, num2: 20)) // 30
print(sum(num1: 10)) // 12
print(sum(num2: 10)) // 11
print(sum()) // 3
  • 函数调用时,必须把参数名给带上。
fn(v: 23)

如果想省略掉参数名,可以在参数名前加_

func fn(_ v: Int) -> Void {
    print(v)
}

fn(23)
  • 函数的“参数”还可以是两个单词的小短句(术语是参数标签),用的时候第二个单词就不会出现,这样可以使得函数更加易读。
func goToWork(at time: String, by vehicle: String) -> Void {
    print("time is \(time), vehicle is \(vehicle)")
}

goToWork(at: "8:00", by: "bike")
  • 可变参数(参数类型后面加上...

如果参数的个数是不确定的,那我们可以用可变参数,但是一个函数只能有一个可变参数。

func fn(vs: Int...) -> Void {
    print(vs)
}

fn(vs: 11, 12) // [11, 12]
  • 输入输出参数(参数类型前面加上inout

通常情况下,我们无法在函数内部通过参数来修改函数外部实参的值,因为参数的本质是值传递。

但是我们可以通过输入输出参数来实现这种需求,因为输入输出参数的本质是引用传递。

var number = 11

func fn(v: inout Int) -> Int {
    v = 12
    return v
}

print(fn(v: &number)) // 12


第三类:闭包表达式与闭包


首先我们要知道闭包表达式和闭包是两个东西,闭包表达式是用来定义函数的另一种方式,而闭包则是“在函数内部再定义一个函数 + 这个内部函数访问局部变量/常量 + 返回这个内部函数”。

1、闭包表达式(匿名函数)

在Swift里,我们可以通过func来定义一个函数,也可以通过闭包表达式来定义一个函数。闭包表达式的格式为:

{
    (函数的参数) -> 函数的返回值 in
    函数的执行体
}

举例:

// 通过func来定义一个函数
func fn(v1: Int, v2: Int) -> Int {
    return v1 + v2
}

fn(v1: 11, v2: 12)
// 通过闭包表达式来定义函数
var fn = {
    (v1: Int, v2: Int) -> Int in
    return v1 + v2
}

fn(11, 12) // 调用时不需要带参数名


{
    (v1: Int, v2: Int) -> Int in
    return v1 + v2
}(11, 12)

从第一公民的角度考虑闭包表达式(匿名函数)的使用场景(2最常用,其它知道即可):

  • 1、匿名函数可以赋值给一个变量供将来调用,也就是说它不能单独存在,因为它单独存在没有意义呀,它没有名字将来你拿什么调用它呢!何况这种场景的话我们一般就去使用普通函数了,所以这种场景也不常用
  • 2、我们也可以把匿名函数直接作为另一个函数的参数来使用,就是我们常说的回调,这种场景比较常见
  • 3、我们也可以把匿名函数直接作为另一个函数的返回值来使用,不过这个一般和第1条连用
  • 4、立即执行函数的优先考虑使用匿名函数,因为这种场景没必要再写一个普通函数了,普通函数之所以要有名字就是想很多地方都调用而不是立即执行,这种场景也比较常见
  • 5、类似于OC的block,可参考block的使用场景
2、闭包
2.1 闭包是什么

我们知道函数内部可以直接访问函数外部定义的全局变量。

var n = 11

func f1() {
    n += 1
    print(n)
}

f1() // 12

但是函数外部却无法直接访问函数内部定义的局部变量。

func f1() {
    var n = 11
    n += 1
}

print(n) // 报错:Use of unresolved identifier 'n'

如果出于某种需求,就是要这样做,那我们就得变通一下才能实现:在函数内部再定义一个函数,让这个内部函数去访问局部变量,然后把这个内部函数作为函数的返回值返回,这样不就能在函数外部访问到函数内部定义的局部变量了嘛。

func f1() -> () -> () {
    var n = 11
    
    func f2() {
        n += 1
        print(n)
    }
    return f2
}

var fn = f1()
fn() // 12

上面的实现其实就会形成一个闭包,所以闭包就是“在函数内部再定义一个函数 + 这个内部函数访问局部变量/常量 + 返回这个内部函数”,这三个条件必须同时满足才是闭包,否则就不是。

也就是说,“在函数内部再定义一个函数 ”它仅仅是个函数而已,并不是非得用闭包表达式定义,你也可以用func定义,但是必须得让“这个内部函数访问局部变量/常量”,只要访问了局部变量/常量,我们在“返回这个内部函数”这一步时,系统就会在堆区开辟一块专门的内存,并把这个局部变量的初始值复制到那块内存里,以后大家调用闭包函数访问的就是这块堆内存里的数据了,和那个局部变量再也没有关系。

2.2 闭包的特点
  • 能访问外层函数的局部变量/常量
func f1() -> () -> () {
    var n = 11
    
    func f2() {
        print(n)
    }
    
    return f2
}

var fn = f1()
fn() // 11
  • 会让“局部变量”始终保持在内存中

本质上就是因为我们在“返回这个内部函数”这一步时,系统就会在堆区开辟一块专门的内存,并把这个局部变量的初始值复制到那块内存里,以后大家调用闭包函数访问的就是这块堆内存里的数据了,和那个局部变量再也没有关系。

func f1() -> () -> () {
    var n = 11
    
    func f2() {
        n += 1
        print(n)
    }
    return f2
}

var fn = f1()
fn() // 12
fn() // 13
fn() // 14

照我们通常的理解,f1一调用完,fn就等于f2,这没问题,但是此时f1的作用域是已经销毁了的,所以局部变量n也就销毁了,即n对应的内存里已经是垃圾数据了,那接下来再调用fnn + 1,是会导致数据错乱的。

但是我们一运行,竟然发现数据没有错乱!而且打印的还是12、13、14,这样累加上去的,就好像n变成了一个全局变量一样!这是怎么回事呢?

这就是因为闭包导致的,内部函数f2访问了外层函数f1的局部变量n,所以在返回f2这一步,系统在堆区开辟一块专门的内存,并把这个局部变量n的初始值11复制到那块内存里,以后大家调用闭包函数访问的就是这块堆内存里的数据了,和那个局部变量再也没有关系。这也就解释了为什么没有发生数据错乱,而且还是累加效果。

不过有一点我们需要注意,那就是:我们每调用一次外层函数,就会导致“在函数内部再定义一个函数 + 这个内部函数访问局部变量/常量 + 返回这个内部函数”,也就是形成一个新的闭包,系统就会在堆区重新开辟一块新的堆内存,以供操作。

func f1() -> () -> () {
    var n = 11
    
    func f2() {
        n += 1
        print(n)
    }
    return f2
}

var fn = f1()
fn() // 12
fn() // 13
fn() // 14

var fn1 = f1() // 再次调用外层函数
fn1() // 12
fn1() // 13
fn1() // 14

我们看到第二次调用f1时,系统又重新开辟了一块堆内存,所以连续调用fn1,打印的依旧也是12、13、14,而不是在连续调用fn的那个堆内存基础上继续累加成15、16、17。

当然多个内部函数访问同一个局部变量时,也照样是调用一次外层函数开辟一块堆内存,而不是为每一个内部函数开辟一块堆内存,因为开辟堆内存是实际上是发生return内部函数这一步。

func f1() -> (() -> (), () -> ()) {
    var n = 11
    
    func f2() {
        n += 1
        print(n)
    }
    
    func f3() {
        n *= 2
        print(n)
    }
    
    return (f2, f3)
}

var (fn, fn1) = f1()
fn() // 12
fn1() // 24
fn() // 25
fn1() // 50
fn() // 51
fn1() // 102

所以打印是:12、24、25、50、51、102,而不是:12、24、13、48、14、96。


第三类:枚举


1、枚举的值
  • C的枚举
// 定义枚举
enum Direction {
    east, // 枚举值0,不是east哦,看着好像枚举值是east,但其实不是,它甚至连个变量都不是,仅仅是个肤浅的别名,C的枚举就是这么设计的,实际上这里就是个数字0
    west,
    south,
    north
};


// 定义枚举变量,把枚举值0赋值给它,那它的值就是数字0了,即它的内存里存储就是数字0
enum Direction direction = east;
printf("%d", direction); // 0

// 枚举变量的内存布局相关
printf("%d", sizeof(direction)); // 枚举变量占4个字节,因为它的本质就是int类型的数字
printf("%p", &direction); // 0x7ffeefbff54c4
C枚举变量占4个字节,里面存储着枚举值
  • Swift的枚举
// 定义枚举
enum Direction {
    case east // 枚举值east,枚举值就是Direction类型的east哦,不是int类型的0
    case west
    case south
    case north
}

// 定义枚举变量,把枚举值east赋值给它,那它的值就是east了,但是它的内存里存储的可不是Direction类型的east,而是int类型的0
// 这很诡异啊:你把某个值赋值给了某个变量,但是它的内存里存储的又不是这个值,而是别人
// 确实很诡异,但Swift的枚举就是这么设计的,它让编译器做了一些事,饶了一圈,节省了内存空间:枚举变量的值肯定是Direction类型的east、west、south、north,但是它的内存里存储的却总是0、1、2、3这样的标识,那么当编译器用到这个枚举变量时,从它内存里读取到0、1、2、3,就知道实际上是要用Direction类型的east、west、south、north,从而去拿相应的枚举值来使用
var direction = Direction.east
print(direction) // east 

// 枚举变量的内存布局相关
print(MemoryLayout<Direction>.size) // 枚举变量的实际大小: 1个字节
print(MemoryLayout<Direction>.stride) // 给枚举变量分配了多少内存: 1个字节
print(MemoryLayout<Direction>.alignment) // 枚举变量的内存对齐大小: 1个字节
print(Mems.ptr(ofVal: &direction)) // 0x00000001000076f8
枚举变量只占1个字节,里面存储着0、1、2、3这样的标识,而不是枚举值
2、枚举的原始值

我们知道我们可以自定义C的枚举值,让它不从0开始。

enum Direction {
    east = 11,
    west,
    south = 1111,
    north
};

enum Direction direction = east;
printf("%d", direction); // 11
printf("%p", &direction); // 0x7ffeefbff54c4

这样枚举变量的内存里就存储的是我们自定义的这个枚举值了。

C枚举变量依旧占4个字节,里面依旧存储着枚举值

Swift里也有类似这样的格式,但要注意仅仅是格式比较像而已,它们是完全不同的两个概念,这种格式在Swift里被称为原始值。它并不像C那样直接就是把枚举值给改掉了,而仅仅是把这个值赋值给了枚举的rawValue属性而已,将来想要拿这个值还得通过rawValue属性去拿。而且因为rawValue属性是个只读计算属性(这个后一篇文章会谈到),而不是存储属性,所以原始值不存储在枚举变量的内存里,因此枚举变量的内存布局是不受影响的,依旧占1个字节,里面依旧存储的是0、1、2、3这样的标识。

enum Direction: Int {
    case east = 11
    case west
    case south = 1111
    case north
    
    // 枚举的rawValue属性,其实就是类似下面这样一个计算属性,而且还是个只读的计算属性(即只有getter方法)
//    var rawValue: Int {
//        get {
//           switch self {
//           case .east:
//               return 11
//           case .west:
//               return 11
//           case .south:
//               return 1111
//           case .north:
//               return 1112
//           }
//        }
//    }
}

var direction = Direction.east
print(direction) // east
print(direction.rawValue) // 11

print(MemoryLayout<Direction>.size) // 枚举变量的实际大小: 1个字节
print(MemoryLayout<Direction>.stride) // 给枚举变量分配了多少内存: 1个字节
print(MemoryLayout<Direction>.alignment) // 枚举变量的内存对齐大小: 1个字节
print(Mems.ptr(ofVal: &direction)) // 0x00000001000076e0
枚举变量依旧只占1个字节,里面依旧存储着0、1、2、3这样的标识,而不是枚举值

而且Swift枚举的原始值不限于Int类型,它也可以是其它类型。

enum Direction: String {
    case east = "→"
    case west = "←"
    case south = "↓"
    case north = "↑"
}

var direction = Direction.east
print(direction) // east
print(direction.rawValue) // →
3、枚举的关联值

除了原始值之外,Swift的枚举还有关联值这么个东西。只不过关联值存储在枚举变量的内存中,枚举变量原来的0、1、2、3这样的标识存储在关联数据后一位,所以带关联值的枚举变量的内存布局会受影响,它也不再是只占1个字节。

比如成绩(Score),有的学校是给分数(point),有的学校是给等级(grade),所以成绩就可以定义为枚举,它有两种情况——即两个枚举值——分数和等级,同时我们又可以把一个具体的分数值或者等级值关联存储在枚举值身上。

enum Score {
    case point(Int) // Swift里Int类型占8个字节
    case grade(String) // Swift里String类型占16个字节
}

// 定义枚举变量,它的值为point,所以它的内存里会存“0”这个标识,关联值为94
var score1 = Score.point(94)
print(Mems.ptr(ofVal: &score1)) // 0x00000001000076d0

// 定义枚举变量,它的值为grade,所以它的内存里会存“1”这个标识,关联值为A
var score2 = Score.grade("A")
print(Mems.ptr(ofVal: &score2)) // 0x00000001000076e8

print(MemoryLayout<Score>.size) // 枚举变量的实际大小: 17个字节 = 16 + 1(最大枚举值String所占内存 + 1个字节的标识位)
print(MemoryLayout<Score>.stride) // 给枚举变量分配了多少内存: 24个字节
print(MemoryLayout<Score>.alignment) // 枚举变量的内存对齐大小: 8个字节
关联值存储在枚举变量的内存中,枚举变量原来的0、1、2、3这样的标识存储在关联数据后一位
关联值存储在枚举变量的内存中,枚举变量原来的0、1、2、3这样的标识存储在关联数据后一位

关联值的使用范例:

enum TestEnum {
    case test1(Int, Int, Int, Int)
    case test2(Bool)
    case test3
}

var e = TestEnum.test1(11, 12, 13, 14)
//e = .test2(false)
//e = .test3
switch e {
case let .test1(v1, v2, v3, v4):
    print(v1, v2, v3, v4)
case let .test2(v1):
    print(v1)
case .test3:
    print("test3")
}


第三类:可选项(平常数据类型后面加一个?


1、?的使用场景

  • 定义一个可选项类型的变量较常用!!!
  • 可选解包不太常用,我们看到的大多都是系统自动帮我们弹出的)
  • as强转较常用!!!Swift中除了NSString <->StringNSArray <->ArrayNSDictionary <->Dictionary外,绝大多数的as强转都需要加?!,如果as前面的类型是可选的就用as?来强转,如果as前面的类型不是可选的就用as!。)

2、!的使用场景

  • 强制解包不太常用
  • as强转较常用!!!Swift中除了NSString <->StringNSArray <->ArrayNSDictionary <->Dictionary外,绝大多数的as强转都需要加?!,如果as前面的类型是可选的就用as?来强转,如果as前面的类型不是可选的就用as!。)
1、可选项是什么

平常数据类型,变量值都不允许为nil

而可选项类型的变量,就既可以有值(但是这个值不是平常数据类型的值,而是可选项类型的值),也可以为nil,而且所有可选项类型的变量都把nil作为默认值。具体地说,可选项也是一种数据类型,它是平常数据类型加一个?形成的包装数据类型。比如Int?则为整型可选项,这种类型的变量既可以是个可选项类型的整型值(即包装整型值),也可以为nilPerson?则为Person?类型可选项,这种类型的变量既可以是个可选项类型的Person对象(即包装Person对象),也可以为nil......

其实可选项的本质就是个泛型枚举:

enum Optional<Wrapped> : ExpressibleByNilLiteral {
    case none // 一个简单的枚举值
    case some(Wrapped) // 枚举的关联值
}

所以下面两段代码是完全等价的。

var age: Int? = 11
age = 12
age = nil


var age: Optional<Int> = .some(11)
age = .some(12)
age = .none
2、可选项的解包

注意:可选项的解包仅仅发生在我们需要使用可选项类型的变量里包裹的具体数据时,因为可选项变量是不能直接使用的,只有它里面包裹着的具体数据才能直接使用。解包的方式有三种:强制解包、可选解包、可选项绑定解包。

2.1 强制解包(可选项变量后面加一个!

实际开发中,我们很少使用强制解包!,因为它有风险,而为了避免风险判断起来又太麻烦。通常使用强制解包的场景就是我们很明确某个变量不是空,然后直接把它强制解包出来使用或参与加减乘除这种计算。

如果我们想使用可选项里的具体数据,可以使用强制解包(可选项变量后面加个!)。

var age: Int? = 11
print(age) // Optional(11)
print(age!) // 11
print(age! + 1) // 12

但是使用强制解包有一个问题就是:nil进行强制解包,会崩。

var age: Int?
print(age!) // Fatal error: Unexpectedly found nil while unwrapping an Optional value

所以我们每次进行强制解包时都得主动判断一下可选项是不是nil,不是nil的情况下再解包。

var age = Int("abc11") // 可选项变量age

if age != nil { // 判断一下可选项变量age是不是nil
    print(age!)
} else {
    print("age为nil")
}
2.2 可选解包(在变量、属性或方法后加个?来访问数据)

实际开发中,可选解包?的使用场景就是对象访问它的属性或者调用它的方法这种链式的调用下,对对象进行一下可选解包来判断对象是否为nil,可选解包不能用来参与加减乘除那种计算。但我们也很少自己写可选解包,基本都是系统帮我们自动弹出来的。

可选解包侧重的是判断一下可选项变量是不是nil,从而决定这个可选项变量能否继续向下层访问数据(如属性、方法等),具体的规则是:

  • 如果可选项变量为nil,则停止向下访问,结果直接返回nil
  • 如果可选项变量不为nil,则继续向下访问,结果会把下层数据包装成一个可选项返回(注意不是原类型的数据了哦,包装了一下),当然如果下层数据本来就是可选项,就不会再包装一层了

举个例子:

我们知道访问字典的数据,是有可能返回nil的,所以拿字典的value时返回值就是一个可选项。

var scoreDict = [
    "Jack": [86, 87, 88],
    "Rose": [96, 97, 98]
]

var scoreArr = scoreDict["Hello"]
print(scoreArr) // nil

假设字典的数据是从服务端返回的,我们并不知道scoreArr拿成了nil,而又想读取scoreArr里的第一条数据,这时如果使用强制解包的话就会崩掉,if判断后再强制解包吧又太麻烦,这种情况使用可选解包就比较方便。

let scoreArr = scoreDict["Hello"]
let scoreArr1 = scoreDict["Jack"]
let scoreArr2 = scoreDict["Rose"]

print(scoreArr?[0]) // nil
print(scoreArr1?[0]) // Optional(86)
print(scoreArr2?[0]) // Optional(96)
2.3 可选项绑定解包(if let/varguard let/var

实际开发中,我们最多使用可选项绑定解包guard let/varif let/var,因为它们可以使我们的代码里很少出现一堆?!这样的很恶心人的解包方式,非常重要的解包方式,切记!

可选项绑定解包是指把可选项变量赋值给let/var,放在if语句里或者guard语句里来判断它是不是nil,并根据情况更简洁地做相应的操作。具体的规则是:

  • 如果可选项变量值为nil,直接返回false,什么都不做
  • 如果可选项变量值不为nil,则返回true,并且自动解包,把解包后的数据赋值给let/var
var age = Int("abc11") // 可选项变量age

// 可选项绑定
if let tempAge = age {
    // tempAge的作用域仅限于这个大括号,并且已经是解包之后的数据了
    print(tempAge)
} else {
    // tempAge的作用域不包含这个大括号
    print("age为nil")
}

举个例子,我们写个登录函数:

func login(with info: [String : String]) -> () {
    let username : String
    if let temp = info["username"] {
        username = temp
    } else {
        print("请输入用户名")
        return
    }
    
    let password : String
    if let temp = info["password"] {
        password = temp
    } else {
        print("请输入密码")
        return
    }

    print("用户名:\(username)  密码:\(password)")
}

let info = ["username" : "18888888888", "pwd" : "888888"]
login(with: info) // 请输入密码

但其实这种“提前退出——即某个条件不满足时就退出”的场景更适合用guard语句来实现。

// 当条件为false时,就会执行执行体
// 当条件为true时,就会跳过guard语句
guard 条件 else {
    // 执行体

    // 执行完执行体后,一定要用return、break、continue或throw error来退出当前guard语句
}

因此上面的登录函数可以替换为:

func login(with info: [String : String]) -> () {
    guard let username = info["username"] else {
        print("请输入用户名")
        return
    }
    
    guard let password = info["password"] else {
        print("请输入密码")
        return
    }
    
    // guard语句里,可选项绑定的let或var,
    // 它们的作用域不限于guard语句自己的大括号,而是外一层的大括号
    print("用户名:\(username)  密码:\(password)")
}

let info = ["username" : "18888888888", "password" : "888888"]
login(with: info) // 用户名:18888888888  密码:888888
2.4 小三目运算符??不解包,提供默认值法

对于解包,我们其实还有一种办法,那就是不解包,当发现数据为空时,提供一个默认值,这在些轻型场景里特别使用,主要用的就是小三目运算符??

比如一个函数的返回值是String类型,而在函数体内我们做了一堆计算后,得到一个String?类型,那么返回时就可以这么写,代码会很轻量:

extension Bundle {
    /// 获取项目的命名空间
    static func namespace() -> String {
        
        // Bundle.main:获取主Bundle,不是可选项类型
        let mainBundle = Bundle.main
        /**
         mainBundle.infoDictionary:获取info.plist文件,是一个可选项类型([String : Any]?)的字典
         所以如果想访问它的key-value时,需要使用可选解包
         */
        let infoDict = mainBundle.infoDictionary
        
        /**
         infoDict?["CFBundleName"]:获取包名,返回的当然也是一个可选项类型(Any?)
         而我们的返回值是一个String类型的,所以即便是把Any?强制解包成Any也是无法返回的,因为这就是子类指针指向父类对象了
         所以我们最好是先把Any?强转成String?,然后再对String?解包返回
         因为infoDict?["CFBundleName"]是个可选项类型,所以我们用as?来强转
         */
        let namespace = infoDict?["CFBundleName"] as? String
        
        /**
         上面我们已经用到了可选解包,它主要是用来访问下层数据
         而到了这一步,我们要直接使用可选项里的具体数据了,就需要用到另外三种解包方式了:
         */
        // 1、强制解包:代码太繁琐,不太推荐使用
//        if namespace != nil {
//            return namespace!
//        } else {
//            return ""
//        }
        // 2、guard let/var:这种场景使用guard let也略显繁琐,不太推荐使用,它还是比较适合那种提前退出的解包
//        guard let namespaceStr = namespace else {
//            return ""
//        }
//        return namespaceStr
        // 3、??:小三目运算符,其实功能类似于guard let/var和强制解包,本质都是发现包内数据为空时返回一个默认值,简洁,推荐使用
        return namespace ?? ""
    }
}

由此我们看出对于解包:

  • 强制解包真得很少用,因为老得先判断数据是不是空,偶尔也会用到,也一般用于那种我们很明确某个变量不是空,然后直接把它强制解包出来使用或参与加减乘除这种计算。
  • 可选解包会用到,但也不需要怎么担心,它只是用在那种访问下层属性、方法的场景
  • guard let/var解包会用到,会广泛用于那种需要提前退出的场景
  • 小三目运算符会用到,会广泛用于那种轻型解包的场景,给它提供一个默认值的方式,其实这种场景即便你用强制解包、guard let/var解包来写也是一样的


其它数据类型:元组


元组是其它数据类型的组合,每个元组的具体类型不是固定的,你等号右边是什么样子,它就是什么类型,它可以是任意数据类型的组合。Void的本质其实就是个空元组,typealias Void = ()

// tuple的类型为(String, String, Int)
var tuple: (String, String, Int) = ("张三", "男", 25)
print(tuple.0) // 张三
print(tuple.1) // 男
print(tuple.2) // 25

// tuple的类型为(name: String, sex: String, age: Int)
var tuple: (name: String, sex: String, age: Int) = (name: "张三", sex: "男", age: 25)
print(tuple.name) // 张三
print(tuple.sex) // 男
print(tuple.age) // 25

我们一般利用元组来同时给多个变量赋值,或者作为函数的返回值,来实现多返回值的效果。

// 同时给多个变量赋值
var (name, sex, age) = ("张三", "男", 25)
print(name) // 张三
print(sex) // 男
print(age) // 25

// 如果我们想忽略其中的某个值,可以用“_”来占位
var (name, _, age) = ("张三", "男", 25)
print(name) // 张三
print(age) // 25
// 作为函数的返回值,来实现多返回值的效果
func fn() -> (name: String, sex: String, age: Int) {
    return (name: "张三", sex: "男", age: 25)
}


其它数据类型:泛型


泛型理解:https://www.jianshu.com/p/2b6aebe33f67

1、泛型是什么

泛型的本质就是数据类型参数化,也就是说把所操作的数据类型指定为一个参数。这种数据类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法,从而编写出可重用的类、接口和方法,以便提高代码复用率。

2、以泛型函数为例

举个例子,我们想交换两个Int类型数据的值:

func swapValues(v1: inout Int, v2: inout Int) {
    (v1, v2) = (v2, v1)
}

var a = 11
var b = 12

swapValues(v1: &a, v2: &b)

print(a) // 12
print(b) // 11

我们又想交换两个Double类型数据的值:

func swapValues(v1: inout Double, v2: inout Double) {
    (v1, v2) = (v2, v1)
}

var a = 11.0
var b = 12.0

swapValues(v1: &a, v2: &b)

print(a) // 12.0
print(b) // 11.0

我们又想交换两个String类型数据的值:

func swapValues(v1: inout String, v2: inout String) {
    (v1, v2) = (v2, v1)
}

var a = "11"
var b = "12"

swapValues(v1: &a, v2: &b)

print(a) // "12"
print(b) // "11"

从上面代码来看,三个函数的功能是相同的,只是数据类型不一样而已,此时我们就可以使用泛型将数据类型参数化,以便提高代码复用率,这就是一个泛型函数。

func swapValues<T>(v1: inout T, v2: inout T) {
    (v1, v2) = (v2, v1)
}


var a = 11
var b = 12
swapValues(v1: &a, v2: &b)
print(a) // 12
print(b) // 11


var a = 11.0
var b = 12.0
swapValues(v1: &a, v2: &b)
print(a) // 12.0
print(b) // 11.0


var a = "11"
var b = "12"
swapValues(v1: &a, v2: &b)
print(a) // "12"
print(b) // "11"

占位数据类型也可以是多个,你想定义多少就定义多少。

func test<T1, T2>(v1: T1, v2: T2) {
    print(v1)
    print(v2)
}

test(v1: 11, v2: "12") // 11、"12"

泛型函数赋值给变量时,变量后面得指定函数的类型。

func test<T1, T2>(v1: T1, v2: T2) {
    print(v1)
    print(v2)
}

var fn: (Int, String) -> () = test
fn(11, "12") // 11、"12",函数赋值给变量,调用时不再需要些形参
3、以泛型结构体/类为例

我们想定义一个栈存储Int类型的数据:

struct Stack {
    var items = [Int]() // 用数组做容器
    
    mutating func push(item: Int) {
        items.append(item)
    }
    
    mutating func pop(item: Int) -> Int {
        return items.removeLast()
    }
}

我们又想定义一个栈存储Double类型的数据:

struct Stack {
    var items = [Double]() // 用数组做容器
    
    mutating func push(item: Double) {
        items.append(item)
    }
    
    mutating func pop(item: Double) -> Double {
        return items.removeLast()
    }
}

我们又想定义一个栈存储String类型的数据:

struct Stack {
    var items = [String]() // 用数组做容器
    
    mutating func push(item: String) {
        items.append(item)
    }
    
    mutating func pop(item: String) -> String {
        return items.removeLast()
    }
}

从上面代码来看,三个结构体的功能是相同的,只是数据类型不一样而已,此时我们就可以使用泛型将数据类型参数化,以便提高代码复用率,这就是一个泛型结构体。

struct Stack<T> {
    var items = [T]() // 用数组做容器
    
    mutating func push(item: T) {
        items.append(item)
    }
    
    mutating func pop(item: T) -> T {
        return items.removeLast()
    }
}


其它数据类型:Any


Any代表可以是任意类型。

var v: Any = Direction.east // 可以是枚举
v = 11 // 可以是整型(结构体)
v = "12" // 可以是字符串(结构体)
v = Person() // 可以指向类的实例
v = fn // 可以是函数
v = (13, 14) // 可以是元组


获取数据类型


func demo() -> Void {
    
    let str = "11"
    print(type(of: str)) // 获取常量、变量的数据类型
    print(String.self) // 获取数据类型的类型,类似于OC里的[NSString class]
}


三、特有的运算符


1、算术运算符

  • 去掉了++--运算符

2、区间运算符

  • 闭区间运算符:a...b,取值范围为[a, b]
  • 半开区间运算符:a..<b,取值范围为[a, b)
  • 单侧区间运算符:a...,取值范围为[a, ∞)...b,取值范围为(-∞, b]

3、比较运算符

  • ==!=运算符只用来判断最基本值类型的数据是否相等,如BoolIntFloatDoubleStringArrayDictionary,还有最简单的枚举、有原始值的枚举

  • 判断两个实例(包括有关联值的枚举、结构体、类的实例)是否相等要遵守Equatable协议并重载==!=运算符——枚举、结构体、类都可以为现有的运算符提供自定义的实现,告诉编译器比较规则是什么,什么情况下就判定为两个实例相等,这就是运算符重载

// 遵守Equatable协议
class Person: Equatable {
    var age: Int
    var name: String

    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
    
    // 重载==运算符,系统默认也会重载掉!=运算符
    static func == (lhs: Person, rhs: Person) -> Bool {
        // 这里是我们自定义的实现,假设当两个person的age相等时,我们就认为这两个person相等
        return lhs.age == rhs.age
    }
}

var p1 = Person(age: 11, name: "张三")
var p2 = Person(age: 11, name: "张三")

print(p1 == p2) // true


四、流程控制语句


1、条件语句

  • if...else
  • switch...case

2、循环语句

  • while
  • repeat...while
  • for...in

3、转向语句

  • break
  • continue
  • return
  • fallthrough