第一次用一门新语言编程,通常要在屏幕上打印“Hello,World”。用Swfit,一行就可以搞定:
println("Hello,world!")
如果你写过C或者Objective-C的代码,上面的语法看来是是这么熟悉——在Swift中,这行代码就是一个完整的程序了。你不需要为了输入/输出功能或者字符串处理而导入一个特定的库。写在全局域的代码被当作程序的入口,所以你不需要一个main函数。也不需要在每条语句的末尾写分号作为结尾。
本章会通过展示如何完成一系列变成任务,来给你的Swift编程之旅开个好头。如果你对某些东西不理解请不必担心——本章介绍的所有内容都会在本书的后续内容中有详细的解释。
NOTE
为了有更好的体验,可以在Xcode中的playground打开本章的代码。playgrund能够让你编辑代码的后马上看到结果。
在Mac电脑上,下载playground,双击连接 https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/GuidedTour.playground.zip 用Xcode将其打开。
简单类型
分别使用let和var定义常量和变量。在编译阶段,常量不需要有值,但是必须要给它赋值一次。也就是说常量只能赋值一次但是可以多次使用。
var myVariable = 42 myVariable = 50 let myConstant = 42
常量值和变量值必须与你赋值的类型相同。但是你不必每次都要填写具体的类型。在创建一个常量或者变量时为其提供一个值就行了,编译器能够推测出它的正确类型。在上面的例子中,因为myVariable的初始值是一个整型,所以编译器推测它的类型是整型。
如果初始值没有提供足够的类型信息(或如果没有初始值),需要在(变量名称)后面写上它的类型,用冒号做分隔开来。
let implicitInteger = 70 let implicitDouble = 70.0 let explicitDouble: Double = 70
实验
创建一个类型为Float值为4的常量。
Swift中的值不会隐式转换类型。如果你需要将一个值转换为其他类型,需要用想要的类型明确的标记那个值。
let label = "The width is " let width = 94 let widthLabel = label + String(width)
实验
试着将Stirng从最后一行移除。你会得到什么错误?
有一种简单的方式将值引入到字符串中:用圆括号包裹值,并在括号前面写上一个反斜线(\)。比如:
let apples = 3 let oranges = 5 let appleSummary = "I have \(apples) apples." let fruitSummary = "I have \(apples + oranges) pieces of fruit."
实验
使用()将浮点计算的结果放置引入到字符串中并加上某个人的名字向其打个招呼。
用一对方括号([])就能创建数组和字典,通过在方括号中的索引和键就能访问它们中的元素。
var shoppingList = ["catfish","water","tulips","blue paint"] shoppingList[1] = "bottle of water" var occupations = [ "Malcolm": "Captain", "Kaylee": "Mechanic",] occupations["Jayne"] = "Public Relations"
为了创建空的数组和字典,需要用构造语法。
let emptyArray = [String]() let emptyDictionary = [String: Float]()
如果类型是可以推测出来的,可以用[]创建一个空的数组,用[:]创建一个空的字典——比如当给变量设置值或者给函数传参。
shoppingList = [] occupations = [:]
控制流
使用if和switch创建条件语句,使用for-in、for、while和do-while创建循环语句。包裹条件或循环变量的圆括号是可选的,但包裹条件和循环体的花括号缺是必须的。
let individualscores = [75,43,103,87,12] var teamscore = 0 for score in individualscores { if score > 50 { teamscore += 3 } else { teamscore += 1 } } teamscore
NOTE
在上面的代码中,teamscore被单独写在一行。这是在playground中查看一个变量的值的简单做法。
在if语句中,条件必须是一个布尔值表达式——这就意味着像“if score { … }”这样的语句是错误的,与0的比较是不明确的(译者:这是在影射某些语言啊)。
你可以将if和let一起使用来处理可能缺失的值。这些值用可选类型表示。一个可选类型的值要末包含一个值,要末包含一个nil,表示值是缺失的。在一个值的类型之后写上一个问号(?)就标记那个值是可选的了。
var optionalString: String? = "Hello" optionalString == nil var optionalName: String? = "John Appleseed" var greeting = "Hello!" if let name = optionalName { greeting = "Hello,\(name)" }
实验
将optionalName 置为nil。你会得到什么问候?添加一个else从句为 optionalName 是nil的时候设置一个不同的问候。
如果可选类型值是nil,if语句的条件结果是false,花括号的内容被忽略了。否则,可选类型值被拆包后赋值给let后的常量,这样让拆包后的值可以在代码块内可以被使用。
Switch支持任何类型的数据和多种比较操作符——没有一定是整型的限制和一定是想等的限制。
let vegetable = "red pepper" switch vegetable { case "celery": let vegetableComment = "Add some raisins and make ants on a log." case "cucumber","watercress": let vegetableComment = "That would make a good tea sandwich." case let x where x.hasSuffix("pepper"): let vegetableComment = "Is it a spicy \(x)?" default: let vegetableComment = "Everything tastes good in soup." }
实验
尝试移除defalut case ,你会得到什么错误?
记住let是如何在一个模式中被使用的:将和模式的一部分匹配的那个值分配给一个常量。
在执行完匹配的case中的代码后,就会离开switch语句。而不会继续执行下一个case,所以这里不必在每个case的结束写上break。
你可以使用for-in 遍历一个字典中的元素,通过提供为每个键值对提供一对名字。字典是无序的集合,所以遍历过程中他们的键值出现的顺序是随机的。
let interestingNumbers = [ "Prime": [2,3,5,7,11,13], "Fibonacci": [1,1,2,8], "Square": [1,4,9,16,25],] var largest = 0 for (kind,numbers) in interestingNumbers { for number in numbers { if number > largest { largest = number } } } largest
实验
添加一个变量来跟踪是哪种类型的数字是最大的,也就是最大的数字是哪个类型的。
使用while来重复执行一个代码块直到条件发生了变化。循环条件可以写在代码块的后面,确保循环至少被执行一次。
var n = 2 while n < 100 { n = n * 2 } n var m = 2 do { m = m * 2 } while m < 100 m
在循环中使用索引——或者用..<表示一个索引期间或者写一个明确的初始化、条件和自增语句。下面两个循环做了同样的事情:
var firstForLoop = 0 for i in 0..<4 { firstForLoop += i } firstForLoop var secondForLoop = 0 for var i = 0; i < 4; ++i { secondForLoop += i } secondForLoop
使用..<来表示删除上限的范围,使用…来表示完整的区间。
函数和闭包
使用func定义一个函数。函数名称后面跟一个元括号,其中写上参数值,这样就调用函数了。使用->分割参数名称和函数的返回值类型。
func greet(name: String,day: String) -> String { return "Hello \(name),today is \(day)." } greet("Bob","Tuesday")
实验
移除day参数。为问候语句添加一个代表今天午餐的参数。
使用元组表示一个组合值——比如,从一个函数返回多个值。元组中的元素可以既可以通过名称也可以通过索引被引用。
func calculateStatistics(scores: [Int]) -> (min: Int,max: Int,sum: Int) { var min = scores[0] var max = scores[0] var sum = 0 for score in scores { if score > max { max = score } else if score < min { min = score } sum += score } return (min,max,sum) } let statistics = calculateStatistics([5,100,9]) statistics.sum statistics.2
函数的参数个数允许是可变化的,函数体内会将其搜集起来放置在一个集合内。
func sumOf(numbers: Int...) -> Int { var sum = 0 for number in numbers { sum += number } return sum } sumOf() sumOf(42,597,12)
实验
写一个计算参数平均值的函数。
函数是可以被嵌套的。嵌套在内部的函数可以访问外部函数定义的变量。可以用嵌套函数组织那些太长或者太复杂的函数。
func returnFifteen() -> Int { var y = 10 func add() { y += 5 } add() return y } returnFifteen()
函数是第一等类型。这意味着一个函数可以将另外一个函数作为返回值。
func makeIncrementer() -> (Int -> Int) { func addOne(number: Int) -> Int { return 1 + number } return addOne } var increment = makeIncrementer() increment(7)
func hasAnyMatches(list: [Int],condition: Int -> Bool) -> Bool { for item in list { if condition(item) { return true } } return false } func lessThanTen(number: Int) -> Bool { return number < 10 } var numbers = [20,19,12] hasAnyMatches(numbers,lessThanTen)
函数实际上是一种特殊的闭包:代码块可以被稍后被调用。闭包中的代码可以访问在背包背创建的作用域内合法的诸如变量和函数,尽管在闭包被执行的时候的作用域已经发生了变化——你可以看看那些嵌套函数。你还可以用“{}”包裹代码但不给出名字的形式写一个闭包。在函数体内用in来分隔参数和返回值。
numbers.map({ (number: Int) -> Int in let result = 3 * number return result })
实验
重写那个闭包,对所有奇数都返回0。
对于闭包的写法,有几种更加简明的写法供选择。当闭包类型是已知的时候,例如闭包是一个委托的回调函数的情况,可以将其参数的类型、返回值类型的一个或全部省略掉。只有单独一条语句的闭包干脆就将唯一的语句作为返回值了。
let mappedNumbers = numbers.map({ number in 3 * number }) mappedNumbers
可以用数字取代名字引用参数——这个做法尤其适合简化闭包的写法。作为函数的最后一个参数的闭包可以立即出现在括号之后。
let sortedNumbers = sorted(numbers) { $0 > $1 } sortedNumbers
对象和类
使用class后面跟上类的名字来定义一个类。类中的属性声明和常量或者变量的声明是一样的,唯一不同的是需要在类的上下文环境中进行。同样的,方法和函数的声明的写法也是一样的。
class Shape { var numberOfSides = 0 func simpleDescription() -> String { return "A shape with \(numberOfSides) sides." } }
实验
用let添加一个常量属性,另外再添加一个带一个参数的方法。
在类名后写一对圆括号可以创建一个类的实例。使用点号来访问实例的属性或者方法。
class NamedShape { var numberOfSides: Int = 0 var name: String init(name: String) { self.name = name } func simpleDescription() -> String { return "A shape with \(numberOfSides) sides." } }
这样需要注意的是selft是如何被使用来区分name属性同构造方法中的name参数的。在创建一个类实例的时候,像调用函数一样给构造方法传递参数。类的每个属性都需要被分配一个值——不论是在声明阶段(如同numberOfSides一样)还是在构造方法中(如同name一样)。
如果有必要在对象被回收前做些清理工作,使用deinit 来创建一个析构函数。
子类包含了超类的名字,在子类的名字之后,用一个冒号分隔。类不必一定是一个标准根类的子类,所以你可以根据需要选择继承一个类或者不继承任意一个类。
子类的方法重写超类的实现用override标记——意外重写了超类方法,没有用override标记,会被编译器当作一个错误。编译器同时还会发现用override标记的方法没有重写超类方法的情况。
class Square: NamedShape { var sideLength: Double init(sideLength: Double,name: String) { self.sideLength = sideLength super.init(name: name) numberOfSides = 4 } func area() -> Double { return sideLength * sideLength } override func simpleDescription() -> String { return "A square with sides of length \(sideLength)." } } let test = Square(sideLength: 5.2,name: "my test square") test.area() test.simpleDescription()
实验
创建NamedShape 类的另外一个子类Circle,它的构造方法有半径和名字作为参数。在Circle类中实现area和simpleDescription方法。
在存储的简单属性基础上,属性可以有getter和setter。
class EquilateralTriangle: NamedShape { var sideLength: Double = 0.0 init(sideLength: Double,name: String) { self.sideLength = sideLength super.init(name: name) numberOfSides = 3 } var perimeter: Double { get { return 3.0 * sideLength } set { sideLength = newValue / 3.0 } } override func simpleDescription() -> String { return "An equilateral triangle with sides of length \(sideLength)." } } var triangle = EquilateralTriangle(sideLength: 3.1,name: "a triangle") triangle.perimeter triangle.perimeter = 9.9 triangle.sideLength
在perimeter的setter中,新的值有一个默认的名字newValue。你也可以在set之后的圆括号内指定新值的名字。
注意EquilateralTriangle 类的构造方法分为了三步:
1.给子类中定义的属性赋值。
2.调用超类的构造方法。
3.修改在超类中定义的属性的值。初始化的其他工作,此时可以使用方法、getter或者setter了。
如果不需要计算一些属性,但是希望在这些属性被设置新值的之前或之后运行一些代码,那就使用willSet和didSet吧。举例说明,下面例子中的类需要确保它的三角形的边长和他的正方形的变长要一致。
class TriangleAndSquare { var triangle: EquilateralTriangle { willSet { square.sideLength = newValue.sideLength } } var square: Square { willSet { triangle.sideLength = newValue.sideLength } } init(size: Double,name: String) { square = Square(sideLength: size,name: name) triangle = EquilateralTriangle(sideLength: size,name: name) } } var triangleAndSquare = TriangleAndSquare(size: 10,name: "another test shape") triangleAndSquare.square.sideLength triangleAndSquare.triangle.sideLength triangleAndSquare.square = Square(sideLength: 50,name: "larger square") triangleAndSquare.triangle.sideLength
类中的方法与函数有一个重要的区别。函数的参数名只在函数内使用,但是方法的参数名还可以在调用方法时(第一个参数除外)使用。默认情况下,一个方法在被调用时和在方法内部有相同的名字。你可以指定在方法内部使用的第二个名字。
class Counter { var count: Int = 0 func incrementBy(amount: Int,numberOfTimes times: Int) { count += amount * times } } var counter = Counter() counter.incrementBy(2,numberOfTimes: 7)
当处理可选类型值的时候,你可以在操作(像方法、属性和下标)之前写上?。如果?之前的值是nil,在?后面的所有的东西就被忽略了而且整个表达式的值是nil。否则,可选类型值被拆包,?之后的按照拆包后的值执行。这两种情况下,整个表达式的值都是一个可选类型值。
let optionalSquare: Square? = Square(sideLength: 2.5,name: "optional square") let sideLength = optionalSquare?.sideLength
枚举和结构体
使用enum定义个枚举。同类和所有其他命名类型一样,枚举也拥有方法。
enum Rank: Int { case Ace = 1 case Two,Three,Four,Five,Six,Seven,Eight,Nine,Ten case Jack,Queen,King func simpleDescription() -> String { switch self { case .Ace: return "ace" case .Jack: return "jack" case .Queen: return "queen" case .King: return "king" default: return String(self.rawValue) } } } let ace = Rank.Ace let aceRawValue = ace.rawValue
实验
写一个函数通过比较两个Rank值的原始值,对其进行比较。
上面的例子中,枚举的原始值类型是Int,所以你只需要指定最开始的原始值。剩下的原始值会被按次序推测出来。当然你可以用字符串或者浮点数作为枚举的原始值。可以使用rawValue属性来访问枚举成员的原始值。
使用init?(rawValue:)构造方法来根据一个原始值创建一个枚举的实例。
if let convertedRank = Rank(rawValue: 3) { let threeDescription = convertedRank.simpleDescription() }
一个枚举成员的值是实际的值,而非实际值的另外一种写法。实际上,如果原始值是没有含义的,你都可以不提供原始值。
enum Suit { case Spades,Hearts,Diamonds,Clubs func simpleDescription() -> String { switch self { case .Spades: return "spades" case .Hearts: return "hearts" case .Diamonds: return "diamonds" case .Clubs: return "clubs" } } } let hearts = Suit.Hearts let heartsDescription = hearts.simpleDescription()
实验
给Suit枚举添加一个color方法,对于spades和clubs返回“black”,对于heart和diamonds返回“red”。
这里注意上面两种对枚举中Hearts成员的引用:当给hearts常量赋值的时候,枚举成员Suit.Hearts被公告全名引用,因为那个常量还没有明确类型指定。在switch语句中,枚举成员被通过简写形式.Hearts引用,这是因为已经知道self的值是一个suit了。在值类型已知的前提下,你可以使用简写方式。
使用struct定义一个结构体。结构和类有很多相同的行为,包括方法和构造方法。结构体和类的一最重要的区别是结构体在代码中被传递时都是复制操作,但是类是引用。
struct Card { var rank: Rank var suit: Suit func simpleDescription() -> String { return "The \(rank.simpleDescription()) of \(suit.simpleDescription())" } } let threeOfSpades = Card(rank: .Three,suit: .Spades) let threeOfSpadesDescription = threeOfSpades.simpleDescription()
实验
给Card添加一个方法,创建全套的纸牌,每张牌都是排位和花色的组合。
一个枚举成员的实例可以有和实例的关联值。相同枚举成员的实例可以有不同的实例关联值。你可以在创建实例时为成员提供实例关联值。关联值和原始值是不同的:枚举成员的原始值对于所有它的实例都是相同的,当定义枚举的时候就可以提供原始值。
例如,向服务器索取日出和日落的时间。服务器会返回正确的内容,或者错误的信息。
enum ServerResponse { case Result(String,String) case Error(String) } let success = ServerResponse.Result("6:00 am","8:09 pm") let failure = ServerResponse.Error("Out of cheese.") switch success { case let .Result(sunrise,sunset): let serverResponse = "Sunrise is at \(sunrise) and sunset is at \(sunset)." case let .Error(error): let serverResponse = "Failure... \(error)" }
实验
给ServerResponse 添加一个case和对应的switch分支。
注意从ServerResponse 值中提取日出和日落时间的方式。(从作为switch分支上匹配值的一部分提取日出日落信息)
协议和扩展
使用protocol来定义一个协议。
protocol ExampleProtocol { var simpleDescription: String { get } mutating func adjust() }
类、枚举和结构体都可以遵循协议。
class SimpleClass: ExampleProtocol { var simpleDescription: String = "A very simple class." var anotherProperty: Int = 69105 func adjust() { simpleDescription += " Now 100% adjusted." } } var a = SimpleClass() a.adjust() let aDescription = a.simpleDescription struct SimpleStructure: ExampleProtocol { var simpleDescription: String = "A simple structure" mutating func adjust() { simpleDescription += " (adjusted)" } } var b = SimpleStructure() b.adjust() let bDescription = b.simpleDescription
实验
写一个遵循这个协议的枚举。
注意在SimpleStructure 定义时采用了mutating关键字标记一个修改结构体的方法。SimpleClass 类的声明就没有用mutating标记方法,因为类的方法都会修改类实例。
使用extension来给已经存在的类型添加功能,比如新的方法和计算属性。无论是在其他地方定义的类型还是导入的库还是框架中的类型,都可以用扩展添加对协议的支持。
extension Int: ExampleProtocol { var simpleDescription: String { return "The number \(self)" } mutating func adjust() { self += 42 } } 7.simpleDescription
实验
写一个对于Double类型的扩展,添加一个absoluteValue 属性。
你可以向其他命名类型一样使用一个协议的名称——比如,创建一个包含不同类型但是都遵循同一协议的对象的集合。当处理这些类型是一个协议的值的时候,协议定义之外的方法是不可用的。
let protocolValue: ExampleProtocol = a protocolValue.simpleDescription // protocolValue.anotherProperty // Uncomment to see the error
尽管变量protocolValue 有一个运行时类型SimpleClass,编译器还会将它按照ExampleProtocol类型处理。这就意味着你不能访问类中协议定义之外的方法和属性。
泛型
在一对尖括号内写一个名字就创建了一个泛型函数或者类型。
func repeat<Item>(item: Item,times: Int) -> [Item] { var result = [Item]() for i in 0..<times { result.append(item) } return result } repeat("knock",4)
// Reimplement the Swift standard library's optional type enum OptionalValue<T> { case None case Some(T) } var possibleInteger: OptionalValue<Int> = .None possibleInteger = .Some(100)
在类型名称之后使用where来指定一个需求列表——例如,要求类型实现一个协议,要求两个类型是相同的,或者要求一个类有特定的超类。
func anyCommonElements <T,U where T: SequenceType,U: SequenceType,T.Generator.Element: Equatable,T.Generator.Element == U.Generator.Element> (lhs: T,rhs: U) -> Bool { for lhsItem in lhs { for rhsItem in rhs { if lhsItem == rhsItem { return true } } } return false } anyCommonElements([1,3],[3])
实验
修改anyCommonElements 函数让它返回一个包含两个序列都有的元素的数组。
这个例子中,你可以省略where,在冒号之后写协议或者类的名字。 和是一个意思。