การกระทำของเหตุการณ์ <> vs เหตุการณ์ EventHandler <>


144

มีความแตกต่างกันระหว่างการประกาศและevent Action<>event EventHandler<>

สมมติว่ามันไม่สำคัญว่าวัตถุจะยกเหตุการณ์ขึ้นจริง

ตัวอย่างเช่น:

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

VS

public event EventHandler<DiagnosticsArgs> DiagnosticsEvent;

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

การใช้งานจะเหมือนกันเกือบทั้งสองกรณี:

obj.DiagnosticsEvent += HandleDiagnosticsEvent;

มีหลายสิ่งที่ฉันไม่ชอบเกี่ยวกับevent EventHandler<>รูปแบบ:

  • การประกาศชนิดพิเศษที่ได้รับมาจาก EventArgs
  • การบังคับส่งผ่านแหล่งวัตถุ - มักจะไม่มีใครสนใจ

รหัสเพิ่มเติมหมายถึงรหัสเพิ่มเติมเพื่อรักษาโดยไม่มีข้อได้เปรียบที่ชัดเจน

เป็นผลให้ฉันชอบ event Action<>

อย่างไรก็ตามเฉพาะในกรณีที่มีอาร์กิวเมนต์ประเภทมากเกินไปใน Action <> จะต้องมีคลาสเพิ่มเติม


2
PlusOne (ผมเพิ่งชนะระบบ) สำหรับ "ใส่ใจไม่มีใคร"
hyankov

@plusOne: ฉันต้องรู้จักผู้ส่งจริงๆ! พูดอะไรบางอย่างเกิดขึ้นและคุณต้องการที่จะรู้ว่าใครทำ คุณต้องการ 'แหล่งที่มาของวัตถุ' (ผู้ส่งหรือที่รู้จัก)
Kamran Bigdely

ผู้ส่งสามารถเป็นคุณสมบัติในส่วนของข้อมูลได้
Thanasis Ioannidis

คำตอบ:


67

ความแตกต่างที่สำคัญคือถ้าคุณใช้ Action<>เหตุการณ์ของคุณจะไม่เป็นไปตามรูปแบบการออกแบบของเหตุการณ์อื่น ๆ ในระบบซึ่งฉันจะพิจารณาข้อเสียเปรียบ

อีกด้านหนึ่งที่มีรูปแบบการออกแบบที่โดดเด่น (นอกเหนือจากพลังแห่งความเหมือน) คือคุณสามารถขยายEventArgsวัตถุด้วยคุณสมบัติใหม่โดยไม่ต้องเปลี่ยนลายเซ็นของเหตุการณ์ สิ่งนี้จะเป็นไปได้หากคุณใช้Action<SomeClassWithProperties>แต่ฉันไม่เห็นจุดที่ไม่ได้ใช้วิธีการปกติในกรณีนั้น


การใช้Action<>ผลลัพธ์ทำให้หน่วยความจำรั่วหรือไม่ ข้อเสียอย่างหนึ่งของEventHandlerรูปแบบการออกแบบคือหน่วยความจำรั่ว ควรที่จะชี้ให้เห็นว่าสามารถมีตัวจัดการเหตุการณ์ได้หลายคน แต่มีเพียงแอคชั่นเดียวเท่านั้น
Luke T O'Brien

4
@ LukeTO'Brien: Action<T>กิจกรรมอยู่ในผู้ได้รับมอบหมายสาระสำคัญดังนั้นความเป็นไปได้เหมือนกันการรั่วไหลของหน่วยความจำที่มีอยู่ด้วย นอกจากนี้ยังAction<T> สามารถอ้างถึงวิธีการต่างๆ นี่คือส่วนสำคัญที่แสดงให้เห็นว่า: gist.github.com/fmork/4a4ddf687fa8398d19ddb2df96f0b434
Fredrik Mörk

89

จากคำตอบก่อนหน้าฉันจะแบ่งคำตอบออกเป็นสามส่วน

ครั้งแรกข้อ จำกัด ทางกายภาพของการใช้Action<T1, T2, T2... >VS EventArgsใช้คลาสที่ได้รับของ มีสาม: ก่อนถ้าคุณเปลี่ยนจำนวนหรือประเภทของพารามิเตอร์ทุกวิธีที่สมัครเป็นสมาชิกจะต้องมีการเปลี่ยนแปลงเพื่อให้สอดคล้องกับรูปแบบใหม่ หากนี่เป็นเหตุการณ์สาธารณะที่แอสเซมบลีของบุคคลที่สามจะใช้และมีโอกาสใด ๆ ที่เหตุการณ์จะเปลี่ยนไปนี่จะเป็นเหตุผลที่จะใช้คลาสที่กำหนดเองที่ได้มาจากเหตุการณ์ args เพื่อความมั่นคง (โปรดจำไว้ว่าคุณยัง ใช้ข้อที่Action<MyCustomClass>สอง) การใช้Action<T1, T2, T2... >จะป้องกันไม่ให้คุณส่งข้อเสนอแนะย้อนกลับไปยังวิธีการโทรเว้นแต่ว่าคุณมีวัตถุบางประเภท ประการที่สามคุณไม่ได้รับพารามิเตอร์ที่มีชื่อดังนั้นหากคุณผ่าน 3 boolอันintไปสองครั้งstringและDateTimeคุณไม่มีความคิดว่าความหมายของค่าเหล่านั้นคืออะไร ในฐานะที่เป็นบันทึกย่อด้านข้างคุณยังสามารถมี "วิธีนี้ให้เหตุการณ์ได้อย่างปลอดภัยในขณะที่ยังใช้Action<T1, T2, T2... >"

ประการที่สองความหมายที่สอดคล้อง หากคุณมีระบบขนาดใหญ่ที่คุณใช้งานอยู่แล้วมันเกือบจะดีกว่าเสมอที่จะทำตามวิธีการที่เหลือของระบบได้รับการออกแบบเว้นแต่คุณจะมีเหตุผลที่ดีไม่เช่นกัน หากคุณต้องเผชิญกับเหตุการณ์ที่ต้องเปิดเผยต่อสาธารณะความสามารถในการทดแทนชั้นเรียนที่ได้รับอาจมีความสำคัญ เก็บไว้ในใจ

ประการที่สามการฝึกฝนในชีวิตจริงฉันพบว่าฉันมักจะสร้างกิจกรรมนอกสถานที่จำนวนมากสำหรับสิ่งต่าง ๆ เช่นการเปลี่ยนแปลงคุณสมบัติที่ฉันต้องการโต้ตอบด้วย (โดยเฉพาะอย่างยิ่งเมื่อทำ MVVM ด้วยแบบจำลองมุมมองที่มีปฏิสัมพันธ์ซึ่งกันและกัน) หรือเหตุการณ์ พารามิเตอร์เดียว เวลาส่วนใหญ่ของเหตุการณ์เหล่านี้จะใช้ในรูปแบบของหรือpublic event Action<[classtype], bool> [PropertyName]Changed; public event Action SomethingHappened;ในกรณีเหล่านี้มีประโยชน์สองประการ ก่อนอื่นฉันจะได้รับประเภทสำหรับชั้นเรียนที่ออก หากMyClassประกาศและเป็นชั้นเรียนเดียวที่จัดกิจกรรมฉันได้รับอินสแตนซ์ที่ชัดเจนว่าMyClassจะทำงานกับในตัวจัดการเหตุการณ์ ประการที่สองสำหรับเหตุการณ์ง่าย ๆ เช่นเหตุการณ์การเปลี่ยนแปลงคุณสมบัติความหมายของพารามิเตอร์นั้นชัดเจนและระบุไว้ในชื่อของตัวจัดการเหตุการณ์และฉันไม่ต้องสร้างคลาสมากมายสำหรับกิจกรรมประเภทนี้


โพสต์บล็อกที่น่ากลัว อ่านแล้วคุ้มค่าแน่นอนถ้าคุณอ่านกระทู้นี้!
Vexir

1
คำตอบที่ละเอียดและคิดมาอย่างดีซึ่งอธิบายถึงเหตุผลที่อยู่เบื้องหลังข้อสรุป
MikeT

18

ส่วนใหญ่ฉันจะบอกว่าทำตามรูปแบบ ฉันได้ผิดไปจากมัน แต่น้อยมากและสำหรับเหตุผลที่เฉพาะเจาะจง ในกรณีนี้ประเด็นที่ใหญ่ที่สุดที่ฉันมีก็คือฉันอาจจะยังคงใช้อยู่Action<SomeObjectType>ทำให้ฉันสามารถเพิ่มคุณสมบัติพิเศษได้ในภายหลังและใช้คุณสมบัติแบบสองทางเป็นครั้งคราว (คิดHandledหรือข้อเสนอแนะอื่น ๆ สมาชิกต้องตั้งค่าคุณสมบัติบนวัตถุเหตุการณ์) และเมื่อคุณได้เริ่มต้นลงเส้นที่คุณอาจรวมทั้งใช้สำหรับบางคนEventHandler<T>T


14

ข้อได้เปรียบของ wordier คือเมื่อโค้ดของคุณอยู่ในโครงการ 300,000 บรรทัด

การใช้แอ็คชั่นอย่างที่คุณมีไม่มีวิธีที่จะบอกฉันว่าบูล, int, และ Blah คืออะไร หากการกระทำของคุณผ่านวัตถุที่กำหนดพารามิเตอร์ก็โอเค

การใช้ EventHandler ที่ต้องการ EventArgs และถ้าคุณจะทำตัวอย่าง DiagnosticsArgs ของคุณให้สมบูรณ์ด้วย getters สำหรับคุณสมบัติที่แสดงความคิดเห็นตามวัตถุประสงค์แล้วแอปพลิเคชันของคุณจะเข้าใจได้ง่ายขึ้น นอกจากนี้โปรดแสดงความคิดเห็นหรือตั้งชื่ออาร์กิวเมนต์อย่างสมบูรณ์ในตัวสร้าง DiagnosticsArgs


6

หากคุณทำตามรูปแบบเหตุการณ์มาตรฐานคุณสามารถเพิ่มวิธีการขยายเพื่อให้การตรวจสอบเหตุการณ์การยิงปลอดภัยขึ้น / ง่ายขึ้น (เช่นรหัสต่อไปนี้เพิ่มวิธีการขยายที่เรียกว่า SafeFire () ซึ่งทำการตรวจสอบค่า 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
... คุณไม่สามารถทำสิ่งเดียวกันกับ Action <T> ได้หรือไม่ SafeFire <T> (การกระทำนี้ <T> theEvent, T theEventArgs) ควรทำงานกับ ... และไม่จำเป็นต้องใช้ "ที่ไหน"
Beachwalker

6

ฉันรู้ว่าคำถามนี้มีอายุมากกว่า 10 ปี แต่ดูเหมือนว่าฉันไม่เพียง แต่มีคำตอบที่ชัดเจนที่สุดที่ไม่ได้รับการตอบสนอง แต่นั่นอาจจะไม่ชัดเจนจากคำถามจริงๆซึ่งเป็นความเข้าใจที่ดีว่าเกิดอะไรขึ้นภายใต้ฝาครอบ นอกจากนี้ยังมีคำถามอื่น ๆ เกี่ยวกับการผูกสายและสิ่งที่เกี่ยวข้องกับผู้ได้รับมอบหมายและ lambdas (เพิ่มเติมในภายหลัง)

ก่อนอื่นให้จัดการกับช้างที่มีน้ำหนัก 800 ปอนด์ / กอริลลาในห้องเมื่อเลือกeventvs Action<T>/ Func<T>:

  • ใช้แลมบ์ดาเพื่อดำเนินการหนึ่งคำสั่งหรือวิธีการ ใช้eventเมื่อคุณต้องการรุ่นของ pub / sub ที่มีหลาย statement / lambdas / ฟังก์ชั่นที่จะทำงาน (นี่คือความ แตกต่างที่สำคัญทันทีจากไม้ตี)
  • ใช้แลมบ์ดาเมื่อคุณต้องการรวบรวมคำสั่ง / ฟังก์ชั่นเพื่อแสดงออกต้นไม้ ใช้ผู้ได้รับมอบหมาย / เหตุการณ์เมื่อคุณต้องการมีส่วนร่วมในการผูกปลายแบบดั้งเดิมมากขึ้นเช่นที่ใช้ในการสะท้อนและ COM interop

เป็นตัวอย่างของเหตุการณ์ให้วางสายเหตุการณ์ที่ง่ายและ 'มาตรฐาน' โดยใช้แอปพลิเคชันคอนโซลขนาดเล็กดังนี้:

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}");
    }
}

ผลลัพธ์จะมีลักษณะดังนี้:

ป้อนคำอธิบายรูปภาพที่นี่

ถ้าคุณทำเช่นเดียวกันกับAction<int>หรือAction<object, SomeStandardArgs>คุณเท่านั้นที่จะเห็นและSomeSimpleEvent2SomeStandardEvent2

แล้วเกิดอะไรขึ้นภายในของevent?

ถ้าเราขยายออกไปFireNiceEventคอมไพเลอร์กำลังสร้างสิ่งต่อไปนี้ (ฉันได้ละเว้นรายละเอียดบางอย่างเกี่ยวกับการซิงโครไนซ์เธรดที่ไม่เกี่ยวข้องกับการสนทนานี้):

   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)

        }
    }

คอมไพเลอร์สร้างตัวแปรผู้แทนส่วนตัวซึ่งไม่สามารถมองเห็นได้ในเนมสเปซของคลาสที่ถูกสร้างขึ้น ผู้รับมอบสิทธิ์นั้นเป็นสิ่งที่ใช้สำหรับการจัดการการสมัครสมาชิกและการมีส่วนร่วมที่ล่าช้าและส่วนต่อประสานสาธารณะที่คุ้นเคย+=และ-=ผู้ประกอบการที่เรารู้จักและชื่นชอบ:)

คุณสามารถปรับแต่งรหัสสำหรับตัวจัดการเพิ่ม / ลบได้โดยเปลี่ยนขอบเขตของFireNiceEventผู้รับมอบสิทธิ์เป็นแบบป้องกัน ตอนนี้ช่วยให้นักพัฒนาสามารถเพิ่ม hooks ที่กำหนดเองไปที่ hooks เช่นการบันทึกหรือขอความปลอดภัย สิ่งนี้สร้างขึ้นสำหรับฟีเจอร์ที่ทรงพลังบางอย่างที่ตอนนี้อนุญาตให้เข้าถึงการสมัครสมาชิกตามบทบาทของผู้ใช้เป็นต้นคุณสามารถทำกับแลมบ์ดาได้หรือไม่? (ที่จริงแล้วคุณสามารถทำได้โดยการรวบรวมต้นไม้การแสดงออกที่กำหนดเอง แต่ที่อยู่นอกเหนือขอบเขตของการตอบสนองนี้)

หากต้องการระบุจุดสองสามข้อจากคำตอบบางส่วนที่นี่:

  • มีจริงๆไม่แตกต่างกันใน 'เปราะบางระหว่างการเปลี่ยน args รายการในและการเปลี่ยนแปลงคุณสมบัติในชั้นเรียนที่ได้มาจากAction<T> EventArgsไม่เพียง แต่จะต้องการการเปลี่ยนแปลงคอมไพล์พวกเขาทั้งสองจะเปลี่ยนอินเทอร์เฟซสาธารณะและจะต้องมีการกำหนดเวอร์ชัน ไม่แตกต่าง.

  • ด้วยความเคารพซึ่งเป็นมาตรฐานอุตสาหกรรมขึ้นอยู่กับว่าจะใช้ที่ใดและเพราะเหตุใด Action<T>และมักใช้ใน IoC และ DI และeventมักใช้ในการกำหนดเส้นทางข้อความเช่นกรอบงานประเภท GUI และ MQ โปรดทราบว่าฉันพูดบ่อยๆไม่เสมอไป

  • ผู้ได้รับมอบหมายมีช่วงอายุที่แตกต่างจาก lambdas นอกจากนี้ยังต้องระวังการจับ ... ไม่เพียง แต่ปิด แต่ยังมีความคิดของ 'ดูสิ่งที่แมวลากเข้ามาใน' สิ่งนี้จะส่งผลกระทบต่อหน่วยความจำ / อายุการใช้งานตลอดจนการจัดการการรั่วไหล

อีกอย่างหนึ่งสิ่งที่ฉันอ้างถึงก่อนหน้านี้ ... ความคิดของการผูกปลาย คุณมักจะเห็นสิ่งนี้เมื่อใช้เฟรมเวิร์กเช่น LINQ เกี่ยวกับเมื่อแลมบ์ดากลายเป็น 'สด' นั่นแตกต่างจากการผูกมัดผู้แทนซึ่งอาจเกิดขึ้นได้มากกว่าหนึ่งครั้ง (เช่นแลมบ์ดาอยู่ที่นั่นเสมอ แต่การผูกมัดเกิดขึ้นตามความต้องการบ่อยเท่าที่ต้องการ) เมื่อเทียบกับแลมบ์ดาซึ่งเกิดขึ้นทันที - เวทย์มนตร์หายไปและวิธีการ / ทรัพย์สิน (IES) จะผูกมัดเสมอ สิ่งที่ต้องจำไว้


4

ดูรูปแบบเหตุการณ์ของ. NET มาตรฐานที่เราพบ

ลายเซ็นมาตรฐานสำหรับตัวแทนเหตุการณ์. NET คือ:

void OnEventRaised(object sender, EventArgs args);

[ ... ]

รายการอาร์กิวเมนต์ประกอบด้วยสองอาร์กิวเมนต์: ผู้ส่งและอาร์กิวเมนต์ของเหตุการณ์ ประเภทเวลารวบรวมของผู้ส่งคือ System.Object ถึงแม้ว่าคุณจะรู้ว่าเป็นประเภทที่ได้รับมากขึ้นที่จะถูกต้องเสมอ โดยการประชุมใช้วัตถุ

ด้านล่างในหน้าเดียวกันเราพบตัวอย่างของคำนิยามเหตุการณ์ทั่วไปซึ่งมีลักษณะคล้ายกัน

public event EventHandler<EventArgs> EventName;

ถ้าเรากำหนดไว้

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

ตัวจัดการอาจเป็นได้

void OnEventRaised(MyClass sender, EventArgs args);

โดยที่senderมีชนิดที่ถูกต้อง ( เพิ่มเติมมา )


ขออภัยไม่ได้ที่จะมีการตั้งข้อสังเกตว่าเป็นความแตกต่างในลายเซ็นจัดการซึ่งจะได้รับประโยชน์จากการพิมพ์อย่างแม่นยำมากขึ้น sender
user1832484
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.