Swift và xử lý lỗi

  • Xử lý lỗi trong Swift đã là một quá trình khá dài rồi, từ Swift 1.0 cơ. Nó bắt đầu lấy cảm hứng từ Objective C. Swift 2.0 với rất nhiều cải tiến và bất ngờ.
  • Cũng như nhiều ngôn ngữ khác, việc xử lý lỗi rất cần thiết và đáng được quan tâm. Các lỗi khác nhau và cách xử lý cũng khác nhau. Tuỳ thuộc vào loại lỗi và kiến trúc app của bạn mà có cách xử lý riêng.
  • Hôm nay mình sẽ hướng dẫn các bạn cách xử lý lỗi trong Swift. Sau đó bạn có thể áp dụng vào dự án của mình để có thể clear hơn ứng dụng của bạn.

1. Chuẩn bị

  • Chúng ta bắt đầu vơi một protocol
protocol MagicalTutorialObject {
    var avatar: String { get }
}

Protocol này được áp dụng xuyên suốt bài hướng dẫn ngày hôm nay, nó cung cấp ảnh đại diện cho tất cả các đối tượng.

  • Một enum định nghĩa các địa danh
enum MagicWords: String {
  case HoChiMinh = "HoChiMinh"
  case DaNang = "DaNang"
  case HaNoi = "Hanoi"
  case BacNinh = "BacNinh"
}
  • Một struct khởi tạo mặc định là .HoChiMinh và avatar là hình ngôi sao.
struct Spell: MagicalTutorialObject {
  var magicWords: MagicWords = .HoChiMinh
  var avatar = "*"
}

2. Tạo sao chúng ta nên quan tâm tới vấn đề xử lý lỗi?

  • Tôi xin trích một câu nói: "Error handling is the art of failing gracefully"
  • Có một vài lý do khiến bạn cần phải quan tâm tới xử lý lỗi.
    • Nâng cao được kinh nghiệm người dùng cuối
    • Dễ dàng xách định hơn trong quá trình maintain và tất nhiên dễ thay đổi và nhanh chóng thay đổi code.
    • Dễ dàng xác định ví trí, mức độ nghiêm trọng khi có lỗi xảy ra hoặc khi cần thay đổi mã nguồn.
    • Tránh xảy ra nhiều lỗi khác gây khó chịu người dùng.

3. Tránh lỗi trong swift bằng cách sử dụng nil

static func createWithMagicWords(words: String) -> Spell? {
    if let incantation = MagicWords(rawValue: words) {
      var spell = Spell()
      spell.magicWords = incantation
      return spell
    }

    return nil
  }
  • Chúng ta để ý rằng với đoạn mã trên thì spell chỉ được khởi tạo khi chúng ta khởi tạo thành công một MagicWords
  • Kiểu trả ra giờ đây là kiểu option. Vậy nếu trong trường hợp khởi tạo không thành công thì kết quả trả ra là nil. Để kiểm tra điều đó chúng ta thử sử dụng 2 dòng mã sau:
//return type of first variable is Spell?
let first = Spell.createWithMagicWords("HoChiMinh")
//return type of second variable is nil
let second = Spell.createWithMagicWords("BacGiang")
  • Nhưng viết mã thế này thật củ chuối phải không, nó là cách sinh viên hay viết thôi.
init?(words: String) {
    if let incantation = MagicWords(rawValue: words) {
        self.magicWords = incantation
    }
    return nil
}//Việc check kiểm tra sẽ đơn giản hơn
let first = Spell(words: "HoChiMinh")
let second = Spell(words: "BacGiang")

4. Sử dụng từ khoá Guard

  • Guard là từ khoá để kiểm tra nhanh các điều kiện đúng, sai hoặc tồn tại. Và sau đó bạn có thể thực thi khối lệnh khác và đảm bảo đúng điều kiện và tồn tại.
  • Guard thường được sử dụng rất nhiều. Guard còn có thể giúp bạn thoát khỏi một khối lệnh sớm khi bạn cần thoát mà không thực hiện thêm dòng code nào sau đó.
  • Vậy hãy thử áp dụng nó luôn vào hàm khởi tạo bên trên nhé:
//Nhìn hàm khởi tạo bạn sẽ thấy nó rất clear và đơn giản phải không?
init?(words: String) {
    guard let incantation = MagicWords(rawValue: words) else {
        return nil
    }
    self.magicWords = incantation
}

5. Tránh lỗi với custom handling

  • Sau phần 4 chúng ta đã có một ví dụ đơn giản về xử lý lỗi mà điển hình trong đó là khởi tạo lỗi
protocol MagicalTutorialObject {
  var avatar: String { get }
}
enum MagicWords: String {
    case HoChiMinh = "HoChiMinh"
    case DaNang = "DaNang"
    case HaNoi = "Hanoi"
    case BacNinh = "BacNinh"
}
struct Spell: MagicalTutorialObject {

  var magicWords: MagicWords = .HoChiMinh
  var avatar = "□"

  init?(words: String) {
    guard let incantation = MagicWords(rawValue: words) else {
      return nil
    }
    self.magicWords = incantation
  }
  init?(magicWords: MagicWords) {
       self.magicWords = magicWords
  }
}
protocol Familiar: MagicalTutorialObject {
  var noise: String { get }
  var name: String? { get set }
  init()
  init(name: String?)
}
extension Familiar {
  init(name: String?) {
    self.init()
    self.name = name
  }
  func speak() {
    print(avatar, "* \(noise)s *")
  }
}
  • Bài toán đặt ra: chúng ta có một struct như sau:
struct Witch: MagicalBeing {
    var avatar = "□□"
    var name: String?
    var familiar: Familiar?
    var spells: [Spell] = []
    var hat: Hat?

    init(name: String?, familiar: Familiar?) {
        self.name = name
        self.familiar = familiar

        if let s = Spell(magicWords: .HaNoi) {
        self.spells = [s]
    }
}

    init(name: String?, familiar: Familiar?, hat: Hat?) {
        self.init(name: name, familiar: familiar)
        self.hat = hat
    }

    func turnFamiliarIntoToad() -> Toad {
        if let hat = hat {
            if hat.isMagical { // When have you ever seen a Witch perform a spell without her magical hat on ? :]
                if let familiar = familiar {   // Check if witch has a familiar
                if let toad = familiar as? Toad {  // Check if familiar is already a toad - no magic required
                    return toad
                } else {
                    if hasSpellOfType(.HaNoi) {
                        if let name = familiar.name {
                            return Toad(name: name)
                        }
                    }
                }
            }
        }
    }
    return Toad(name: "New Toad")  // This is an entirely new Toad.
}

    func hasSpellOfType(type: MagicWords) -> Bool { // Check if witch currently has appropriate spell in their spellbook
        return spells.contains { $0.magicWords == type }
    }
}
  • Để ý thấy method turnFamiliarIntoToad các if lồng nhau rất nhiều phải không các bạn? If bên trong lại phục thuộc vào if bên ngoài, viết code như vậy là điều không nên, vừa khó hiểu, vừa làm khối code nhìn mất thẩm mĩ, chưa kể tới viết như vậy bạn không hề handle được hết các lỗi.

  • Chúng ta hãy thử refactor và đảm bảo xử lý lỗi nhé. Và sau đây sẽ là một số key word sẽ sử dụng:

    • throws
    • do
    • catch
    • try
    • ErrorType
  • B1. Sử dụng enum định nghĩa lỗi
enum ChangoSpellError: ErrorType {
  case HatMissingOrNotMagical
  case NoFamiliar
  case FamiliarAlreadyAToad
  case SpellFailed(reason: String)
  case SpellNotKnownToWitch
}
  • B2. Handle error sử dụng guard with condition
func turnFamiliarIntoToad() throws -> Toad {

    // Sử dụng guard check nil và điều kiện where
    // throw error khi không thoả mãi 2 điều kiện (let, where)
    guard let hat = hat where hat.isMagical else {
      throw ChangoSpellError.HatMissingOrNotMagical
    }

    // check exist use let and throw error
    guard let familiar = familiar else {
      throw ChangoSpellError.NoFamiliar
    }

    // Check type Toad and throw error
    if familiar is Toad {
      throw ChangoSpellError.FamiliarAlreadyAToad
    }
    guard hasSpellOfType(.PrestoChango) else {
      throw ChangoSpellError.SpellNotKnownToWitch
    }

    // Custom error
    guard let name = familiar.name else {
      let reason = "Familiar doesn’t have a name."
      throw ChangoSpellError.SpellFailed(reason: reason)
    }

    return Toad(name: name)
}

6. Sử dụng try-catch

  • Chúng ta xem ví dụ sau:
func exampleOne() {
  print("") // Add an empty line in the debug area

  // 1
  let salem = Cat(name: "Salem Saberhagen")
  salem.speak()

  // 2
  let witchOne = Witch(name: "Sabrina", familiar: salem)
  do {
    // 3
    try witchOne.turnFamiliarIntoToad()
  }
  // 4
  catch let error as ChangoSpellError {
    handleSpellError(error)
  }
  // 5
  catch {
    print("Something went wrong, are you feeling OK?")
  }
}
  • Trong đoạn mã trên có sử dụng try-catch để xử lý lỗi
  • Trong đó có sử dụng kết hợp do-catch chỉ ra nơi có thể xảy ra lỗi và catch lỗi.

Việc sử dụng try-catch tuy nó không phải là phần chính trong ngôn ngữ khác nhưng với Swift nó lại hiệu quả. Một điều quan trong nữa trong xử lý lỗi tôi muốn nói ở đây đó là cần phải chú ý một số điều:

  • Sử dụng Optional nơi dễ xảy ra lỗi
  • Nên sử dụng custom error
  • Đảm bảo các error được định nghĩa rõ ràng phù hợp với code của bạn

Trong bài có một số hàm tôi không đề cập tới. Mục đích bài này là tôi mong muốn các bạn có thể biết cách nhận biết loại lỗi và cách xử lý nó, tuỳ biến xử lý trong mỗi trường hợp làm sao dễ maintain nhất có thể.