Swift Notes

一些关于Swift6 和SwiftUI 的笔记

Basic

打印常量和变量 Printing Constants and Variables

print(_:separator:terminator:)

  • separator : 用于分隔
  • terminator : 用于结尾
  • 字符串插值(string interpolation):"\(value)"

Decimal

  • 四则运算一定要同类型,否则会造成精度缺失或者一些更严重的问题。

  • 0.1 + 0.2 != 0.3

    计算机内部使用二进制来表示浮点数,而二进制无法精确表示十进制的小数点后无限位的数值。 具体来说,十进制小数 0.1 在二进制中是一个无限循环小数。二进制中没有直接的表示方法来精确表示 0.1,因此计算机使用最接近的浮点数表示来近似它。同样,0.2 也是一个无法精确表示的十进制小数,在二进制中有类似的近似问题。 当你把两个这样的近似值相加时,结果会受到这种近似的影响,产生一个稍微大于 0.3 的值。这是浮点数精度问题的一个常见例子,也是为什么在涉及浮点数的比较时,经常需要设置一个误差范围(或者使用特定的数学库来处理高精度的小数运算)。

    在Swift中,你可以使用 Double 类型来进行浮点数运算,但是由于其内部表示的限制,你可能会遇到这种精度问题。如果你需要更精确的小数运算,可以考虑使用 Decimal 类型,它提供了更高的精度,但相应的也会消耗更多的内存和计算资源。

Decimal 需要 Foundation Framework,是一个可选类型(Optional),你不能保证这个数字一定能转化成功

import Foundation

import Foundation 
let decimal: Decimal = 1 
let decimal2 = Decimal(1)
let decimal3: Decimal = 3.24 //这个写法是有问题的,因为相当于在Decimal中存了一个Double
print(decima13)//3.2400000000003

let decimal4 = Decimal(string: "hel1o")//
print(decima14)//nil

Decimal 类型用于表示高精度的十进制数。由于直接将浮点数(如 Double)转换为 Decimal 可能会导致精度问题,因此需要使用字符串初始化 Decimal 以避免这种问题。

let a = Decimal(string: "0.1")! 
let b = Decimal(string: "0.2")!
print(a + b) //0.3

//address the issue right?

Character & String 字符与字符串

在 Swift 中 String 类型是值类型。如果你创建了一个新的字符串,那么当其进行常量、变量赋值操作,或在函数/方法中传递时,会进行值拷贝。在前述任一情况下,都会对已有字符串值创建新副本,并对该新副本而非原始字符串进行传递或赋值操作。值类型在 结构体和枚举是值类型 中进行了详细描述。

值类型是这样一种类型,当它被赋值给一个变量、常量或者被传递给一个函数的时候,其值会被拷贝

在之前的章节中,你已经大量使用了值类型。实际上,Swift 中所有的基本类型:整数(integer)、浮点数(floating-point number)、布尔值(boolean)、字符串(string)、数组(array)和字典(dictionary),都是值类型,其底层也是使用结构体实现的。

扩展字符串分隔符

您可以将字符串文字放在扩展分隔符中,这样字符串中的特殊字符将会被直接包含而非转义后的效果。将字符串放在引号(")中并用数字符号(#)括起来。例如,打印字符串文字 #"Line 1 \nLine 2"# 会打印换行符转义序列()而不是给文字换行。

print(#"Line 1 \nLine 2"#)
//结果:Line 1 \nLine 2

如果要在使用扩展字符串分隔符的字符串中使用字符串插值,需要在反斜杠后面添加与开头和结尾数量相同扩展字符串分隔符。例如:

print(#"6 times 7 is \#(6 * 7)."#)
// 打印 "6 times 7 is 42."

注意

插值字符串中写在括号中的表达式不能包含非转义反斜杠(\),并且不能包含回车或换行符。不过,插值字符串可以包含其他字面量。

Unicode

Character由Unicode组成,StringCharacter组成

每一个 Swift 的 Character类型代表一个可扩展的字形群 (Extended Grapheme Clusters)。而一个可扩展的字形群构成了人类可读的单个字符,它由一个或多个(当组合时) Unicode 标量的序列组成。

具体来说,一个 String 可以包含一个或多个 Character,而每个 Character 可以包含一个或多个 Unicode 标量值。Swift 使用这种方式来确保字符串操作的正确性和一致性。

举个例子,String 的存储细节:

你好棒👍 => 20320 22909 26834 128077 127998

一个工具叫zalgo text文字生成器,可以随机组合Unicode生成一些乱码的文字

在 Swift 中,当你操作字符串(比如连接或修改字符串)时,即使涉及到可扩展的字形群集,字符串的字符数量(Character 的数量)可能不会改变。这是因为 Character 代表的是一个完整的可扩展字形群集,而不仅仅是单个的 Unicode 标量值。

也就是说,当你使用.count属性计算一个 String 的长度时,计算的数字不是 Unicode 标量值(Unicode Scalar)的数量,而是字符串中可视字符(即 Character)的数量。

四舍五入小数

let number = 123124.74023402394 

import Foundation 
let formatter = NumberFormatter()
formatter.maximumFractiohDigits = 2
print(formatter.string(for: number)!)

Enum 枚举声明

你可以用Bool处理有两种可能性的东西,但是当一种东西不只有两种可能性,你可以用enum,比如美国人的性别但是,当然,任然是有限种选择。

Optional也是一种enum

Range

三元条件运算符 ternary conditional operator

三元条件运算符是一种特殊的运算符,由三部分组成,形式为question ? answer1 : answer2

如果question为真,则评估answer1并返回其值;否则,评估answer2并返回其值。

三元条件运算符是以下代码的简写:

if question {
    answer1
} else {
    answer2
}

元组 Tuples & 类型别名 type aliases

元组(tuples) 把多个值组合成一个复合值。元组内的值可以是任意类型,并不要求是相同类型。

可以减去命名变量的痛苦,

let http404Error = (404, "Not Found")
// http404Error 的类型是 (Int, String),值是 (404, "Not Found")
//http404Error.0 == 404
//http404Error.1 == "Not Found"

你可以把任意顺序的类型组合成一个元组,这个元组可以包含所有类型。只要你想,你可以创建一个类型为 (Int, Int, Int) 或者 (String, Bool) 或者其他任何你想要的组合的元组。

元组分解(decompose):

let (statusCode, statusMessage) = http404Error
print("The status code is \(statusCode)")
// 输出“The status code is 404”
print("The status message is \(statusMessage)")
// 输出“The status message is Not Found”

你可以使用 typealias 关键字来定义类型别名。

当你想要给现有类型起一个更有意义的名字时,类型别名非常有用。假设你正在处理特定长度的外部资源的数据:

typealias AudioSample = UInt16

定义了一个类型别名之后,你可以在任何使用原始名的地方使用别名:

var maxAmplitudeFound = AudioSample.min
// maxAmplitudeFound 现在是 0

本例中,AudioSample 被定义为 UInt16 的一个别名。因为它是别名,AudioSample.min 实际上是 UInt16.min,所以会给 maxAmplitudeFound 赋一个初值 0

你可以把Tuples当做一个简单的类型使用

typealias Human = (String, Double, String)
let man: Human = ("A",10.2,"B")// 建立了一个“Human类型",但其实本质上是一个元祖

Set & Hashable

在 Swift 中,Hashable 是一种协议,类型可以遵循该协议以便在字典中用作键或存储在集合中。当一个类型遵循 Hashable 时,这意味着该类型的实例可以被哈希为 Int 类型的值,该值用于唯一标识该实例。

Why Hashable?

  • Dictionary Keys: When using a type as a key in a dictionary, the dictionary needs to quickly look up, add, and remove values. Hashing allows for efficient access.
  • Set Elements: Sets rely on hashing to ensure that elements are unique and to quickly check for membership.
  • Performance: Hashing provides constant-time complexity for operations like lookups, insertions, and deletions in hash-based collections.

Hashing is a fundamental concept in computer science that provides efficient data retrieval, especially in hash-based collections like dictionaries and sets.

  • Hash Function: Transforms keys into hash values.
  • Hash Table: Uses hash values to index and store key-value pairs.
  • Collision Handling: Manages cases where multiple keys produce the same hash value.
  • Efficiency: Direct indexing allows for constant-time complexity (O(1)) for insertions, deletions, and lookups on average.

How Hashing Works

  1. Hash Function: When an object (like a key in a dictionary) needs to be stored or retrieved, a hash function is applied to the object. This function computes a hash value (an integer) from the object’s data.

  2. Hash Table: This hash value is then used as an index to place the object in a hash table (an array). The hash table is a data structure that maps keys to values using the computed hash values.

Ensuring Quick Lookups

  • Direct Access: Since the hash value directly maps to an index in the hash table, accessing the data can be done in constant time, O(1). This means that the time it takes to retrieve the value does not depend on the number of elements in the table.

  • Uniform Distribution: A good hash function distributes hash values uniformly across the hash table, minimizing the number of collisions (when two different objects produce the same hash value).

Handling Collisions 处理哈希碰撞

Collisions are inevitable in hashing because different objects can produce the same hash value. However, efficient techniques exist to handle collisions:

  1. Chaining: Each index in the hash table points to a linked list (or another collection) of entries that have the same hash value. When a collision occurs, the new entry is added to the list at that index. Lookup operations involve scanning the list, which is generally short if the hash function distributes values well.

    Hash Table (using chaining):
    Index  | Entries
    0      | [ ]
    1      | [ ]
    2      | [ ]
    3      | [ "Key1" -> "Value1", "Key2" -> "Value2" ]
    4      | [ ]
    
  2. Open Addressing: When a collision occurs, the hash table looks for another open slot using a probing sequence (linear probing, quadratic probing, or double hashing). The lookup operation follows the same probing sequence to find the correct slot.

    Hash Table (using linear probing):
    Index  | Entry
    0      | [ ]
    1      | [ ]
    2      | [ "Key2" -> "Value2" ]
    3      | [ "Key1" -> "Value1" ]
    4      | [ ]
    

Constant-Time Complexity

Hashing ensures that the average time complexity for insertions, deletions, and lookups is O(1) under the following conditions:

  • Good Hash Function: A well-designed hash function that minimizes collisions and distributes keys uniformly.
  • Load Factor Management: The load factor (number of entries divided by the number of slots in the hash table) is kept low by resizing the table when necessary (rehashing). This ensures that the number of entries in each slot remains small.

一个类型为了存储在集合中,该类型必须是可哈希化的——也就是说,该类型必须提供一个方法来计算它的哈希值。一个哈希值是 Int 类型的,相等的对象哈希值必须相同,比如 a == b,因此必须 a.hashValue == b.hashValue

Swift 的所有基本类型(比如 StringIntDoubleBool)默认都是可哈希化的,可以作为集合值的类型或者字典键的类型。没有关联值的枚举成员值(在 枚举 有讲述)默认也是可哈希化的。

Set是一种遵照Hashable的类型,也就是数学中的集合,可以做集合运算,以及在集合中不能有重复的元素,所以很好理解,可以它声明为Hashable。

Swift 中的集合类型被写为 Set<Element>,这里的 Element 表示集合中允许存储的类型。和数组不同的是,集合没有等价的简化形式。

var letters = Set<Character>()
print("letters is of type Set<Character> with \(letters.count) items.")
// 打印“letters is of type Set<Character> with 0 items.”

注意

通过构造器,这里 letters 变量的类型被推断为 Set<Character>

一个集合类型不能从数组字面量中被直接推断出来,因此 Set 类型必须显式声明。然而,由于 Swift 的类型推断功能,如果你想使用一个数组字面量构造一个集合并且与该数组字面量中的所有元素类型相同,那么无须写出集合的具体类型。favoriteGenres 的构造形式可以采用简化的方式代替:

// Two ways to declare
var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"]
// favoriteGenres 被构造成含有三个初始值的集合

var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]

由于数组字面量中的所有元素类型相同,Swift 可以推断出 Set<String> 作为 favoriteGenres 变量的正确类型。

集合的一些数学操作

  • 使用 intersection(_:) 方法根据两个集合的交集创建一个新的集合。
  • 使用 symmetricDifference(_:) 方法根据两个集合不相交的值创建一个新的集合。
  • 使用 union(_:) 方法根据两个集合的所有值创建一个新的集合。
  • 使用 subtracting(_:) 方法根据不在另一个集合中的值创建一个新的集合。
let oddDigits: Set = [1, 3, 5, 7, 9]
let evenDigits: Set = [0, 2, 4, 6, 8]
let singleDigitPrimeNumbers: Set = [2, 3, 5, 7]

oddDigits.union(evenDigits).sorted()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
oddDigits.intersection(evenDigits).sorted()
// []
oddDigits.subtracting(singleDigitPrimeNumbers).sorted()
// [1, 9]
oddDigits.symmetricDifference(singleDigitPrimeNumbers).sorted()
// [1, 2, 9]
  • 使用“是否相等”运算符(==)来判断两个集合包含的值是否全部相同。
  • 使用 isSubset(of:) 方法来判断一个集合中的所有值是否也被包含在另外一个集合中。
  • 使用 isSuperset(of:) 方法来判断一个集合是否包含另一个集合中所有的值。
  • 使用 isStrictSubset(of:) 或者 isStrictSuperset(of:) 方法来判断一个集合是否是另外一个集合的子集合或者父集合并且两个集合并不相等。
  • 使用 isDisjoint(with:) 方法来判断两个集合是否不含有相同的值(是否没有交集)。
let houseAnimals: Set = ["🐶", "🐱"]
let farmAnimals: Set = ["🐮", "🐔", "🐑", "🐶", "🐱"]
let cityAnimals: Set = ["🐦", "🐭"]

houseAnimals.isSubset(of: farmAnimals)
// true
farmAnimals.isSuperset(of: houseAnimals)
// true
farmAnimals.isDisjoint(with: cityAnimals)
// true

可选类型*(optionals)*

可选类型表示两种可能: 或者有值, 你可以解析可选类型访问这个值, 或者根本没有值。

An Example:

Swift 的 Int 类型有一种构造器,作用是将一个 String 值转换成一个 Int 值。然而,并不是所有的字符串都可以转换成一个整数。字符串 "123" 可以被转换成数字 123 ,但是字符串 "hello, world" 不行。

构造器是一种特殊的函数,用于创建类、结构体或枚举的实例。它们不返回值,而是负责初始化实例的所有属性,并确保实例在第一次使用前处于有效状态。

struct Person {
 var name: String
 var age: Int

 init(name: String, age: Int) { //就是构造函数
     self.name = name
     self.age = age
 }
}

let person = Person(name: "John", age: 25)

下面的例子使用这种构造器来尝试将一个 String 转换成 Int

let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber 被推测为类型 "Int?", 或者类型 "optional Int"

因为该构造器可能会失败,所以它返回一个 可选类型 (optional)Int,而不是一个 Int。一个可选的 Int 被写作 Int? 而不是 Int。问号暗示包含的值是可选类型,也就是说可能包含 Int 值也可能 不包含值 。(不能包含其他任何值比如 Bool 值或者 String 值。只能是 Int 或者什么都没有。)

nil

你可以给可选变量赋值为 nil 来表示它没有值:

var serverResponseCode: Int? = 404
// serverResponseCode 包含一个可选的 Int 值 404
serverResponseCode = nil
// serverResponseCode 现在不包含值

注意

nil 不能用于非可选的常量和变量。如果你的代码中有常量或者变量需要处理值缺失的情况,请把它们声明成对应的可选类型。

如果你声明一个可选变量但是没有赋值,它们会自动被设置为 nil

var surveyAnswer: String?
// surveyAnswer 被自动设置为 nil

注意

Swift 的 nil 和 Objective-C 中的 nil 并不一样。在 Objective-C 中,nil 是一个指向不存在对象的指针。在 Swift 中,nil 不是指针——它是一个确定的值,用来表示值缺失。任何类型的可选状态都可以被设置为 nil,不只是对象类型。

if 语句以及强制解析

你可以使用 if 语句和 nil 比较来判断一个可选值是否包含值。你可以使用“相等”(==)或“不等”(!=)来执行比较。

如果可选类型有值,它将不等于 nil

if convertedNumber != nil {
    print("convertedNumber contains some integer value.")
}
// 输出“convertedNumber contains some integer value.”

当你确定可选类型确实包含值之后,你可以在可选的名字后面加一个感叹号(!)来获取值。这个惊叹号表示“我知道这个可选有值,请使用它。”这被称为可选值的 强制解析(forced unwrapping)

if convertedNumber != nil {
    print("convertedNumber has an integer value of \(convertedNumber!).")
}
// 输出“convertedNumber has an integer value of 123.”

更多关于 if 语句的内容,请参考 控制流

注意

使用 ! 来获取一个不存在的可选值会导致运行时错误。使用 ! 来强制解析值之前,一定要确定可选包含一个非 nil 的值。

函数

一些关于函数的概念:

  1. 形参(Formal Parameters 形式参数):在函数定义中声明的参数,它们是函数内部使用的变量,用于接收传递给函数的值。
  2. 实参(Actual Parameters 实际参数):在函数调用时传递给函数的值,它们是实际的数据,用于初始化形参。
  3. 返回类型:函数可以返回一个值,这个值的类型就是返回类型。如果函数不返回任何值,则返回类型为void
  4. 方法(Method):在面向对象编程中,方法是与对象相关联的函数。它通常可以访问和操作对象的属性。
  5. 属性(Property):对象的属性是与对象状态相关的变量。在面向对象编程中,属性可以是变量或函数,用于表示对象的状态。

属性如何作为函数?在面向对象编程(OOP)中,属性可以被看作是对象的特征或状态,它们可以是简单的变量,也可以是通过计算得到的值,这就涉及到计算属性的概念。

函数参数名称与参数标签

在Swift中,函数参数可以有两个名字:一个是参数标签(argument label),另一个是参数名称(parameter name)。参数标签在函数调用时使用,而参数名称在函数体内使用。

有点绕,简单点说就是第一个参数标签用于调用的时候,第二个参数名称用于函数体里面用

func greet(person name: String) {
   //greet(参数标签 参数名称: String)
    print("Hello, \(name)!")
}

greet(person: "Alice")
// 调用时使用参数标签 person

可以省略参数标签:

func greet(_ name: String) {
    print("Hello, \(name)!")
}

greet("Alice")
// 这样就类似C++里面的调用方式

参数标签和参数名称

  • 两个名称(参数标签和参数名称):第一个名称是参数标签,用于函数调用时,第二个名称是参数名称,用于函数体内。
  • 一个名称(参数标签和参数名称相同):如果只有一个名称,它既是参数标签,又是参数名称。
print(_: separator: terminator:)
//很明显这个separator就是参数标签==参数名称

可变参数

一个*可变参数(variadic parameter)*可以接受零个或多个值。函数调用时,你可以用可变参数来指定函数参数可以被传入不确定数量的输入值。通过在变量类型名后面加入(...)的方式来定义可变参数。

可变参数的传入值在函数体中变为此类型的一个数组。例如,一个叫做 numbersDouble... 型可变参数,在函数体内可以当做一个叫 numbers[Double] 型的数组常量。

下面的这个函数用来计算一组任意长度数字的 算术平均数(arithmetic mean)

func arithmeticMean(_ numbers: Double...) -> Double {
    var total: Double = 0
    for number in numbers {
        total += number
    }
    return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
// 返回 3.0, 是这 5 个数的平均数。
arithmeticMean(3, 8.25, 18.75)
// 返回 10.0, 是这 3 个数的平均数。

一个函数能拥有多个可变参数。可变参数后的第一个形参前必须加上实参标签。实参标签用于区分实参是传递给可变参数,还是后面的形参。

print中:

func print(_ items: Any..., separator: String = " ", terminator: String = "\n")

使用的Any...就表示可以用很多不同的类型,而且可以同时传入很多,(比如Int String Character etc.)

String = " "这个是预设数值

多重返回值函数

你可以用元组(tuple)类型让多个值作为一个复合值从函数中返回。

下例中定义了一个名为 minMax(array:) 的函数,作用是在一个 Int 类型的数组中找出最小值与最大值。

func minMax(array: [Int]) -> (min: Int, max: Int) { //多了一个(min: Int, max: Int)用于查询函数的返回值
    var currentMin = array[0]
    var currentMax = array[0]
    for value in array[1..<array.count] {
        if value < currentMin {
            currentMin = value
        } else if value > currentMax {
            currentMax = value
        }
    }
    return (currentMin, currentMax)
}

//(currentMin, currentMax) -> (min: Int, max: Int) 变量改为了上面所定义的min与max

minMax(array:) 函数返回一个包含两个 Int 值的元组,这些值被标记为 minmax以便查询函数的返回值时可以通过名字访问它们。

minMax(array:) 的函数体中,在开始的时候设置两个工作变量 currentMincurrentMax 的值为数组中的第一个数。然后函数会遍历数组中剩余的值并检查该值是否比 currentMincurrentMax 更小或更大。最后数组中的最小值与最大值作为一个包含两个 Int 值的元组返回。

因为元组的成员值已被命名,因此可以通过 . 语法来检索找到的最小值与最大值:

let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("min is \(bounds.min) and max is \(bounds.max)")
// 打印“min is -6 and max is 109”

需要注意的是,元组的成员不需要在元组从函数中返回时命名,因为它们的名字已经在函数返回类型中指定了。

面向对象 OOP 的哲学

面向对象编程(OOP)是一种编程范式,强调将数据和操作数据的代码封装在一起,称之为“对象”。OOP的哲学在于模拟现实世界,通过对象和类来表示和组织程序。

核心概念

  1. 对象(Object):现实世界中的实体在程序中的抽象。对象具有属性(数据)和方法(行为)。
  2. 类(Class):对象的蓝图或模板。类定义了对象的属性和方法。
  3. 封装(Encapsulation):将数据和操作数据的方法封装在对象内部,隐藏实现细节。
  4. 继承(Inheritance):一个类可以继承另一个类的属性和方法,促进代码重用。
  5. 多态(Polymorphism):对象可以用多种形式存在,允许不同对象以相同接口调用。

为什么叫“面向对象”

“面向对象”这个词反映了这种编程方法的核心:以对象为中心。程序中的所有东西都是对象,或者与对象相关。对象代表了程序中的实体,类定义了这些实体的结构和行为。

面向对象 vs 面向过程
  • 面向过程编程(Procedural Programming):将程序视为一系列步骤或过程(函数)的集合。程序的焦点在于函数和过程的调用顺序。
  • 面向对象编程(Object-Oriented Programming):将程序视为对象的集合。程序的焦点在于对象及其交互。
面向对象的本质和内涵
  • 模拟现实:通过对象和类模拟现实世界中的实体和关系。
  • 组织代码:通过类和对象组织代码,使其更具可读性和可维护性。
  • 封装和抽象:隐藏实现细节,只暴露必要的接口。
  • 复用性和可扩展性:通过继承和多态实现代码复用和扩展。
对象的真实含义

在OOP中,对象是程序中的基本单元,包含了属性和方法:

  • 属性(Attributes):对象的状态或数据。
  • 方法(Methods):对象的行为或功能。

例如,一个“Person”对象可能有“name”和“age”属性,以及“greet”方法。

计算属性

**计算属性是类或结构体中的属性,但它们的值不是直接存储的,而是通过计算得来的。**计算属性本质上是getter和setter函数:

  • Getter:返回计算属性的值。
  • Setter:设置计算属性的值。

为什么属性可以作为函数

计算属性之所以可以作为函数,是因为它们通过getter和setter函数计算和设置值。这样,属性的值可以根据其他属性动态计算,而不需要显式存储。

示例:计算属性

class Circle {
    var radius: Double

    var circumference: Double {
        get {
            return 2 * .pi * radius
        }
        set {
            radius = newValue / (2 * .pi)
        }
    }

    init(radius: Double) {
        self.radius = radius
    }
}

let circle = Circle(radius: 5)
print(circle.circumference) // 输出 31.4159

circle.circumference = 31.4159
print(circle.radius) // 输出 5

在这个示例中,circumference 是一个计算属性,通过getter和setter函数计算和设置圆的周长和半径。