简体   繁体   English

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

[英]C# - Event subscribing and variable overwriting

I have been fiddling around with static events and am curious about a few things.. 我一直在摆弄静态事件,并对一些事情感到好奇。

This is the base code I am using and altering for these questions. 这是我正在使用的基本代码,并为这些问题进行了修改。

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

Output: 输出:

a
b
  1. When altering the code and removing the unsubscribe, the Console.WriteLine outputs are still the same. 更改代码并删除取消订阅时,Console.WriteLine输出仍然相同。 Why does this occur? 为什么会这样? Why is this bad? 为什么这么糟糕?
    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());
    }

Output: 输出:

a
b
  1. When altering the code and removing both the unsubscribe and the resubscribe, the Console.WriteLine outputs are different. 更改代码并删除取消订阅和重新订阅时,Console.WriteLine输出是不同的。 Why isn't the output the a then b ? 为什么不输出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());
    }

Output: 输出:

a
a
  1. When altering the code and removing the unsubscribe, the Console.WriteLine outputs are still the same. 更改代码并删除取消订阅时,Console.WriteLine输出仍然相同。 Why does this occur? 为什么会这样? Why is this bad? 为什么这么糟糕?

AC# delegate is actually a "multicast" delegate. AC#delegate实际上是一个“多播”委托。 That is, a single delegate instance can have multiple invocation targets. 也就是说,单个委托实例可以具有多个调用目标。 But when a delegate has a return value, only one value can be used. 但是当委托具有返回值时,只能使用一个值。 In your example, it just happens that because of the way the delegate subscriptions are ordered, if you remove the first unsubscribe operation, it is the second delegate subscribed to the event whose return value is returned by the event's invocation. 在您的示例中,恰好由于委托订阅的排序方式,如果您删除第一个取消订阅操作,它是订阅事件的第二个委托,其返回值由事件的调用返回。

So in that particular example, unsubscribing the first delegate from the event has no effect on the string value returned. 因此,在该特定示例中,从事件中取消订阅第一个委托对返回的string值没有影响。 You still get the string value returned from the second delegate instance, even though both delegates are being invoked. 您仍然可以获取从第二个委托实例返回的string值,即使正在调用这两个委托。

As for "Why is this bad?", well…is it? 至于“为什么这么糟糕?”,好吧......是吗? Whether it is or not depends on the context. 是否取决于具体情况。 I would say, it's a good example of why you should avoid events that use delegate types with other than void return type. 我想说,这是一个很好的例子,说明为什么你应该避免使用委托类型而不是void返回类型的事件。 It can be confusing to say the least, to have multiple return values but only see one of those values actually returned from the invocation. 至少可以说有多个返回值,但只能看到从调用中实际返回的其中一个值,这可能会令人困惑。

At a minimum, if you do use such a delegate types for an event, you should either be willing to accept the default behavior or decompose the multicast delegate instance into its individual invocation targets (see Delegate.GetInvocationList() ) and explicitly decide which return value you want yourself. 至少,如果您确实为事件使用了这样的委托类型,您应该愿意接受默认行为或将多播委托实例分解为其各自的调用目标(请参阅Delegate.GetInvocationList() )并明确决定哪个返回你想要自己的价值。

If you actually know what you're doing, and are familiar with how multicast delegates work, and are comfortable with the idea of losing all but one of the return values (or explicitly capturing all the return values in the code raising the event), then I wouldn't say it's necessarily "bad" per se. 如果您确实知道自己在做什么,并且熟悉多播委托的工作方式,并且对于丢失除了其中一个返回值(或明确捕获引发事件的代码中的所有返回值)的想法感到满意,那么我不会说它本身就一定是“坏”。 But it's definitely non-standard, and when done carelessly it will almost certainly mean the code doesn't work as intended. 但它肯定是非标准的,并且当不小心完成时,它几乎肯定意味着代码不能按预期工作。 Which is bad. 糟糕。 :) :)

  1. When altering the code and removing both the unsubscribe and the resubscribe, the Console.WriteLine outputs are different. 更改代码并删除取消订阅和重新订阅时,Console.WriteLine输出是不同的。 Why isn't the output the a then b? 为什么输出不是a?

You are expecting that, since you've modified the col variable, that somehow the event handler subscribed previously will automatically refer to the new instance assigned to the col variable. 您期望这样,因为您已经修改了col变量,以前以某种方式订阅的事件处理程序将自动引用分配给col变量的新实例。 But that's not how the event subscription works. 但这不是事件订阅的工作方式。

When you subscribe to the event the first time, with aa.evGatherstringa += col.gatherstring; 当您第一次订阅该事件时,使用aa.evGatherstringa += col.gatherstring; , the col variable is used only to provide the reference to the instance of aa.collection where the event handler method is found. col变量仅用于提供对找到事件处理程序方法的aa.collection 实例的引用。 The event subscription uses only that instance reference. 事件订阅仅使用该实例引用。 The variable itself isn't observed by the event subscription, and so changes to the variable later also don't affect the event subscription. 事件订阅不会观察到变量本身,因此稍后对变量的更改也不会影响事件订阅。

Instead, the original instance of the aa.collection object remains subscribed to the event. 相反, aa.collection对象的原始实例仍然订阅了该事件。 Raising the event again, even after you've modified the col variable, still invokes the event handler in that original object, not the new object now assigned to the col variable. 再次引发事件,即使在修改了col变量之后,仍然会调用该原始对象中的事件处理程序,而不是现在分配给col变量的新对象。

More generally, you'll want to be very careful to not confuse an actual object, with its reference that can be stored in a variety of places, with any individual variable storing that reference. 更一般地说,您需要非常小心,不要将实际对象与可以存储在各种位置的引用混淆,并将任何单个变量存储在该引用中。

It's the same reason that if you have the following code: 如果您有以下代码,原因相同:

aa.collection c1, c2;

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

…the value of c2 is not changed, even when you've assigned a new value to the variable c1 . ...即使您已为变量c1指定了新值, c2的值也不会更改。 You're only changing the variable value by reassigning c1 . 你只能改变通过重新分配变量c1 The original object reference still exists, and remains stored in the variable c2 . 原始对象引用仍然存在,并且仍然存储在变量c2


Addendum: 附录:

To address your two follow-up questions posted in the comments… 解决评论中发布的两个后续问题......

1a. 1A。 In relation to your q1 response, I was more curious if it was bad in terms of variable disposing. 关于你的q1响应,我更好奇的是它在变量处理方面是不好的。 As q2 seems to suggest, the initial col (and its subscription) are not removed even after col is set to a new instance. 正如q2似乎暗示的那样,即使将col设置为新实例,也不会删除初始col (及其订阅)。 Would this eventually cause a memory leak, or would gc pick it up? 这会最终导致内存泄漏,还是gc会把它拿起来?

It's not clear to me what you mean by "variable disposing" . 我不清楚“变量处理”是什么意思。 Variables themselves aren't actually "disposed", in any usual sense of that word. 在任何通常的意义上,变量本身并不是“处置”的。 So, I infer that you're really talking about garbage collection. 所以,我推断你真的在谈论垃圾收集。 With that inference in mind… 考虑到这种推断......

The answer is that, if you don't unsubscribe that original delegate, which is referencing the original object, the original object won't be collected. 答案是,如果您不取消订阅引用原始对象的原始委托,则不会收集原始对象。 Some people do use the term "memory leak" to describe that situation (I don't, because doing so fails to distinguish the situation from real memory leaks that can occur in other types of memory-management scenarios, where the memory allocated for an object is truly and permanently lost). 有些人确实使用术语“内存泄漏”来描述这种情况(我没有,因为这样做无法区分情况与其他类型的内存管理方案中可能发生的实际内存泄漏,其中内存分配给对象真实而永久地丢失了)。

In .NET, an object is eligible for garbage collection when it is no longer reachable. 在.NET中,当对象不再可访问时,它有资格进行垃圾回收。 When that object will actually be collected is up to the GC. 该对象实际上将收集到的到GC。 Typically, we concern ourselves only with the eligibility, not the actual collection. 通常情况下,我们只关注自己的资格,而不是实际的收藏。

In the case of the object originally referred to by the col variable, it is reachable so long as that local variable is still in scope and could still be used in the method. 在最初由col变量引用的对象的情况下,只要该局部变量仍在范围内并且仍然可以在该方法中使用,则它是可到达的。 Once the object that variable references is used to subscribe to an event, the event itself now also has a reference to that object, via the delegate that was subscribed (obviously…otherwise, how would the delegate be able to pass the correct this value when calling the instance method handling the event?). 一旦变量引用的对象用于订阅事件,事件本身现在也通过订阅的委托引用该对象(显然......否则,委托如何能够传递正确的this值调用处理事件的实例方法?)。

If you do not remove that delegate, with its reference to the original object, from the subscribers of the event, then the object itself remains reachable, and thus not eligible for garbage collection. 如果您没有从事件的订阅者那里删除该委托及其对原始对象的引用,那么该对象本身仍然可以访问,因此符合垃圾回收的条件。

In the case of an event that's a non- static member of a class, this is usually not a problem as one typically wants to remain subscribed to the event as long as the object itself exists. 在事件是类的非static成员的情况下,这通常不是问题,因为只要对象本身存在,通常希望保持订阅事件。 And when the object itself is no longer reachable, so too would any event handling objects that had subscribed to its event. 当对象本身不再可访问时,任何事件处理订阅其事件的对象也是如此。

In your case, you are dealing with a static event. 在您的情况下,您正在处理static事件。 This indeed can be a potential source of memory leaks, because a static member of a class is always reachable. 这确实可能是内存泄漏的潜在来源,因为类的static成员总是可以访问的。 So until you unsubscribe the delegate that references the original object created, that original object also remains reachable and cannot be collected. 因此,在您取消订阅引用创建的原始对象的委托之前,该原始对象也仍然可以访问且无法收集。

2a. 2A。 As for q2, would it make more sense to simply change the strings property itself, rather than entirely replacing col ? 至于q2,简单地更改strings属性本身更有意义,而不是完全替换col Not entirely sure why, but your response brought that to mind. 不完全确定原因,但你的反应让人想到了这一点。 Code: col.strings = new [] { "b", "b"}; 代码: col.strings = new [] { "b", "b"};

Without more context, I can't say what would "make more sense" . 没有更多的背景,我不能说什么会“更有意义” But it is true that your code would produce your expected results in all four scenarios (ie whether or not you've commented out event-subscription and -unsubscription code in the two examples) if you did that. 但是,如果您这样做,您的代码确实会在所有四种情况下产生预期结果(即,您是否已在两个示例中注释了事件订阅和-unsubscription代码)。 And by avoiding the allocation of a new object, you side-step the whole question of accidentally failing to unsubscribe an object's handler from an event, or of having that object remain reachable unintentionally. 并且通过避免分配新对象,您可以侧面执行整个问题,即无意中从事件中取消订阅对象的处理程序,或者无意中保持该对象的可访问性。

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

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