简体   繁体   中英

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. 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. Why isn't the output the a then 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());
    }

Output:

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?

AC# delegate is actually a "multicast" 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. You still get the string value returned from the second delegate instance, even though both delegates are being invoked.

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. 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.

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. Why isn't the output the a then b?

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. But that's not how the event subscription works.

When you subscribe to the event the first time, with 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. 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. 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.

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 . You're only changing the variable value by reassigning c1 . The original object reference still exists, and remains stored in the variable c2 .


Addendum:

To address your two follow-up questions posted in the comments…

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?

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. When that object will actually be collected is up to the 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. 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?).

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. 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. This indeed can be a potential source of memory leaks, because a static member of a class is always reachable. So until you unsubscribe the delegate that references the original object created, that original object also remains reachable and cannot be collected.

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

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. 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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