不需要分配结构类型的out参数


9

当我在代码审查期间不小心在函数中注释一行时,我注意到我的代码中有一些奇怪的行为。复制非常困难,但在此我将描述一个类似的示例。

我有这个测试课:

public class Test
{
    public void GetOut(out EmailAddress email)
    {
        try
        {
            Foo(email);
        }
        catch
        {
        }
    }

    public void Foo(EmailAddress email)
    {
    }
}

没有分配给的电子邮件GetOut,通常会引发错误:

必须在控制离开当前方法之前将out参数'email'分配给

但是,如果EmailAddress在单独程序集中的结构中,则不会创建错误,并且一切都可以编译。

public struct EmailAddress
{
    #region Constructors

    public EmailAddress(string email)
        : this(email, string.Empty)
    {
    }

    public EmailAddress(string email, string name)
    {
        this.Email = email;
        this.Name = name;
    }

    #endregion

    #region Properties

    public string Email { get; private set; }
    public string Name { get; private set; }

    #endregion
}

为什么编译器不强制必须将电子邮件分配给?如果结构是在单独的程序集中创建的,为什么此代码会编译,而如果结构是在现有程序集中定义的,为什么不编译?


2
如果您使用的是类,则必须“新建”该对象的实例。对于结构来说不是必需的。docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/…(在该页面上专门搜索此文本:与类不同,可以在不使用新操作符的情况下实例化结构)
Dortimer,

1
一旦您的Dog结构体获取变量,它就不会编译:)
安德烈·桑森

在此示例中,使用struct Dog{},一切都很好。
Henk Holterman

2
@ johnny5然后显示示例。
安德烈·桑森(

1
好,这很有趣。使用Core 3 Console应用程序和.Standard类库复制。
汉克·霍尔特曼

Answers:


12

TLDR:这是一个长期存在的已知错误。我在2010年首次写到:

https://blogs.msdn.microsoft.com/ericlippert/2010/01/18/a-definite-assignment-anomaly/

它是无害的,您可以放心地忽略它,并祝贺您发现了一个不太明显的错误。

为什么编译器不强制Email必须明确分配?

哦,确实如此。就像我们将看到的那样,它只是错误地认为什么条件意味着必须明确分配变量。

如果结构是在单独的程序集中创建的,为什么此代码会编译,而如果结构是在现有程序集中定义的,为什么不编译?

这就是漏洞的症结所在。该错误是C#编译器如何对结构进行确定分配检查以及编译器如何从库中加载元数据的交叉结果。

考虑一下:

struct Foo 
{ 
  public int x; 
  public int y; 
}
// Yes, public fields are bad, but this is just 
// to illustrate the situation.
void M(out Foo f)
{

好的,这时我们知道什么? f是type变量的别名Foo,因此存储已被分配,并且至少在从存储分配器中出来的状态中可以确定。如果调用者在变量中放置了一个值,则该值在那里。

我们需要什么?我们要求f在控制权M正常离开的任何地方明确分配。因此,您会期望像:

void M(out Foo f)
{
  f = new Foo();
}

该套f.xf.y它们的默认值。但是呢?

void M(out Foo f)
{
  f = new Foo();
  f.x = 123;
  f.y = 456;
}

那也应该很好。但是,这就是问题所在,为什么我们只需要分配默认值以便稍后将其删除呢? C#的确定分配检查器检查是否每个字段都被分配了!这是合法的:

void M(out Foo f)
{
  f.x = 123;
  f.y = 456;
}

那为什么不合法呢?这是一个值类型。 f是一个变量,并且已经包含type的有效值Foo,因此我们只需设置字段即可,对吧?

对。那是什么错误呢?

您发现的错误是: 为了节省成本,C#编译器不会为引用库中的结构的私有字段加载元数据。该元数据可能非常庞大,并且每次都将其全部加载到内存中时,会使编译器的速度减慢,几乎没有胜利。

现在您应该可以推断出发现的错误的原因。当编译器检查是否确实分配了out参数时,它会将已知字段的数量与已明确初始化的字段数量进行比较,并且在您的情况下,由于未加载私有字段元数据,因此它只知道零个公共字段。编译器得出结论:“需要零字段,已初始化零字段,我们很好。”

就像我说的那样,这个错误已经存在了十多年了,像您这样的人偶尔会重新发现并报告它。它是无害的,并且不太可能得到修复,因为修复它几乎没有什么好处,但是性能成本却很高。

当然,该错误不会复制项目中源代码中结构的私有字段,因为显然编译器已经掌握了有关私有字段的信息。


@ johnny5:您不应该出错。参见dotnetfiddle.net/ZEKiUk。您可以发布一个简单的复制品吗?
埃里克·利珀特

1
感谢小提琴,这是因为我将x和y定义为属性而不是成员
johnny

1
@ johnny5:如果您只是定义了普通的C#1.0样式属性,那么从确定分配检查器的角度来看,这是一种方法,而不是字段。如果您定义了C#3.0+样式的自动属性,则编译器将知道有一个私有字段来支持它;否则,它会被保存。多年来,已经对该事物的明确分配规则进行了调整,我现在不记得确切的规则。
埃里克·利珀特

如果您改用a System.TimeSpan,则会出现错误:error CS0269: Use of unassigned out parameter 'email'error CS0177: The out parameter 'email' must be assigned to before control leaves the current method。的只有一个非静态字段TimeSpan,即_ticks。它是internal其程序集mscorlib。这个组装特别吗?与相同System.DateTime,其字段为private
Jeppe Stig Nielsen

@JeppeStigNielsen:我不知道这是怎么回事!如果您知道了,请告诉我。
埃里克·利珀特

1

尽管它看起来像个错误,但确实有一定道理。

“丢失错误”仅在使用类库时出现。一个类库可能已经用另一种.net语言编写,例如VB.Net。“确定分配跟踪”是C#的功能,而不是框架的功能。

因此,总的来说,我不认为这是一个错误,但是我不知道有关此问题的权威性声明。


如果要将程序集导入C#中,即使该结构可以位于用另一种语言编写的程序集中,则使用该代码的代码仍在c#中,因此它不必使用确定的分配跟踪。
约翰尼

1
不必要。C#不允许您使用单元化(局部)变量,但同时框架保证将其设置为0(default(T))。因此,没有违反内存安全性或类似的规定。
Henk Holterman

3
我可以发表这样的权威性声明。:)这是一个长期存在的错误。
埃里克·利珀特

1
@EricLippert谢谢,我希望您能在某个时候看到它
约翰尼
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.