Swift 笔记-1 基本类型,集合类型,控制流与基本函数
最近对 iOS 开发有兴趣,学习 SwiftUI,主要跟的是 hackingwithswift 的 swiftui 教程,这个教程做的非常棒
在这里简单记录一下
基本类型
变量与常量
变量 var
常量 let
swift 的常量控制更加严格,像结构体声明为常量是无法修改内部的值的,不像 js 的 const 对象还可以修改内部值,不过毕竟 struct 定义也不是引用类型,所以不能修改也是符合设计的
swift 的变量声明带有自动类型推导,就像 golang 中的 a := 3
,自动类型推导的常见问题是会产生不符合预期的效果,所以必要的时候,你需要明确指定类型
var number: Double = 2 // 如果用自动推导就会得到一个整型
字符串
单行
let str1 = "A"
let str2 = "A \(str1)" // 插值表达式,类似于 js 中的 `abc ${variable}`,python 中的 `name = f"hello {variable}"`...
多行
let movie = """
A day in
the life of an
Apple engineer
"""
类似于 python 中的注释语法,同样支持插值表达式
类似于 php 的 heredoc 语法,php 的末尾的符号必须贴边,swift 不用,但是不得超过字符起始位置
$a = <<<DOC
DOC;
字符串是被装箱过的,可以直接在字符串上调用方法
str.hasPrefix("ABC")
str.hasSuffix("ABC")
整型
这个 Int 型细分种类(U)Int
(U)Int8
(U)Int16
(U)Int32
(U)Int64
,但是官方建议通常只使用 Int,除非特别清楚自己在干什么
let cost = 18
print(cost.isMultiple(of: 6))
基本类型都带一种装箱功能
浮点
一样的,绝大大部分现代语言最佳实践都是强烈建议用 Double,把 Float 扔了吧
这是原因 Should I use double or float?
let height = 185.5 // 自动类型推导默认也是 Double 类型
布尔值
var timeout = false
timeout.toggle() // timeout value is: true
集合类型
数组
var games = ["factorio", "gta5", "diablo"]
// 非推导形式类型声明 1
// var games: Array<String> = ["factorio", "gta5", "diablo"]
// 非推导形式类型声明 2
// var games: [String] = ["factorio", "gta5", "diablo"]
// 空数组创建
// var foo = [String]()
// var bar: [String] = []
games.append("starcraft") // 所有数组项数据类型必须一致
print(games.count) // 4
games.remove(at: 0) // 注意到 `at:` 了吗,这是一个外部参数名,可以别名化
games.contains("gta5") // true
字典 Dictionaries
和 js 语法差不多,除了括号不一样
let book = [
"name": "the c programming language",
"isbn": "xxx",
]
// 非推导形式类型声明 1
// let book: Dictionary<String, String> = [
// "name": "the c programming language",
// "isbn": "xxx",
// ]
// 非推导形式类型声明 2
// let book: [String: String] = [
// "name": "the c programming language",
// "isbn": "xxx",
// ]
print(book["name", default: "empty"]) // 数组访问 [index] 外还支持 `defualt: "value"` 语法糖,类似于 optional 的 `book[name] ?? "empty"`
集合 Sets
不能重复,不保证有序的类型
js 也在 es6 引入了这种类型,去重数组项挺好用
var colors = Set(["Red", "Black", "White"])
// 非推导形式类型声明
// var colors: Set<String> = Set(["Red", "Black", "White"])
print(colors)
colors.insert("Blue")
colors.contains("Black") //
contains 在 Set 中是一个查找算法复杂度优势 Advantage 的操作(相比于数组)
even a set with 10,000,000 items will respond instantly
枚举 Enums
swift 中的枚举是强化过的,可以带方法
enum Version {
case A, B, C, D
}
var gv = Version.A
gv = .B // 直接忽略了前置调用对象
gv = .D
var gv2: Version = .A
控制流
条件判断
类似 golang 的判断语法格式
if a < 10 {
print(10)
} else if a < 20 {
print(20)
} else {
print(30)
}
switch 特殊一点,不需要 break,必须完全枚举所有可能性,无法穷举的值类型就使用 default:
let variable = 10
switch variable {
case 1:
print(1)
case 2:
print(2)
default:
print(3)
}
有三元表达式 expression ? A : B
循环
let b = [1,2,3,4,5]
for a in b {
print(a)
}
// 这种写法与 ruby 一样,`...` 与 `..` 的区别来控制是否包含末尾的值,或者也可以叫做开区间闭区间
for c in 1...3 { // 含有3本身
print(c)
}
for d in 1..< 3 { // 不含 3
print(d)
}
使用下划线对值进行忽略
var timer = 10
while timer > 0 {
timer -= 1
}
循环同样支持惯例的 continue
和 break
,但是通常不建议使用这两个关键字,有其他方法避免掉。
函数
声明函数
使用 func
func foo(number: Int) {
print(number)
}
foo(number: 10)
函数的参数名 number
必须在调用的时候明确指明,除非你使用 _
表示忽略
func foo(_ number: Int) {
print(number)
}
foo(10)
swift 函数拥有参数别名这种东西
返回类型声明
func foo(number: Int) -> Int {
return number
}
// 只有一行默认会 return,可以不写 return 关键字
func foo(number: Int) -> Int {
number
}
返回多个值
func getPoint() -> (x: Int, y: Int) {
(x: 10, y: 20)
}
let point = getPoint()
print("x: \(point.x)" y: \(point.y))
可以使用 _
忽略多值返回中的某个值
自定义参数标签
增强函数语境上下文,下面的例子中,参数 name
给调用方使用,参数 str
函数内部使用
func printUser(name str: String) {
print(str)
}
printUser(name: "Alice")
函数参数默认值
大部分现代编程语言都支持在函数参数中设置一个默认参数值
其中有个在函数调用时,明确指定参数名的好处是:你不需要将带有默认参数值的参数置后
func printUser(name: String = "Bob") {
print(name)
}
printUser()
函数与错误
函数声明
enum HttpError: Error {
case ClientError, ServerError
}
func check(_ statusCode: Int) throws -> String {
if statusCode == 400 {
throw HttpError.ClientError
}
if statusCode == 500 {
throw HttpError.ServerError
}
return "OK"
}
let code = 300
do {
let res = try check(code)
print("response is \(res)")
} catch HttpError.ClientError {
print("client error")
} catch {
print("error")
}
闭包 Closure
let sayHello = { (name: String) -> String in
"Hi \(name)!"
}
sayHello()
闭包的简写演化形式
let onlyT = team.filter({ (name: String) -> Bool in
name.hasPrefix("T")
})
// 去掉了显性类型指定
let onlyT = team.filter({ name in
name.hasPrefix("T")
})
// 去掉了外括号
let onlyT = team.filter { name in
name.hasPrefix("T")
}
// 更进一步,去掉 in 代码块:$0, $1 直接代指某位置参数
let onlyT = team.filter {
$0.hasPrefix("T")
}
结构体 Structs
最早接触结构体是在 C 语言当中,其实很多人不知道 C 语言其实是可以模拟出 OOP 的,只需要将指针变量指向函数就行,有一个 GLib 库就是这样搞的
所以这样再看 swift 中的结构体带有方法,其实本来就应该是很合理的一件事
struct Album {
let title: String
let artist: String
var isReleased = true
func printSummary() {
print("\(title) by \(artist)")
}
}
let red = Album(title: "Red", artist: "Taylor Swift")
print(red.title)
red.printSummary()
计算属性
和 vue 中的计算属性概念类似,不知道谁先模仿的谁,祖宗又是谁
struct Employee {
let name: String
var vacationAllocated = 14
var vacationTaken = 0
var vacationRemaining: Int {
vacationAllocated - vacationTaken
}
}
var vacationRemaining: Int {
get {
vacationAllocated - vacationTaken
}
set {
vacationAllocated = vacationTaken + newValue
}
}
属性观察者
当属性发生变化的时候,进行一些操作,同样的模式在 Vue 中也有,watch
willSet
在属性变化前运行
didSet
在属性变化后运行
struct Game {
var score = 0 {
didSet {
print("Score is now \(score)")
}
}
}
var game = Game()
game.score += 10
game.score -= 3
自定义构造器
swift 中的结构体默认会自己生成一个 properties 构造器,将所有自己有的属性全部进行一遍赋值操作的那种。
struct Player {
let name: String
let number: Int
// 你也可以手动创建一个构造器
init(name: String) {
self.name = name
number = Int.random(in: 1...99)
}
}
访问控制
private
私有,外部无法访问
private(set)
私有,外部可以访问,但无法设置值
fileprivate
文件内可访问
public
无限制
静态属性&方法
struct AppData {
static let version = "1.3 beta 2"
static let settings = "settings.json"
}
类 Classes
用于创建自定义数据类型,与结构体相似,但是有五个不同的地方
- 拥有传统 OOP 语言中类的
继承机制
- 因为继承机制,initializers(构造器) 多了一些特质
- 不会自动生成 initializers
- 如果一个子类有自定义 initializers,必须在设置完自己的 properties 后调用父类 initializers
- 如果子类没有 initializers,会自动继承父类的 initializers
- 类实例使用引用复制
- 有析构函数
deinit
- 哪怕类实例是声明为静态的,一样可以修改类的 properties
协议 Protocols
协议定义我们期望一个数据类型支持的功能,确保我们的代码符合规则要求,与其他语言中接口 Interface
的区别是:
- 可以声明属性
- 可以添加默认实现,类似于抽象类
protocol Vehicle {
var name: String { get } // 只读属性
var currentPassengers: Int { get set } // 读写属性
func estimateTime(for distance: Int) -> Int // 方法
func travel(distance: Int) // 方法
}
扩展 Extensions
这个和 Ruby
的打开类非常像,所谓的 打开类
就是为现有的任何类型(包括系统内建类型,自己创建的类型,以及第三方包中的类型)打自定义功能 Patch(但是要非常小心,Patch 打的不够优雅,就容易变成猴子补丁东一块西一块,关于如何打优雅的补丁是一个哲学和工程实践问题了)
// 为内置类型添加新的功能
extension String {
// trimmed 将字符串去掉两边的空白字符并返回一个修改好的字符串
func trimmed() -> String {
self.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
var quote = " The truth is rarely pure and never simple "
let trimmed = quote.trimmed() // outputs: "The truth is rarely pure and never simple"
如果想直接的修改原有字符串,而不是返回一个新的字符串,需要使用 mutating
关键字
extension String {
mutating func trim() {
self = self.trimmed()
}
}
quote.trim()
也可以添加计算属性
到类型当中
extension String {
var lines: [String] {
self.components(separatedBy: .newlines)
}
}
let lyrics = """
But I keep cruising
Can't stop, won't stop moving
"""
print(lyrics.lines.count) // ["But I keep cruising", "Can't stop, won't stop moving"]
协议扩展 Protocol extensions
为所有实现了此协议的类添加计算属性
和方法实现
,比如 Array
Dictionary
Set
均实现了 Collection
协议,我们能添加计算属性到这三个内建类型中
extension Collection {
var isNotEmpty: Bool {
isEmpty == false
}
}
let guests = ["Mario", "Luigi", "Peach"]
if guests.isNotEmpty {
print("Guest count: \(guests.count)")
}
这种方法允许我们在协议
中列出所有需要的方法,然后添加默认的实现到协议扩展
当中
所有实现了的类型会使用这些默认实现,当不能满足需求的时候,可以提供自己的实现
可选值 Optionals
表示可能会没有值的变量
实际上如果你经常开发数据库型应用,并且接触过很多的同事,就会发现你的同事当中肯定有两种数据表设计流派,一类是用
0
或者空字符串
表示不存在,另一类则完全遵守数据库标准,用NULL
表示不存在,因为关系型数据库是基于集合论而设计出来的,所以在涉及到关系型运算时会需要进行逻辑判断true
ORfalse
来取集合子集,而逻辑判断有一个前提条件为符合要求的值:一要存在,二要符合值的查询条件
这是一个区分神的逻辑
与人的逻辑
的问题,神知道世间万物的所有事情,那么所有值他都知道是什么,那么就是二元逻辑:true
ORfalse
但是人不一样,人有一些事是不知道的,比如现在问你 Bob 多少岁?你可能会说谁 tm 的是 Bob,哪个 Bob,你只能给出答案不知道
所以人的逻辑是三元逻辑:true
ORfalse
ORNULL
,因此因为数据库是人的数据库,就设计了NULL
这个特殊值表示不存在
,这是一个数据库设计中非常重要的东西,甚至为NULL
单独设计了判断语法is not null
,is null
编程语言一样存在这类问题,值存不存在和值是空值是两个不同的问题,可以看一下 Swift 是如何处理的
let opposites = [
"Mario": "Wario",
"Luigi": "Waluigi"
]
let peachOpposite = opposites["Peach"] // 访问不存在的字典索引 Peach
Swift 用 nil
表示没有值,连关键字都和 Ruby 一样
任何的数据都可以标记为 Optional,包括内建类型 Int
, Double
, Bool
,也包括 Enum
, Struct
, Class
使用可选值方式一 If
因为在表达式当中不能直接假设值存在,所以 Optional 数据无法被直接使用,必须要做 解包处理 unwrap
// 变量 marioOpposite = opposites["Mario"]
// 如果 marioOpposite 不存在,则打印一条信息提示
if let marioOpposite = opposites["Mario"] {
print("Mario's opposite is \(marioOpposite)")
}
使用可选值方式二 Guards
这是一种守卫判断,在《重构》中有所提及,也是我经常使用的代码技巧,简单来说就是先将可能发生的错误统一置于事务处理流的前面统一进行判断,好处就是不用将错误嵌入到事务代码中,可以让代码逻辑更清晰一些,不过有些人认为所有的 if 都需要 else,这种思维通常来源于函数式语言当中:所有表达式都必须返回一个值。
func printSquare(of number: Int?) {
guard let number = number else {
// number 不存在执行这里的代码
print("Missing input")
return // 这个是必须的
}
// number 存在执行这里的代码...
print("\(number) x \(number) is \(number * number)")
}
guard 语法也可以用于其他条件,不限于 unwrap optionals
使用可选值方式三 Nil Coalescing
PHP 的 ??
,?.
JS 的 ||
,?.
let tvShows = ["Archer", "Babylon 5", "Ted Lasso"]
let favorite = tvShows.randomElement() ?? "None"
// 链式 Optional
let names = ["Arya", "Bran", "Robb", "Sansa"]
let chosen = names.randomElement()?.uppercased()
print("Next in line: \(chosen ?? "No one")")
解包处理异常 try?
可能有值,也可能抛出异常的处理方式
// 继承 Error,然后实现自己的错误处理
enum UserError: Error {
case badID, networkFailed
}
// 这个函数可能返回 String,也可能抛出异常
func getUser(id: Int) throws -> String {
throw UserError.networkFailed
}
// unwrap 处理含有异常的函数
if let user = try? getUser(id: 23) {
// 如果用户存在,则打印用户名
print("User: \(user)")
}