13

在 C# 6 之前,属性的初始化不使用支持字段来初始化默认值。在 C#6 中,它使用支持字段通过新的自动初始化属性进行初始化。

我很好奇为什么在 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();
    }
  } 
4

5 回答 5

12

我很好奇为什么在 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方法!我们必须直接访问支持字段来执行此操作。自动属性允许程序员创建不可变的值,同时仍然能够从良好的语法中受益。

于 2016-10-04T12:35:50.807 回答
2

获得所示代码的一种方法是,您的 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 或更早版本中进行自动属性初始化,并且不同的代码编译方式不同。

于 2016-10-04T10:11:41.963 回答
2

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

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

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

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

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


如果您在构造函数中赋值,它的行为仍然与以前相同:

具有在 ctor 中自动初始化和更改的属性的示例

为什么不优化呢?

请参阅关于此主题的问题:

但它不应该优化吗?

它可能可以,但只有当该类没有从另一个在其构造函数中使用该值的类继承时,它才知道它是一个自动属性并且 setter 不做任何其他事情。

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


边注:

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

http://tryroslyn.azurewebsites.net/例如有这样的评论:

// This is not valid C#, but it represents the IL correctly.
于 2016-10-04T09:32:58.217 回答
1

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

于 2016-10-03T22:15:45.523 回答
0

我假设您的 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,因此构造函数必须直接访问支持字段。

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

于 2016-10-03T22:10:18.657 回答