Delegate và Event trong Unity (phần 2)

Sử dụng Delegate và Event trong Unity (phần 2)

 

     Trong bài viết trước, mình đã trình bày các khái niệm về Delegate và Event cũng như cách sử dụng chúng trong Unity. Như đã nói, trong bài viết này mình sẽ trình bày về các kiểu tạo delegate khác để sử dụng trong khi lập trình với Unity, đó là: Action, Func, Lambda Expression.

 

     1. Action: Action cũng là 1 kiểu delegate được định nghĩa trong namespace System. Lợi ích của việc sử dụng nó là gì? Đó là thường thì chúng ta sẽ phải tốn từ 2 dòng code trở lên để định nghĩa 1 delegate sau đó là 1 biến từ nó, hoặc 1 event được tạo từ delegate đó, Ví dụ:

// Tạo 1 biến delegate sau khi định nghĩa nó
public delegate void PressKey();
public PressKey pk;

// hoặc tạo 1 event từ delegate
public delegate void PressKey();
public static event PressKey onPressKey;

 

     Cũng khá là mất thời gian nếu chúng ta cứ phải tự định nghĩa ra 1 delegate rồi sau đó là đến event từ nó, trong delegate đó biết đâu còn có các tham số khác nhau nữa thì sao? có thể là 1, 2, 3… tham số, có thể là int, float, string… Nói chung là hơi lười khi nghĩ đến cái chuyện đó, thế nên là ta có thể sử dụng luôn delegate Action cho nó nhanh, Action được định nghĩa để chúng ta có thể sử dụng đến tận tối đa là 16 tham số (params). Chúng ta có thể thử luôn, trong bài viết trước của mình, chúng ta đã có 1 ví dụ nho nhỏ về cách sử dụng event, nếu các bạn còn nhớ thì chúng ta có 1 script là EventDispatcher.cs, bây giờ chúng ta sẽ thay 2 dòng code khai báo delegate và event thành 1 dòng code sử dụng delegate Action như sau:

public class EventDispatcher 
{
     // Chúng ta comment 2 dòng code này lại
     // public delegate void PressKeyDown();
     // public static event PressKeyDown onPressKeyDown;

     // Và sử dụng delegate Action thay thế
     public static Action onPressKeyDown;      

     // Hàm yêu cầu thực thi sự kiện nhấn phím
     public static void ExecutePressKeyDownEvent()
     {
          onPressKeyDown?.Invoke();
     }
}

 

     Tất cả những phần còn lại của script này và các script khác vẫn giữ nguyên, chức năng không có gì thay đổi, chúng ta vẫn sẽ có được kết quả khi nhấn phím Space là chuỗi “Pressed Key – Space” , rất tuyệt vời, ngắn gọn súc tích, nếu trong hàm có tham số nào đó, ví dụ void ExecutePressKeyDownEvent(int i, string s, float f) thì chỉ việc sử dụng Action tương ứng vì Action hỗ trợ lên đến 16 tham số: Action<T1, T2,..., T16>. Nhưng có 1 điểm cần lưu ý là Action chỉ trả về kiểu void, nên nếu chúng ta muốn có kiểu trả về khác như int, string hay gì khác thì không sử dụng được Action mà phải đọc tiếp phần bên dưới 😋. À quên, 1 vấn đề nho nhỏ đó là về cơ bản thì sử dụng Action chỉ có thế, trong nhiều bài hướng dẫn về Unity người ta cũng sẽ chỉ hướng dẫn đến thế, nhưng cá nhân mình thì không thích sử dụng thẳng Action để làm việc thay cho event như thế vì dù sao thì nó vẫn chỉ là delegate, thế nên cá nhân mình thường sẽ thêm khai báo event vào như này public static event Action onPressKeyDown; để cho biết đây là cái event được tạo ra từ delegate Action chứ không dùng thẳng Action để làm event.

 

     2. Các kiểu trả về của Delegate và Func: Như đã nói trong bài viết trước, việc định nghĩa delegate có kiểu trả về cũng tương tự như kiểu void, hãy xem ví dụ đơn giản sau:

...
     // Định nghĩa delegate có kiểu trả về là int
     private delegate int Subtract(int number);

     // Hàm thực hiện chức năng trừ đi 1 đơn vị của tham số
     private int SubtractNumber(int number)
     {
          return --number;
     }
...
     // Kích hoạt delegate khi nhấn 1 phím
     if (Input.GetKeyDown(KeyCode.Space))
     {
          // Khởi tạo delegate và gán nó tới hàm
          Subtract s = SubtractNumber;
          Debug.Log(s(9));
     }
...

 

     Bấm Play trên Editor chúng ta sẽ thấy màn hình Console hiển thị kết quả là 8, đó chính là cách để chúng ta tạo các delegate với các kiểu trả về như int, float, string… Tương tự như Action, Func cũng là 1 kiểu delegate được định nghĩa trong namespace System. Lợi ích của nó thì cũng y như Action, tức là sử dụng nó thì chúng ta sẽ không phải tự định nghĩa như ví dụ trên nữa, và như lúc nãy mình đã nói, Action thì sẽ trả về kiểu void, nên nếu muốn có kiểu trả về khác thì chúng ta sẽ sử dụng delegate Func này. Func sẽ có kiểu khai báo dạng như sau Func<T1, T2,.., out TResult>, trong đó T1 cho đến Tn là các tham số đầu vào còn TResult là kết quả trả về của hàm, ví dụ với hàm Subtract như trên chúng ta sẽ thay delegate tự định nghĩa đó bằng Func này:

... 
     // Chúng ta sẽ comment delegate tự định nghĩa này đi 
     // private delegate int Subtract(int number);
     // Và thay thế bằng delegate Func<kiểu tham số, kiểu trả về>
     private Func<int, int> Subtract;

     // Hàm thực hiện chức năng trừ đi 1 đơn vị của tham số 
     private int SubtractNumber(int number)
     {
         return --number;
     } 
... 
     // Kích hoạt delegate khi nhấn 1 phím 
     if (Input.GetKeyDown(KeyCode.Space)) 
     { 
         // Như Action, không cần phải tự khởi tạo thể hiện của delegate nữa 
         Subtract = SubtractNumber;
         Debug.Log(Subtract(9)); 
     } 
...

 

     Chạy đoạn code trên ta sẽ có được kết quả là 8, giống như lúc trước dùng delegate tự định nghĩa, không có gì thay đổi. Vậy là ta đã vừa tìm hiểu về ActionFunc, 2 delegate được định nghĩa trong namespace System, chúng ta sẽ đến tiếp với loại thứ 3 – Lambda.

 

     3. Biểu thức Lambda (Lambda Expressions): Một cách ngắn gọn thì biểu thức Lambda là 1 cách ngắn hơn để biểu diễn phương thức ẩn danh (anonymous method) bằng cách sử dụng 1 số cú pháp đặc biệt. Thực sự thì mình cũng không có tìm hiểu nhiều về biểu thức Lambda nên mình sẽ không lan man, những ai muốn tìm hiểu sâu hơn có thể tham khảo ở google.com 😂. Mình sẽ đi thẳng vào chi tiết sử dụng nó như thế nào, cú pháp của Lambda sẽ là (params-list) => (expression), nghĩa là (danh sách các tham số) => (biểu thức), cái => này đọc là go-to, nếu không có tham số thì nó sẽ là () => (expression). Trở lại với ví dụ bên trên, chúng ta sẽ có thể viết lại việc tính toán mà không cần trực tiếp khai báo hàm SubtractNumber ra nữa, nó sẽ như sau:

... 
     // Định nghĩa delegate Func<kiểu tham số, kiểu trả về>
     private Func<int, int> Subtract; 
... 
     // Kích hoạt delegate khi nhấn 1 phím 
     if (Input.GetKeyDown(KeyCode.Space)) 
     { 
          // Gắn delegate tới 1 hàm in-line bằng cách tạo 1 biểu thức Lambda
          // Ở đây number trong () chính là tham số còn những gì bên trong {}
          // chính là biểu thức được thực hiện
          Subtract = (number) => { return --number; };
          Debug.Log(Subtract(9));
     }
...

 

     Chạy đoạn code trên ta sẽ vẫn có được kết quả trên màn hình là 8, chúa ơi mọi thứ thật tuyệt vời, đầu tiên là tạo ra 1 phương thức vô danh và sau đó là gắn delegate tới nó, mọi thứ được thực hiện chỉ trên cùng 1 dòng code, và sau đó là thực thi, đó chính là cách mà chúng ta sử dụng Lambda trong những trường hợp như thế này. Vậy là chúng ta đã biết về 1 số cách sử dụng delegate để ứng dụng trong khi lập trình với Unity, và để chốt hạ cho bài viết ngày hôm nay, mình sẽ trình bày cách để tạo ra 1 hàm call-back đơn giản.

 

     4. Hàm call-back: callback – gọi lại, là 1 hàm sẽ được gọi sau khi 1 hàm khác đã thực thi xong nhiệm vụ của nó, tại sao nó lại cần thiết? Hình dung trường hợp sau, chúng ta có 1 hàm người chơi chết – PlayerDie(), trong hàm này sau khi thiết lập xong vài thứ lăng nhăng như là trừ điểm, lưu điểm số… chúng ta muốn thực hiện thêm 1 hành động tùy chọn nào đó, có thể là hiện 1 hình mặt lêu lêu lên để chọc tức người chơi, có thể là hiển thị lại hướng dẫn cách chơi, 1 bản nhạc buồn vãi đái hoặc chẳng làm gì cả, đây chỉ là 1 option. Thế thì trong trường hợp đó chúng ta không thể code 1 đống các cái đó vào hàm PlayerDie() được mà ta sẽ code riêng thành hàm hiện hình lêu lêu, hàm hiển thị cách chơi, hàm chạy nhạc… rồi sau đó chúng ta có thể cho chạy các hàm đó như 1 tùy chọn cho hàm PlayerDie(). Ví dụ này nghe hơi ngu ngu, nhưng mà đại khái là như vậy, thôi bỏ qua đi, hãy xem ví dụ code sau:

...
     // Đây là 1 hàm đơn giản, tham số của nó là 1 delegate mặc định bằng null
     // Nghĩa là tham số này chỉ là 1 tùy chọn, không bắt buộc phải có
     private DemoCallBack(Action action = null)
     {
          // Chức năng chính của hàm là in ra dòng này
          Debug.Log("Đây là hàm DemoCallBack!");
          
          // Đây là chức năng tùy chọn, nếu action khác null thì sẽ thực thi
          // Nhớ rằng delegate action này đang tham chiếu tới 1 hàm nào đó
          action?.Invoke();
     }
...
     // Kích hoạt hàm DemoCallBack với sự kiện nhấn nút
     if (Input.GetKeyDown(KeyCode.Space))
     {
          // Đây là cách thực thi 1 hàm như bình thường
          DemoCallBack();
     }
...

     Bây giờ khi chúng ta nhấn phím Space, nó sẽ in ra dòng chữ “Đây là hàm DemoCallBack!”, vì chúng ta không đưa vào hàm tham số gì cả nên đó là cách mà 1 hàm bình thường được thực thi, bây giờ chúng ta sẽ đưa vào hàm DemoCallBack() này 1 tham số là 1 hàm khác (hoặc 1 delegate đang tham chiếu tới 1 hàm khác), như vậy hàm sẽ được đưa vào này được gọi là hàm callback.

...
     // Đây là 1 hàm callback
     private void ThisIsCallBack()
     {
          Debug.Log("Đây là hàm callback!");
     }

     // Kích hoạt hàm DemoCallBack với sự kiện nhấn nút 
     if (Input.GetKeyDown(KeyCode.Space)) 
     { 
          // Đưa ThisIsCallBack vào làm tham số. Điều này nghĩa là ThisIsCallBack
          // sẽ thực thi ngay sau khi DemoCallBack làm xong nhiệm vụ của nó
          DemoCallBack(ThisIsCallBack);
     }
...

     Chạy chương trình, bấm phím Space lần nữa, lần này màn hình Console sẽ xuất hiện 2 dòng chữ tuần tự là “Đây là hàm DemoCallBack!” sau đó là “Đây là hàm callback!”. Vậy là xong, như ví dụ ban nãy của mình, bây giờ chúng ta có thể tạo ra rất nhiều hàm với các chức năng khác nhau, sau đó chỉ việc đưa vào trong hàm DemoCallBack() ở trên, và chúng sẽ thực thi ngay sau khi chức năng chính của hàm DemoCallBack() thực thi xong, như vậy chúng ta sẽ không phải code tùm lum cả đống chỉ ở trong hàm DemoCallBack() nữa, và hãy nhớ rằng, chúng ta luôn có thể sử dụng Lambda để tạo hàm callback nhé, ví dụ trên có thể được viết dưới dạng Lambda như sau:

...
     // Kích hoạt hàm DemoCallBack với sự kiện nhấn nút 
     if (Input.GetKeyDown(KeyCode.Space)) 
     { 
          DemoCallBack(() => Debug.Log("Đây là hàm callback!")); 
     }
...

     

     Vậy là xong, 2 phần của bài viết về delegate và event đã kết thúc, hy vọng nó hữu ích cho mọi người. Hẹn gặp lại ở 1 bài viết khác ✌.

 

Bạn nghĩ sao về bài viết này?

[Total: 1    Average: 5/5]

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *