Sự kiện Hành động <> so với sự kiện EventHandler <>


144

Có sự khác biệt nào giữa khai báo event Action<>event EventHandler<>.

Giả sử nó không quan trọng đối tượng thực sự gây ra một sự kiện.

ví dụ:

public event Action<bool, int, Blah> DiagnosticsEvent;

đấu với

public event EventHandler<DiagnosticsArgs> DiagnosticsEvent;

class DiagnosticsArgs : EventArgs
{
    public DiagnosticsArgs(bool b, int i, Blah bl)
    {...}
    ...
}

việc sử dụng sẽ gần như giống nhau trong cả hai trường hợp:

obj.DiagnosticsEvent += HandleDiagnosticsEvent;

Có một số điều mà tôi không thích về event EventHandler<>mẫu:

  • Khai báo loại bổ sung có nguồn gốc từ EventArss
  • Bắt buộc chuyển nguồn đối tượng - thường không ai quan tâm

Nhiều mã hơn có nghĩa là nhiều mã hơn để duy trì mà không có bất kỳ lợi thế rõ ràng.

Kết quả là, tôi thích event Action<>

Tuy nhiên, chỉ khi có quá nhiều đối số loại trong Hành động <>, thì sẽ cần thêm một lớp bổ sung.


2
plusOne (Tôi vừa đánh bại hệ thống) vì "không ai quan tâm"
hyankov

@plusOne: Tôi thực sự cần biết người gửi! Nói điều gì đó xảy ra và bạn muốn biết ai đã làm điều đó. Đó là bạn cần 'nguồn đối tượng' (còn gọi là người gửi).
Kamran Bigdely

người gửi có thể là một tài sản trong tải trọng của sự kiện
Thanasis Ioannidis

Câu trả lời:


67

Sự khác biệt chính là nếu bạn sử dụng Action<>sự kiện của bạn sẽ không tuân theo mẫu thiết kế của hầu như bất kỳ sự kiện nào khác trong hệ thống, mà tôi sẽ xem xét một nhược điểm.

Một ưu điểm với mẫu thiết kế thống trị (ngoài sức mạnh của sự giống nhau) là bạn có thể mở rộng EventArgsđối tượng với các thuộc tính mới mà không làm thay đổi chữ ký của sự kiện. Điều này vẫn có thể xảy ra nếu bạn đã sử dụng Action<SomeClassWithProperties>, nhưng tôi thực sự không thấy vấn đề gì khi không sử dụng phương pháp thường xuyên trong trường hợp đó.


Có thể sử dụng Action<>kết quả trong rò rỉ bộ nhớ? Một nhược điểm với EventHandlermẫu thiết kế là rò rỉ bộ nhớ. Cũng cần chỉ ra rằng có thể có nhiều Trình xử lý sự kiện nhưng chỉ có một Hành động
Luke T O'Brien

4
@ LukeTO'Brien: Các sự kiện thực chất là đại biểu, do đó, khả năng rò rỉ bộ nhớ tương tự tồn tại với Action<T>. Ngoài ra, một Action<T> có thể tham khảo một số phương pháp. Đây là một ý chính chứng minh rằng: gist.github.com/fmork/4a4ddf687fa8398d19ddb2df96f0b434
Fredrik Mörk

88

Dựa trên một số câu trả lời trước đó, tôi sẽ chia câu trả lời của mình thành ba lĩnh vực.

Đầu tiên, các giới hạn vật lý của việc sử dụng Action<T1, T2, T2... >so với sử dụng một lớp dẫn xuất của EventArgs. Có ba: Thứ nhất, nếu bạn thay đổi số lượng hoặc loại tham số, mọi phương thức đăng ký sẽ phải được thay đổi để phù hợp với mẫu mới. Nếu đây là một sự kiện phải đối mặt công khai mà các hội đồng bên thứ 3 sẽ sử dụng và có bất kỳ khả năng nào mà sự kiện đó sẽ thay đổi, thì đây sẽ là một lý do để sử dụng một lớp tùy chỉnh xuất phát từ sự kiện tranh luận vì sự thống nhất vì lợi ích của bạn sử dụng một Action<MyCustomClass>) Thứ hai, việc sử dụng Action<T1, T2, T2... >sẽ ngăn bạn chuyển phản hồi TRỞ LẠI sang phương thức gọi trừ khi bạn có một loại đối tượng nào đó (ví dụ với thuộc tính Handled) được truyền cùng với Hành động. Thứ ba, bạn không được đặt tên thông số, vì vậy nếu bạn đang đi qua 3 boollà một int, haistringVà, a DateTime, bạn không biết ý nghĩa của những giá trị đó là gì. Là một lưu ý phụ, bạn vẫn có thể có "Phương pháp an toàn cho sự kiện này trong khi vẫn sử dụng Action<T1, T2, T2... >".

Thứ hai, ý nghĩa nhất quán. Nếu bạn có một hệ thống lớn mà bạn đã làm việc cùng, thì hầu như luôn luôn tốt hơn để làm theo cách mà phần còn lại của hệ thống được thiết kế trừ khi bạn có một lý do rất chính đáng. Nếu bạn phải đối mặt công khai với các sự kiện cần được duy trì, khả năng thay thế các lớp dẫn xuất có thể quan trọng. Ghi nhớ nó trong tâm trí.

Thứ ba, thực tế thực tế, cá nhân tôi thấy rằng tôi có xu hướng tạo ra nhiều sự kiện một lần cho những thứ như thay đổi thuộc tính mà tôi cần tương tác (Đặc biệt khi thực hiện MVVM với các mô hình xem tương tác với nhau) hoặc nơi có sự kiện một tham số duy nhất. Hầu hết thời gian các sự kiện này có hình thức public event Action<[classtype], bool> [PropertyName]Changed;hoặc public event Action SomethingHappened;. Trong những trường hợp này, có hai lợi ích. Đầu tiên, tôi nhận được một loại cho lớp phát hành. Nếu MyClasstuyên bố và là lớp duy nhất kích hoạt sự kiện, tôi nhận được một ví dụ rõ ràng MyClassđể làm việc với trình xử lý sự kiện. Thứ hai, đối với các sự kiện đơn giản như sự kiện thay đổi thuộc tính, ý nghĩa của các tham số là rõ ràng và được nêu trong tên của trình xử lý sự kiện và tôi không phải tạo vô số các lớp cho các loại sự kiện này.


Bài đăng blog tuyệt vời. Chắc chắn giá trị đọc nếu bạn đang đọc chủ đề này!
Vexir

1
Câu trả lời chi tiết và được
cân nhắc kỹ lưỡng

18

Phần lớn, tôi muốn nói theo mô hình. Tôi đã đi chệch khỏi nó, nhưng rất hiếm khi, và vì những lý do cụ thể. Trong trường hợp cụ thể, vấn đề lớn nhất mà tôi gặp phải là tôi có thể vẫn sử dụng Action<SomeObjectType>, cho phép tôi thêm các thuộc tính bổ sung sau và sử dụng thuộc tính 2 chiều không thường xuyên (nghĩ Handledhoặc các sự kiện phản hồi khác trong đó thuê bao cần thiết lập một tài sản trên đối tượng sự kiện). Và một khi bạn đã bắt đầu xuống dòng đó, bạn cũng có thể sử dụng EventHandler<T>cho một số T.


14

Lợi thế của cách tiếp cận dài hơn xuất hiện khi mã của bạn nằm trong dự án 300.000 dòng.

Sử dụng hành động, như bạn có, không có cách nào để cho tôi biết bool, int và Blah là gì. Nếu hành động của bạn thông qua một đối tượng xác định các tham số thì ok.

Sử dụng một EventHandler muốn có EventArss và nếu bạn sẽ hoàn thành ví dụ Chẩn đoán của mình với getters cho các thuộc tính nhận xét mục đích của họ thì ứng dụng của bạn sẽ dễ hiểu hơn. Ngoài ra, vui lòng bình luận hoặc đặt tên đầy đủ cho các đối số trong hàm tạo Chẩn đoán.


6

Nếu bạn làm theo mẫu sự kiện tiêu chuẩn, thì bạn có thể thêm một phương thức mở rộng để kiểm tra việc bắn sự kiện an toàn / dễ dàng hơn. (vd

(Mặc dù tôi có hai loại suy nghĩ liệu bạn có nên sử dụng các phương thức mở rộng trên các đối tượng null ...)

public static class EventFirer
{
    public static void SafeFire<TEventArgs>(this EventHandler<TEventArgs> theEvent, object obj, TEventArgs theEventArgs)
        where TEventArgs : EventArgs
    {
        if (theEvent != null)
            theEvent(obj, theEventArgs);
    }
}

class MyEventArgs : EventArgs
{
    // Blah, blah, blah...
}

class UseSafeEventFirer
{
    event EventHandler<MyEventArgs> MyEvent;

    void DemoSafeFire()
    {
        MyEvent.SafeFire(this, new MyEventArgs());
    }

    static void Main(string[] args)
    {
        var x = new UseSafeEventFirer();

        Console.WriteLine("Null:");
        x.DemoSafeFire();

        Console.WriteLine();

        x.MyEvent += delegate { Console.WriteLine("Hello, World!"); };
        Console.WriteLine("Not null:");
        x.DemoSafeFire();
    }
}

4
... bạn không thể làm tương tự với Hành động <T>? SafeFire <T> (Hành động này <T> theEvent, T theEventArss) sẽ hoạt động để ... và không cần sử dụng "đâu"
Beachwalker

6

Tôi nhận ra rằng câu hỏi này đã hơn 10 tuổi, nhưng dường như tôi không chỉ có câu trả lời rõ ràng nhất mà còn không thực sự rõ ràng từ câu hỏi về sự hiểu biết tốt về những gì diễn ra dưới vỏ bọc. Ngoài ra, có những câu hỏi khác về ràng buộc muộn và điều đó có nghĩa gì đối với các đại biểu và lambdas (nhiều hơn về điều đó sau).

Đầu tiên để giải quyết con voi / khỉ đột 800 lb trong phòng, khi nào nên chọn eventvs Action<T>/ Func<T>:

  • Sử dụng lambda để thực thi một câu lệnh hoặc phương thức. Sử dụng eventkhi bạn muốn nhiều hơn một mô hình pub / sub với nhiều câu lệnh / lambdas / hàm sẽ thực thi (đây là một sự khác biệt lớn ngay lập tức).
  • Sử dụng lambda khi bạn muốn biên dịch các câu lệnh / hàm để biểu thị cây. Sử dụng các đại biểu / sự kiện khi bạn muốn tham gia vào ràng buộc muộn truyền thống hơn, chẳng hạn như được sử dụng trong phản chiếu và xen kẽ COM.

Ví dụ về một sự kiện, cho phép kết nối một tập hợp các sự kiện đơn giản và 'tiêu chuẩn' bằng ứng dụng bảng điều khiển nhỏ như sau:

public delegate void FireEvent(int num);

public delegate void FireNiceEvent(object sender, SomeStandardArgs args);

public class SomeStandardArgs : EventArgs
{
    public SomeStandardArgs(string id)
    {
        ID = id;
    }

    public string ID { get; set; }
}

class Program
{
    public static event FireEvent OnFireEvent;

    public static event FireNiceEvent OnFireNiceEvent;


    static void Main(string[] args)
    {
        OnFireEvent += SomeSimpleEvent1;
        OnFireEvent += SomeSimpleEvent2;

        OnFireNiceEvent += SomeStandardEvent1;
        OnFireNiceEvent += SomeStandardEvent2;


        Console.WriteLine("Firing events.....");
        OnFireEvent?.Invoke(3);
        OnFireNiceEvent?.Invoke(null, new SomeStandardArgs("Fred"));

        //Console.WriteLine($"{HeightSensorTypes.Keyence_IL030}:{(int)HeightSensorTypes.Keyence_IL030}");
        Console.ReadLine();
    }

    private static void SomeSimpleEvent1(int num)
    {
        Console.WriteLine($"{nameof(SomeSimpleEvent1)}:{num}");
    }
    private static void SomeSimpleEvent2(int num)
    {
        Console.WriteLine($"{nameof(SomeSimpleEvent2)}:{num}");
    }

    private static void SomeStandardEvent1(object sender, SomeStandardArgs args)
    {

        Console.WriteLine($"{nameof(SomeStandardEvent1)}:{args.ID}");
    }
    private static void SomeStandardEvent2(object sender, SomeStandardArgs args)
    {
        Console.WriteLine($"{nameof(SomeStandardEvent2)}:{args.ID}");
    }
}

Đầu ra sẽ như sau:

nhập mô tả hình ảnh ở đây

Nếu bạn đã làm tương tự với Action<int>hoặc Action<object, SomeStandardArgs>, bạn sẽ chỉ nhìn thấy SomeSimpleEvent2SomeStandardEvent2.

Vì vậy, những gì đang xảy ra bên trong event?

Nếu chúng tôi mở rộng ra FireNiceEvent, trình biên dịch thực sự tạo ra những điều sau đây (tôi đã bỏ qua một số chi tiết liên quan đến đồng bộ hóa luồng không liên quan đến cuộc thảo luận này):

   private EventHandler<SomeStandardArgs> _OnFireNiceEvent;

    public void add_OnFireNiceEvent(EventHandler<SomeStandardArgs> handler)
    {
        Delegate.Combine(_OnFireNiceEvent, handler);
    }

    public void remove_OnFireNiceEvent(EventHandler<SomeStandardArgs> handler)
    {
        Delegate.Remove(_OnFireNiceEvent, handler);
    }

    public event EventHandler<SomeStandardArgs> OnFireNiceEvent
    {
        add
        {
            add_OnFireNiceEvent(value)
        }
        remove
        {
            remove_OnFireNiceEvent(value)

        }
    }

Trình biên dịch tạo ra một biến đại biểu riêng không thể nhìn thấy đối với không gian tên lớp mà nó được tạo. Đại biểu đó là những gì được sử dụng để quản lý đăng ký và tham gia ràng buộc muộn và giao diện công khai là giao diện quen thuộc +=và các -=nhà khai thác mà tất cả chúng ta đều biết và yêu thích :)

Bạn có thể tùy chỉnh mã cho trình xử lý thêm / xóa bằng cách thay đổi phạm vi của FireNiceEventđại biểu thành được bảo vệ. Điều này hiện cho phép các nhà phát triển thêm các móc tùy chỉnh vào các móc, chẳng hạn như ghi nhật ký hoặc móc bảo mật. Điều này thực sự làm cho một số tính năng rất mạnh mẽ hiện cho phép tùy chỉnh khả năng truy cập để đăng ký dựa trên vai trò của người dùng, v.v. Bạn có thể làm điều đó với lambdas không? (Trên thực tế bạn có thể bằng cách biên dịch cây biểu thức tùy chỉnh, nhưng điều đó nằm ngoài phạm vi của phản hồi này).

Để giải quyết một vài điểm từ một số câu trả lời ở đây:

  • Thực sự không có sự khác biệt về 'độ giòn' giữa việc thay đổi danh sách đối số Action<T>và thay đổi các thuộc tính trong một lớp có nguồn gốc từ EventArgs. Cả hai sẽ không chỉ yêu cầu thay đổi biên dịch, cả hai sẽ thay đổi giao diện công cộng và sẽ yêu cầu phiên bản. Không khác nhau.

  • Đối với tiêu chuẩn ngành nào, điều đó phụ thuộc vào việc sử dụng cái này ở đâu và tại sao. Action<T>và như vậy thường được sử dụng trong IoC và DI, và eventthường được sử dụng trong định tuyến thư như khung kiểu GUI và MQ. Lưu ý rằng tôi nói thường xuyên , không phải lúc nào .

  • Các đại biểu có tuổi thọ khác với lambdas. Người ta cũng phải nhận thức được việc bắt giữ ... không chỉ với việc đóng cửa, mà còn với khái niệm 'nhìn những gì con mèo kéo vào'. Điều này không ảnh hưởng đến dung lượng bộ nhớ / tuổi thọ cũng như quản lý hay rò rỉ.

Một điều nữa, một cái gì đó tôi đã tham khảo trước đó ... khái niệm ràng buộc muộn. Bạn sẽ thường thấy điều này khi sử dụng khung như LINQ, liên quan đến thời điểm lambda trở thành 'sống'. Điều đó rất khác so với ràng buộc muộn của một đại biểu, điều này có thể xảy ra nhiều hơn một lần (tức là lambda luôn ở đó, nhưng ràng buộc xảy ra theo yêu cầu thường xuyên như cần thiết), trái ngược với lambda, một khi nó xảy ra, nó đã được thực hiện - ma thuật đã biến mất và phương thức / tài sản (ies) sẽ luôn luôn ràng buộc. Một cái gì đó để giữ trong tâm trí.


4

Nhìn vào các mẫu sự kiện Standard .NET, chúng tôi tìm thấy

Chữ ký tiêu chuẩn cho đại biểu sự kiện .NET là:

void OnEventRaised(object sender, EventArgs args);

[...]

Danh sách đối số chứa hai đối số: người gửi và đối số sự kiện. Kiểu thời gian biên dịch của người gửi là System.Object, mặc dù bạn có thể biết một loại dẫn xuất hơn sẽ luôn luôn đúng. Theo quy ước, sử dụng đối tượng .

Dưới đây trên cùng một trang, chúng tôi tìm thấy một ví dụ về định nghĩa sự kiện điển hình giống như

public event EventHandler<EventArgs> EventName;

Chúng tôi đã xác định

class MyClass
{
  public event Action<MyClass, EventArgs> EventName;
}

người xử lý có thể đã được

void OnEventRaised(MyClass sender, EventArgs args);

trong đó sendercó loại chính xác ( dẫn xuất nhiều hơn ).


Xin lỗi đã không nhận xét rằng sự khác biệt là trong chữ ký xử lý, sẽ có lợi cho việc gõ chính xác hơn sender .
dùng1832484
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.