为什么在C#中使用自动属性时需要调用:this() 在结构体中?

54

如果我在C#中使用自动属性定义一个结构体,就像这样:

public struct Address
{
    public Address(string line1, string line2, string city, string state, string zip)
    {
        Line1 = line1;
        Line2 = line2;
        City = city;
        State = state;
        Zip = zip;
    }

    public string Line1 { get; protected set; }
    public string Line2 { get; protected set; }
    public string City { get; protected set; }
    public string State { get; protected set; }
    public string Zip { get; protected set; }
}

当我尝试构建文件时,我遇到了编译错误,提示The 'this' object cannot be used before all of its fields are assigned to。这可以通过更改构造函数,使其链接到默认构造函数来解决,如下所示:

public Address(string line1, string line2, string city, string state, string zip): this()
{
    Line1 = line1;
    Line2 = line2;
    City = city;
    State = state;
    Zip = zip;
}

我的问题是,为什么这个方法有效,发生了什么?我有一个猜想,并试图通过查看IL证明它,但如果我认为我可以分解IL,那只是在自欺欺人。但我的猜测是,自动属性通过使编译器在幕后生成属性的字段来工作。这些字段不能通过代码访问,所有的设置和获取都必须通过属性完成。创建结构时,不能显式定义默认构造函数。因此,在幕后,编译器必须生成一个默认构造函数,设置开发者看不到的字段的值。

任何IL专家都可以验证或反驳我的理论。


在一个“结构体”中,就像在“密封类”中一样,不应该允许新的“protected”成员。 - Jeppe Stig Nielsen
2个回答

53
注意:从C# 6开始,这不再是必需的 - 但您应该在C# 6中使用只读自动实现属性... this()确保编译器认为字段被赋值了 - 它将所有字段设置为它们的默认值。在您可以开始访问任何属性之前,必须拥有一个完全构建的结构体。
这很烦人,但事实如此。您确定您真的希望这是一个结构体吗?为什么在结构体上使用受保护的setter(无法派生)?

7
不可变对象并不等同于结构体。对我来说,它并不很像结构体。这并不一定是一个糟糕的选择,但这不是我会做出的选择。虽然自动属性不能拥有“只读”设置器,但我会让不可变性更加明显:使用一个“普通”的(只读)属性和一个只读字段。 - Jon Skeet
5
这样做只能在构造函数中访问,就像为只读字段分配值一样。编译器可以在后台生成一个只读字段,并将setter访问转换为直接的字段访问。 - Jon Skeet
1
您可能需要编辑说明,从C# 6开始不再需要此操作。 - Marc Gravell
好奇,为什么C#编译器在这种情况下不会隐式插入“:this()”? - Vlad
@Vlad:那可能会隐藏一个错误,即您忘记为特定字段(而不是属性)分配值。在我的看法中,不要求使用this()来自动设置属性是更好的解决方案。 - Jon Skeet
显示剩余4条评论

0

属性本质上只是一个Get方法和/或Set方法的封装。CLR具有元数据,指示特定方法应被视为属性,这意味着编译器应该允许一些构造,而这些构造在使用方法时则不允许。例如,如果XFoo的可读写属性,则编译器将把Foo.X += 5转换为Foo.SET_X_METHOD(Foo.GET_X_METHOD() + 5)(虽然方法的名称不同,并且通常无法通过名称访问)。

虽然自动属性实现了一对get/set方法,这些方法以某种方式访问私有字段,使其表现得更像字段,但从属性外部代码的角度来看,自动属性就像任何其他属性一样是一对get/set方法。因此,像Foo.X = 5;这样的语句被翻译为Foo.SET_X_METHOD(5)。由于C#编译器只将其视为方法调用,而方法不包括任何元数据来指示它们读取或写入哪些字段,因此除非它知道Foo的每个字段都已被写入,否则编译器将禁止该方法调用。

就我个人而言,我的建议是避免在结构类型中使用自动属性。自动属性在类中是有意义的,因为类属性可以支持更新通知等功能。即使类的早期版本不支持更新通知,使用自动属性而不是字段将意味着未来的版本可以添加更新通知功能,而无需重新设计类的消费者。然而,结构体不能有意义地支持大多数希望添加到类似于字段的属性中的功能。

此外,对于大型结构体而言,字段和属性之间的性能差异要比类类型大得多。实际上,避免使用大型结构体的建议很大程度上是由于这种差异所导致的。如果避免不必要的复制,大型结构体实际上可以非常高效。即使有一个巨大的结构体HexDecet<HexDecet<HexDecet<Integer>>>,其中HexDecet<T>包含了类型为T的公开字段F0..F15,像Foo = MyThing.F3.F6.F9;这样的语句只需要从MyThing读取一个整数并将其存储到Foo中,即使按照结构体标准,MyThing也会非常庞大(占用16K的4096个整数)。此外,可以非常容易地更新该元素,例如:MyThing.F3.F6.F9 += 26;。相比之下,如果F0..F15是自动属性,则语句Foo = MyThing.F3.F6.F9将需要将1K的数据从MyThing.F3复制到一个临时变量(称为temp1),然后将64字节的数据从temp1.F6复制到temp2,最后才读取temp2.F9中的4个字节数据。更糟糕的是,尝试将26添加到MyThing.F3.F6.F9中的值将需要类似于var t1 = MyThing.F3; var t2 = t1.F6; t2.F9 += 26; t1.F6 = f2; MyThing.F3 = t1;这样的代码。

许多长期存在的关于“可变结构类型”的抱怨实际上是关于具有读/写属性的结构类型的抱怨。只需用字段替换属性,问题就会消失。

PS:有时候拥有一个结构体很有用,它的属性访问它持有引用的类对象。例如,希望有一个版本的ArraySegment<T>类,允许使用Var foo[] = new int[100]; Var MyArrSeg = New ArraySegment<int>(foo, 25, 25); MyArrSeg[6] += 9;,并且使最后一条语句将九添加到foo的(25+6)元素中。在早期版本的C#中,人们可以这样做。不幸的是,在框架中频繁使用自动属性而应该使用字段导致了广泛的抱怨,即编译器允许在只读结构上无用地调用属性设置器;因此,现在禁止在只读结构上调用任何属性设置器,无论属性设置器是否实际修改结构体的任何字段。如果人们简单地通过属性设置器使结构体不可变(在适当的情况下直接访问字段),编译器就永远不必实施该限制。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接