2017-02-09 133 views
3

我一直與靜態事件擺弄周圍和好奇的一些東西..C# - 事件訂閱和可變覆蓋

這是我使用的基本代碼,並改變了這些問題。

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 
  • 當改變代碼和除去兩個退訂和重新訂閱,Console.WriteLine命令輸出是不同的。爲什麼不是輸出a然後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()); 
        } 
    

    輸出:

    a 
    a 
    

    回答

    3
    1. 當改變代碼併除去退訂,Console.WriteLine命令輸出仍然是相同的。爲什麼會發生?爲什麼這不好?

    C#委託實際上是一個「多播」委託。也就是說,一個委託實例可以有多個調用目標。但是,當委託具有返回值時,只能使用一個值。在你的例子中,只是因爲委託訂閱被排序的方式,如果你刪除了第一個取消訂閱操作,它就是第二個訂閱了事件調用返回值的事件的委託。

    因此,在該特定示例中,取消訂閱事件中的第一個委託對返回的string值沒有影響。儘管調用了兩個委託,但您仍然可以獲得從第二個委託實例返回的string值。

    至於「爲什麼這樣不好?」,好嗎?無論是否取決於上下文。我想說,這是一個很好的例子,說明爲什麼您應該避免使用與void返回類型不同的委託類型的事件。至少可以說,具有多個返回值但只能看到實際從調用中返回的那些值中的一個可能會令人困惑。

    至少,如果確實爲事件使用了這種委託類型,則應該願意接受默認行爲或將多路廣播委託實例分解爲其各個調用目標(請參閱Delegate.GetInvocationList()),並明確決定哪個返回你想要自己的價值。

    如果你真的知道自己在做什麼,並且熟悉多播代理的工作方式,並且對丟失所有返回值的想法感到滿意(或者明確捕獲代碼提升中的所有返回值事件),那麼我不會說它本身必然是「壞」的。但它絕對不是標準的,當不小心做到時,它幾乎肯定意味着代碼無法按預期工作。其中不好。 :)

  • 當改變代碼和除去兩個退訂和重新訂閱,Console.WriteLine命令輸出是不同的。爲什麼不是a和b的輸出?
  • 你期待的是,既然你已經修改了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(及其訂閱)。這最終會導致內存泄漏,還是會撿起它?

    我不清楚你的意思是「可變配置」。變量本身實際上並不是以任何通常意義上的「處置」。所以,我推斷你真的在談論垃圾收集。有了這個推論......…

    答案是,如果您不取消訂閱那個引用原始對象的原始委託,則原始對象將不會被收集。有些人確實使用術語「內存泄漏」來描述這種情況(我不這樣做,因爲這樣做不能區分在其他類型的內存管理方案中可能發生的實際內存泄漏的情況,其中內存分配對於一個對象是真正和永久丟失)。

    在.NET中,對象是合格垃圾收集時不再可及。 該對象實際上將被收集是由GC決定的。通常,我們只關心自己的資格,而不是實際的收集。

    對於最初由col變量引用的對象,只要該局部變量仍在範圍內並且仍然可以在方法中使用,它就是可到達的。一旦變量引用的對象用於訂閱事件,事件本身現在也通過訂閱的委託對該對象進行引用(顯然,否則,代理將如何通過正確的this值調用處理事件的實例方法?)。

    如果您沒有從該事件的訂閱者中刪除該委託,並且其對該原始對象的引用,則該對象本身保持可到達,因此而不是有資格進行垃圾收集。

    如果事件是類的非static成員,通常這不是一個問題,因爲只要對象本身存在,通常就想保持訂閱該事件。當對象本身不再可達時,任何事件處理對象都會訂閱它的事件。

    就你而言,你正在處理一個static事件。這確實可能是潛在的內存泄漏源,因爲一個類的static成員總是可到達。因此,直到您取消訂閱引用創建的原始對象的委託,該原始對象也保持可及並且不能收集。

    2a。至於q2,簡單地更改strings屬性本身更有意義,而不是完全取代col?不完全確定爲什麼,但你的迴應帶來了這個想法。代碼:col.strings = new [] { "b", "b"};

    沒有更多的情況下,我不能說什麼會「更有意義」。但是,如果您這樣做了,那麼您的代碼確實會在所有四種情況下產生您的預期結果(即您是否在兩個示例中註釋了event-subscription和-nsubscription代碼)。並且通過避免分配新對象,您可以避免意外地無法從事件中取消訂閱對象的處理程序,或者無意中使對象保持可訪問狀態。

    +0

    謝謝您的深入響應!兩個後續問題: ** 1a。**就你的q1迴應而言,如果在變量處置方面是_bad_,我更好奇。正如q2似乎表明的那樣,即使在將col設置爲新實例後,也不會刪除最初的「col」(及其訂閱)。這最終會導致內存泄漏,還是會撿起它? ** 2a。**至於q2,簡單地改變'strings'屬性本身而不是完全替換'col'會更有意義嗎?不完全確定爲什麼,但你的迴應帶來了這個想法。代碼:'col.strings = new [] {「b」,「b」};' – DisplayedName

    +0

    @DisplayedName:請參閱我的「附錄」 –