在数组的方括号内必须写上类型,或者在Array后面的尖括号<>内写上类型。
对于字典而言,你一共需要提供两个类型,一个用于键,而另一个用于值。
Swift要求所有的变量和常量都必须有值。你可以在声明它们的时候给它们指定一个值,也可以通过init方法给它们分配值。
有时,你需要一个变量可以没有值,这种情况,你需要将变量声明为可选型:
var checklistToEdit: Checklist?
你不能直接使用这种类型的变量;你必须在使用它们之前,侦测一下其中是否有值,这个行为就叫做可选型解包:
if let checklist = checklistToEdit {
// “checklist” now contains the real object
} else {
// the optional was nil
}
下面例子中的变量age就是一个可选型,因为没有任何保证说字典中存在一个名为"Jony Ive"的键,所以age的类型是Int?,而不是Int:
if let age = dict["Jony Ive"] {
// 使用age变量
}
如果你100%的确定字典中存在一个叫做"Jony Ive"的键的话,那么你就可以对age变量进行强制解包:
var age = dict["Jony Ive"]!
你使用感叹号来通知Swift,‘这个可选型不会为nil,我用我的名誉打赌!’,当然,如果你错了的话,这个变量的值为nil,那么app就会挂掉,你也就名誉扫地了,所以你在使用强制解包的时候一定要小心。
另一种稍微安全点的强制解包方式叫做可选型链接。例如,下面的语句会在navigationController为nil时把app挂掉。
navigationController!.delegate = self
但是像这样做则不会把app挂掉:
navigationController?.delegate = self
位于问号后面的任何东西,都会在navigationController为nil时把它忽视掉。这个使用问号强制解包的语句等价于下面的语句:
if navigationController != nil {
navigationController!.delegate = self
}
在声明可选型的时候,也可以用感叹号来代替问号,这样就是一个隐式解包可选型了:
var dataModel: DataModel!
这样的变量会带来潜在的危险,因为你可以向使用常规变量那样直接使用它,并不需要先解包。如果它的值为空,那么app就挂了,而常规变量为空时,编译器会提示你怎么做。
可选型平时被包裹起来,以避免app崩溃,但是使用了感叹号以后,就解除了可选型的安全级别。
然而,有时使用隐式解包可选型比使用可选型要方便一些。当你无法给一个变量初始值,也无法用init方法对其初始化的时候,你就会需要到这种隐式解包可选型。
如果你给了一个变量一个值后,就不应该在使它为nil,如果一个变量可以从有值变为nil,那么你最好还是使用用问号声明的可选型。
方法与函数(Methods and functions)
你已经学习过这样的一种对象了,它是所有app的基础组成部分,同时具有数据和功能。实例变量及常量提供数据,方法提供功能。
当你调用一个方法,app就会跳转到方法中,逐条的执行其中的语句,当方法中最后一条语句被执行完毕后,app就会会到之前离开的地方:
let result = performUselessCalculation(314)
print(result)
...
func performUselessCalculation(_ a: Int) -> Int {
var b = Int(arc4random_uniform(100))
var c = a / 2
return (a + b) * c
}
方法经常会返回一个值给调用者,比如一个计算结果或者从一个集合中找到的一个对象。返回值的类型会写在->符号的后面。在上面的例子中,返回值的类型是Int。如果不存在->这个符号,那么就是说这个方法不返回任何值。
方法就是属于某一特定对象的函数,Swift中也存在独立的函数,比如print()或者arc4random_uniform()。
函数和方法的工作原理一样,一个可重复使用的功能块,但是函数不属于任何对象。像这种函数也被称为自由函数或者全局函数。
下面是一些关于方法的例子:
// 这个方法没有返回值及参数的方法
override func viewDidLoad()
// 这个方法有一个slider参数,但是一样没有返回值
// 关键字@IBAction意味着这个方法可以被连接到界面建造器的控件上
@IBAction func sliderMoved(_ slider: UISlider)
// 这个方法没有参数,但是有一个Int型的返回值
func countUncheckedItems() -> Int
// 这个方法有两个参数,cell和item,但是没有返回值
// 注意一下,第一个参数有一个外部名称for,而第二个参数有一个外部名称with
func configureCheckmarkFor(for cell: UITableViewCell,
with item: ChecklistItem)
// 这个方法有两个参数, tableView和section. 并且有一个Int型的返回值。
// 第一个参数前的下划线代表这个参数没有外部名称。
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
// 这个方法有两个参数, tableView和indexPath.
// 问号代表它返回一个为可选型的IndexPath对象。
override func tableView(_ tableView: UITableView,
willSelectRowAt indexPath: IndexPath) -> IndexPath?
在一个对象上调用一个方法,语法是object.method(parameters)。例如:
// Calling a method on the lists object:
lists.append(checklist)
// Calling a method with more than one parameter:
tableView.insertRows(at: indexPaths, with: .fade)
你可以把调用方法想象为从一个对象向另一个对象传递消息:“嗨 lists,我从checklist对象中向你发送了append的消息。”
你调用消息所属的对象被称为消息的接收者。
从同一个对象中调用方法非常常见,下面的例子中,loadChecklists()调用了sortChecklists()。它们都是DataModel对象中的成员:
class DataModel {
fun loadChecklists() {
...
sortChecklists()
}
fun sortChecklists() {
...
}
}
有时你会写为下面这个样子:
fun loadChecklists() {
...
self.sortChecklists()
}
关键字self清晰的表明了DataModel对象自己是这个消息的接受者。
⚠️:在我们的课程中,调用方法的时候,我省略了self关键字,因为并不是必须要这样做。Object-C开发者会非常乐意在每个地方都写上self,所以你也许会见到它在Swift中也被大量使用。到底写与不写,这是程序员间可以引发战争的一个话题,但是无论如何,app并不是太关心这点。
在一个方法的内部,你也可以使用self关键字来引用这个对象自己:
@IBAction fund cancel() {
delegate?.itemDetailViewControllerDidCancel(self)
}
这里cancel()方法将对象自身的引用发送给delegate,所以delegate知道谁发送了这个itemDetailViewControllerDidCancel()消息。
同时注意一下这里的可选型链接。这个delegate属性是个可选型,所以它可以为nil。在调用方法前使用一个问号来确保delegate为nil时,app不会挂掉。
方法经常会具有一个或多个参数,所以你可以让它们接收不同数据源上的数据工作。一个被限定了数据源的方法,可能不会非常有价值。看看下面的sumValuesFormArray()方法,它没有参数:
class MyObject {
var numbers = [Int]()
fun sunValuesFromArray() ->Int{
var total = 0
for number in numbers {
total += number
}
return total
}
}
这里,numbers是一个实例变量。方法sumValuesFromArray()被这个实例变量绑定死了,如果这个变量不存在,那么这个方法就没用了。
假设你在这个app中添加了第二数组,也想要应用上面的计算,那么其中一个方法是把这个方法复制一遍,重新命名为一个新的方法来处理这个新的数组。这样做确实可行,但是你也从此和聪明绝缘了。
另一个好一点的选择是,给这个方法一个参数,使得你可以传送任何你想要计算的数组,这样,这个方法就从实例变量中解放出来了:
func sumValues(from array: [Int])-> Int {
var total = 0
for number in array {
total += number
}
return total
}
现在你可以用任何整数型的数组作为它的参数了。
这并不是说方法不应该使用实例变量,只是说你想要一个方法的应用更加广泛,那么给它一个参数是个很好的选择。
方法的参数经常会有两个名字,一个外部名称,一个内部名称,例如:
fun downloadImage(for searchResult: SearchResult,withTimeout timeout: TimeInterval,andPlaceOn button: UIButton) {
...
}
这个方法有三个参数:searchResult,timeout和button。这些是内部名称,你在方法的内部用这些名称来调用参数。
方法的外部名称是方法名称的一部分。所以这个方法的全名是downloadImage(for,withTimeout,andPlaceOn),Swift中的方法名称经常会特别的长。
调用这个方法的时候,你需要使用外部名称:
downloadImage(for:result,withTimeout:10,andPlaceButton)
有时,你会看到一个方法它的第一个参数没有外部名称,取而代之的是一个下划线:
override func tableView(_ tableView: UITableView,numberOfRowsInSection section: Int)-> Int
这种情况经常出现在委托方法中,它是Object-C的遗留物,第一个参数的内部和外部名称都会被包含在方法名称中,比如在Object-C中downloadImage()方法的全名会是downloadImageForSearchResult。像这样的命名方式,以后会非常少见。如果是在Object-C中,这个方法的名称会是tableViewTableVIew,非常古怪是吧,而Swift 中,以下划线代替外部名称时,方法名称中就可以省略这个参数的外部名称,在Swift中,这个方法的全名是tableView(numberOfRowsInSection)。这样是不是容易明白多了?Swift在对方法命名时更加灵活,但它还是会保留一些旧的惯例。
在一个方法的内部,你可以做以下事情:
1、创建局部变量或者常量
2、进行基本的数学运算,比如加减乘除
3、将一个新的值放入变量(局部变量或实例变量)
4、调用其他方法
5、使用if或者switch作出判断
6、用for或者while进行循环处理
7、返回一个值给调用者
让我们来看看if和for语句的更多细节。
作出判断(Making decisions)
if语句的基本结构是这个样子的:
if count == 0 {
text = "No Items"
} else if count == 1 {
text = "1 Item"
} else {
text = "\(count) Items"
}
if后面的表达式称之为条件。如果条件为真,那么if后面花括号内的语句会被执行。如果没有一个条件为真,那么最后一个else后面的花括号内的语句会被执行。
你使用比较运算符来对两个值进行比较:
== 等于
!= 不等于
< 小于
<= 小于等于
大于 >
大于等于 >=
使用等于操作时,被比较的两个对象仅在相等时返回true,比如:
let a = "Hello,world"
let b = "Hello," + "world"
print(a == b) //打印结果为true
这个和Object-C有所不同,在Object-C中,必须两个对象是内存中的同一个实例,才会返回为true。而Switf中的==操作,仅仅是比较对象的值,而不管它在内存中是不是同一个对象,如果在Swift中像做这个操作的话,需要使用运算符 ===,三个等号。
你还可以使用逻辑操作符来连接两个表达式:
&& 与操作,a && b必须在a和b都为true时才返回true
||或操作符,a || b当a,b其中之一为true时,返回true
还有逻辑非操作符!,它的作用是将原本的true转为false,原本的false转为true。(不要和可选型弄混了,逻辑非操作符出现在对象的前面,而可选型的感叹号出现在对象的后面)
可以使用括号()来对表达式分组:
if ((this && that) || (such && so)) && !other {
...
}
它读作:
if ((this and that) or (such and so)) and not other {
...
}
为了看起来更加清晰一些,我们写的有层次一点:
if (
(this and that)
or
(such and so)
)
and
(not other)
当然,你弄的越复杂,越难记清楚自己在做什么!
Swift中还有一种非常强大的结构,可以用来做出判断,那就是switch语句:
switch condition {
case value1:
//语句
case value2:
//语句
case value3:
//语句
default:
//语句
它的效果和多个if else的效果是一致的,上面的代码等同于:
if condition == value1 {
//语句
} else if condition == value2 {
//语句
}else if condition == value3 {
//语句
} else {
//语句
}
相较之下,switch在这种情况中更加便利,而且意思清晰。而且Swift版的switc比Object-C版的更加强大。例如,你可以使用区间范围:
switch difference {
case 0:
title = "Perfect!"
case 1..<5:
title = "You almost had it!"
case 5..<10:
title = "Pretty good!"
default:
title = "Not even close..."
这里的..<是半开区间操作符。它可以创建两个值之间的区间,其中的值都是不重复的,半开区间1..<5等价于闭区间1...4。
你会在后面的课程中见到switch语句的实际用例。
注意一下,if语句中的reture会比方法中的returen更早的返回:
fun divide(_ a: Int, by b: Int) ->Int {
if b == 0 {
print("不能除以0")
return 0
}
return a / b
}
对于没有返回值的方法而言,if中的return甚至可以结束掉方法:
fun performDifficultCalculation(list: [Double]) {
if list.count < 2 {
print("样本过少")
return
}
//这里执行复杂的运算
}
在这个例子中,return的意思就是“我们退出方法吧”。任何return后面的语句都会被忽略掉。
你也可以把上面的方法写成下面这个样子:
fun performDifficultCalculation(list: [Double]) {
if list.count < 2 {
print("样本过少")
} else {
//这里执行复杂的运算
}
}
像这种只有两种可能的情况下,上面两个方法的作用一样,使用哪个都可以,我个人比较喜欢第二种。
有时你会看到下面这个样子的代码:
fun someMethod() {
if condition1 {
if condition2 {
if condition3 {
//语句
} else {
//语句
}
} else {
//语句
} else {
//语句
}
}
这种代码非常难读,我喜欢将它们重构为下面这个样子:
fun someMethod() {
if !condition1 {
//语句
}
if !condition2 {
//语句
}
if !condition3 {
//语句
}
//语句
}
这两段代码的作用其实是一样的,但是后一种更加容易理解。(注意一下,第二种写法中使用了!逻辑非来转换了表达式的意思)
Swift中有一种特殊的语句,guard来帮助你处理这种复杂的情况,用guard重写一下上面的方法就是:
fun someMethod() {
guard condition1 else {
//语句
return
}
guard condition21 else {
//语句
return
}
...
你要自己尝试这些方法,比较看看哪种可读性最好,哪种看起来最好,这样慢慢的你就会很有经验了。
循环(Loops)
你之前已经见识过了,如何用for in来历遍一个数组:
for item in items {
if !item.checked {
count += 1
}
}
也可以写作:
for item in items where !item.checked {
count += 1
}
for in中的语句会对每个items数组中的对象执行一遍。
注意一下,变量item的仅在for语句中有效,你不能在外面引用它,它的生命期比局部变量还要短。
有些语言,也包括Swift 2,中的for语句是这个样子的:
for var i = 0;i<5;++i {
print(i)
}
当你运行这个代码,会得到如下结果:
0
1
2
3
4
然而,在Swift 3种,这种for循环已经被抛弃了,取而代之的是,你可以直接使用区间范围,就像下面这样:
for i in 0 ... 4 {
print(i)
}
顺便说一下,也可写作:
for i in stride(from: 0,to: 5,by: 1) {
print(i)
}
stride函数创建了一个专门的对象来代表从1到5,每次增加1。如果你只想要偶数,你可以把by参数改为2。如果你给by参数一个负数的话,那么stride就可以实现倒着数的功能。
for语句并不是唯一的执行循环的语句,另一个非常强大的循环结构就是while语句:
while something is true {
//语句
}
while语句会一直保持循环,知道条件为false为止。还可以使用下面这种形式:
repeat {
//语句
} while something is true
在这种情况中,条件是在语句执行后才判断的,所以括号内的语句至少也会被执行一次。
你可以使用while语句重写一下循环Checklists中的对象:
var count = 0
var i = 0
while i < items.count {
let item = items[i]
if !item.checked {
count += 1
}
i += 1
}
这些循环结构的作用大致相同,只是看起来有些不一样。每一种都可以使你重复执行一段语句,直到条件不符合为止。
然而,使用while会比for in要看起来复杂一些,所以大多数时候,我们都会使用for in。
使用for in、while、repeat并没有什么不同,只是可读性上有所区别。
⚠️:上面例子中的item.count和count是两种不同的东西,只是名字一样。item.count中的count是数组items中的属性用于返回数组中元素的个数;后面的一个count是一个局部变量,用于对没有激活对勾符号的item对象计数。
就你可以在方法中使用return退出方法一样,你可以使用break来提前退出循环:
var found = false
for item in array {
if item == searchText {
found = true
break
}
}
这个例子中,for语句在数组中循环,直到找到第一个与searchText的值相当的值后,将found设置为true,然后退出循环,不再查看数组中剩下的对象。因为你已经找到了你想要的东西,所以没有必要把整个数组都循环完毕。
还存在一个contiue语句,和break的作用正好相反。它的作用是立即跳到下一个迭代中,当你使用contiue时,你的意思就是“目前这个item已经结束了,我们去看看下一个吧!”
在函数编程中,循环经常会被map,filter或者reduce替代。它们是一些操作集合的函数,对集合中每一个元素执行一段代码,并且返回一个新的集合作为结果。
例如,在数组上使用filter,会保留符合某些条件的元素。比如要得到未激活对勾符号的ChecklistItem对象,你可以这样写:
var uncheckedItems = items.filter { item in !item.checked}
这样写比循环看起来要简单多了。函数编程是一个非常大的话题,所以在这里我们不会展开太多。
对象(Objects)
将功能和数据结合在一起的可重用单元,都是对象。
数据是由对象中的实例变量和实例常量组成。我们经常以对象的属性形式引用它们。功能由对象的方法提供。
在你的Swift程序中,你使用过已存在的对象,比如String,Array,Date,UITableView,以及你自己创建的对象。
定义一个新的对象,你需要一个新的Swift文件,比如MyObject.swift,并且包含一个类(class)。比如:
class MyObject {
var text: String
var count = 0
let maximum = 100
init() {
text = "Hello World"
}
fun doSomething() {
//语句
}
}
在class的花括号内,你添加了属性(实例变量和实例常量)和方法。
属性有两种类型:
1、存储属性,它们通常是实例变量和实例常量。
2、计算属性,不存储东西,而是执行某些逻辑
下面是一个关于计算属性的例子:
var indexOfSelectedChecklist: Int {
get {
return UserDefaults.standard().integerForKey("ChecklistIndex")
}
set {
UserDefaults.standard().set(newValue,forKey: "ChecklistIndex")
}
}
indexOfSelectedChecklist属性并不存储一个值,取而代之的是,每次有人使用这个属性时,它执行get或者set内的代码。另一个选择是,分别写一个setIndexOfSelectedChecklist()和getIndexOfSelectedChecklist()方法,但是这样读起来不是很好。
关键字@IBOutlet的意思是,这个属性可以被界面建造器中的用户接口元素引用,比如label和button。这种属性通常都被声明为weak和可选型。类似的,@IBAction关键字被用于和用户交互时被触发的方法。
这里有三种类型的方法:
1、实例方法
2、类方法
3、init方法
你已经知道了方法就是属于某一个对象的函数。调用这种类型的方法你首先需要一个这个对象的实例:
let myInstance = MyObject() //创建一个对象的实例
myInstance.doSomething() //调用方法
你也可以创建一个类方法,这样就可以在没有实例的情况下使用这个方法。事实上,类方法经常被当作“工厂”方法使用,用来创建新的实例:
class MyObject {
...
class fun makeObject(text: String)-> MyObject {
let m = MyObject()
m.text = text
return m
}
}
let MyInstance = MyObject.makeObject(text: "Hello world")
init方法,或者叫做初始化设置,在创建一个新的对象实例的过程中被使用。你也可以使用init方法来取代上面的那个工厂方法:
class MyObject {
...
init(text: String) {
self.text = text
}
}
init方法的主要目的是将对象中的实例变量填满。任何没有值的实例变量和实例常量都必须在init方法中被给予一个值。
Swift不允许变量或者常量没有值(可选型例外),并且init方法是你给变量或者常量赋值的最后一次机会。
对象可以拥有一个以上的init方法;具体使用哪一个要依据具体情况而定。
例如,一个UITableViewController,从故事模版中自动被读取时,使用init?(coder)初始化,手工从nib文件中读取时,使用init(nibName,bundle)初始化,或者没有从故事模版和nib文件中构造时,使用init(style)初始化。有时你会用到这个,而有时你会用到那个。
当对象不再被使用时,你可以提供一个deinit方法。在对象被破坏掉前调用它。
顺便说一下,class并不是Swift中唯一定义对象的方法。还存在其他类型的对象,比如structs和enums。你会在后面学到这些,所以在这里,我们就点到为止了。
协议(Protocols)
一个协议就是一组方法名称的列表:
protocol MyProtocol {
func someMethod(value: Int)
func anotherMethod()-> String
}
协议就类似于工作列表。它列出了你的公司中每个具体职位的工作。
但是列表自己本身并不工作,它仅仅是打印出来给大家看的东西。所以你需要雇佣具体的员工来完成列表上的工作。而这些员工,就是具体的对象。
对象需要被指明自己需要遵守的协议:
class MyObject: MyProtocol {
...
}
这样,这个对象就需要完成协议中列出的所有方法。(否则,就炒了它)
此时,你就可以引用这个对象,同时还有协议:
var m1: MyObject = MyObject()
var m2: MyProtocol = MyObject()
对于代码中任何使用m2变量的部分,它是否是MyObject对象并不重要。m2类型是MyProtocol,不是MyObject。
所有你的代码看到的是,m2是某个遵守MyProtocol协议的对象,但是具体是什么样的对象并不重要。
换而言之,你并不关心你雇用的员工,是不是兼职其他工作,只要他和你需要的东西不冲突,你就可以雇佣他。
我们已经快速的回顾了一遍你所遇到的Swift语法知识。在结束了这些理论之后,是时候开始我们的app开发了。