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

时间:2017-02-09 03:56:56

标签: c# .net events

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

这是我正在使用的基本代码并改变了这些问题。

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
  1. 更改代码并删除取消订阅时,Console.WriteLine输出仍然相同。为什么会这样?为什么这么糟糕?
  2.     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
    
    1. 更改代码并同时删除取消订阅和重新订阅时,Console.WriteLine输出会有所不同。为什么输出不是a,而是b
    2.     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 个答案:

答案 0 :(得分:3)

  
      
  1. 更改代码并删除取消订阅时,Console.WriteLine输出仍然相同。为什么会这样?为什么这么糟糕?
  2.   

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

因此,在该特定示例中,取消订阅事件中的第一个委托对返回的string值没有影响。您仍然可以从第二个委托实例返回string值,即使正在调用这两个委托。

至于"为什么这么糟糕?",好吧......是吗?是否取决于具体情况。我想说,这是一个很好的例子,说明为什么你应该避免使用除void返回类型以外的委托类型的事件。至少可以说有多个返回值,但只能看到调用中实际返回的值之一,这可能会令人困惑。

至少,如果您确实为事件使用了这样的委托类型,您应该愿意接受默认行为或将多播委托实例分解为其各个调用目标(请参阅Delegate.GetInvocationList())并明确决定你想要的回报值。

如果您确实知道自己正在做什么,并且熟悉多播委托的工作方式,并且对丢失除一个返回值之外的所有内容(或明确捕获代码中的所有返回值)感到满意举起活动),然后我不会说它必然是坏的"本身。但它绝对是非标准的,如果不小心完成,几乎可以肯定意味着代码没有按预期工作。哪个 坏。 :)

  
      
  1. 更改代码并同时删除取消订阅和重新订阅时,Console.WriteLine输出会有所不同。为什么输出a不是b?
  2.   

您期望这样,因为您已修改了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" });

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


附录:

解决评论中发布的两个后续问题......

  

1a上。关于你的q1响应,我更好奇的是它在变量处理方面是不好的。正如q2似乎暗示的那样,即使将col设置为新实例,也不会删除初始col(及其订阅)。这会最终导致内存泄漏,还是gc会把它拿起来?

我不清楚"变量处理" 的含义。在任何通常的意义上,变量本身实际上并没有被处理掉。所以,我推断你真的在谈论垃圾收集。考虑到这种推断......

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

在.NET中,当一个对象不再可访问时,该对象符合条件进行垃圾回收。 实际收集该对象时,由GC决定。通常情况下,我们只关注自己的资格,而不是实际的收藏。

对于最初由col变量引用的对象,只要该局部变量仍在范围内且仍可在该方法中使用,则它是可到达的。一旦变量引用的对象用于订阅事件,事件本身现在也通过订阅的委托引用该对象(显然......否则,委托如何能够传递正确的{{1}调用处理事件的实例方法时的值?)。

如果您没有从事件的订阅者那里删除该委托及其对原始对象的引用,那么该对象本身仍然可以访问,因此可以进行垃圾回收。

如果事件是某个类的非this成员,这通常不是问题,因为只要对象本身存在,人们通常希望保持订阅该事件。当对象本身不再可达时,任何事件处理订阅其事件的对象也是如此。

在您的情况下,您正在处理static事件。这确实可能是内存泄漏的潜在来源,因为类的static成员总是可达。因此,在您取消订阅引用创建的原始对象的委托之前,该原始对象也仍然可以访问,并且无法收集。

  

2a上。至于q2,简单地更改static属性本身而不是完全替换strings会更有意义吗?不完全确定原因,但你的反应让人想到了这一点。代码:col

如果没有更多背景信息,我无法说出"更有意义" 。但是,如果您这样做,您的代码确实会在所有四种情况下产生预期结果(即,您是否已在两个示例中注释了事件订阅和-unsubscription代码)。并且通过避免分配新对象,您可以在整个问题上采取措辞,即无意中从事件中取消订阅对象的处理程序,或者无意中使该对象无法访问。