Bài 13: Closure trong swift – Lập trình Swift cơ bản

Trang chủ » Training » Bài 13: Closure trong swift – Lập trình Swift cơ bản
01/03/2022 Training 9 viewed

I . Khái niệm.

Closure là gì ?

Closure bạn có thể hiểu đây là một cấu trúc code khá đặc biệt vì được nằm bên trong dấu ngoặc nhọn { }, bạn vừa thể sử dụng nó như một tham số của hàm số hoặc là một hàm số độc lập đều được.
Ví dụ như sau

{ (parameters) -> return_type in

     statements

 }
  • parameters : là các tham số truyền vào
  • return_type : là kiểu trả về
  • in : là từ khoá mặc định ngăn cách giữa kiểu trả về và đoạn code xử lý logic phía dưới
  • statements : đây là vị trí các đoạn code xử lý logic, và cuối cùng sẽ return về giá trị mà bạn định nghĩa .
Đa phần, chúng ta thường chỉ nhìn được phần đuôi của closure và chỉ muốn khai báo thay vì viết thêm đoạn code xử lý. Ở bài viết này, tôi sẽ giúp bạn khai báo phần đầu của closure như sau :
var closure_name: ((parameters) -> return_type )?
  • var : vì ở đây chỉ là khai báo nên chúng ta sử dụng var để có thể khởi tạo sau, bạn có thể dùng let nếu khởi tạo luôn
  • parameters : là các tham số truyền vào
  • return_type : là kiểu trả về
  • ? : đơn giản là mình chọn kiểu optional như một biến option thông thường.
Chốt lại để cho dễ hiểu hãy nhìn ví dụ này :
let closure = { (param1: String, param2: Int) -> String in

    let result = "Đây là ví dụ về closure, gồm có 2 tham số \(param1) và \(param2), kiểu trả về là 1 string"

    return result

 }

  print(closure("Hello", 1))
Kết quả trả về :
Đây là ví dụ về closure, gồm có 2 tham số Hello và 1, kiểu trả về là 1 string
Trong ví dụ trên, mình khởi tạo 1 closure gồm có 2 tham số là param1 và param2, và nó sẽ trả về kết quả là 1 String. Để sử dụng thì rất đơn giản chỉ cần gọi như một hàm bình thường.
Chúng ta dễ dàng thấy, closure có một dạng đặc biệt :
  • Đầu có dạng biến số (1)
  • Đuôi có dạng hàm số có tham số truyền vào và có giá trị trả về. (2)
Nếu chỉ có (2) thì closure chẳng khác gì hàm số, tuy nhiên chính bởi tính chất (1) mà closure có thể sử dụng để truyền như một tham số trong hàm số.

II. Cách sử dụng.

Closure có rất nhiều cách để sử dụng nhưng trong giới hạn của bài viết, mình sẽ chỉ ra 3 cách thông dụng mà các lập trình viên hay sử dụng nhất.

1. Sử dụng như một hàm số

Đầu tiên, mình sẽ tạo class có tên là Student và class này có một closure là studentName với 2 tham số truyền vào là classId và studentId và kết quả nhận được là tên của student đó.
class Student {

   var studentName: ((String, Int) -> String) = { (classId: String, studentId: Int) -> String in

      return "Nguyễn Văn A"

   }
Phía trên là cách viết đầy đủ nhưng trong code để tiết kiệm thời gian thì mình thường viết rút gọn lại theo 2 cách như sau:
class Student {

   var studentName = { (classId: String, studentId: Int) -> String in

      return "Nguyễn Văn A"

   }
hoặc
class Student {

   var studentName: ((String, Int) -> String) = { (classId, studentId) in

       return "Nguyễn Văn A"

   }
Bạn có thể chọn cách nào dễ dùng và phù hợp nhất với  bạn và có một lưu ý nhỏ là trong trường hợp viết đầy đủ, không lược bỏ gì thì bạn phải để tham số giống nhau ở cả 2 phía như trong ví dụ các bạn có thể thấy tham số đều là (String, Int).
Về cách dùng thì rất đơn giản :

2. Sử dụng với cơ chế callback như delegate.

Đấy là một trong những tính chất đặc biệt của closure như mình đã nói ở phần khái niệm, đó là vì closure được khai báo như một biến số.
Đoạn code trên mình viết tiếp class Student :
  • (1) Khai báo 1 closure để check việc student nói những gì.
  • (2) Viết hàm thinkingAboutYourTeacher để hỏi học sinh nghĩ gì về giáo viên của họ, sau khi nghĩ 2s sẽ đưa ra câu trả lời,lúc này closure đóng vai trò callback bằng cách gọi hàm speak và truyền vào tham số là một String.
  • (3) Khởi tạo đối tượng student
  • (4) Thực hiện implement speak để lắng nghe callBack từ student
  • (5) Cuối cùng gọi hàm thinkingAboutYourTeacher
Kết quả thu được là : Students say that : Teacher is always right
Vậy là mình hướng dẫn xong cho bạn cách thứ 2, trong cách sử dụng này mình chỉ lưu ý một chút như sau :
  • Thứ nhất tại sao mình sử dụng optional khi kháo bảo speak ? Bạn hoàn toàn có thể sử dụng unwrapped optional tuy nhiên, trong trường hợp này, nếu không implement speak mà vẫn gọi hàm thinkingAboutYourTeacher thì sẽ bị crash do speak = nil.
  • Thứ hai, bạn nên chú ý implement closure luôn phải thực hiện trước khi hàm callBack được gọi. Như ở trong ví dụ này là implement speak trước khi gọi hàm thinkingAboutYourTeacher. Tại sao như vậy, vì nếu làm ngược lại thì khi callBack được thưc hiện thì closure = nil và không có gì được gọi ở đây cả.

3. Sử dụng như một tham số trong hàm số.

Cách cuối cùng, cũng là cách mà mình thấy mọi người vẫn sử dụng để viết API hoặc  muốn thực hiện một chức năng nhưng phải chờ kết quả từ một chức năng khác.
class Teacher {   
var name: String

   init(name: String) {

      self.name = name

   }

}

class Student {

   func thinkingAboutYourTeacher(name: String, completion: @escaping (String) -> Void) {

        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {

             completion("is a good Teacher")

        }

    }

}

let student = Student() 

let teacher = Teacher(name: "Bob")

student.thinkingAboutYourTeacher(name: teacher.name) { (thinking) in 

   print("\(teacher.name) \(thinking)")

}
--> Kểt quả thu được là : Bob is a good Teacher
Vẫn sử dụng class Student từ cách 1 , lần này hàm thinkingAboutYourTeacher có sự thay đổi so với cách thứ 2, closure sẽ được khai báo thành 1 tham số của hàm thay vì khai báo bên ngoài như một số biến khác.
So sánh về 2 cách dùng này chúng ta có thể thấy sự khác biệt chỉ đến từ cách dùng, còn về cơ chế sử dụng vẫn chỉ là một.
  • Ở cách sử dụng thứ 2, speak có thể được gọi đi gọi lại ở nhiều hàm khác nhau, ở bất cứ chỗ nào chỉ cần học sinh nói thì chúng ta sẽ lắng nghe được.
  • Ở cách dùng thứ 3, closure chỉ được truyền trong hàm vì thế chỉ có hàm này mới được sử dụng, kết thúc hàm thì closure cũng kết thúc.

III. Quản lý memory khi sử dụng closure.

Khi sử dụng closure, bạn cần phải thực sự lưu ý với mình về vấn đề memory leak.
Chúng ta sẽ đi vào một số ví dụ để hiểu rõ hơn về closure.
Vẫn từ ví dụ nêu ra ở mục ii.3 mình sẽ thay đổi code một chút. Mình sẽ đưa đoan code vào trong 1 hàm askStudentAboutHisTeacher:
func askStudentAboutHisTeacher() {

   let teacher = Teacher(name: "Bob") //1

   student.thinkingAboutYourTeacher(name: teacher.name) { (thinking) in //2

      print("\(teacher.name) \(thinking)")

   }

   print("finish askStudentAboutHisTeacher") //3

}

askStudentAboutHisTeacher() 

--> Kết quả thu được là :  Bob is a good Teacher
Trong hàm askStudentAboutHisTeacher sẽ gọi theo thứ tự 1-2-3, theo logic khi 3 đã được gọi thì đối tượng teacher cũng phải tự giải phóng nhưng chỉ sau 2s khi closure kết thúc nó vẫn còn giá trị. Vậy lý do là gì? Nó có đang bị sai không?
Hãy cùng mình làm thêm 1 ví dụ nữa, mình sẽ chỉ sửa 1 chút bằng việc sử dụng [weak teacher] :
student.thinkingAboutYourTeacher(name: teacher.name) { [weak teacher] (thinking) in //2

      print("\(teacher?.name) \(thinking)")

 }

-->  nil is a good Teacher
Vậy nguyên nhân gì dẫn đến  sự bất thường có trong ví dụ thứ nhất ??
Đó chính là khả năng của closure- Capturing Values của closure, nếu như chúng ta không sử dụng weak hay unowned cho biến số bên ngoài khối closure thì closure sẽ tạo một strong reference đến biến số đó và chỉ giải phóng khi closure kết thúc.
Đây là một tính năng giúp cho dữ liệu của bạn luôn được nguyên vẹn khi dùng closure, nó khá giống cách mà bạn tạo một vòng tròn bảo vệ cho các dữ liệu bên trong khối code. Cũng chính bởi tính năng này mà chúng ta luôn phải cẩn thận để tránh rơi vào trường hợp bị memory leak.
Tìm hiểu ví dụ dưới đây với mình để hiểu rõ hơn nhé:
class SchoolViewController: UIViewController {

   let student = Student() // 1

   override func viewDidLoad() {

       super.viewDidLoad()

       student.speak = { thinking in //2

           self.showThinking(thinking)

       }

   }

   private func showThinking(_ thinking: String) { //3

       print("\(thinking)")

   }

   @IBAction func doTask(_ sender: Any) { //4

       student.thinkingAboutYourTeacher()

   }

   @IBAction func backToParent(_ sender: Any) { //5

       self.navigationController?.popViewController(animated: true)

   }

   deinit { //6

   }

}

Ở đây mình viết môt viewcontroller là SchoolViewController có 2 button với 2 action là doTask (3) làm nhiệm vụ gọi hàm thinkingAboutYourTeacher và action backToParent(4) để pop về viewcontroller cha của SchoolViewController.Số (2) là thực hiện việc implement speak của Student, số (6) là hàm deinit, nếu SchoolViewController được giải phóng thì mình sẽ nhìn thấy log được in ra.
Kịch bản được thực hiện là : Click button để gọi (3) sau đó khi nhìn thấy log được print từ (3) thì ấn nút để gọi (5). Chúng ta hãy xem có gì được in ra :
Teacher is always right
Mặc dù đã pop về SchoolViewController nhưng SchoolViewController deinit thì không được gọi bởi vì đã xảy ra memory leak !!!
Bạn có thể thấy rõ  SchoolViewController có 1 strong reference với friend, tiếp sau đó student lại có 1 strong reference với closure speak và cuối cùng là closure lại có 1 strong reference với chính SchoolViewController thông qua gọi self. Khi chúng ta gọi hàm popViewController thì SchoolViewController lúc này vẫn không được giải phóng bởi vì closure vẫn giữ 1 strong reference đến nó và gây ra memoryleak.
Và để giải quyết vấn đề này, cách duy nhất là bạn phải loại bỏ 1 strong reference, bằng cách sử dụng [weak self]
  override func viewDidLoad() {

       super.viewDidLoad()

       student.speak = { [weak self] thinking in //2

           self?.showThinking(thinking)

       }

   }
Chia sẻ:
Tags:
TOP HOME