在结构方法中将"this"存储在局部变量中有什么好处?

Joe*_*nta 33 .net c#

我今天正在浏览.NET Core源代码树,并在以下模式中运行System.Collections.Immutable.ImmutableArray<T>:

T IList<T>.this[int index]
{
    get
    {
        var self = this;
        self.ThrowInvalidOperationIfNotInitialized();
        return self[index];
    }
    set { throw new NotSupportedException(); }
}
Run Code Online (Sandbox Code Playgroud)

这种模式(存储this在局部变量中)似乎始终在此文件中应用,this否则将在同一方法中多次引用,但在仅引用一次时则不会.所以我开始思考这样做的相对优势是什么; 在我看来,优势可能与性能有关,所以我走了这条路线更远......也许我忽略了别的东西.

为" 本地存储"模式发出的CILthis似乎看起来像a ldarg.0,然后ldobj UnderlyingType,stloc.0以便后来的引用来自ldloc.0而不是ldarg.0像只使用this多次一样.

对于C#-to-CIL转换或者JITter寻找机会为我们优化这一点可能ldarg.0要慢得多ldloc.0,但是不够,这样在C#代码中随时编写这种奇怪的模式更有意义否则我们会ldarg.0在struct实例方法中发出两条指令?

更新:或者,你知道,我可以查看该文件顶部的注释,它可以准确地解释发生了什么......

小智 20

正如您已经注意到的,System.Collections.Immutable.ImmutableArray <T>是一个结构:

public partial struct ImmutableArray<T> : ...
{
    ...

    T IList<T>.this[int index]
    {
        get
        {
            var self = this;
            self.ThrowInvalidOperationIfNotInitialized();
            return self[index];
        }
        set { throw new NotSupportedException(); }
    }

    ...
Run Code Online (Sandbox Code Playgroud)

var self = this;创建结构的副本被提及.为什么要这样做呢?该结构源注释解释了为什么有必要:

///这种类型应该是线程安全的.作为一个结构,它不能保护自己的领域
///从而其成员在其他线程中执行一个线程被改变
///因为结构可以改变的地方简单地通过重新分配含有场
///这个结构.因此,
///**每个成员只应取消引用此ONCE 非常重要.**
///如果成员需要引用数组字段,那么这将取消引用它.
///调用其他实例成员(属性或方法)也算作取消引用它.
///任何需要多次使用它的成员必须
///将其分配给局部变量,并将其用于代码的其余部分.
///这有效地将struct中的一个字段复制到一个局部变量,以便
///它与其他线程隔离.

简而言之,如果有可能其他线程正在对结构的字段进行更改或更改结构(例如,通过重新分配此结构类型的类成员字段),而get方法正在执行,因此可以导致不良副作用,然后get方法必须首先在处理结构之前制作结构的(本地)副本.

更新:还请阅读supercats的答案,它详细解释了必须满足哪些条件,以便像构建结构(即var self = this;)的本地副本这样的操作是线程安全的,以及如果不满足这些条件会发生什么.

  • @JoeAmenta,阅读该数据类型的源代码注释("*此类型应该是线程安全的.作为结构,它不能保护自己的字段在其成员在其他线程上执行时从一个线程更改,因为结构可以只需通过重新分配包含此结构的字段即可"改变".*").另请注意,您正在查看的类可能不总是不可变的(假设它确实是不可变的,它似乎不是根据源注释)并且您看到的代码只是一个"遗留下来的"旧修订......谁知道...... (2认同)
  • @elgonzo关键是即使是一个不可变的`struct`也可能不是线程安全的,因为整个`struct`可以就地改变. (2认同)

sup*_*cat 8

如果底层存储位置是可变的,则.NET中的结构实例总是可变的,并且如果底层存储位置是不可变的,则始终是不可变的.结构类型可能"假装"是不可变的,但.NET将允许结构类型实例被任何可以编写它们所在的存储位置的东西修改,结构类型本身在这个问题上没有发言权.

因此,如果有一个结构:

struct foo {
  String x;
  override String ToString() {
    String result = x;
    System.Threading.Thread.Sleep(2000);
    return result & "+" & x;
  }
  foo(String xx) { x = xx; }
}
Run Code Online (Sandbox Code Playgroud)

一个是在具有相同myFoos类型数组的两个线程上调用以下方法foo[]:

myFoos[0] = new foo(DateTime.Now.ToString());
var st = myFoos[0].ToString();
Run Code Online (Sandbox Code Playgroud)

完全可能的是,首先启动的线程将使其ToString()值报告由其构造函数调用写入的时间和其他线程的构造函数调用报告的时间,而不是两次报告相同的字符串.对于其目的是验证结构字段然后使用它的方法,在验证和使用之间进行字段更改将导致该方法使用未经验证的字段.复制结构字段的内容(通过仅复制字段,或通过复制整个结构)避免了这种危险.

请注意,对于包含类型Int64,UInt64Double,或包含多个字段的字段的结构,可能var temp=this;在一个线程中发生的语句而另一个线程覆盖this已存储的位置,可能最终会复制保持新旧内容的任意混合的结构.仅当结构包含引用类型的单个字段或32位或更小的基元的单个字段时,才能保证与写入同时发生的读取将产生结构实际保持的某个值,甚至可能有一些怪癖(例如,至少在VB.NET中,一个声明someField = New foo("george")可能会someField在调用构造函数之前清除).