【问题标题】:C# 6 Auto Initialization Property and the use of backing fieldsC# 6 自动初始化属性和支持字段的使用
【发布时间】:2017-02-11 22:24:56
【问题描述】:

在 C# 6 之前,属性的初始化不使用支持字段来初始化默认值。 在 C#6 中,它使用支持字段来初始化新的Auto initialization properties

我很好奇为什么在 C#6 IL 之前使用属性定义来初始化。这有什么具体原因吗?还是在 C#6 之前没有正确实现?

在 C# 6.0 之前

public class PropertyInitialization
{
    public string First { get; set; }

    public string Last { get; set; }

    public PropertyInitialization()
    {
      this.First = "Adam";
      this.Last = "Smith";
    }
}

编译器生成的代码(IL 表示)

public class PropertyInitialisation
  {
    [CompilerGenerated]
    private string \u003CFirst\u003Ek__BackingField;
    [CompilerGenerated]
    private string \u003CLast\u003Ek__BackingField;

    public string First
    {
      get
      {
        return this.\u003CFirst\u003Ek__BackingField;
      }
      set
      {
        this.\u003CFirst\u003Ek__BackingField = value;
      }
    }

    public string Last
    {
      get
      {
        return this.\u003CLast\u003Ek__BackingField;
      }
      set
      {
        this.\u003CLast\u003Ek__BackingField = value;
      }
    }

    public PropertyInitialisation()
    {
      base.\u002Ector();
      this.First = "Adam";
      this.Last = "Smith";
    }
  }

C#6

public class AutoPropertyInitialization
{
    public string First { get; set; } = "Adam";
    public string Last { get; set; } = "Smith";
}

编译器生成的代码(IL 表示)

public class AutoPropertyInitialization
  {
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private string \u003CFirst\u003Ek__BackingField;
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private string \u003CLast\u003Ek__BackingField;

    public string First
    {
      get
      {
        return this.\u003CFirst\u003Ek__BackingField;
      }
      set
      {
        this.\u003CFirst\u003Ek__BackingField = value;
      }
    }

    public string Last
    {
      get
      {
        return this.\u003CLast\u003Ek__BackingField;
      }
      set
      {
        this.\u003CLast\u003Ek__BackingField = value;
      }
    }

    public AutoPropertyInitialization()
    {
      this.\u003CFirst\u003Ek__BackingField = "Adam";
      this.\u003CLast\u003Ek__BackingField = "Smith";
      base.\u002Ector();
    }
  } 

【问题讨论】:

  • 您能否向我们展示导致此 IL 的 C# 5 和/或 6 代码?您的第一句话让我感到困惑,因为您无法在 C#6 之前自动初始化属性 - 您必须在构造函数中手动完成。
  • 必须在具有自动属性初始化的 ctor 之前分配文件(规范是这样说的)。在 ctor 之前 - 没有正确初始化属性支持字段 -> 这是在 ctor 中完成的 -> 直接使用属性将导致使用未初始化的文件
  • 你必须显示整段代码,基本上显示整个类,无论是在 C# 5 中还是在 C# 6 中,否则我们只能猜测编译器行为的原因它显然对你有用。
  • @LasseV.Karlsen 不需要。海事组织很安静,很清楚他在说什么。他的“IL 代码”不是 IL 代码,而是逻辑上代表 IL 的 C# 代码;)
  • 是的,但正如已经评论的那样,在 C# 6 之前,您无法自动初始化属性,您可以使用字段初始化,或者将特定代码放入构造函数中以将值分配给属性,后者其中将产生第一个代码示例。因此,谈论“C# 6 之前的自动属性初始化”颇具误导性。我完全理解他所展示的代码,但我希望他能具体说明他的问题。

标签: c# .net c#-6.0 intermediate-language


【解决方案1】:

我很好奇为什么在 C#6 IL 之前使用属性定义来初始化。这有什么具体原因吗?

因为通过自动属性初始化设置值和在构造函数中设置值是两件不同的事情。他们有不同的行为。

回想一下,属性是环绕字段的访问器方法。所以这一行:

this.First = "Adam";

相当于:

this.set_First("Adam");

您甚至可以在 Visual Studio 中看到这一点!尝试在你的类中编写一个带有签名 public string set_First(string value) 的方法,然后观察编译器抱怨你踩到它的脚趾。

就像方法一样,这些可以在子类中被覆盖。看看这段代码:

public class PropertyInitialization
{
    public virtual string First { get; set; }

    public PropertyInitialization()
    {
        this.First = "Adam";
    }
}

public class ZopertyInitalization : PropertyInitialization
{
    public override string First
    {
        get { return base.First; }
        set
        {
            Console.WriteLine($"Child property hit with the value: '{0}'");
            base.First = value;
        }
    }
}

在本例中,this.First = "Adam" 行将调用子类中的设置器。因为你在调用一个方法,记得吗?如果编译器将此方法调用解释为对支持字段的直接调用,它最终不会调用子设置器。 编译代码的行为会改变程序的行为。不好!

自动属性不同。让我们通过使用自动属性初始化器来更改第一个示例:

public class PropertyInitialization
{
    public virtual string First { get; set; } = "Adam";
}

public class ZopertyInitalization : PropertyInitialization
{
    public override string First
    {
        get { return base.First; }
        set
        {
            Console.WriteLine($"Child property hit with the value: '{0}'");
            base.First = value;
        }
    }
}

使用此代码,将不会调用子类中的 setter 方法。这是故意的。自动属性初始化程序旨在直接设置支持字段。它们的外观和行为类似于字段初始化器,这就是为什么我们甚至可以在没有设置器的属性上使用它们,如下所示:

public string First { get; } = "Adam";

这里没有setter方法!我们必须直接访问支持字段来执行此操作。自动属性允许程序员创建不可变的值,同时仍然能够从良好的语法中受益。

【讨论】:

  • 您的答案似乎更合适/更完整,并解释了确切的区别。即“如果编译器将此方法调用解释为对支持字段的直接调用,它最终不会调用子设置器。”。其他一些答案也正确,但可能有点复杂/不直接,或者有点离题。谢谢!
【解决方案2】:

请记住,在构造函数中没有设置属性的默认值(您的代码显示:分配,然后是构造函数)。

现在,C# 规范说自动初始化值是在构造函数之前设置的。这是有道理的:当在构造函数中再次设置这些值时,它们会被覆盖。

现在 - 在调用构造函数之前 - 没有初始化 getter 和 setter 方法。应该如何使用?

这就是为什么(当时未初始化的支持字段)被直接初始化的原因。

正如 hvd 提到的,虚拟调用也会有问题,但它们甚至没有被初始化是主要原因。


如果你在构造函数中赋值,它的行为仍然和以前一样:

Example with property that is autoinitialized and changed in the ctor

为什么不进行优化?

关于这个话题请见my question

但它不应该优化它吗?

它可能可以,但前提是该类不从另一个类继承 在其构造函数中使用该值的类,它知道它是 auto-property 和 setter 不做任何其他事情。

那将是很多(危险的)假设。编译器需要 在进行这样的优化之前检查很多东西。


旁注:

我假设您使用了一些工具来查看编译器生成的 c# 代码——它并不完全准确。为构造函数生成的 IL 代码没有准确的表达式 - ctor 不是 IL 中的方法,它有所不同。为了便于理解,我们可以假设它是相同的。

http://tryroslyn.azurewebsites.net/ 作为例子有这样的评论:

// This is not valid C#, but it represents the IL correctly.

【讨论】:

  • 这似乎是对这个问题最直接的答案,因此 IMO 也应该是公认的答案。
  • @Mafi 是编译器从 JetBrains Dot Peek 生成的代码。
【解决方案3】:

获取所示代码的一种方法是,您的 C# 5 代码如下:

public class Test : Base
{
    public Test()
    {
        A = "test";
    }

    public string A { get; set; }
}

这将产生如下(IL)代码:

public Test..ctor()
{
    Base..ctor();
    A = "test";
}

您的 C# 6 代码将如下所示:

public class Test : Base
{
    public Test()
    {
    }

    public string A { get; set; } = "test";
}

产生这样的 (IL) 代码:

public Test..ctor()
{
    <A>k__BackingField = "test";
    Base..ctor();
}

请注意,如果您在构造函数中专门初始化您的属性,并且有一个 getter/setter 属性,那么在 C# 6 中它仍然看起来像我上面答案中的第一段代码,而如果您有一个仅 getter 字段它看起来像这样:

public Test..ctor()
{
    Base..ctor();
    <A>k__BackingField = "test";
}

所以很清楚,您的 C# 5 代码看起来像上面的第一段代码,而您的 C# 6 代码看起来像第二段代码。

所以回答你的问题:为什么 C# 5 和 C# 6 在编译自动属性初始化方面的行为不同?原因是在 C# 5 或更早版本中不能进行自动属性初始化,而且不同的代码编译方式不同。

【讨论】:

    【解决方案4】:

    唯一不同的是,如果属性设置器比简单地设置值有更多的效果。对于自动实现的属性,唯一可能发生的情况是它们是否为virtual 并被覆盖。在这种情况下,在基类构造函数运行之前调用派生类方法是一个非常糟糕的主意。 C# 经历了很多麻烦,以确保您不会意外地引用尚未完全初始化的对象。所以它必须直接设置字段来防止这种情况。

    【讨论】:

    • 还有一点:初始化属性的值可以被构造函数中设置的值覆盖。这意味着必须在构造函数之前设置默认值(以确保这确实发生)。但是在构造函数调用之前,值和方法还没有初始化(包括setter)。这就是他们必须直接访问这些字段的原因。
    • @Mafii IIRC,在基础构造函数调用之前,所有方法都可以调用,使用它通常是一个非常糟糕的主意:CIL规范只说在基础构造函数之前调用实例方法调用意味着代码是不可验证的,不可验证的代码仍然可以是正确的。
    【解决方案5】:

    我假设您的 C# 5.0 代码如下所示:

    class C
    {
        public C()
        {
            First = "Adam";
        }
    
        public string First { get; private set; }
    }
    

    然后在 C# 6.0 中,您所做的唯一更改是使 First 成为 get-only 自动属性:

    class C
    {
        public C()
        {
            First = "Adam";
        }
    
        public string First { get; }
    }
    

    在 C# 5.0 的情况下,First 是一个带有 setter 的属性,您可以在构造函数中使用它,因此生成的 IL 反映了这一点。

    在 C# 6.0 版本中,First 没有 setter,因此构造函数必须直接访问支持字段。

    这两种情况对我来说都很有意义。

    【讨论】:

    • 我认为是关于public string First { get; private set; } = "Adam";,那里确实存在setter。
    • public string First { get; private set; },然后在构造函数中,First = "Adam"; 换句话说,就是你认为的 C# 5.0 代码的样子。
    • 他不是在谈论这个。您的说法是正确的,但并没有真正回答问题
    猜你喜欢
    • 2016-02-14
    • 1970-01-01
    • 2016-01-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-28
    • 2019-01-27
    相关资源
    最近更新 更多