C#-事件订阅和变量覆盖

我一直在摆弄静态事件,并对一些事情感到好奇.

这是我正在使用和修改这些问题的基本代码.

class Program
{
    static void Main()
    {
        aa.collection col = null;

        col = new aa.collection(new [] { "a", "a"});
        aa.evGatherstringa += col.gatherstring;

        Console.WriteLine(aa.gatherstring());

        // Used in question 1
        aa.evGatherstringa -= col.gatherstring;

        col = new aa.collection(new [] { "b", "b"});

        // Used in question 2
        aa.evGatherstringa += col.gatherstring;

        Console.WriteLine(aa.gatherstring());
    }

    public static class aa
    {
        public delegate string gatherstringa();
        public static event gatherstringa evGatherstringa;

        public static string gatherstring() { return evGatherstringa.Invoke(); }

        public class collection
        {
            public collection(string[] strings) { this.strings = strings; }

            public string gatherstring()
            {
                return this.strings[0];
            }

            public string[] strings { get; set; }
        }
    }
}

输出:

a
b

>更改代码并删除退订时,Console.WriteLine输出仍然相同.为什么会发生这种情况?为什么这样不好?

    static void Main()
    {
        aa.collection col = null;

        col = new aa.collection(new [] { "a", "a"});
        aa.evGatherstringa += col.gatherstring;

        Console.WriteLine(aa.gatherstring());

        // Used in question 1
        //aa.evGatherstringa -= col.gatherstring;

        col = new aa.collection(new [] { "b", "b"});

        // Used in question 2
        aa.evGatherstringa += col.gatherstring;

        Console.WriteLine(aa.gatherstring());
    }

输出:

a
b

>更改代码并同时取消取消订阅和重新订阅时,Console.WriteLine输出是不同的.为什么输出的结果不是a然后b?

    static void Main()
    {
        aa.collection col = null;

        col = new aa.collection(new [] { "a", "a"});
        aa.evGatherstringa += col.gatherstring;

        Console.WriteLine(aa.gatherstring());

        // Used in question 1 and 2
        //aa.evGatherstringa -= col.gatherstring;

        col = new aa.collection(new [] { "b", "b"});

        // Used in question 2
        //aa.evGatherstringa += col.gatherstring;

        Console.WriteLine(aa.gatherstring());
    }

输出:

a
a

解决方法:

  1. When altering the code and removing the unsubscribe, the Console.WriteLine outputs are still the same. Why does this occur? Why is this bad?

C#委托实际上是“多播”委托.也就是说,单个委托实例可以具有多个调用目标.但是,当委托具有返回值时,只能使用一个值.在您的示例中,恰好由于委托订购的排序方式,如果删除第一个取消订阅操作,则是第二个订阅该事件的委托,其返回值由事件的调用返回.

因此,在该特定示例中,取消订阅该事件的第一个委托对返回的字符串值没有影响.即使同时调用了两个委托,您仍然可以获得从第二个委托实例返回的字符串值.

至于“为什么这样不好?”,嗯……是吗?是否取决于上下文.我想说,这是一个很好的例子,说明为什么您应该避免使用除void返回类型以外的委托类型的事件.至少可以说,有多个返回值,但只能看到从调用实际返回的那些值之一,这可能会造成混淆.

至少,如果确实为事件使用了此类委托类型,则您要么愿意接受默认行为,要么应该将多播委托实例分解成其单独的调用目标(请参见Delegate.GetInvocationList()),并明确决定您想要的返回值.

如果您实际上知道自己在做什么,并且熟悉多播委托的工作方式,并且对丢失一个返回值(或者仅在引发事件的代码中明确捕获所有返回值)的想法感到满意,那么我不会说这本身必然是“不好的”.但这绝对是非标准的,如果不小心进行,几乎可以肯定意味着该代码无法按预期工作.不好:)

  1. When altering the code and removing both the unsubscribe and the resubscribe, the Console.WriteLine outputs are different. Why isn’t the output the a then b?

您期望,由于已经修改了col变量,因此先前订阅的事件处理程序将以某种方式自动引用分配给col变量的新实例.但这不是事件订阅的工作方式.

首次使用aa.evGatherstringa = col.gatherstring;订阅事件时,col变量仅用于提供对找到事件处理程序方法的aa.collection实例的引用.事件订阅仅使用该实例引用.事件订阅不会观察到变量本身,因此以后对变量的更改也不会影响事件订阅.

而是,aa.collection对象的原始实例仍然订阅该事件.即使在修改了col变量之后,仍然再次引发事件,仍然会在原始对象中调用事件处理程序,而不是现在分配给col变量的新对象.

更一般而言,您将要非常小心,不要将实际的对象与可以存储在多个位置的引用混淆,而将任何单个变量都存储在该引用中.

同样的原因是如果您具有以下代码:

aa.collection c1, c2;

c1 = new aa.collection(new [] { "a" });
c2 = c1;
c1 = new aa.collection(new [] { "b" });

…即使为变量c1分配了新值,c2的值也不会改变.您只需通过重新分配c1来更改变量值.原始对象引用仍然存在,并保留存储在变量c2中.

附录:

为了解决您在评论中发布的两个后续问题……

1a. In relation to your q1 response, I was more curious if it was bad in terms of variable disposing. As q2 seems to suggest, the initial col (and its subscription) are not removed even after col is set to a new instance. Would this eventually cause a memory leak, or would gc pick it up?

我不清楚您所说的“可变处置”是什么意思.从通常的意义上讲,变量本身实际上并不是“配置”的.因此,我推断您实际上是在谈论垃圾回收.考虑到这种推论…

答案是,如果不取消订阅引用原始对象的原始委托,则不会收集原始对象.有些人确实使用“内存泄漏”一词来描述这种情况(我没有,因为这样做无法将这种情况与其他类型的内存管理方案中可能发生的实际内存泄漏区分开来,在这种情况下,内存分配给对象是真正永久丢失的对象).

在.NET中,当对象不再可访问时,可以进行垃圾回收.何时实际收集该对象取决于GC.通常,我们只关注资格,而不关注实际收藏.

对于最初由col变量引用的对象,只要该局部变量仍在范围内并且仍可在该方法中使用,则该对象是可访问的.一旦变量引用的对象用于预订事件,事件本身现在也将通过已预订的委托来引用该对象(显然,否则,委托将如何在正确的情况下传递正确的值)调用处理事件的实例方法?).

如果您不从事件的订阅者中删除该委托(对原始对象的引用),则该对象本身仍然可以访问,因此不符合垃圾回收的条件.

对于事件是类的非静态成员的情况,这通常不是问题,因为只要对象本身存在,通常就希望保留对该事件的订阅.当对象本身不再可访问时,任何已订阅其事件的事件处理对象也将到达.

就您而言,您正在处理静态事件.实际上,这确实可能是内存泄漏的潜在原因,因为类的静态成员始终可以访问.因此,在取消订阅引用创建的原始对象的委托之前,该原始对象也将保持可访问状态并且无法被收集.

2a. As for q2, would it make more sense to simply change the strings property itself, rather than entirely replacing col? Not entirely sure why, but your response brought that to mind. Code: col.strings = new [] { "b", "b"};

没有更多的上下文,我不能说什么将“更有意义”.但是,如果这样做的话,您的代码确实会在所有四种情况下都产生预期的结果(即您是否在两个示例中都注释掉了event-subscription和-unsubscription代码).并且,通过避免分配新对象,您可以避免整个问题,即意外地未能从事件中取消订阅对象的处理程序,或者使该对象意外地保持可访问状态.

上一篇:在C#中,如何暂停事件的绑定委托之一?


下一篇:最好的方式来处理多个类的事件?