动态编译时出现意外错误

本文关键字:意外 错误 编译 动态 | 更新日期: 2023-09-27 18:14:14

澄清问题:我不是在寻找如何解决这个问题的答案(下面列出了几个(,而是在寻找为什么会发生这种情况。

我希望编译以下代码:

struct Alice
{
    public string Alpha;
    public string Beta;
}
struct Bob
{
    public long Gamma;
}
static object Foo(dynamic alice)
{
    decimal alpha;
    long beta;
    if (!decimal.TryParse(alice.Alpha, out alpha) // *
        || !long.TryParse(alice.Beta, out beta)) // **
    {
        return alice;
    }
    var bob = new Bob { Gamma = beta }; // ***
    // do some stuff with alice and bob
    return alice;
}

但是,在// ***抛出以下编译时错误:

使用未赋值的局部变量"beta">

我可以在以下情况下使程序编译:

  1. 如果我将签名更改为

    static object Foo(Alice alice)

  2. 明确地在// *// **行上强制转换,例如:

    !long.TryParse((string)alice.Beta, out beta) .

  3. 删除联机// *上的decimal.TryParse

  4. |代替短路或||感谢汉斯·帕桑特

  5. 交换TryParse

  6. TryParse的结果拉入bool 感谢克里斯

  7. beta分配默认值

我是否错过了一些明显的东西,或者有什么微妙的事情发生了,或者这是一个错误?

动态编译时出现意外错误

我不确定答案,但对我来说,它看起来像编译器错误或"设计"问题。

我玩了一下你的样本,一点一点地减少它,这是它留下的东西:

    private static bool Default<T>(out T result)
    {
        result = default(T);
        return true;
    }
    private static void Foo()
    {
        int result;
        if (true || Default(out result))
        {
            return;
        }
        Console.WriteLine(result);
    }

这也失败了

错误 CS0165:使用未赋值的局部变量"结果">

您可以在Foo中使用int result来检查您想要的任何类型。

请注意,没有dynamic用法,也请注意true分支应立即返回。

所以对我来说 VS.Net 编译器在这里看起来"不够智能"。

这段代码有什么好处 - 它可以使用 .Net 4 之前的编译器编译(使用来自适当框架的csc.exe(,因此结果如下:

  • .Net 2.0

构建正常,警告:

警告 CS0429:检测到无法访问的表达式代码

警告 CS0162:检测到无法访问的代码

  • .Net 3.5

构建失败:

错误 CS0165:使用未赋值的局部变量"结果">

因此,如果它是一个错误,它出现在.NET 2和.NET 3.5之间

发生这种情况是因为 dynamic 关键字会导致生成的代码结构(由 C# 编译器生成(发生大量更改。

您可以使用 .NET 反射器等工具观察这一点(我建议为 C# 推理选择"无",以便您可以真正看到所有生成的内容(。基本上,每次访问dynamic对象时,生成的代码都会至少添加一个if大小写。这些ifs可能会导致重要的代码路径更改。

例如,这个简单的 C# 代码

    static void MyFoo(dynamic dyn)
    {
        if (dyn == null)
            return;
        var x = dyn;
    }

生成为:

private static void MyFoo([Dynamic] object dyn)
{
    object obj2;
    CSharpArgumentInfo[] infoArray;
    bool flag;
    if (<MyFoo>o__SiteContainer0.<>p__Site1 != null)
    {
        goto Label_0038;
    }
    <MyFoo>o__SiteContainer0.<>p__Site1 = CallSite<Func<CallSite, object, bool>>.Create(Binder.UnaryOperation(0, 0x53, typeof(Program), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(0, null) }));
Label_0038:
    if (<MyFoo>o__SiteContainer0.<>p__Site2 != null)
    {
        goto Label_0088;
    }
    <MyFoo>o__SiteContainer0.<>p__Site2 = CallSite<Func<CallSite, object, object, object>>.Create(Binder.BinaryOperation(0, 13, typeof(Program), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(0, null), CSharpArgumentInfo.Create(2, null) }));
Label_0088:
    if ((<MyFoo>o__SiteContainer0.<>p__Site1.Target(<MyFoo>o__SiteContainer0.<>p__Site1, <MyFoo>o__SiteContainer0.<>p__Site2.Target(<MyFoo>o__SiteContainer0.<>p__Site2, dyn, null)) == 0) == null)
    {
        goto Label_00AE;
    }
    obj2 = dyn;
Label_00AE:
    return;
}

让我们再举一个简单的例子。此代码:

    static void MyFoo1(dynamic dyn)
    {
        long value;
        if (long.TryParse(dyn, out value))
            return;
        var x = value;
    }

编译正常。这个

    static void MyFoo2(dynamic dyn)
    {
        long value;
        if (true || long.TryParse(dyn, out value))
            return;
        var x = value;
    }

不。如果您查看生成的代码(将值设置为 0 以确保其编译(,它将只包含一些额外的ifsgoto,这些会极大地改变整个代码路径。我不认为这是一个错误,也许更多的是dynamic关键字的限制(它有很多,例如:C#中动态类型的限制(

它只是为了|| 运算符用法(逻辑 OR(,

编译器从左到右检查条件,因此如果第一个操作数的计算结果为 TRUE,则不需要计算第二个操作数,因此 Beta 可能不会获得值,编译器会抛出以下警告:

"Use of unassigned local variable 'beta'"

您的代码:

    if (
        !decimal.TryParse(alice.Alpha, out alpha)  // If evaluated as TRUE...
        ||                                         // 
        !long.TryParse(alice.Beta, out beta))      // ....so Will not evaluated this.
{

请注意,如果您更改操作数顺序,您将收到以下消息:

"Use of unassigned local variable 'alpha'"

声明时将默认值分配给 beta 和 alpha

decimal alpha=default(decimal);
long beta =default(long); 

好吧,这是我的尝试:

alice的类型为"Alice"时,编译器在编译时知道将调用哪个TryParse方法,并且它知道该方法具有"out"参数,这意味着它将由该方法初始化。因此,如果alice的类型为 Alice,则decimal.TryParse将返回 false,在这种情况下,Bob不会被构造(因为该方法返回 alice(,或者decimal.TryParse将返回 true,在这种情况下,将调用long.TryParse,并且编译器知道这将为beta赋值(因为 out 关键字(。

现在,如果alice是类型 dynamic ,编译器不关心 TryParse 方法的签名,并在运行时推迟方法的解析(基于alice的实际类型(。这意味着他不能对beta是否会通过调用TryParse()来分配值的事实做出任何假设,因此无法确定beta在用于 Bob 的构造函数时是否会被赋值。

我的2美分...

看起来我找到了答案。 这似乎是一个编译表达式树问题。 在编译时,它会检查是否为变量分配了值。 当您对 alpha 和 beta 变量的值赋值发生在 if 条件中时,它假定只计算第一个表达式。

这一直在评估......但由于它是一个"或"条件。当第一个表达式为真时,它会从 if 中断开而不计算第二个表达式。

!十进制。TryParse(alice.阿尔法,出阿尔法(

如果您想自己查看此内容,请按如下所示顺序放置 TryParse 语句。 那么这个错误就不会出现。.

!十进制。TryParse(alice.阿尔法,出阿尔法(!长。TryParse(alice.阿尔法,出贝塔(;

在这里保证一些值将被分配给 beta。我希望我说得有道理。

我认为这种情况正在发生,因为在当前代码中,包含dynamic,编译器用于确定运行时是否实际调用TryParse的算法是不确定的。 我认为您看到的解决方案是通过使那些TryParse调用的评估变得确定性(即它们将被评估(来避免这个问题。 当这些TryParse调用使用 out 参数进行评估时,它们最坏的情况是接收 default(decimal)default(long) 的值。 这样,当您稍后分配它时,该值不是未分配的。这是基于生成的代码中出现的goto跳跃的假设

Int32.TryParse 文档稍微支持这一点

此方法返回时,如果转换成功,则包含与 s 中包含的数字等效的 32 位有符号整数值,如果转换失败,则包含零。如果 s 参数为 null 或 String.Empty,格式不正确,或者表示小于 MinValue 或大于 MaxValue 的数字,则转换将失败。此参数以未初始化的方式传递

这绝对是基于西蒙·穆里尔(Simon Mourier(在他的回答中的发现。

所以我知道你想知道为什么会这样。就像错误所说的那样,编译器发现了一些无需初始化即可使用变量 beta 的情况。正如文档所说:

请注意,当编译器遇到可能导致使用未赋值变量的构造时,将生成此错误,即使您的特定代码没有使用。

编译器可能不够聪明,无法意识到如果代码没有在"if"语句中返回,变量"beta"将始终被初始化。这种情况对于编译器来说似乎很简单,但我怀疑这可能是编译器中实现的一般功能。

似乎即使是杰出的人也很难决定这是一个编译器错误还是设计使然,这可以通过查看Visual Basic和C#语言团队项目经理Anthony D. Green的两个答案来看到。此错误报告基本上具有与此处描述相同的问题,即使用可能导致CS0165的动态值,而使用另一种类型可以解决它。还将 &&操作拆分为两个 if 语句,而是解决了该错误报告中的问题。

格林先生的回答与这个问题有联系,这似乎是这个问题的重复。这一切背后的奥秘在这个答案中得到了很好的解释,格林先生在上面链接的错误报告中也得到了很好的解释。问题在于动态类型的值可能有一个重载的true运算符,该运算符由||调用,if语句和自己的实现可以简单地做任何事情,因此编译器对此持谨慎态度。链接的信息给出了更好的解释:)

long beta;

语句中实际发生的是此变量的声明,而不是该变量的实例化。 执行此语句时,运行时会为此变量保留内存空间作为 long,但它不会将值实例化为任何内容。 因此,通过out参数,您尝试将对象分配给未实例化的对象,除了默认值之外,您还可以通过简单地实例化对象而不是简单地保留所需的内存来解决此问题

long beta = new long();