简体   繁体   English

null条件运算符对委托和常规对象的作用是否相同?

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

Reference 参考

I'm currently dealing with some thread sensitive code. 我目前正在处理一些线程敏感的代码。

In my code I have a list of objects that is manipulated by two different threads. 在我的代码中,我有一个由两个不同线程操纵的对象列表。 One thread can add objects to this list, while the other may set it to null. 一个线程可以向此列表添加对象,而另一个线程可以将其设置为null。

In the above reference it specifically mentions that for delegates: 在上面的参考文献中,它特别提到代表们:

myDelegate?.Invoke()

is equivalent to: 相当于:

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

My question is, is this behavior the same for say, a List<> ? 我的问题是,这个行为是否相同,例如List<> Eg: 例如:

Is: 方法是:

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

guaranteed to be equivalent to: 保证相当于:

var myList = new List<object>();

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

?


EDIT: 编辑:

Note that there is a difference between (how a delegate works): 请注意(委托如何工作)之间存在差异:

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
}

And

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

This is a subtle problem that requires careful analysis. 这是一个微妙的问题,需要仔细分析。

First off, the code posed in the question is pointless, because it does a null check on a local variable that is guaranteed to not be null. 首先,问题中提出的代码是没有意义的,因为它对保证不为null的局部变量进行空检查。 Presumably the real code reads from a non-local variable that may or may not be null, and may be altered on multiple threads. 据推测,真实代码从非局部变量读取,该变量可能为空,也可能不为空,并且可以在多个线程上进行更改。

This is a super dangerous position to be in and I strongly discourage you from pursuing this architectural decision . 这是一个非常危险的位置,我强烈反对你不要追求这个架构决定 Find another way to share memory across workers. 找到另一种在工人之间共享内存的方法。

To address your question: 要解决您的问题:

The first version of the question is: does the ?. 问题的第一个版本是:是?. operator have the same semantics as your version where you introduce a temporary? 运算符与您引入临时的版本具有相同的语义?

Yes, it does. 是的,它确实。 But we're not done. 但我们还没有完成。

The second question, that you did not ask, is: is it possible that the C# compiler, jitter, or CPU causes the version with the temporary to introduce an extra read? 你没有问的第二个问题是:C#编译器,抖动或CPU是否可能导致带有临时版本的版本引入额外的读取? That is, are we guaranteed that 也就是说,我们是否有保证

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

is never executed as though you wrote 永远不会像你写的那样被执行

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

The question of "introduced reads" is complicated in C#, but the short version is: generally speaking you can assume that reads will not be introduced in this manner. “引入读取”的问题在C#中很复杂,但简短版本是:一般来说,您可以假设读取不会以这种方式引入。

Are we good? 我们好吗? Of course not. 当然不是。 The code is completely not threadsafe because Add might be called on multiple threads, which is undefined behaviour! 代码完全不是线程安全的,因为可能会在多个线程上调用Add ,这是未定义的行为!

Suppose we fix that, somehow. 假设我们以某种方式解决了这个问题。 Are things good now? 现在好事吗?

No. We still should not have confidence in this code. 不,我们仍然不应该对此代码有信心。

Why not? 为什么不?

The original poster has shown no mechanism which guarantees that an up-to-date value of someListThatCouldBeNull is being read. 原始海报没有显示任何保证正在读取someListThatCouldBeNull的最新值的someListThatCouldBeNull Is it accessed under a lock? 它是在锁定下访问的吗? Is it volatile? 它不稳定吗? Are memory barriers introduced? 是否引入了记忆障碍? The C# specification is very clear on the fact that reads may be moved arbitrarily backwards in time if there are no special effects such as locks or volatiles involved. C#规范非常明确,如果没有涉及锁或挥发物的特殊效果,读数可能会被任意向后移动。 You might be reading a cached value. 您可能正在读取缓存的值。

Similarly, we have not seen the code which does the writes; 同样,我们还没有看到执行写操作的代码; those writes can be moved arbitrarily far into the future. 这些写作可以任意移动到将来。 Any combination of a read moved into the past or a write moved into the future can lead to a "stale" value being read. 移动到过去的读取或移动到将来的写入的任何组合都可能导致读取“陈旧”值。

Now suppose we solve that problem. 现在假设我们解决了这个问题。 Does that solve the whole problem? 这会解决整个问题吗? Certainly not. 当然不是。 We do not know how many threads there are involved, or if any of those threads are also reading related variables, and if there are any assumed ordering constraints on those reads . 我们不知道涉及多少线程,或者这些线程中是否有任何线程也在读取相关变量,以及这些读取是否存在任何假设的排序约束 C# does not require that there be a globally consistent view of the order of all reads and writes! C# 要求有秩序的全球一致的视图的所有读取和写入! Two threads may disagree on the order in which reads and writes to volatile variables happened. 两个线程可能不同意对volatile变量进行读写操作的顺序。 That is, if the memory model permits two possible observed orderings, it is legal for one thread to observe one, and the other thread to observe the other. 也就是说,如果内存模型允许两个可能的观察顺序,则一个线程观察一个线程是合法的,而另一个线程观察另一个线程是合法的。 If your program logic implicitly depends on there being a single observed ordering of reads and writes, your program is wrong . 如果您的程序逻辑隐含地依赖于单个观察到的读写顺序,则您的程序是错误的

Now perhaps you see why I strongly advise against sharing memory in this manner. 现在也许你明白为什么我强烈建议不要以这种方式分享内存。 It is a minefield of subtle bugs. 这是一个微妙的错误的雷区。

So what should you do? 那你该怎么办?

  • If you can: stop using threads . 如果你能: 停止使用线程 Find a different way to handle your asynchrony. 找到一种不同的方法来处理异步。
  • If you cannot do that, use threads as workers that solve a problem and then go back to the pool . 如果您不能这样做,请使用线程作为解决问题的工作者,然后返回池中 Having two threads both hammering on the same memory at the same time is hard to get right. 有两个线程同时敲击同一个内存很难做对。 Having one thread go off and compute something and return the value when it is done is a lot easier to get right, and you can... 让一个线程关闭并计算一些东西并在完成后返回值更容易正确,你可以......
  • ... use the task parallel library or another tool designed to manage inter-thread communication properly. ...使用任务并行库或其他工具来正确管理线程间通信。
  • If you cannot do that, try to mutate as few variables as possible . 如果你不能这样做, 试着改变尽可能少的变量 Do not be setting a variable to null. 不要将变量设置为null。 If you're filling in a list, initialize the list with a threadsafe list type once, and then only read from that variable. 如果您正在填写列表,请使用线程安全列表类型初始化列表一次,然后仅从该变量中读取。 Let the list object handle the threading concerns for you. 让列表对象为您处理线程问题。

The answer is yes. 答案是肯定的。

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

Compiles to the following ( as seen here ) 编译如下( 如此处所示

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

In this answer , Eric Lippert confirms that a temporary variable is used in all cases, which will prevent the "?." 这个答案中 ,Eric Lippert确认在所有情况下都使用临时变量,这将阻止“?”。 operator from causing a NullReferenceException or accessing two different objects. 运算符导致NullReferenceException或访问两个不同的对象。 There are, however, many other factors which can make this code not thread safe, see Eric's answer . 但是,有许多其他因素可以使这些代码不是线程安全的,请参阅Eric的回答

UPD: to address the claim that a temporary variable is not created: there is no need to introduce a temp variable for a local variable. UPD:解决未创建临时变量的声明:不需要为局部变量引入临时变量。 However, if you try to access something that may conceivably be modified, a variable gets created. 但是,如果您尝试访问可能被修改的内容,则会创建一个变量。 Using the same SharpLab with slightly modified code we get: 使用相同的SharpLab和略微修改的代码,我们得到:

using System;
using System.Collections.Generic;

public class C {
    public List<Object> mList;

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

becomes

public class C
{
    public List<object> mList;

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

Yes, they are same. 是的,他们是一样的。 You can also see the underlying IL below, generated by Ildasm : 您还可以在下面看到由Ildasm生成的基础IL:

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

This will be: 这将是:

.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

And: 和:

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

This will be: 这将是:

.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