【Swift】:基本语法

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


一、常量与变量


常量是指第一次赋值后,不能再次赋值的量,变量是指第一次赋值后,还能再次赋值的量。Swift要求我们在定义标识符时必须显式地告诉编译器它是一个常量还是一个变量,用let定义一个常量,用var定义一个变量。

// 常量
let a: Int = 11
//a = 1111 // 不能再次赋值

// 变量
var b: Int = 12
b = 1212 // 还能再次赋值

如果我们在定义一个量时会对它直接赋值,那就可以省略掉数据类型,编译器能自动推断出量的数据类型,这就是Swift的类型推断;如果我们在定义一个量时不对它直接赋值,那就不能省略掉数据类型、必须显性地告诉编译器它是什么数据类型。

//let a: Int = 11
let a = 11 // 定义一个量时会对它直接赋值,那就可以省略掉数据类型,编译器能自动推断出a的数据类型为Int

//var b // 编译报错
var b: Int // 定义一个量时不对它直接赋值,那就不能省略掉数据类型、必须显性地告诉编译器b是Int类型

tips:

  • 1️⃣因为常量是不可变的,数据更加安全,所以我们在实际开发中应该总是优先把一个量定义为常量,当发现某处需要变化这个量时再把它改成变量。


二、数据类型



基本数据类型:Bool、Int、Float、Double


let bool = true
print("bool = \(bool)") // bool = true

let int = 11
print("int = \(int)") // int = 11

let float = 11.11
print("float = \(float)") // float = 11.11

let double = 11.12
print("double = \(double)") // double = 11.12


基本数据类型:String、数组[T]、字典[T1 : T2]、元组


1、String
/*
 一、定义字符串:
 用let定义的字符串是不可变字符串
 用var定义的字符串是可变字符串
 */
var stringM = "可变字符串"
print(stringM) // 可变字符串

let string = "Hello World 你好世界"
print(string) // Hello World 你好世界

/*
 二、字符串的基本操作
 */
// 1、获取字符串的长度
print(string.count) // 16

// 2、截取字符串,也可以转换成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 你好世界

// 3、拼接字符串
let name = "张三"
let age = 18
let height = 180
let final = "我叫\(name) 年龄\(age) 身高\(height)"
print(final) // 我叫张三 年龄18 身高180

// 4、字符串的格式化
let h = 8
let m = 29
let s = 9
let dateStr = String(format: "%02d:%02d:%02d", h, m, s)
print(dateStr) // 08:29:09

/*
 三、字符串的遍历
 */
for char in string {
    print("char = \(char)")
}
2、数组[T]

Swift里的数组是个泛型集合,它的类型为[T]或Array<T>,[T]为语法糖。

/*
 一、定义数组:
 用let定义的数组是不可变数组
 用var定义的数组是可变数组
 */
let arr = [1, 1, 2]
print(arr) // [1, 1, 2]
print(type(of: arr)) // [T]或Array<Int>

var arrM = [Int]() // var arrayM = Array<Int>()也可以
print(arrM) // []
print(type(of: arrM)) // [T]或Array<Int>

/*
 二、数组的基本操作
 */
// 1、增
arrM.append(100)
arrM.append(101)
arrM.append(contentsOf: [102, 103])
print(arrM) // [100, 101, 102, 103]

// 2、删
arrM.removeFirst()
print(arrM) // [101, 102, 103]
arrM.removeLast()
print(arrM) // [101, 102]
arrM.remove(at: 1)
print(arrM) // [101]
arrM.removeAll()
print(arrM) // []

// 3、改
arrM.append(100)
arrM[0] = 1000
print(arrM) // [1000]

// 4、查
let num = arrM[0]
print(num) // 1000

/*
 三、数组的遍历
 传统for循环在Swift里被取消了,所有的遍历都是以for-in的形式出现
 */
// 1、for-in模拟传统for循环(实际开发很少用)
for index in 0..<arr.count {
    print(arr[index]) // 1 1 2
}
// 2、for-in直接拿数组内元素
for item in arr {
    print(item) // 1 1 2
}
// 3、enum block遍历:同时拿下标和元素(元组接收)
for (index, item) in arr.enumerated() {
    print(index, item)
    // 0 1
    // 1 1
    // 2 2
}
// 4、反向遍历数组,for-in直接拿数据内元素
for item in arr.reversed() {
    print(item) // 2 1 1
}
// 5、反向遍历数组,enum block遍历:同时拿下标和元素(元组接收):必须先enumerated,后reversed
for (index, item) in arr.enumerated().reversed() {
    print(index, item)
    // 2 2
    // 1 1
    // 0 1
}
3、字典[T1 : T2]

Swift里的字典是个泛型集合,它的类型为[T1 : T2]或Dictionary <T1, T2>,[T1 : T2]为语法糖。需要注意的是Swift里的字典不再是{}了,而是跟数组一样是[]

/*
 一、定义字典:
 用let定义的字典是不可变字典
 用var定义的字典是可变字典
 */
let dict = ["1": 1, "2": 2]
print(dict) // ["1": 1, "2": 2]
print(type(of: dict)) // [String : Int]或Dictionary<String, Int>

var dictM = [String : Any]() // var dictM = Dictionary<String, Any>()也可以
print(dictM) // [:]
print(type(of: dictM)) // [String : Any]或Dictionary<String, Any>

/*
 二、字典的基本操作
 */
// 1、增
dictM["name"] = "张三"
dictM["age"] = 18
dictM["height"] = 1.88
print(dictM) // ["age": 18, "height": 1.88, "name": "张三"]

// 2、删
dictM.removeValue(forKey: "height")
print(dictM) // ["age": 18, "name": "张三"]
dictM.removeAll()
print(dictM) // [:]

// 3、改
dictM["age"] = 18
dictM["age"] = 19
print(dictM) // ["age": 19]

// 4、查
let age = dictM["age"]
print(age) // Optional(19)

/*
 三、字典的遍历
 传统for循环在Swift里被取消了,所有的遍历都是以for-in的形式出现
 */
// 同时拿key和value(元组接收)
for (key, value) in dict {
    print(key, value)
    // 1 1
    // 2 2
}
4、元组

元组类似于数组或字典,也可以用来定义一组数据,但是元组没有具体的类型,等号右边是什么样子,它就是什么类型。Void的本质其实就是一个空元组,typealias Void = ()

// 像数组一样使用元组,只不过元组的语法糖是个小括号()
let tuple = ("张三", 18, 1.88)
print(type(of: tuple)) // (String, Int, Double)
print(tuple.0) // 张三
print(tuple.1) // 18
print(tuple.2) // 1.88

// 像字典一样使用元组,只不过元组的语法糖是个小括号()
var tuple1 = (name: "张三", age: 18, height: 1.88)
print(tuple1.name) // 张三
print(tuple1.age) // 18
print(tuple1.height) // 1.88

元组的应用场景主要有两个:1️⃣同时给多个变量赋值;2️⃣作为函数的返回值,来实现多返回值的效果。

// 同时给多个变量赋值
let (name, _, height) = ("张三", 18, 1.88)
print(name) // 张三
print(height) // 1.88

// 作为函数的返回值,来实现多返回值的效果(当然我们也可以通过数组或字典来实现,但元组明显更简单)
func fn() -> (name: String, age: Int, height: Double) {
    return (name: "张三", age: 18, height: 1.88)
}


基本数据类型:函数(参数类型T1, 参数类型T2) -> 返回值类型T


在很多面向对象的语言里(例如Java和OC),类是一等公民,所谓一等公民是指类创建出来的对象可以赋值给一个变量,也可以作为函数的参数和返回值,函数并不是一等公民。而在Swift里函数也是一等公民,这就意味着函数也可以赋值给一个变量,或者作为另一个函数的参数和返回值。

1、函数的定义

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

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

举个例子:

// 无参无返回值(无返回值时-> Void可省略)
//func fn1() -> Void {
//    print(23)
//}
func fn1() {
    print(23)
}
print(type(of: fn1)) // () -> Void

// 无参有返回值
func fn2() -> Int {
    return 23
}
print(type(of: fn2)) // () -> Int

// 有参无返回值(无返回值时-> Void可省略)
//func fn3(v: Int) -> Void {
//    print(v)
//}
func fn3(v: Int) {
    print(v)
}
print(type(of: fn3)) // (Int) -> Void

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

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

3、函数的调用
fn1() // 23
print(fn2()) // 23
fn3(v: 23) // 23
print(fn4(v1: 11, v2: 12)) // 23
4、函数的使用注意
  • 1️⃣函数的参数类型

函数定义时,必须得指定参数的数据类型。

func fn(v: Int) -> Void {
    print(v)
}
  • 2️⃣函数的必选参数和可选参数

当我们不给函数的参数赋默认值时,这个参数就是个必选参数,函数调用时就必须得传这个参数;当我们给函数的参数赋默认值时,这个参数就是个可选参数,函数调用时就可以选择性地传这个参数。

// num1必选参数,num2可选参数
func sum(num1 : Int, num2 : Int = 2) -> Int {
    return num1 + num2;
}

print(sum(num1: 11)) // num1必须得传,num2可以不传,结果为13
print(sum(num1: 11, num2: 22)) // num1必须得传,num2也可以传,结果为33

当然如果我们想在函数调用时省略掉参数名,那就可以在函数定义时参数名前面加一个_,这不影响参数的必选和可选性。

func sum(_ num1 : Int, _ num2 : Int = 2) -> Int {
    return num1 + num2;
}

print(sum(11)) // 11
print(sum(11, 22)) // 33
  • 3️⃣函数的可变参数(参数类型后面加上...

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

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

fn(args: 11, 12) // [11, 12],可见其实args变成了一个[Int]数组
  • 4️⃣函数的参数标签

函数的“参数”还可以是两个单词的小短句(术语是参数标签),用的时候第二个单词就不会出现,这样可以使得函数更加易读。

func goToWork(at time: String, by vehicle: String) -> Void {
    print("time is \(time), vehicle is \(vehicle)")
}

goToWork(at: "8:00", by: "bike")
  • 5️⃣函数的输入输出参数(参数类型前面加上inout

通常情况下,我们无法在函数内部通过参数来修改函数外部实参的值,因为参数的本质是值传递。但是我们可以通过输入输出参数来实现这种需求,因为输入输出参数的本质是引用传递。

var number = 11

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

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


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


1、可选项是什么

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

而可选项类型的变量,就既可以有值(但是这个值不直接是平常数据类型的值,而是可选项类型的值),也可以为nil,而且所有可选项类型的变量的默认值都是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、可选项的解包

可选项的解包方式有三种:强制解包、可选项绑定解包、小三目运算符解包。

  • 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? = 11
if age != nil { // 判断一下可选项变量age是不是nil
    print(age!)
} else {
    print("age为nil")
}
  • 2️⃣可选项绑定解包(if let/varguard let/var

上面所说的强制解包使用起来有点麻烦,因为它需要我们开发者做两步:第一步判断是不是nil,第二步不是nil的情况下解包,因此Swift就推出了一个语法——可选项绑定解包来帮我们简化解包过程。可选项绑定解包是指把可选项变量赋值给let/var并放在if语句或guard语句里,然后这个语法就会根据情况来帮我们自动解包,具体的规则是:如果可选项变量的值为nil,直接返回false,什么都不做;如果可选项变量的值不为nil,则返回true,并且自动解包,把解包后的数据赋值给let/var

var age: Int? = 11
if let age = age {
    // 临时常量age的作用域仅限于这个大括号,并且已经是解包之后的数据了
    print(age)
} else {
    // 临时常量age的作用域不包含这个大括号
    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语句来实现,因此上面的登录函数可以替换为:

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
  • 3️⃣小三目运算符解包??

如果??前面的可选项有值,那么就会自动解包后使用该值;如果??前面的可选项为nil,就使用??后面的值。

let str1: String? = 11
let str2: String = str1 ?? ""
print(str2) // 11

let str1: String? = nil
let str2: String = str1 ?? ""
print(str2) // ""

tips:

1、各个层怎么定义变量或属性?

  • 1️⃣Model层里的一些变量或属性:全都定义成可选项(无论是从服务器请求回来的model还是本地自定义的model),一来我们不确定服务器确实能给我们返回有效数据,二来我们可以通过这种方式强制让UI层面向VM层而非Model层开发(因为只有添上VM层,UI层才不会出现那么多对Model层数据解包的数据处理操作);
  • 2️⃣View层和Controller层的一些变量或属性:(1)优先定义为非可选项,能初始化的就直接初始化掉;(2)不方便初始化的——是指直接初始化代码太长了,那就写个lazy + 闭包给它初始化掉;(3)没办法初始化的——是指初始化这个属性需要依赖别的属性才能初始化,那没办法,此时再定义为可选项;
  • 3️⃣VM层的一些变量或属性:全都定义成非可选项存储属性或非可选项计算属性(即set/get方法)供UI层直接使用,VM里负责对Model的数据进行判空处理;
  • 4️⃣以上已经定下了基本规则,遵守就行了,但也不能完全拘泥,比如上面本来应该定义成非可选的地方你发现这个变量或属性的nil有具体的作用,那就定义成可选项就ok了,如nil代表全部学科、0代表语文、1代表数据,又如我给你传nil代表不使用你的回调方法。

2、怎么对可选项解包?(类型强转也是同样的处理方式)

  • 1️⃣优先考虑使用可选项绑定解包,guard let/varif let/var优先使用guard let/var,当你觉得使用guard let/var写出来的代码别扭时再换成if let/var
  • 2️⃣次则考虑使用小三目运算符解包;
  • 3️⃣最后考虑使用强制解包,这是当你非常肯定某个可选项绝对不可能为nil时才使用的解包方式。


补充数据类型:闭包表达式/匿名函数


闭包表达式又叫匿名函数,它和普通函数没什么大区别,只是没有名字而已。闭包表达式的格式为:

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

所以我们可以从一等公民的角度来考虑闭包表达式的应用场景:

  • 1️⃣闭包表达式可以赋值给一个变量供将来调用,也就是说它不能单独存在,因为它单独存在没有意义,它没有名字将来你拿什么调用它,常用来做属性类型的callback
var fn = {
    (v1: Int, v2: Int) -> Int in
    let temp1 = v1 * 2
    let temp2 = v2 * 2
    return temp1 + temp2
}
print(fn(11, 12)) // 46
  • 2️⃣我们可以把闭包表达式作为另一个函数的参数来使用,也就是我们常说的回调,常用来函数参数类型的callback
func loadData(method: String, url: String, params: [String : Any]?, completionHandler: @escaping (_ jsonData: Any?, _ success: Bool) -> Void) {
    DispatchQueue.global().async {
        // 子线程请求数据中...
        
        DispatchQueue.main.async {
            // 回到主线程...
            
            completionHandler(["name": "张三", "age": 18, "height": 1.88], true)
        }
    }
}
loadData(method: "GET", url: "https://xx", params: nil) { jsonData, success in
    if let jsonData = jsonData as? [String : Any] {
        print(jsonData) // ["name": "张三", "age": 18, "height": 1.88]
    }
}

// 在大括号后面紧跟着一个[weak self],用来解决循环引用问题
loadData(method: "GET", url: "https://xx", params: nil) {[weak self] jsonData, success in
    print(self)
    
    if let jsonData = jsonData as? [String : Any] {
        print(jsonData) // ["name": "张三", "age": 18, "height": 1.88]
    }
}
  • 3️⃣我们可以把闭包表达式作为另一个函数的返回值来使用,不过这个一般需要和第1️⃣条连用,不常用
func getFn() -> ((Int, Int) -> Int) {
    return {
        (v1: Int, v2: Int) -> Int in
        let temp1 = v1 * 2
        let temp2 = v2 * 2
        return temp1 + temp2
    }
}
let fn = getFn()
print(fn(11, 12)) // 46
  • 4️⃣立即执行的函数优先考虑使用闭包表达式,常用来初始化不方便初始化的属性
lazy var tableView: UITableView = {
    let tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 414, height: 736), style: .plain)
    tableView.dataSource = self
    tableView.delegate = self
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseId)
    return tableView
}()


补充数据类型:枚举


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")
}


补充数据类型:泛型


泛型理解: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()
    }
}


补充数据类型:AnyObject和Any


AnyObject代表任意class类型的实例,Any的范围更广,代表任意类型。

class Pig {}
class Dog {}

let arr : [AnyObject] = [Pig(), Dog()]
let arr1: [Any] = [Pig(), Dog(), true, 1, 2.2, "3", ["你好"], ["name" : "张三"]]


补充数据类型:类名.self、类名.Type、AnyClass


  • 类名.self用来获取类,类似于OC里的[类名 class];
  • 那类的类型是什么呢?类的类型就是.Type,比如类名.self的类型就是类名.Type,类似于OC里的元类;
  • AnyClass代表任意class ,类似于OC里的Class
class Person {}
var p = Person()

print(type(of: p)) // Person,p对象的类型为Person
print(type(of: Person.self)) // Person.Type,Person类的类型为Person.Type
print(Person.self is AnyClass) // true


补充:判断是否为某种类型is


print(11 is Bool) // false
print(11 is Int) // true
print(11 is Float) // false
print(11 is Double) // false

print([11, 12] is String) // false
print([11, 12] is [Int]) // true
print([11, 12] is [Int : Int]) // false
print([11, 12] is ()) // false

func fn() {}
print(fn is () -> ()) // true

print(11 is Int?) // true

// is用在类上时,作用类似于OC里的isKindOfClass
class Person: NSObject {}
class Student: Person {}
var v = Person()
print(v is Person) // true
print(v is Student) // false
// 当然因为这里Person和Student继承自NSObject,所以也可以用isKindOfClass和isMemberOfClass来判断
print(v.isKind(of: Person.self)) // true
print(v.isKind(of: Student.self)) // false
print(v.isMember(of: Person.self)) // true
print(v.isMember(of: Student.self)) // false


补充:类型强转asas?as!


  • 1️⃣as一般用来做Swift和OC的桥接转换
Swift和OC的桥接转换表
// String -> NSString
let str = "11 12"
print(type(of: str)) // String
let str1 = str as NSString
print(type(of: str1)) // NSTaggedPointerString

// Array -> NSArray
let arr = [11, 12]
print(type(of: arr)) // [Int]
let arr1 = arr as NSArray
print(type(of: arr1)) // __SwiftDeferredNSArray
 
// Dictionary -> NSDictionary
let dict = ["11": 11, "12": 12]
print(type(of: dict)) // [String : Int]
let dict1 = dict as NSDictionary
print(type(of: dict1)) // _SwiftDeferredNSDictionary
  • 2️⃣as?一般用来把一个东西强转成可选项,as!一般用来把一个东西强转成平常数据类型,但是as!很危险,所以实际开发中我们都是用as? + 自己解包,非你非常确定被强转的内容肯定不为nil再用as!
// 姓名,我们定义为可选项,nil代表还没取名字
var name: String?
// 年龄
var age: Int
// 身高
var height: Double

// 假设我们从服务端请求到了一个人的信息
let info: [String : Any] = [
    "name": "张三",
    "age": 18,
    "height": 1.88,
]

// 系统设定从字典里拿数据的时候,总是返回可选项,因为它不确定字典里到底有没有我们想拿的数据
print(type(of: info["name"]), info["name"]) // Any? Optional("张三")
print(type(of: info["age"]), info["age"]) // Any? Optional(18)
print(type(of: info["height"]), info["height"]) // Any? Optional(1.88)

/*
 name = info["name"] // 不能直接赋值,因为这相当于是一个子类的指针指向父类的对象,会报错Cannot assign value of type 'Any?' to type 'String?',所以我们得把Any?类型强转为String?类型
 name = info["name"] as String? // 不能这么写,因为info["name"]是Any?类型,服务端给我们传过来的东西的真正类型可能不是一个字符串,所以转字符串是有可能失败的,所以得用as?
 */
name = info["name"] as? String
print(type(of: name), name) // String? Optional("张三")

/*
 age = info["age"] // 不能直接赋值,因为这相当于是一个子类的指针指向父类的对象,会报错Cannot assign value of type 'Any?' to type 'Int',所以我们得把Any?类型强转为Int类型
 age = info["age"] as Int // 不能这么写,因为info["age"]是Any?类型,服务端给我们传过来的东西的真正类型可能不是一个整型,所以转整型是有可能失败的,所以得as?,当然我们也可以冒着崩溃的风险使用as!
 */
age = info["age"] as! Int
print(type(of: age), age) // Int 18

/*
 我们当然可以像上面拿age一样用as!来拿height,但as!和强制解包面临同样的问题:对nil进行类型强转,会崩,所以实际开发中我们很少用as!
 遇到这种场景时,我们一般会分成两步来做:第一步先用as?做一下类型转换,第二步我们自己做可选项绑定解包
 let optionalHeight: Double? = info["height"] as? Double
 if let tempHeight = optionalHeight {
     height = tempHeight
     print(type(of: height), height)
 }
 */
if let tempHeight = info["height"] as? Double {
    height = tempHeight
    print(type(of: height), height) // Double 1.88
}


三、特有的运算符


1、算术运算符

  • 必须得是相同类型的数据才能做运算,不会像C语言那样做自动类型转换,比如Int和Double数据不能做加减乘除
  • 去掉了++--运算符,因为这俩运算符不太易读,比如++age + age++甚至在不同的编译器下都有不同的结果

2、区间运算符

  • 闭区间运算符:a...b,取值范围为[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
/*
 和OC的区别:
 1、if后面的小括号可以省略
 2、判断条件没有非0即真了,也就是说判断条件必须是一个明确的Bool值(true or false)
 */
let score = 94
if score < 0 || score > 100 {
    print("分数出错")
} else if score < 60 {
    print("不及格")
} else if score < 80 {
    print("及格")
} else if score < 90 {
    print("良好")
} else {
    print("优秀")
}
  • guard...else

guard...elseif...else的功能几乎一模一样,所以它俩往往可以换着用,但guard...else更适合于那种“提前退出——即某个条件不满足时就退出”的场景,设计它的目的就是为了提高在这种场景下程序的可读性。

/*
 OC里没有guard...else,是Swift里特有的
 当判断条件为false时,就会执行执行体
 当判断条件为true时,就会跳过guard...else语句, 执行后面的代码
 */
guard 判断条件 else {
    // 执行体

    // 执行完执行体后,一定要用return、break、continue或throw error来退出当前guard...else语句
}
// 使用if...else
func login1(username: String?, password: String?) {
    if username == nil {
        print("请输入用户名")
        return
    }
    
    if password == nil {
        print("请输入密码")
        return
    }
    
    print("登录中...")
}
login1(username: nil, password: "456")

// 使用guard...else
func login2(username: String?, password: String?) {
    // 守卫第一个条件,不通过就别往下走了
    guard username != nil else {
        print("请输入用户名")
        return
    }
    
    // 守卫第二个条件,不通过也别往下走了
    guard password != nil else {
        print("请输入密码")
        return
    }
    
    // 当过了层层守卫之后,才能执行guard...else后面的代码
    print("登录中...")
}
login2(username: "123", password: "456")
  • switch...case
/*
 和OC的区别:
 1、switch后面的小括号可以省略
 2、case、default后面的break可以省略(系统会帮我们添上)
 3、如果想要多个case执行同样的代码,直接在case后面写多个条件即可,而不是像OC里那样并列多个case
 4、如果想要case穿透,可以在case后面加一个fallthrough
 5、OC的里switch-case只能判断整型,而Swift里的switch-case可以判断很多东西,如:Bool、整型、浮点型、字符串、枚举、区间运算等
 */
// 0:女
// 1:男
// 2、3:人
// 4:试试穿透
let sex = 2
switch sex {
case 0:
    print("女")
case 1:
    print("男")
case 2, 3:
    print("人")
case 4:
    print("试试穿透")
    fallthrough
default:
    print("性别出错")
}
  • 三目运算符
// 和OC没有区别
let num1 = 20
let num2 = 30
let max = num1 >= num2 ? num1 : num2

2、循环语句

  • while
/*
 和OC的区别:
 1、while后面的小括号可以省略
 2、判断条件没有非0即真了,也就是说判断条件必须是一个明确的Bool值(true or false)
 */
var a = 0
while a < 10 {
    print("a=\(a)")
    a += 1
}
  • repeat...while
/*
 和OC do...while的区别: 
 1、while后面的小括号可以省略
 2、判断条件没有非0即真了,也就是说判断条件必须是一个明确的Bool值(true or false)
 */
var b = 0
repeat {
    print("b=\(b)")
    b += 1
} while b < 10
  • for...in
/*
 和OC的区别:
 1、没有普通的for循环写法,只有for...in循环
 2、如果我们只是想循环打印某个东西,而用不到次数这个变量,那就可以用下划线_来代替省略这个变量(Swift中如果一个标识符不会被使用,那就可以用下划线_来代替省略这个标识符)
 */
for i in 0...9 {
    print("i=\(i)")
}

for _ in 0..<10 {
    print("Hello World")
}

3、转向语句

  • break:和OC没有区别,用来终止整个循环
  • continue:和OC没有区别,用来终止本次循环
  • return:和OC没有区别,用来退出函数
  • fallthrough:OC里没有fallthrough,是Swift里特有的,用来做case穿透
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352