จำเป็นต้องลบตัวจัดการเหตุการณ์ใน C # อย่างชัดเจนหรือไม่


120

ฉันมีชั้นเรียนที่เสนอกิจกรรมบางอย่าง คลาสนั้นได้รับการประกาศทั่วโลก แต่ไม่รวมอยู่ในการประกาศระดับโลกนั้น - มันได้รับการติดตั้งบนพื้นฐานที่จำเป็นในวิธีการที่จำเป็น

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

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

คำตอบ:


184

ในกรณีของคุณทุกอย่างเรียบร้อยดี เป็นวัตถุที่เผยแพร่เหตุการณ์ที่ทำให้เป้าหมายของตัวจัดการเหตุการณ์ยังคงอยู่ ดังนั้นถ้าฉันมี:

publisher.SomeEvent += target.DoSomething;

จากนั้นpublisherมีการอ้างอิงถึงtargetแต่ไม่ใช่ในทางกลับกัน

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

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

BandwidthUI ui = new BandwidthUI();
transferService.BandwidthChanged += ui.HandleBandwidthChange;
// Suppose this blocks until the transfer is complete
transferService.Transfer(source, destination);
// We now have to unsusbcribe from the event
transferService.BandwidthChanged -= ui.HandleBandwidthChange;

(คุณต้องการใช้การบล็อกในที่สุดเพื่อให้แน่ใจว่าคุณจะไม่รั่วไหลของตัวจัดการเหตุการณ์) หากเราไม่ได้ยกเลิกการสมัครบริการBandwidthUIจะมีผลอย่างน้อยตราบเท่าที่บริการโอน

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

แก้ไข:นี่คือการตอบความคิดเห็นของโจนาธานดิกคินสัน ประการแรกดูเอกสารสำหรับDelegate.Equals (object)ซึ่งให้พฤติกรรมความเท่าเทียมกันอย่างชัดเจน

ประการที่สองนี่คือโปรแกรมสั้น ๆ แต่สมบูรณ์เพื่อแสดงการยกเลิกการสมัครทำงาน:

using System;

public class Publisher
{
    public event EventHandler Foo;

    public void RaiseFoo()
    {
        Console.WriteLine("Raising Foo");
        EventHandler handler = Foo;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
        else
        {
            Console.WriteLine("No handlers");
        }
    }
}

public class Subscriber
{
    public void FooHandler(object sender, EventArgs e)
    {
        Console.WriteLine("Subscriber.FooHandler()");
    }
}

public class Test
{
    static void Main()
    {
         Publisher publisher = new Publisher();
         Subscriber subscriber = new Subscriber();
         publisher.Foo += subscriber.FooHandler;
         publisher.RaiseFoo();
         publisher.Foo -= subscriber.FooHandler;
         publisher.RaiseFoo();
    }
}

ผล:

Raising Foo
Subscriber.FooHandler()
Raising Foo
No handlers

(ทดสอบกับ Mono และ. NET 3.5SP1.)

แก้ไขเพิ่มเติม:

นี่เป็นการพิสูจน์ว่าสามารถรวบรวมผู้เผยแพร่กิจกรรมได้ในขณะที่ยังคงมีการอ้างอิงถึงผู้สมัครสมาชิก

using System;

public class Publisher
{
    ~Publisher()
    {
        Console.WriteLine("~Publisher");
        Console.WriteLine("Foo==null ? {0}", Foo == null);
    }

    public event EventHandler Foo;
}

public class Subscriber
{
    ~Subscriber()
    {
        Console.WriteLine("~Subscriber");
    }

    public void FooHandler(object sender, EventArgs e) {}
}

public class Test
{
    static void Main()
    {
         Publisher publisher = new Publisher();
         Subscriber subscriber = new Subscriber();
         publisher.Foo += subscriber.FooHandler;

         Console.WriteLine("No more refs to publisher, "
             + "but subscriber is alive");
         GC.Collect();
         GC.WaitForPendingFinalizers();         

         Console.WriteLine("End of Main method. Subscriber is about to "
             + "become eligible for collection");
         GC.KeepAlive(subscriber);
    }
}

ผลลัพธ์ (ใน. NET 3.5SP1; Mono ดูเหมือนจะทำงานผิดปกติเล็กน้อยที่นี่จะตรวจสอบในบางครั้ง):

No more refs to publisher, but subscriber is alive
~Publisher
Foo==null ? False
End of Main method. Subscriber is about to become eligible for collection
~Subscriber

2
ฉันเห็นด้วยกับสิ่งนี้ แต่ถ้าเป็นไปได้คุณสามารถอธิบายสั้น ๆ โดยละเอียดหรือควรอ้างอิงถึงตัวอย่างของสิ่งที่คุณหมายถึง "แต่สมาชิกไม่ต้องการเป็น"
Peter McG

@ จอน: ชื่นชมมากไม่ใช่เรื่องธรรมดา แต่อย่างที่คุณบอกว่าฉันเห็นคนกังวลเรื่องนี้โดยไม่จำเป็น
Peter McG

- = ไม่ทำงาน - = จะส่งผลให้มีผู้รับมอบสิทธิ์ใหม่และผู้รับมอบสิทธิ์ไม่ตรวจสอบความเท่าเทียมกันโดยใช้เมธอดเป้าหมายพวกเขาทำ object.ReferenceEquals () บนผู้รับมอบสิทธิ์ ไม่มีผู้รับมอบสิทธิ์ใหม่ในรายการ: ไม่มีผลใด ๆ (และไม่มีข้อผิดพลาดที่ผิดปกติเพียงพอ)
Jonathan C Dickinson

2
@ โจนาธาน: ไม่ผู้ได้รับมอบหมายตรวจสอบความเท่าเทียมกันโดยใช้วิธีการกำหนดเป้าหมาย จะพิสูจน์ในการแก้ไข
Jon Skeet

ฉันยอมรับ ฉันสับสนกับผู้รับมอบสิทธิ์ที่ไม่เปิดเผยนาม
Jonathan C Dickinson

8

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

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

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

เพียงเพราะออบเจ็กต์อยู่นอกขอบเขตของการจัดสรรเดิมไม่ได้หมายความว่าเป็นตัวเลือกสำหรับ GC ตราบใดที่การอ้างอิงยังคงอยู่การอ้างอิงนั้นยังคงอยู่


1
ฉันไม่เชื่อว่าจำเป็นต้องยกเลิกการสมัครสมาชิกที่นี่ - GC เห็นการอ้างอิงจากผู้จัดพิมพ์เหตุการณ์ไม่ใช่ถึงมันและเป็นผู้เผยแพร่ที่เรากังวลที่นี่
Jon Skeet

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