簡體   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