Tour khám phá qua về Swift

Theo truyền thống, chương trình đầu tiên mà chúng ta sẽ viết khi tìm hiểu về một ngôn ngữ mới là chương trình hiển thị hay còn gọi là in (print) ra dòng chữ “Hello, world!” trên màn hình. Đối với Swift, bạn có thể thực hiện điều đó bằng chỉ một dòng code:

print("Hello, world!")

Nếu bạn đã từng viết code bằng C hoặc Objective-C, cú pháp này bạn sẽ thấy quen, trong Swift, dòng code này thực sự là một chương trình hoàn chỉnh. Bạn không cần phải nhập (import) bất kỳ thư viện (library) như thư viện xử lý nhập/xuất (input/output) hoặc xử lý chuỗi (string handling). Câu lệnh được viết toàn cục (global scope) được sử dụng ở toàn bộ chương trình, vì vậy bạn sẽ không cần một hàm main(). Bạn cũng không cần viết dấu chấm phẩy (semicolon) ở cuối mỗi câu lệnh (statement). Tour khám phá này sẽ cung cấp đủ thông tin cho bạn để có thể bắt đầu lập trình bằng Swift bằng cách trải qua một số phần khác nhau trong lập trình. Đừng quá lo lắng nếu đôi khi bạn không hiểu - mọi thứ giới thiệu trong tour này đều sẽ được giải thích cụ thể ở phần còn lại của cuốn sách này.

Chú thích

Ở máy Mac, hãy tải chương trình Playground và click đúp vào file playground này để mở bằng Xcode: https://developer.apple.com/go/?id=prerelease-swift-tour

Các giá trị đơn

Để khai báo một hằng số (constant) bạn sử dụng từ khoá let và từ khoá var để khai báo biến (variable). Giá trị (value) của một hằng số không cần phải được xác định tại lúc biên dịch, tuy nhiên bạn chỉ được gán (assign) một giá trị duy nhất cho nó. Điều này có nghĩa là bạn chỉ khai báo tên của hằng số một lần và sử dụng ở nhiều nơi.

var myVariable = 42
myVariable = 50
let myConstant = 42

Một hằng số hay biến đều phải có cùng kiểu như giá trị mà bạn gán cho nó. Tuy nhiên, bạn không phải lúc nào cũng cần viết tường minh kiểu của nó ra. Khi bạn cung cấp một giá trị cho một hằng số hay một biến, điều đó cho phép trình biên dịch suy (infer) kiểu của nó. Ở ví dụ trên, trình biên dịch sẽ suy ra kiểu của biến myVariable là số nguyên (integer), đơn giản bởi giá trị ban đầu của nó là một số nguyên.

Nếu giá trị ban đầu không cung cấp đủ thông tin (hoặc không có giá trị ban đầu), bạn có thể khai báo kiểu của nó bằng cách viết ngay sau tên biến và được phân cách bởi dấu hai chấm (colon).

let implicitInteger = 70
let implicitDouble = 70.0
let explicitDouble: Double = 70

Bài tập

Hãy tạo một hằng số (constant) với kiểu tường minh (explicit) là Float và có giá trị bằng 4.

Các giá trị sẽ không bao giờ được đổi ngầm định sang một kiểu khác. Nếu bạn cần đổi kiểu của một giá trị sang một kiểu khác, đơn giản hãy tạo một thể hiện của kiểu mong muốn.

let label = "The width is "
let width = 94
let widthLabel = label + String(width)

Bài tập

Hãy thử xoá phần đổi kiểu sang String ở dòng cuối. Bạn sẽ nhận được lỗi (error) gì?

Còn một cách đơn giản hơn nữa để thêm giá trị vào một chuỗi (String). Hãy viết giá trị đó vào trong ngoặc đơn (parenthese) và viết dấu gạch chéo ngược (blackslash) () trước dấu ngoặc đơn.

let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."

Bài tập

Sử dụng () để thêm tên và tuổi của một người vào câu chào. Ví dụ: Xin chào, tôi tên là Khả Chương, tôi năm nay 24 tuổi!

Sử dụng ba dấu nháy kép (“””) đối với chuỗi để có thể viết trên nhiều dòng. Dấu thụt dòng ở mỗi dòng sẽ bị bỏ đi.

let quotation = """
Even though there's whitespace to the left,
the actual lines aren't indented.
Except for this line.
Double quotes (") can appear without being escaped.
I still have \(apples + oranges) pieces of fruit.
"""

Bạn có thể tạo mảng (array) hay bộ từ điển (dictionary) bằng cách sử dụng dấu ngoặc vuông ([]) và bạn có thể truy cập các phần tử (element) bằng cách diền chỉ số (index) hoặc khoá (key) vào trong ngoặc vuông (bracket). Dấu phẩy cũng được phép điền ở phần tử cuối cùng.

var shoppingList = ["catfish", "water", "tulips", "blue paint"]
shoppingList[1] = "bottle of water"
var occupations = ["Malcolm" : "Captain", "Kaylee" : "Mechanic"]
occupations["Jayne"] = "Public Relations"

Để tạo một mảng hay dictionary (kiểu từ điển) rỗng, chúng ta sử dụng cú pháp khởi tạo như sau:

let emptyArray = [String]()
let emptyDictionary = [String: Float]()

Thông tin về kiểu của mảng hay dictionary có thể tự suy, do vậy bạn có thể tạo một mảng rỗng bằng cách viết [] và tạo một dictionary rỗng bằng cách viết [:]

shoppingList = []
occupations = [:]

Luồng điều khiển (Control Flow)

Chúng ta sử dụng từ khoá ifswitch để tạo ra các câu lệnh điều kiện (condition), và sử dụng for-in, while, repeat-while để tạo ra các vòng lặp (loop). Dấu ngoặc đơn ở các câu lệnh điều kiện hay vòng lặp là không bắt buộc (optional). Dấu ngoặc nhọn (braces) ở phần thân (body) của khối lệnh là bắt buộc (require).

let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
    if score > 50 {
        teamScore += 3
    } else {
        teamScore += 1
    }
}
print(teamScore)

Trong câu lệnh if, điều kiện phải là biểu thức logic. Bạn có thể sử dụng iflet cùng nhau để kiểm tra một giá trị có thể không tồn tại. Các giá trị này được hiểu là giá trị không bắt buộc (optional - do đây là một chủ đề quan trọng trong Swif nên bản dịch này từ sau sẽ dùng từ optional thay vì dịch nghĩa ra). Một giá trị Optional có thể có giá trị hoặc có thể chứa giá trị nil - là giá trị không có giá trị. Để chỉ ra giá trị đó là một giá trị Optional, bạn có thể viết dấu chấm hỏi ? ngay sau kiểu của giá trị.

var optionalString: String? = "Hello"
print(optionalString == nil)

var optionalName: String? = "John Appleseed"
var greeting = "Hello!"
if let name = optionalName {
  greeting = "Hello, \(name)"
}

Bài tập

Thay đổi giá trị của biến optionalName thành nil. Câu chào (greeting) nào bạn sẽ nhận được? Hãy thêm mệnh đề else để thực hiện mộ câu chào khác nếu (if) biến optionalName bằng nil

Nếu một giá trị Optional là nil, điều đó có nghĩa là câu lệnh điều kiện if sẽ là false (sai) và khối lệnh bên trong câu lệnh if sẽ bị bỏ qua. Ngược lại, giá trị Optional sẽ được unwrapped (mở) và gán cho một hằng số ngay từ khóa let, điều này giúp cho giá trị vừa được mở (unwrapped) sẽ có thể sử dụng được ngay trong khối lệnh if.

Còn một cách nữa để có thể kiểm soát được giá trị Optional, đó là cung cấp một giá trị mặc định bằng cách sử dụng ??. Nếu như giá trị Optional không có giá trị (nil), giá trị mặc định sẽ được dùng thay thế.

let nickName: String? = nil
let fullName: String = "John Appleseed"
let informationGreeting = "Hi \(nickName ?? fullName)"

Câu lệnh chuyển (switch) hỗ trợ toán tử so sánh nhiều kiểu dữ liệu - chúng không bị giới hạn bởi số nguyên hay toán tử so sánh bằng.

let vegetable = "red pepper"
switch vegetable {
  case "celery":
    print("Add some raisins and make ants on a log.")
  case "cucumber", "watercress":
    print("That would make a good tea sandwich")
  case let x where x.hasSuffix("pepper"):
    print("Is it a spicy \(x)")
  default:
    print("Everything tastes good in soup.")
}

Bài tập

Hãy thử bỏ trường hợp default. Bạn sẽ nhận được lỗi gì?

Hãy chú ý đến cách từ khóa let được sử dụng trong phần gán giá trị tìm được vào một hằng số khi mẫu so sánh trùng với mẫu cung cấp. Ở đây, chúng ta kiểm tra xem có mẫu rau (vegetable) nào có từ pepper hay không, nếu có thì chúng ta sẽ gán giá trị cho hằng số và thực hiện in ra màn hình Is it a spicy

Sau khi thực thi (execute) đoạn code trong khối lệnh switchcase (trường hợp) đúng, chương trình sẽ thoát khỏi khối lệnh switch. Việc thực thi sẽ không được tiếp tục chạy ở các case tiếp theo, do vậy bạn không nhất thiết phải thoát (break) khỏi khối lệnh switch ở cuối mỗi case.

Chúng ta sử dụng for-in để lặp (iterate) qua từng phần tử (cặp giá trị key-value, trong dictionary, mỗi một cặp giá trị sẽ là key-value, ví dụ: "Ten" : "Khả Chương") của một dictionary. Kiểu Dictionary là kiểu có danh sách các phần tử không được sắp xếp (order), vì vậy vòng lặp sẽ lặp qua từng cặp giá trị một cách tùy ý.

//Đoạn code dưới đây sẽ tìm ra con số lớn nhất (largest number)
let interestingNumbers = ["Prime": [2, 3, 5, 7, 11, 14],
                          "Fibonacci": [1, 1, 2, 3, 5, 8],
                          "Square": [1, 4, 9, 16, 25],]
var largest = 0
for (kind, numbers) in interestingNumbers {
  for number in numbers {
    if number > largest {
      largest = number
    }
  }
}
print(largest)

Bài tập

Hãy thêm một biến nữa để lưu lại con số lớn nhất vừa tìm được thuộc loại số nào (Prime: Số nguyên tố, Fibonacci, SquareSquare: Số bình phương)

Sử dụng while để lặp lại khối lệnh (block of code) cho đến khi (until) điều kiện bị thay đổi. Điều kiện của vòng lặp có thể đặt ở cuối cũng được, đảm bảo rằng vòng lặp sẽ chạy ít nhất một lần.

var n = 2
while n < 100 {
  n = n * 2
}
print(n)

var m = 2
repeat {
  m = m * 2
} while m < 100
print(m)

Bạn có thể lấy ra chỉ số (index) trong vòng lặp bằng cách sử dụng ..< để tạo ra một khoảng các chỉ số.

var total = 0
for i in 0..<4 {
  total += i
}
print(total)

Sử dụng ..< để lặp trong khoảng bé hơn giá trị sau dấu < , sử dụng ... để lặp trong khoảng bao gồm cả hai giá trị đầu và cuối.

Hàm và Closure (Function and Closure)

Để định nghĩa (declare) một hàm, chúng ta sử dụng từ khóa func, và để gọi một hàm, ta sử dụng tên của hàm cùng với các tham số/đối số (argument) trong ngoặc đơn. Sử dụng -> để liệt kê ra các kiểu dữ liệu trả về của hàm này.

func greet(person: String, day: String) -> String {
  return "Hello \(person), today is \(day)."
}
greet(person: "Bob", day: "Tuesday")

Bài tập
Hãy thử xóa đi tham số day. Sau đó thêm vào một tham số tên là myName có kiểu là String. Và cuối cùng là trả về câu chào dưới dạng: Hello, Bob! My name is Khả Chương.

Mặc định, các hàm sẽ sử dụng tên của các tham số làm nhãn (label) cho các tham số. Bạn có thể viết một nhãn tùy biến trước tên của tham số, hoặc viết _ để sử dụng mà không có một nhãn tham số.

func greet(_ person: String, on day: String) -> String {
  return "Hello \(person), today is \(day)."
}
greet("John", on: "Wednesday")

Đối với hàm trả về nhiều hơn một giá trị, chúng ta sử dụng một tuple (bộ, tập hợp) các giá trị. Các thành phần (element) của một tuple có thể tham chiếu bằng tên hoặc số.

func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
  var min = scores[0]
  var max = scores[1]
  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(scores: [5, 3, 100, 3, 9])
print(statistics.sum)
print(statistics.2)

Hàm cũng có thể có nhiều tham số khác nhau và hợp chúng lại với nhau thành một mảng.

func sumOf(numbers: Int...) -> Int {
  vả sum = 0
  for number in numbers {
    sum += number
  }
  return sum
}
sumOf()
sumOf(numbers: 45, 597, 12)

Bài tập
Hãy sửa lại hàm sumOf() để có thể trả về giá trị trung bình của các số truyển vào.

Các hàm có thể lồng vào nhau (nest). Hàm lồng nhau có thể truy cập các biến dược định nghĩa bởi một hàm khác. Bạn có thể sử dụng hàm lồng nhau để tổ chức lại code của bạn khi hàm đó quá dài và phức tạp.

func returnFifteen() -> Int {
  var y = 10
  func add() {
    y += 5
  }
  add()
  return y
}
returnFifteen()

Một hàm có thể trả về một hàm khác như chính giá trị của nó.

func makeIncrementer() -> ((Int) -> Int) {
  func addOne(number: Int) -> Int {
    return 1 + number
  }
  return addOne
}
var increment = makeIncrementer()
increment(7)

Một hàm cũng có thể nhận một hàm khác như là một tham số của nó.

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, 7, 12]
hasAnyMatches(list: numbers, condition: lessThanTen)

Về bản chất, hàm là một trường hợp đặc biết của Closure: một khối lệnh có thể gọi sau đó. Câu lệnh trong một closure có thể truy cập đến các biến, hàm sẵn có đã được tạo trong khối lệnh closure, thậm chí nếu một closure đang nằm ở một khối lệnh khác khi nó được thực thi - như ở ví dụ hàm lồng mà bạn đã thử ở phía trên. Bạn có thể viết một closure mà không cần đặt tên cho nó, bằng cách sử dụng dấu ngoặc kép {} bao quanh khối code. Sử dụng từ khóa in để tách giữa các tham số và giá trị trả về trong phần code.

numbers.map({
  (number: Int) -> Int in
  let result = 3 * number
  return result
  })

Bài tập
Hãy viết lại closure trên để trả về giá trị là 0 khi tất cả các số đều là só lẻ (odd number).

Bạn sẽ có một số cách để viết closure súc tích hơn. Khi kiểu của closure đã biết, chẳng hạn như hàm gọi lại (callback) cho một delegate, bạn có thể bỏ qua kiểu của các tham số, kiểu trả về hay thậm chí là cả hai. Một câu lệnh closure sẽ ngầm định chỉ trả về giá trị cho chính câu lệnh đó.

let mappedNumbers = numbers.map({ number in 3 * number })
print(mappedNumbers)

Bạn có thể tham chiếu đến các tham số bằng cách sử dụng số thay vì sử dụng tên - điều này rất hữu ích trong các closure ngắn. Một closure được truyền như tham số cuối cùng vào một hàm thì có thể xuất hiện sau dấu ngoặc đơn (parentheses). Khi một closure là một tham số duy nhất của một hàm, bạn có thể bỏ qua dấu ngoặc đơn.

let sortedNumbers = numbers.sorted { $0 > $1 }
print(sortedNumbers)

Đối tượng và Lớp (Object and Class)

Để tạo một lớp chúng ta sử dụng từ khóa class ngay đằng trước tên của lớp. Thuộc tính (property) của lớp được định nghĩa giống như định nghĩa biến hoặc hằng số, ngoài trừ việc thuộc tính này phải nằm trong lớp đó. Tương tự, phương thức (method) và hàm (function) được định nghĩa cùng một cách giống nhau.

class Shape {
  var numberOfSides = 0
  func simpleDescription() -> String {
    return "A shape with \(numberOfSides) sides."
  }
}

Bài tập
Thêm một thuộc tính là một hằng số với từ khóa let, và thêm một phương thức khác có một tham số truyền vào.

Khởi tạo một thể hiện (instance) của một lớp bằng cách thêm dấu ngoặc đơn vào sau tên của lớp. Sử dụng dấu chấm . để truy cập vào các thuộc tính và phương thức của thể hiện đó.

var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()

Phiên bản của lớp Shape hiện tại đang thiếu đi một phần quan trong: đó là một hàm khởi tạo (initializer) để thiết lập khi một thể hiện (instance) của lớp được tạo ra. Sử dụng init để tạo phương thức đó.

class NamedShape {
  var numberOfSides: Int = 0
  var name: String
  //Hàm khởi tạo
  init(name: String) {
    self.name = name
  }
  func simpleDescription() -> String {
    return "A shape with \(numberOfSides) sides."
  }
}

Từ khóa self được sử dụng để phân biệt giữa thuộc tính name với tham số name ở hàm khởi tạo. Các tham số ở hàm khởi tạo được truyền vào như việc gọi một hàm khi bạn tạo một thể hiện của một lớp. Mỗi một thuộc tính đều cần phải gán một giá trị khi nó được định nghĩa (ví dụ như thuộc tính numberOfSides) hoặc ngay ở hàm khởi tạo (vị dụ như thuộc tính name).

Sử dụng từ khóa deinit để tạo một hàm giải phóng (deinitializer) nếu bạn cần thực hiện một vài hành động dọn dẹp hay loại bỏ các đối tượng đã được giải phóng (deallowcated).

Lớp con (subclass) bao gồm tên lớp cha (superclass) ngay sau tên lớp con, cách nhau bởi dấu hai chấm. Không hề có ràng buộc gì đối với những lớp trở thành lớp con, bạn có thể thêm hoặc bỏ qua một lớp cha nếu cần thiết.

Các phương thức của một lớp con được viết lại (override) các phương thức của lớp cha bằng cách sử dụng từ khóa override, nếu như không có từ khóa override, trình biên dịch sẽ báo lỗi. Trình biên dịch cũng đồng thời phát hiện được những phương thức có từ khóa override nhưng thực chất lại không thuộc bất kỳ phương thức nào của lớp cha.

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()

Bài tập
Hãy tạo ra một lớp con khác của lớp cha NamedShape có tên là Circle (hình tròn) và hàm khởi tạo có hai tham số là radius (bán kính) và tên. Thừa kế lại hai phương thức area() và simpleDescription() của lớp cha.

Thêm vào đó, để đơn giản hóa các thuộc tính được chứa dữ liệu (stored), thuộc tính có thể có một phương thức get (hàm trả về kết quả) và một phương thức set (phương thức gán giá trị).

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")
print(triangle.perimeter)
triangle.perimeter = 9.9
print(triangle.sideLength)

Ở phương thức set, giá trị mới sẽ có tên ngầm định là newValue. Bạn có thể cung cấp một tên tường minh trong ngoặcngoặc đơn sau từ khóa set. Lưu ý rằng, hàm khởi tạo của lớp EquilateralTriangle thực hiện ba bước sau:

  1. Gán giá trị cho các thuộc tính của lớp con đã được định nghĩa
  2. Gọi hàm khởi tạo của lớp cha
  3. Thay đổi giá trị của thuộc tính được định nghĩa bởi lớp cha. Bất kỳ thiết lập thêm như sử dụng các phương thức, hàm get và set đều được thực hiện ở bước này.

Nếu bạn không cần thực hiện tính toán cho các thuộc tính nhưng vẫn cần cung cấp câu lệnh để chạy trước và sau bước gán một giá trị mới, hãy sử dụng willSetdidSet. Câu lệnh mà bạn cung cấp sẽ chạy bất cứ khi nào giá trị thay đổi ngoài hàm khởi tạo. Ví dụ, lớp dưới đây đảm bảo chiều dài của hình tam giác luôn luôn bằng chiều dài của hình vuông.

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")
print(triangleAndSquare.square.sideLength)
print(triangleAndSquare.triangle.sideLength)
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
print(triangleAndSquare.triangle.sideLength)

Khi làm việc với các giá trị Optional, bạn có thể viết dấu chấm hỏi ? trước toán tử như phương thức, thuộc tính. Nếu giá trị trước dấu ?nil, mọi thứ đăng sau ? đều sẽ bị bỏ qua và giá trị của cả biểu thức sẽ là nil. Ngược lại, nếu giá trị Optional có giá trị, thì mọi thứ đằng sau dấu ? sẽ có giá trị. Ở cả hai trường hợp giá trị của cả biểu thức sẽ là một giá trị Optional.

let optionalSquare? = Square(sideLength: 2.5, name: "optional square")
let sideLength = optionalSquare?.sideLength

Kiểu liệt kê và cấu trúc (Enumerations and Structures)

Để tạo ra một kiểu liệt kê, chúng ta sử dụng enum. Cũng giống như lớp hay các kiểu khác, kiểu liệt kê có thể có các phương thức.

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

Bài tập
Hãy viết một hàm so sánh hai giá trị Rank bằng cách so sánh hai giá trị thô (raw value) của chúng

Mặc định, Swift sẽ gán giá trị thô bắt đầu từ 0 và tăng mỗi lần lên một giá trị, tuy nhiên bạn cũng có thể thay đổi việc này bằng cách gán tường minh các giá trị. Ở ví dụ phía trên, Ace được gán tường minh giá trị thô bằng 1, và các giá trị thô còn lại sẽ được gán theo thứ tự. Bạn cũng có thể sử dụng kiểu chuỗi, hay kiểu số dấu phẩy động (floating-point) làm kiểu giá trị thô của một enum. Bạn có thể sử dụng thuộc tính rawValue để truy cập vào giá trị thô của một case. Sử dụng hàm khởi tạo init>(rawValue:) để tạo một thể hiện của một enum từ giá trị thô.

if let convertedRank = Rank(rawValue: 3){
  let threeDescription = convertedRank.simpleDescription()
}

Trong trường hợp các giá trị của enum là các giá trị thực thì không có cách nào khác để viết các giá trị thô của chúng. Thực tế là trong các trường hợp không có một giá trị thô nào có nghĩa, bạn không cần phải cung cấp chúng.

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()

Bài tập
Hãy thêm một phương thức tên là color() và trả về giá trị là màu đen "black" cho quân bài spade (quân bích) và club (quân nhép), và trả về màu đỏ (red) cho heart ( quân cơ) và diamond (quân rô).

Hãy chú ý đến hai cách mà trường hợp quân cơ (hearts</code>) của enum được tham chiếu ở ví dụ phía trên: Khi gán một giá trị cho hằng số hearts, trường hợp Suit.hearts được tham chiếu đến tên đầy đủ của nó vì nó không có một kiểu tường minh. Trong khối lệnh switch, trường hợp enum được tham chiếu bằng cách viết ngắn lại thành .hearts bởi vì giá trị self đã được biết đến như một quân bài (Suit). Bạn có thể rút ngắn cách viết bất cứ khi nào kiểu dữ liệu được biết rõ.

Sử dụng từ khóa struct để tạo ra một cấu trúc (hay còn gọi là struct). Các struct có nhiều hành vi (behavior) giống với class, bao gồm cả các phương thức và hàm khởi tạo. Một trong những điểm khác biệt lớn nhất giữa struct và class là struct luôn luôn được sao chép khi nó được truyền trực tiếp vào code của bạn, tuy nhiên đối với class thì nó được truyền theo kiểu tham chiếu.

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

Bài tập
Hãy thêm một phương thức vào struct Card để tạo một bộ bài đầy đủ, mỗi quân bài được kết hợp giữa rank và suit ( cấp và màu).

Một thể hiện của trường hợp enum có thể có các giá trị tương ứng với thể hiện đó. Các thể hiện của cùng trường hợp enum có thể có các giá trị khác nhau tương ứng với các thể hện đó. Bạn có thể cung cấp các giá trị tương ứng khi bạn tạo một thể hiện. Giá trị tương ứng và giá trị thô là khác nhau: giá trị thô của một trường hợp enum là giống nhau với tất cả thể hiện, và bạn cung cấp giá trị thô khi bạn định nghĩa enum.

Ví dụ, hãy xem xét trường hợp yêu cầu (request) thời gian mặt trời mọc (sunrise) và lặn (sunset) từ server. Server có thể trả về thông tin hoặc phản hồi về một vài lỗi.

enum ServerResponse {
  case result(String, String)
  case failure(String)
}
let success = ServerResponse.result("6:00 am", "8:09 pm")
let failure = ServerResponse.failure("Out of cheese.")
switch success {
  case let .result(sunrise, sunset):
    print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
  case let .failure(message):
    print("Failure... \(message)")
}

Bài tập
Hãy thêm một trường hợp (case) thứ ba vào enum ServerResponse và khối switch.

Hãy chú ý đến cách thời gian mặt trời mọc và lặn được tách ra từ ServerResponse trùng với các case ở khối switch.

Protocol và Extension

Chúng ta sử dụng protocol để định nghĩa một giao thức (protocol).

protocol ExampleProtocol {
  var simpleDescription: String { get }
  mutating func adjust()
}

Các lớp, enum hay struct đều có thể sử dụng protocol.

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

Ở hàm adjust() của struct SimpleStructure có sử dụng từ khóa mutating nhằm mục đích chỉ ra rằng phương thức này thay đổi nội dung bên trong. Ở phần định nghĩa của lớp SimpleClass không cất bất kỳ phương thức nào được đánh dấu là mutating bởi vì các phương thức thuộc lớp này có thể luôn luôn thay đổi. Để có thể thêm các hàm chức năng của một kiểu, chúng ta sử dụng extension. Bạn có thể sử dụng extension (mở rộng) để thêm protocol phù hợp được định nghĩa ở một nơi nào đó hay thậm chí là một kiểu mà bạn nhập (import) từ một thư viện hay một framework.

extension Int: ExampleProtocol {
  var simpleDescription: String {
    return "The number \(self)"
  }
  mutating func adjust() {
    self += 42
  }
}
print(7.simpleDescription)

Bài tập
Hãy viết một extension cho kiểu Double có thêm một thuộc tính tên là absoluteValue

Bạn có thể sử dụng tên protocol giống như tên của các kiểu khác, ví dụ, khi tạo một danh sách các đối tượng có nhiều kiểu khác nhau nhưng tất cả các đối tượng đều có chung một protocol. Khi bạn làm việc với các giá trị mà có kiểu là protocol, các phương thức bên ngoài phần định nghĩa protocol sẽ không sẵn có (available).

let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)
// print(protocolValue.anotherProperty)  // Bỏ comment để thấy thông báo lỗi

Mặc dù trong lúc chạy, biến protocolValue có kiểu là SimpleClass, tuy nhiên trình biên dịch vẫn xem nó thuộc kiểu ExampleProtocol. Điều này có nghĩa rằng bạn không thể ngẫu nhiên truy cập vào các phương thức và thuộc tính mà không thuộc protocol đó.

Xử lý lỗi (Error handling)

Bạn có thể biểu diễn lại các lỗi bằng cách sử dụng bất kỳ kiểu nào thừa kế protocol ErrorProtocol.

enum PrinterError: ErrorProtocol {
  case outOfPaper
  case noToner
  case onFire
}

Sử dụng từ khóa throw để ném (throw) ra một lỗi và throws để đánh dấu một hàm có thể văng ra lỗi. Nếu bạn ném ra một lỗi ở một hàm, hàm sẽ trả về ngay lập tức và đoạn code sẽ gọi đến hàm xử lý lỗi.

func send(job: Int, toPrinter printerName: String) throws -> String {
  if printerName = "Never Has Toner" {
    throw PrinterError.noToner
  }
  return "Job sent"
}

Có một số cách để có thể xử lý lỗi. Cách thứ nhất là sử dụng do-catch. Ở trong khối lệnh do, bạn đánh dấu câu lệnh có thể văng ra lỗi bằng cách viết try trước câu lệnh đó. Trong khối lệnh catch, lỗi sẽ tự động có tên là error trừ khi bạn đổi nó thành tên khác.

do {
  let printerResponse = try send(job: 1040, toPrinter: "Bi Sheng")
  print(printerResponse)
} catch {
  print(error)
}

Bài tập
Hãy thay đổi tên máy in thành "Never Has Toner", khiến cho hàm send(job:toPrinter:) ném ra một lỗi.

Bạn có thể cung cấp nhiều khối lệnh catch để xử lý các lỗi cụ thể. Bạn có thể viết ra mẫu lỗi ngay sau từ catch giống như bạn viết sau case trong một khối switch.

do {
  let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")
  print(printerResponse)
} catch PrinterError.onFire {
  print("I'll just put this over here, with the rest of the fire.")
} catch let printerError as PrinterError {
  print("Printer error: \(printerError).")
} catch {
  print(error)
}

Bài tập
Hãy thêm một đoạn code để ném ra một lỗi ở trong khối do. Loại lỗi gì mà bạn cần phải ném để lỗi đó được xử lý bởi khối catch đầu tiên? Còn khối thứ hai và thứ ba thì sao?

Còn một cách khác để xử lý lỗi, đó là sử dụng try? để chuyển đổi (convert) kết quả sang một giá trị Optional. Nếu hàm văng ra một lỗi, lỗi cụ thể sẽ bị loại bỏ và kết quả trả về sẽ là nil. Ngược lại, kết quả là một giá trị Optional chưa dữ liệu mà hàm đã trả về.

let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")

Sử dụng defer để viết một khối lệnh cho phép thực thi sau tất cả các câu lệnh khác trong cùng một hàm và trước khi câu lệnh trả về của hàm. Câu lệnh sẽ vẫn được thực thi dù cho hàm có văng ra lỗi. Bạn có thể sử dụng defer để viết các câu lệnh thiết lập và dọn dẹp giữa các hàm khác nhau, mặc dù chúng được thực thi ở các thời điểm khác nhau.

var fridgeIsOpen = false
let fridgeContent = ["milk", "eggs", "leftovers"]
func fridgeContains(_ food: String) -> Bool {
  fridgeIsOpen = true
  defer {
    fridgeIsOpen = false
  }
  let result = fridgeContent.contains(food)
  return result
}
fridgeContains("banana")
print(fridgeIsOpen)

Kiểu generic (Generic - Kiểu chung)

Viết tên nằm trong dấu <> để tạo một hàm hoặc kiểu generic.

func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
  var result = [Item]()
  for _ in 0..<numberOfTimes {
    result.append(item)
  }
  return result
}
makeArray(repeating: "knock", numberOfTimes: 4)

Bạn có thể tạo kiểu generic cho hàm, phương thức cũng như class, enum và struct.

enum OptionalValue<Wrapped> {
  case none
  case some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)

Sử dụng từ khóa where ngay sau tên của kiểu để chỉ rõ một danh sách các yêu cầu - ví dụ, để yêu cầu kiểu thực thi một protocol, hay để yêu cầu 2 kiểu trở thành giống nhau, hay thậm chí là yêu cầu một class phải có một lớp cha nhất định.

func anyCommonElements<TL Sequence, U: Sequence where T.Iterator.Element: Equatable,          T.Iterator.Element == U.Iterator.Element>(_ lhs: T, _ rhs: U) -> Bool {
  for lhsItem in lhs {
    for rhsItem in rhs {
      if lhsItem == rhsItem {
        return true
      }
    }
  }
  return false
}
anyCommonElements([1, 2, 3], [3])

Bài tập
Hãy sửa hàm anyCommonElements(::) thành một hàm - trả về một mảng các phần từ mà bất kỳ 2 sequence (dãy) có điểm chung.

Cách viết T: Equatable cũng giống với cách viết T where T:Equatable .

results matching ""

    No results matching ""