繁体   English   中英

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

[英]C# - Event subscribing and variable overwriting

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

这是我正在使用的基本代码,并为这些问题进行了修改。

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输出仍然相同。 为什么会这样? 为什么这么糟糕?
    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输出是不同的。 为什么不输出ab
    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. 更改代码并删除取消订阅时,Console.WriteLine输出仍然相同。 为什么会这样? 为什么这么糟糕?

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

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

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

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

如果您确实知道自己在做什么,并且熟悉多播委托的工作方式,并且对于丢失除了其中一个返回值(或明确捕获引发事件的代码中的所有返回值)的想法感到满意,那么我不会说它本身就一定是“坏”。 但它肯定是非标准的,并且当不小心完成时,它几乎肯定意味着代码不能按预期工作。 糟糕。 :)

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

您期望这样,因为您已经修改了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。 关于你的q1响应,我更好奇的是它在变量处理方面是不好的。 正如q2似乎暗示的那样,即使将col设置为新实例,也不会删除初始col (及其订阅)。 这会最终导致内存泄漏,还是gc会把它拿起来?

我不清楚“变量处理”是什么意思。 在任何通常的意义上,变量本身并不是“处置”的。 所以,我推断你真的在谈论垃圾收集。 考虑到这种推断......

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

在.NET中,当对象不再可访问时,它有资格进行垃圾回收。 该对象实际上将收集到的到GC。 通常情况下,我们只关注自己的资格,而不是实际的收藏。

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

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

在事件是类的非static成员的情况下,这通常不是问题,因为只要对象本身存在,通常希望保持订阅事件。 当对象本身不再可访问时,任何事件处理订阅其事件的对象也是如此。

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

2A。 至于q2,简单地更改strings属性本身更有意义,而不是完全替换col 不完全确定原因,但你的反应让人想到了这一点。 代码: col.strings = new [] { "b", "b"};

没有更多的背景,我不能说什么会“更有意义” 但是,如果您这样做,您的代码确实会在所有四种情况下产生预期结果(即,您是否已在两个示例中注释了事件订阅和-unsubscription代码)。 并且通过避免分配新对象,您可以侧面执行整个问题,即无意中从事件中取消订阅对象的处理程序,或者无意中保持该对象的可访问性。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM