簡體   English   中英

null條件運算符對委托和常規對象的作用是否相同?

[英]Does the null-conditional operator function the same with delegates and regular objects?

參考

我目前正在處理一些線程敏感的代碼。

在我的代碼中,我有一個由兩個不同線程操縱的對象列表。 一個線程可以向此列表添加對象,而另一個線程可以將其設置為null。

在上面的參考文獻中,它特別提到代表們:

myDelegate?.Invoke()

相當於:

var handler = myDelegate;
if (handler != null)
{
    handler(…);
}

我的問題是,這個行為是否相同,例如List<> 例如:

方法是:

var myList = new List<object>();    
myList?.Add(new object());

保證相當於:

var myList = new List<object>();

var tempList = myList;
if (tempList != null)
{
    tempList.Add(new object());
}


編輯:

請注意(委托如何工作)之間存在差異:

var myList = new List<int>();
var tempList = myList;
if (tempList != null)
{
    myList = null; // another thread sets myList to null here
    tempList.Add(1); // doesn't crash
}

var myList = new List<int>();
if (myList != null)
{
    myList = null; // another thread sets myList to null here
    myList.Add(1); // crashes
}

這是一個微妙的問題,需要仔細分析。

首先,問題中提出的代碼是沒有意義的,因為它對保證不為null的局部變量進行空檢查。 據推測,真實代碼從非局部變量讀取,該變量可能為空,也可能不為空,並且可以在多個線程上進行更改。

這是一個非常危險的位置,我強烈反對你不要追求這個架構決定 找到另一種在工人之間共享內存的方法。

要解決您的問題:

問題的第一個版本是:是?. 運算符與您引入臨時的版本具有相同的語義?

是的,它確實。 但我們還沒有完成。

你沒有問的第二個問題是:C#編譯器,抖動或CPU是否可能導致帶有臨時版本的版本引入額外的讀取? 也就是說,我們是否有保證

var tempList = someListThatCouldBeNull;
if (tempList != null)
    tempList.Add(new object());

永遠不會像你寫的那樣被執行

var tempList = someListThatCouldBeNull;
if (tempList != null) 
    someListThatCouldBeNull.Add(new object());

“引入讀取”的問題在C#中很復雜,但簡短版本是:一般來說,您可以假設讀取不會以這種方式引入。

我們好嗎? 當然不是。 代碼完全不是線程安全的,因為可能會在多個線程上調用Add ,這是未定義的行為!

假設我們以某種方式解決了這個問題。 現在好事嗎?

不,我們仍然不應該對此代碼有信心。

為什么不?

原始海報沒有顯示任何保證正在讀取someListThatCouldBeNull的最新值的someListThatCouldBeNull 它是在鎖定下訪問的嗎? 它不穩定嗎? 是否引入了記憶障礙? C#規范非常明確,如果沒有涉及鎖或揮發物的特殊效果,讀數可能會被任意向后移動。 您可能正在讀取緩存的值。

同樣,我們還沒有看到執行寫操作的代碼; 這些寫作可以任意移動到將來。 移動到過去的讀取或移動到將來的寫入的任何組合都可能導致讀取“陳舊”值。

現在假設我們解決了這個問題。 這會解決整個問題嗎? 當然不是。 我們不知道涉及多少線程,或者這些線程中是否有任何線程也在讀取相關變量,以及這些讀取是否存在任何假設的排序約束 C# 要求有秩序的全球一致的視圖的所有讀取和寫入! 兩個線程可能不同意對volatile變量進行讀寫操作的順序。 也就是說,如果內存模型允許兩個可能的觀察順序,則一個線程觀察一個線程是合法的,而另一個線程觀察另一個線程是合法的。 如果您的程序邏輯隱含地依賴於單個觀察到的讀寫順序,則您的程序是錯誤的

現在也許你明白為什么我強烈建議不要以這種方式分享內存。 這是一個微妙的錯誤的雷區。

那你該怎么辦?

  • 如果你能: 停止使用線程 找到一種不同的方法來處理異步。
  • 如果您不能這樣做,請使用線程作為解決問題的工作者,然后返回池中 有兩個線程同時敲擊同一個內存很難做對。 讓一個線程關閉並計算一些東西並在完成后返回值更容易正確,你可以......
  • ...使用任務並行庫或其他工具來正確管理線程間通信。
  • 如果你不能這樣做, 試着改變盡可能少的變量 不要將變量設置為null。 如果您正在填寫列表,請使用線程安全列表類型初始化列表一次,然后僅從該變量中讀取。 讓列表對象為您處理線程問題。

答案是肯定的。

var myList = new List<object>();    
myList?.Add(new object());

編譯如下( 如此處所示

List<object> list = new List<object>();
if (list != null)
{
    list.Add(new object());
}

這個答案中 ,Eric Lippert確認在所有情況下都使用臨時變量,這將阻止“?”。 運算符導致NullReferenceException或訪問兩個不同的對象。 但是,有許多其他因素可以使這些代碼不是線程安全的,請參閱Eric的回答

UPD:解決未創建臨時變量的聲明:不需要為局部變量引入臨時變量。 但是,如果您嘗試訪問可能被修改的內容,則會創建一個變量。 使用相同的SharpLab和略微修改的代碼,我們得到:

using System;
using System.Collections.Generic;

public class C {
    public List<Object> mList;

    public void M() {
        this.mList?.Add(new object());
    }
}

public class C
{
    public List<object> mList;

    public void M()
    {
        List<object> list = mList;
        if (list != null)
        {
            list.Add(new object());
        }
    }
}

是的,他們是一樣的。 您還可以在下面看到由Ildasm生成的基礎IL:

public void M()
{
    var myList = new List<object>();
    myList?.Add(new object());
}

這將是:

.method public hidebysig instance void  M() cil managed
{
  // Code size       25 (0x19)
  .maxstack  2
  .locals init (class [System.Collections]System.Collections.Generic.List`1<object> V_0)
  IL_0000:  nop
  IL_0001:  newobj     instance void class [System.Collections]System.Collections.Generic.List`1<object>::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  brtrue.s   IL_000c
  IL_000a:  br.s       IL_0018
  IL_000c:  ldloc.0
  IL_000d:  newobj     instance void [System.Runtime]System.Object::.ctor()
  IL_0012:  call       instance void class [System.Collections]System.Collections.Generic.List`1<object>::Add(!0)
  IL_0017:  nop
  IL_0018:  ret
} // end of method C::M

和:

public void M2()
{
    List<object> list = new List<object>();
    if (list != null)
    {
        list.Add(new object());
    }
}

這將是:

.method public hidebysig instance void  M2() cil managed
{
  // Code size       30 (0x1e)
  .maxstack  2
  .locals init (class [System.Collections]System.Collections.Generic.List`1<object> V_0,
           bool V_1)
  IL_0000:  nop
  IL_0001:  newobj     instance void class [System.Collections]System.Collections.Generic.List`1<object>::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  ldnull
  IL_0009:  cgt.un
  IL_000b:  stloc.1
  IL_000c:  ldloc.1
  IL_000d:  brfalse.s  IL_001d
  IL_000f:  nop
  IL_0010:  ldloc.0
  IL_0011:  newobj     instance void [System.Runtime]System.Object::.ctor()
  IL_0016:  callvirt   instance void class [System.Collections]System.Collections.Generic.List`1<object>::Add(!0)
  IL_001b:  nop
  IL_001c:  nop
  IL_001d:  ret
} // end of method C::M2

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM