+2

Mở đầu về Delegate và Event trong C# với góc nhìn bị ngược

1, delegate và vị trí của nó trong mã nguồn

1.1, "delegate" là cái gì?

  • Trước hết cần nhớ "delegate" (không viết hoa) là một từ khoá để khai kiểu dữ liệu. Và cụ thể hơn nó là một kiểu dữ liệu tham chiếu (reference type), nghĩa là nó cũng như string, object, class, struct,... (chúng đều là từ khoá để khai báo). Nhìn hình sau để thấy vị trí của Delegate: image.png

  • "delegate" không phải một kiểu dữ liệu, nó chỉ là một từ khoá để khai báo kiểu dữ liệu thôi. Vậy nó khai báo kiểu dữ liệu gì? Câu trả lời là một kiểu dữ liệu "tuỳ chỉnh". Cái này có vẻ hơi phức tạp hơn so với bình thường như các từ khoá khác, như string chẳng hạn, string là tạo ra một biến có kiểu dữ liệu System.String. Nhưng từ khoá "delegate" đang tạo ra một kiểu dữ liệu mới. Hãy tạm chạy nhanh đến phần khởi tạo:

    string str = "abc";  
    public delegate int MyDelegate(string s);
    

Câu thứ hai này tạo ra một kiểu dữ liệu mới tên là "MyDelegate". Cụ thể hơn, khi viết như thế này, compiler dịch ra một class như sau (pseudo-code IL/C#):

public sealed class MyDelegate : MulticastDelegate  
{  
    public MyDelegate(object target, IntPtr method);  
    public int Invoke(string s);  
    public IAsyncResult BeginInvoke(string s, AsyncCallback callback, object @object);  
    public int EndInvoke(IAsyncResult result);  
}

Nghĩa là MyDelegate thực chất là một class, nhưng ta viết gọn bằng từ khóa "delegate".

1.2, Về cấu trúc của class Delegate và những class con

Trong .NET, tất cả các delegate được định nghĩa (như "MyDelegate") đều được compiler biến thành một class kế thừa từ "System.MulticastDelegate", mà bản thân "System.MulticastDelegate" lại kế thừa từ "System.Delegate".

Cấu trúc kế thừa (inheritance chain) như sau:

System.Object  
└── System.Delegate  
└── System.MulticastDelegate  
└── MyDelegate (do mình định nghĩa)

1.2.1, Cấu trúc cơ bản của class System.Delegate

Không thể viết lại toàn bộ mã nguồn ở đây (vì nó được runtime sinh ra và optimized rất sâu), nhưng Microsoft có reference source. Đây là phiên bản rút gọn từ source.dot.net:

[Serializable]
[System.Runtime.InteropServices.ComVisible(true)]
public abstract class Delegate : ICloneable, ISerializable
{
    // Trỏ tới object chứa method (nếu là instance method)
    public object? Target { get; }

    // Metadata của method mà delegate trỏ đến
    public MethodInfo Method { get; }

    // Tạo bản sao delegate
    public virtual Delegate Clone();

    // Invoke method (late-binding, dùng reflection)
    public virtual object? DynamicInvoke(params object?[]? args);

    // Quản lý danh sách delegate
    public static Delegate Combine(Delegate? a, Delegate? b);
    public static Delegate Remove(Delegate source, Delegate value);

    // Một số API hệ thống
    ...
}

  • Nhìn vào code ta có thể thấy: một Delegate có 2 property chính (Target, Method). Cái này cho thấy rõ tính chất "con trỏ hàm" của Delegate: mỗi một Delegate tương ứng với một method và được liên kết chặt chẽ với chúng. Thực tế thì System.Delegate là một con trỏ hàm đơn, mỗi đối tượng kiểu System.Delegate trỏ đến một method, như cách tên của string trỏ đến địa chỉ ô nhớ chứa giá trị string đó vậy. Và nhớ nó là con trỏ hàm "đơn", chỉ liên kết với 1 hàm.

Từ đó suy ra có thể thực hiện gọi hàm hay lấy thông tin của hàm thông qua delegate.

Hơi dài, nhưng nó có mấy đặc tính cần nhớ thôi:

  • Là một class abstract, không thể new Delegate() trực tiếp, chỉ có compiler mới sinh ra các class con (ví dụ MyDelegate).

  • Là reference type, giống như class, string, delegate là kiểu dữ liệu tham chiếu, lưu ở heap.

  • Immutable (bất biến), một khi delegate đã trỏ tới method nào thì không đổi được. Khi dùng toán tử += hay -=, thật ra runtime tạo một delegate mới rồi gán lại, chứ không thay đổi delegate cũ. (mấy cái toán tử này tính sau)

  • Invocation (thực thi hàm), Delegate overload toán tử () nên ta có thể gọi trực tiếp:

    MyDelegate d = SomeMethod;
    int result = d("hello");
    

Nhưng bên trong thực ra khi gọi d("hello"), nó gọi Invoke hoặc DynamicInvoke.

Vì Delegate chỉ trỏ tới một method duy nhất, nên để hỗ trợ việc gọi nhiều method liên tiếp (multicast), .NET cần thêm một lớp con, là "MulticastDelegate".

1.2.2, Chúng ta có thêm gì ở System.MulticastDelegate?

Trong .NET, tất cả delegate mà ta khai báo bằng từ khóa "delegate" thực ra compiler dịch thành một class con kế thừa từ "MulticastDelegate", chứ không kế thừa trực tiếp từ "Delegate".

Điểm khác biệt chính được thêm vào của MultiDelegate là Delegate cơ bản chỉ trỏ tới 1 method (target + method info), còn MulticastDelegate thêm _invocationList và _invocationCount để quản lý nhiều delegate con.

Chúng ta sẽ thấy rõ hơn trong code reference:

[Serializable]
[System.Runtime.InteropServices.ComVisible(true)]
public abstract class MulticastDelegate : Delegate
{
    // Danh sách delegate con (chỉ khi multicast)
    private Delegate[] _invocationList;
    private int _invocationCount;

    // Constructor đặc biệt, runtime gọi, dev không được new trực tiếp
    protected MulticastDelegate(object target, string method);

    // Gọi lần lượt toàn bộ method trong danh sách
    public override object? DynamicInvoke(params object?[]? args);

    // Trả về mảng các delegate trong danh sách
    public virtual Delegate[] GetInvocationList();

    // Clone: copy delegate + invocation list
    public sealed override Delegate Clone();

    // Combine / Remove override lại để quản lý danh sách delegate
    protected override Delegate CombineImpl(Delegate follow);
    protected override Delegate RemoveImpl(Delegate value);

    // Một số API hệ thống
    ...
}
  • Hiểu đơn giản là MulticastDelegate có một mảng _invocationList các delegate con, Mỗi phần tử trong _invocationList là một delegate đơn.

(không phải là chứa một mảng các method đâu, mà là chứa một mảng các delegate)

  • Các method trong _invocationList có cùng chung “signature” (tức là cùng kiểu trả về (return type) và danh sách tham số (số lượng + kiểu)). Cái này là bắt buộc. Tuy nhiên nó không phân biệt static/instance method, tức là chỉ cần cùng kiểu thôi.

Vì vậy nên nó có một số tính chất sau:

  • Khi gọi d(), nếu d là multicast, runtime sẽ duyệt _invocationList và gọi từng delegate theo thứ tự.
  • Giá trị trả về:
  • Nếu delegate có void return type: tất cả method đều chạy.
  • Nếu delegate có return type: chỉ lấy giá trị trả về của method cuối cùng. Các method trước đó vẫn chạy bình thường, nhưng giá trị trả về bị bỏ qua.
  • Nếu một method trong _invocationList ném exception, việc gọi sẽ dừng tại đó, các method sau không được gọi.
  • Các toán tử += / -= sẽ thêm bớt các delegate con vào mảng. Thực chất nó chỉ là thực hiện CombineImpl và RemoveImpl thôi. Và vì tính bất biến, nên các phép += / -= không chỉnh sửa delegate gốc, mà tạo ra một delegate mới có danh sách _invocationList đã được thêm/bớt.

1.3, Cú pháp sử dụng Delegate

  • Về cú pháp, thì nó đơn giản chứ không bị phức tạp như phần bản chất bên trên. Nhưng bản chất giải thích cho cách cú pháp hoạt động.

Trước hết cần biết: từ khoá delegate sẽ tạo ra một System.MultiDelegate chứ không phải là System.Delegate.

1.3.0, Delegate khai báo ở đâu

Delegate trong C# không thể khai báo trong method (nó không phải local function), mà luôn phải khai báo ở cấp lớp (class/struct) hoặc cấp namespace.

  • Khi khai báo ở namespace (ngoài class/struct):

Delegate trở thành kiểu dữ liệu toàn cục trong namespace, mọi class trong namespace đó (hoặc dùng "using ...") đều có thể dùng.

  • Khi khai báo bên trong class/struct/interface:
    Delegate trở thành nested type, chỉ truy cập qua "TênLớp.TênDelegate". Dùng để gắn chặt delegate với class đó.

1.3.1, Khai báo delegate

Cú pháp:

[access-modifier] delegate <return-type> <DelegateName>(<parameters>);

Ví dụ:

public delegate int MyDelegate(string s);

Ở đây MyDelegate là một kiểu dữ liệu mới, đại diện cho hàm nhận một string và trả về int. Nhìn vào đây có thể hiểu lầm MyDelegate nhận tham số string và trả về kiểu int, nhưng delegate không hề nhận tham số. Nó chỉ đang trỏ tới một method sẽ làm vậy (nhận tham số string và trả về kiểu int).

1.3.2, Khởi tạo delegate

Có thể gán delegate bằng cách trỏ tới một method có cùng signature:

class Program
{
    public delegate int MyDelegate(string s);
    static int GetLength(string s) => s.Length;
    static void Main()
        {
            MyDelegate d = new MyDelegate(GetLength);
        }
}

1.3.3, Gọi delegate

Delegate được gọi giống như gọi method bình thường:

int len = d("hello");   // len = 5

Gọi delegate này là nó sẽ gọi tới method để thực thi chứ nó không làm gì cả ngoài việc ủy quyền.

1.3.4, Multicast delegate (+=, -=)

Một delegate có thể trỏ tới nhiều method thông qua toán tử +=. Khi gọi, tất cả method trong danh sách sẽ chạy tuần tự.

void PrintA(string s) => Console.WriteLine("A: " + s);
void PrintB(string s) => Console.WriteLine("B: " + s);
MyDelegate d = PrintA;
d += PrintB;
d("test");  
// Output:
// A: test
// B: test

Toán tử -= sẽ loại bỏ một method ra khỏi danh sách.

1.3.5, Anonymous method

Thay vì định nghĩa method riêng, có thể gán trực tiếp biểu thức/hàm ẩn danh vào delegate:

MyDelegate d = delegate (string s)
{
    return s.Length;
};

Nó là cú pháp nên đừng thắc mắc gì, cứ dùng thôi (dù tôi thắc mắc tại sao lại có kiểu khai báo thế này).

1.3.6, Lambda expression

Cú pháp rút gọn hơn nữa là lambda:

MyDelegate d = s => s.Length;

Ngoài ra còn các delegate build in (Func, Action, Predicate) nhưng sẽ không nêu ở đây.

2, Event

Trước hết, cần biết trong lập trình có nhiều Pattern để giải quyết việc truyền thông tin giữa các thành phần của chương trình (callback/delegate để gọi lại hàm, observer để theo dõi và phản ứng khi có thay đổi, mediator để liên lạc qua trung gian, publish–subscribe để gửi/nhận qua event bus, ...). Ở đây event thuộc cơ chế observer (một đối tượng phát tín hiệu, nhiều đối tượng khác có thể lắng nghe và phản ứng), với mục tiêu là giúp tách rời nơi phát sinh sự kiện và nơi xử lý sự kiện.

2.1, Cơ chế Observer nói chung và trong C#

2.1.1, Observer Pattern nói chung

  • Là một mẫu thiết kế hành vi (behavioral pattern).
  • Ý tưởng: có một đối tượng Subject (chủ thể, nơi phát sinh sự kiện, Publisher) và nhiều đối tượng Observer (người quan sát, nơi xử lý sự kiện, Subscriber).
  • Khi Subject thay đổi trạng thái hoặc phát sinh sự kiện, nó sẽ thông báo cho tất cả Observer đã đăng ký, để chúng phản ứng lại. Sự kiện (event) ở đây có thể là bất cứ thứ gì thay đổi từ SubjectObserver quan tâm tới.

như một cái button thì sự kiện được quan tâm là click chẳng hạn.

  • Mục tiêu: giảm sự phụ thuộc trực tiếp (loose coupling) giữa nơi phát sự kiện và nơi xử lý sự kiện. Cụ thể hơn thì như này:

image.png

  • Khi không có Observer/Event (Tight Coupling)
    • Subject (nơi phát sinh sự kiện) nối trực tiếp mũi tên đến Observer AObserver B.
    • Điều này có nghĩa là:
      • Subject phải biết rõ các observer nào tồn tại.
      • Khi cần thông báo, Subject tự gọi trực tiếp hàm xử lý trong Observer.
    • Hậu quả:
      • Nếu thêm một Observer mới → phải sửa code trong Subject.
      • Nếu Observer đổi tên hàm hoặc interface → Subject cũng phải sửa theo.
      • Hai bên phụ thuộc chặt chẽ (tight coupling) → khó mở rộng, khó bảo trì.
  • Khi có Observer/Event (Loose Coupling)
    • Subject không gọi Observer trực tiếp nữa, mà chỉ phát ra một Event.
    • Event hoạt động như một “điểm phát tín hiệu” trung gian.
    • Observer AObserver B nếu quan tâm thì sẽ tự đăng ký (subscribe) với Event này.
    • Khi sự kiện xảy ra, Subject chỉ việc gọi "Event.Invoke(...)" (hành động thực thi khi phát hiện ra sự kiện)
    • Toàn bộ Observer đã đăng ký sẽ được thông báo, Subject không cần biết chúng là ai.

Kết quả:

  • Loose coupling: Subject và Observer tách rời, không phụ thuộc vào nhau.
  • Có thể thêm/bớt Observer mà không cần đụng đến code của Subject.

2.1.2, Vị trí Event trong Observer Pattern

  • Trong mô hình trên, Event đóng vai trò trung gian, nhưng nó không có vị trí "ngang hàng" với Publisher (Subject) hay Subscriber (Observer).

Cụ thể:

  • Publisher là thành phần chủ động phát ra sự kiện. Nó quyết định khi nào và làm thế nào để kích hoạt một sự kiện (ví dụ: gọi Event.Invoke trong C#). Publisher có trạng thái và hành vi riêng, như xử lý logic nội bộ hoặc theo dõi các thay đổi để quyết định phát sự kiện.
  • Subscriber cũng là thành phần chủ động, tự quyết định việc đăng ký (subscribe) hoặc hủy đăng ký (unsubscribe) với event, đồng thời tự xử lý thông tin nhận được từ sự kiện theo cách riêng của nó.
  • Event, ngược lại, chỉ là một cơ chế truyền tín hiệu, không có trạng thái hay hành vi riêng. Nó không tự hoạt động mà phụ thuộc hoàn toàn vào:
  • Publisher để được định nghĩa và kích hoạt.
  • Subscriber để có ý nghĩa (nếu không có Subscriber đăng ký, Event chỉ là một "tín hiệu rỗng", vì nó không tự lưu trữ trạng thái hay thực hiện bất kỳ logic nào.).
  • Điều này khác với Publisher và Subscriber, vốn là các thực thể (thường là các lớp hoặc đối tượng) có khả năng thực hiện các hành động độc lập, như lưu trữ dữ liệu, xử lý logic, hoặc tương tác với các thành phần khác.

Mặc dù không ngang hàng, Event là yếu tố cốt lõi để đảm bảo loose coupling giữa Publisher và Subscriber:

  • Event cho phép Publisher gửi thông báo mà không cần biết Subscriber là ai hay có bao nhiêu Subscriber. Điều này giúp Publisher và Subscriber không phụ thuộc trực tiếp vào nhau.
  • Subscriber có thể tự do đăng ký hoặc hủy đăng ký với Event mà không cần Publisher thay đổi mã nguồn. Điều này làm tăng tính linh hoạt và khả năng mở rộng của hệ thống.
  • Tuy nhiên cũng chính vì Event chỉ là một cầu nối, nó không thể được coi là "ngang hàng" với Publisher hay Subscriber, vì nó không có khả năng tự quản lý hay đưa ra quyết định.

2.1.3, Event trong C#

Trước hết làm rõ: không có từ khoá hay class trong C# nào tên là "Event" cả.

Còn với "event" (không viết hoa), thì nó là một từ khoá để khai báo một sự kiện (event member) trong class, struct hoặc interface. Dưới góc độ OOP thì có thể coi event tương đương với property và method trong class.

Như đã phân tích ở trên thì event không phải là một sự kiện độc lập, nó không thể tồn tại/không có ý nghĩa nếu không có Publisher và Subcriber. Nên nó chỉ ở trong phạm vi class, struct hoặc interface, không đi ra ngoài tới phạm vi namespace.

Nếu học OOP cơ bản chỉ có 2 loại property và method thì bây giờ biết có thêm loại event này nữa là được. Ngoài ra delegate, như đã nói ở phần trên, cũng có thể là một thành phần tương đương nếu được khai báo trong class.

=> chốt lại: "event" là từ khoá.

2.1.4, Vậy cụ thể hơn, cái được khai báo nhờ từ khoá "event" trong C#, thực chất là gì?

  • Trong thực tế khi một "sự kiện" là một hành động nào đó được thực hiện, hoặc trạng thái của một đối tượng nào đó bị thay đổi, được phát ra từ Publisher, và được Subcriber quan tâm. Khi có "sự kiện", Subcriber sẽ thực hiện một hành động nào đó.
  • Như việc khách mua một mặt hàng, làm số lượng mặt hàng đó giảm xuống dưới 5 cái trong cửa hàng (thay đổi trạng thái từ còn hàng => sắp hết hàng), đó là một "sự kiện". Hay một ai đó bước vào cửa hàng (một hành động được thực hiện), cũng là một "sự kiện". Với sự kiện hết hàng, thì nhân viên sẽ phải đi lấy thêm hàng mới, với sự kiện mở cửa, thì nhân viên sẽ phải cúi người chào (ví dụ thế). Đó là "phản ứng khi có sự kiện".
  • Tuy nhiên! Trong C#, từ khoá "event" không phải để khai báo một sự kiện theo nghĩa đời thực như vậy (Nếu để khai báo sự kiện như vậy thì nó đã phải là một thực thể ngang hàng với Publisher và Subcriber rồi). Từ khoá "event" trong C# dùng để định danh một cơ chế kết nối của sự kiện với hành động xử lý khi có sự kiện.
  • Thứ được định nghĩa bởi từ khoá "event" không phải là sự kiện như mở cửa hoặc giảm số hàng, cũng không phải hành động lấy hàng hay mở cửa, mà là sự liên kết giữa "sự kiện" và "cách xử lý sự kiện". Với ví dụ trên áp vào C#, thứ được định nghĩa bởi từ khoá "event" là "sự ý thức của nhân viên dẫn đến phản xạ có điều kiện với các sự kiện diễn ra".

Nghe cấn cấn phải không. Nhưng đó là cách diễn giải tốt nhất rồi.

2.1.5, "Cơ chế kết nối" đó triển khai như thế nào trong C#?

Để biểu diễn một thứ trừu tượng như bên trên vừa mô tả thì cũng cần một thứ trừu tượng tương đương.

  • Phân tích: "event" biểu diễn một "sự phản xạ" nhắm tới một "hành động". Trong C#, delegate lại là một "con trỏ" dẫn tới một "function". Nhìn vào sự tương đồng này thì đứa trẻ con cũng nghĩ tới: event là một delegate thì hợp lý.
  • Nhưng đấy là trẻ con nghĩ. Còn thực tế hơn, dựa trên những phân tích bên trên, delegate là một "kiểu dữ liệu": nó là một thực thể hữu hình và nhìn thấy, truy cập được, chạy được. "event" lại là một định nghĩa phụ thuộc, chỉ tồn tại và có ý nghĩa khi thuộc về một Publisher và được quan tâm bởi một Subcriber.

=> Vì vậy, trong C#, "event" được xây dựng là một lớp bọc (wrapper) quanh delegate, để biến delegate từ một “thực thể có thể gọi được trực tiếp” thành một “cơ chế hành động khi có sự kiện” giữa Publisher và Subscriber.

  • Có thể coi "event" là lớp ý thức cho hành động bên trong delegate. Delegate là “một hành động cụ thể”, còn Event là “quy tắc phản xạ được dạy”. Việc một event bọc ngoài một delegate như việc dạy nhân viên cách làm việc, và một event trong class sau khi code xong có thể coi là một nhân viên đã biết làm việc theo yêu cầu. Người chủ cửa hàng (Subscriber) có thể gắn thêm hành động cho phản xạ đó (dạy thêm: khi có khách vào thì cúi chào), nhưng chỉ nhân viên (Publisher) mới quyết định khi nào phản xạ xảy ra (khi nào Invoke).

2.1.6, Lớp bọc event được xây dựng thế nào trong code C#?

  • Về mặt cú pháp, một event trong C# được khai báo như thế này:

C#
public event MyDelegate MyEvent; //với MyDelegate là một delegate

  • Trông thì giống như "MyEvent" là một biến delegate.
    Nhưng thực ra compiler không tạo ra một biến delegate trực tiếp, mà nó sẽ sinh thêm code ẩn (syntactic sugar - ngọt ngào cho con mắt).
  • C# sẽ biến đoạn khai báo event ở trên thành:
public delegate void MyEvent(string message);

public class MyClass
{
    // delegate field ẩn phía sau event
    private MyDelegate HiddenDelegate;

    // event member
    public event MyDelegate MyEvent
    {
        add    { HiddenDelegate = (MyDelegate)Delegate.Combine(HiddenDelegate, value); }
        remove { HiddenDelegate = (MyDelegate)Delegate.Remove(HiddenDelegate, value); }
    }
}

Như vậy, về bản chất:

  • "MyEvent" không phải là một biến delegate bình thường, mà là một event member có thêm hai hàm: add và remove (hay còn gọi là "event accessors")
  • Khi code bên ngoài viết: "obj.MyEvent += Handler;" thì compiler sẽ dịch thành: "obj.add_MyEvent(Handler);" và hàm "add" sẽ thực hiện "Delegate.Combine" để gắn thêm "Handler" vào delegate ẩn "HiddenDelegate".
  • khi bên trong Publisher muốn phát ra sự kiện, nó sẽ gọi trực tiếp delegate ẩn phía sau, ví dụ:
public void CheckStock()
{
    // Khi có tình huống cần báo (ví dụ: hàng sắp hết), 
    // Publisher chủ động "kích hoạt" event
    HiddenDelegate?.Invoke("Hàng sắp hết!");
}
  • Điểm quan trọng là, chỉ Publisher (bên trong class) mới được phép Invoke event. Bên ngoài (Subscriber) không thể làm gì ngoài phép "+=" hoặc "-=" để gắn/hủy hàm xử lý.

Vì vậy:

  • Subscriber không được phép trực tiếp thay đổi cách nhân viên được huấn luyện (tức không thể gọi thực thi "Invoke" hay truy cập delegate ẩn).
  • Subscriber chỉ được đưa thêm nhiệm vụ hoặc rút bớt nhiệm vụ thông qua += và -=.

Subcriber chỉ được phép chọn nhân viên đã qua huấn luyện chứ ko đc phép huấn luyện nhân viên.

Nếu thế thì có một câu hỏi nhỏ là: "như vậy muốn thay đổi cách xử lý thì phải thay đổi ở Publisher, vậy nó chưa đủ tính loose coupling à?"


Thực ra, phải phân biệt 2 tầng coupling:

Giữa Publisher và Subscriber

  • Publisher không biết Subscriber nào, chỉ biết mình có một event.
  • Subscriber không biết nội bộ Publisher xử lý ra sao, chỉ cần thấy event và gắn handler.
    → Ở mức này thì đúng là loose coupling rồi: hai bên không phụ thuộc trực tiếp.

Giữa Event và Delegate (nội bộ Publisher)

  • Cách Publisher triển khai event (khi nào gọi, truyền tham số gì, có lọc điều kiện gì…) là cố định bên trong Publisher.
  • Subscriber chỉ có thể chọn có chấp nhận theo dõi sự kiện với phản ứng đó hay không.
    => Ở mức này thì coupling vẫn còn, vì Subscriber phụ thuộc vào Publisher để “kích hoạt” event. Subscriber không thể chủ động thay đổi cơ chế kích hoạt.

=> trong nhiều hệ thống người ta hay đưa thêm EventBus / Mediator để đạt mức loose coupling cao hơn.

2.1.7, Cú pháp cơ bản của event trong C#

Nếu không cần hiểu thì bỏ qua 6 phần trên đi vào phần này luôn cũng được 😃))

  1. Khai báo event đơn giản
public delegate void MyDelegate(string message);

public class Publisher
{
    public event MyDelegate MyEvent; // khai báo event dựa trên delegate
}
  • "MyEvent" là một event member.
  • dùng được cú pháp += và -= để gắn hoặc gỡ handler.
  1. Gắn và gỡ handler
Publisher pub = new Publisher();
pub.MyEvent += Handler1;   // gắn Subscriber
pub.MyEvent -= Handler1;   // gỡ Subscriber
  1. Kích hoạt (raise) event
public class Publisher
{
    public event MyDelegate MyEvent;
    public void DoSomething()
    {// cách an toàn với null-conditional
        MyEvent?.Invoke("Hello event!");
    }
}
  • Chỉ Publisher (nội bộ class) mới có quyền gọi MyEvent().
  • Subscriber không thể gọi trực tiếp MyEvent().

Ngoài ra, có thể tìm hiểu thêm về EventHandler, nhưng sau khi có bài viết này thì tìm hiểu cái đó cũng chỉ cần chatGPT thôi, nên tôi lười không viết ở đây.

Cảm ơn các bạn đã đọc tới đây. Bài viết được viết trong lúc mình không biết gì nên chắc chắn có sai sót, mong mọi người thông cảm và góp ý.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.