null 条件运算符与委托和常规对象的功能是否相同?
Does the null-conditional operator function the same with delegates and regular objects?
我目前正在处理一些线程敏感代码。
在我的代码中,我有一个由两个不同线程操作的对象列表。一个线程可以将对象添加到此列表,而另一个线程可以将其设置为空。
在上面的参考资料中,它特别提到代表:
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
}
答案是肯定的。
var myList = new List<object>();
myList?.Add(new object());
编译为以下 (as seen here)
List<object> list = new List<object>();
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
在this answer, Eric Lippert confirms that a temporary variable is used in all cases, which will prevent the "?." operator from causing a NullReferenceException or accessing two different objects. There are, however, many other factors which can make this code not thread safe, see .
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());
}
}
}
这是一个需要仔细分析的微妙问题。
首先,问题中提出的代码毫无意义,因为它对保证不为空的局部变量进行空检查。据推测,真正的代码是从一个 non-local 变量中读取的,该变量可能为 null 也可能不为 null,并且可能在多个线程上被更改。
这是一个非常危险的位置,我强烈建议您不要追求这个架构决策。找到另一种在工作人员之间共享内存的方法。
解决您的问题:
问题的第一个版本是:?.
运算符是否与您引入临时变量的版本具有相同的语义?
是的,确实如此。但我们还没有完成。
您没有问的第二个问题是:C# 编译器、抖动或 CPU 是否可能导致带有临时文件的版本引入额外的读取?也就是说,我们是否保证
var tempList = someListThatCouldBeNull;
if (tempList != null)
tempList.Add(new object());
永远不会执行,就好像你写
var tempList = someListThatCouldBeNull;
if (tempList != null)
someListThatCouldBeNull.Add(new object());
"introduced reads"的问题在C#中比较复杂,但是简而言之就是:一般来说,你可以假设不会以这种方式引入读取。
我们好吗?当然不是。该代码完全不是线程安全的,因为 Add
可能会在多个线程上调用,这是未定义的行为!
假设我们以某种方式修复它。现在情况好了吗?
没有。我们仍然不应该对这段代码有信心。
为什么不呢?
楼主没有显示任何机制可以保证 someListThatCouldBeNull
的 up-to-date 值正在被读取。 它是在锁下访问的吗?它易挥发吗?是否引入了内存屏障? C# 规范非常清楚,如果不涉及锁或易失性等特殊效果,读取可能会在时间上任意向后移动。您可能正在读取缓存值。
同样,我们还没有看到写的代码;这些写入可以任意移动到未来。读取移至过去或写入移至未来的任何组合都可能导致读取 "stale" 值。
现在假设我们解决了那个问题。这能解决整个问题吗?当然不是。 我们不知道涉及多少个线程,或者这些线程中是否有任何线程也在读取相关变量,以及这些读取是否有任何假设的顺序约束。 C# 不 要求对所有读写顺序有一个全局一致的视图!两个线程可能 不同意 读取和写入 volatile 变量的顺序。也就是说,如果内存模型允许两种可能的观察顺序,则一个线程观察一个,另一个线程观察另一个是合法的。 如果您的程序逻辑隐含地依赖于单个观察到的读写顺序,那么您的程序是错误的。
现在您可能明白为什么我强烈建议不要以这种方式共享内存。这是一个充满微妙错误的雷区。
那你该怎么办?
- 如果可以:停止使用线程。寻找一种不同的方式来处理异步。
- 如果您不能这样做,使用线程作为解决问题的工作者,然后返回池。让两个线程同时对同一个内存进行锤击很难做到正确。让一个线程停止并计算一些东西,return 完成后的值更容易正确,你可以...
- ...使用任务并行库或其他旨在正确管理 inter-thread 通信的工具。
- 如果您做不到,尝试变异尽可能少的变量。不要将变量设置为空。如果您要填写列表,请使用线程安全列表类型 初始化列表一次,然后只从该变量中读取。让列表对象为您处理线程问题。
我目前正在处理一些线程敏感代码。
在我的代码中,我有一个由两个不同线程操作的对象列表。一个线程可以将对象添加到此列表,而另一个线程可以将其设置为空。
在上面的参考资料中,它特别提到代表:
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
}
答案是肯定的。
var myList = new List<object>();
myList?.Add(new object());
编译为以下 (as seen here)
List<object> list = new List<object>();
if (list != null)
{
list.Add(new object());
}
是的,它们是一样的。您还可以在下面看到由 Ildasm:
生成的底层 ILpublic 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
在this answer, Eric Lippert confirms that a temporary variable is used in all cases, which will prevent the "?." operator from causing a NullReferenceException or accessing two different objects. There are, however, many other factors which can make this code not thread safe, see
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());
}
}
}
这是一个需要仔细分析的微妙问题。
首先,问题中提出的代码毫无意义,因为它对保证不为空的局部变量进行空检查。据推测,真正的代码是从一个 non-local 变量中读取的,该变量可能为 null 也可能不为 null,并且可能在多个线程上被更改。
这是一个非常危险的位置,我强烈建议您不要追求这个架构决策。找到另一种在工作人员之间共享内存的方法。
解决您的问题:
问题的第一个版本是:?.
运算符是否与您引入临时变量的版本具有相同的语义?
是的,确实如此。但我们还没有完成。
您没有问的第二个问题是:C# 编译器、抖动或 CPU 是否可能导致带有临时文件的版本引入额外的读取?也就是说,我们是否保证
var tempList = someListThatCouldBeNull;
if (tempList != null)
tempList.Add(new object());
永远不会执行,就好像你写
var tempList = someListThatCouldBeNull;
if (tempList != null)
someListThatCouldBeNull.Add(new object());
"introduced reads"的问题在C#中比较复杂,但是简而言之就是:一般来说,你可以假设不会以这种方式引入读取。
我们好吗?当然不是。该代码完全不是线程安全的,因为 Add
可能会在多个线程上调用,这是未定义的行为!
假设我们以某种方式修复它。现在情况好了吗?
没有。我们仍然不应该对这段代码有信心。
为什么不呢?
楼主没有显示任何机制可以保证 someListThatCouldBeNull
的 up-to-date 值正在被读取。 它是在锁下访问的吗?它易挥发吗?是否引入了内存屏障? C# 规范非常清楚,如果不涉及锁或易失性等特殊效果,读取可能会在时间上任意向后移动。您可能正在读取缓存值。
同样,我们还没有看到写的代码;这些写入可以任意移动到未来。读取移至过去或写入移至未来的任何组合都可能导致读取 "stale" 值。
现在假设我们解决了那个问题。这能解决整个问题吗?当然不是。 我们不知道涉及多少个线程,或者这些线程中是否有任何线程也在读取相关变量,以及这些读取是否有任何假设的顺序约束。 C# 不 要求对所有读写顺序有一个全局一致的视图!两个线程可能 不同意 读取和写入 volatile 变量的顺序。也就是说,如果内存模型允许两种可能的观察顺序,则一个线程观察一个,另一个线程观察另一个是合法的。 如果您的程序逻辑隐含地依赖于单个观察到的读写顺序,那么您的程序是错误的。
现在您可能明白为什么我强烈建议不要以这种方式共享内存。这是一个充满微妙错误的雷区。
那你该怎么办?
- 如果可以:停止使用线程。寻找一种不同的方式来处理异步。
- 如果您不能这样做,使用线程作为解决问题的工作者,然后返回池。让两个线程同时对同一个内存进行锤击很难做到正确。让一个线程停止并计算一些东西,return 完成后的值更容易正确,你可以...
- ...使用任务并行库或其他旨在正确管理 inter-thread 通信的工具。
- 如果您做不到,尝试变异尽可能少的变量。不要将变量设置为空。如果您要填写列表,请使用线程安全列表类型 初始化列表一次,然后只从该变量中读取。让列表对象为您处理线程问题。