【问题标题】:Bug with detection of unassigned local variables (when dynamic variables affect code flow prediction)检测未分配的局部变量的错误(当动态变量影响代码流预测时)
【发布时间】:2018-09-27 16:56:35
【问题描述】:

The Documentation 表示 out 参数在发送到函数之前不需要初始化(仅声明)。但是,这段代码:

class Program
{
    static void Main()
    {
        dynamic p = "";
        string s;
        if (p != null && T(out s))
            System.Console.WriteLine(s);
    }

    static bool T(out string s)
    {
        s = "";
        return true;
    }
}

给出 build 错误:

使用未赋值的局部变量's'

仅当pdynamic。如果p 输入为stringobject,则不会产生错误。

方法 T 需要在返回之前设置变量,所以这个错误对我来说似乎是废话(请注意,即使使用短路 &&,第二个语句也必须执行才能"then" 块执行)。

注意:你也可以下载这个repro repo进行复现。

那么,这是一个合法的错误吗(我使用的是 C# 7.0)?我该如何处理?

【问题讨论】:

  • 您是否在 else 块中使用了这些变量?因为我没有得到提供的代码的错误,我唯一的解释是TryGetURLParams()没有被执行,因为page != null是假的。
  • 我已经在几个不同版本的 C# 中尝试了您的代码,但无法重现该问题。请提供一个小完整程序来演示问题以及您正在使用的编译器/Visual Studio/等的确切版本号。
  • 我怀疑@clcto 是正确的;你没有编译你展示给我们的代码。如果在 else 中使用变量,您会得到这样的错误,因为page 可能为空,所以它们可能不会被分配。
  • 仅当pagedynamic 时才会出现问题,您在问题中不方便地省略了它。您发布的复制品揭示了这一事实;请在问题中而不是在外部网站中发布repros。
  • 我已经解决了这个问题并将投票重新打开它。

标签: c#


【解决方案1】:

更新:这个问题是the subject of my blog in November 2018。感谢您提出有趣的问题!

文档暗示out 参数在发送到方法之前不需要初始化(仅声明)。

没错。此外,当调用返回时,传递给out 参数的变量肯定会被分配,因为正如您所注意到的:

方法 T 需要在返回之前设置变量,所以这个错误对我来说似乎是废话

看起来是这样,不是吗?外表可能具有欺骗性!

请注意,即使&& 短路,也必须执行第二个表达式才能执行if 的“结果”块。

令人惊讶的是,这是错误的。 即使对T 的调用没有执行,也有办法执行结果。这样做需要我们严重滥用 C# 的规则,但我们可以 ,所以让我们开始吧!

代替

    dynamic p = "";
    string s;
    if (p != null && T(out s))
        System.Console.WriteLine(s);

我们会做的

    P p = new P();
    if (p != null && T())
        System.Console.WriteLine("in the consequence");

并给出class P 的定义,它会导致该程序运行结果,但运行对T 的调用。

我们要做的第一件事是把p != null变成一个方法调用而不是空检查,并且那个方法不能返回bool

class P
{
    public static C operator ==(P p1, P p2)
    {
        System.Console.WriteLine("P ==");
        return new C();
    }
    public static C operator !=(P p1, P p2)
    {
        System.Console.WriteLine("P !=");
        return new C();
    }
}

我们需要在 C# 中同时重载 ==!=。覆盖 EqualsGetHashCode 是个好主意,但不是必需的,而且这个程序中没有什么是好主意,所以我们将跳过它。

好的,所以我们现在有了if (something_of_type_C && T()),并且由于C 不是bool,我们需要覆盖&& 运算符。但是 C# 不允许您直接覆盖 && 运算符。让我们跑题,谈谈&& 的语义。对于布尔返回函数ABbool result = A() && B(); 的语义是:

bool a = A();
bool c;
if (a == false) // interesting operation
  c = a;
else
{
  bool b = B(); 
  c = a & b;    // interesting operation
}
bool r = c;

所以我们生成三个临时变量abc,我们评估左侧A(),我们检查a是否为假。如果是,我们使用它的值。如果不是,我们计算B(),然后计算a & b

该工作流程中特定于布尔类型的仅有的两个操作是检查虚假非短路&,所以*那些是在用户定义的&& 中重载的操作。 C# 要求您重载三个操作:用户定义的&,用户定义的“我是真的吗?”和用户定义的“我是假的吗?”。 (如==!=,最后两个必须成对定义。)

现在,明智的人会写operator trueoperator false,这样它们总是会返回相反的内容。我们今天不是理智的人:

class C
{
    public static bool operator true(C c)
    {
        System.Console.WriteLine("C operator true");
        return true;
    }

    public static bool operator false(C c)
    {
        System.Console.WriteLine("C operator false");
        return true; // Oops
    }

    public static C operator &(C a, C b)
    {
        System.Console.WriteLine("C operator &");
        return a;
    }
}

请注意,我们还要求用户定义的 & 采用两个 Cs 并返回一个 C,它确实如此。

好吧,所以,记得我们有

if (p != null && T())

p != null 的类型为C。所以我们现在必须将其生成为:

C a = p != null; // Call to P.operator_!=
C c;
bool is_false = a is logically false; // call to C.operator_false
if (is_false) 
  c = a;
else
{
  bool b = T();
  c = a & b; // Call to C.operator_&
}

但是现在我们遇到了一个问题。 operator & 接受两个 Cs 并返回一个 C,但我们有一个从 T 返回的 bool。我们需要一个C。没问题,我们将添加一个从boolC 的隐式用户定义转换:

public static implicit operator C(bool b)
{
    System.Console.WriteLine("C implicit conversion from bool");
    return new C();
}

好的,所以我们现在的逻辑是:

C a = p != null; // Call to P.operator_!=
C c;
bool is_false = C.operator_false(a);
if (is_false)
  c = a;
else
{
  bool t = T(); 
  C b = t; // call to C.operator_implicit_C(bool)
  c = a & b; // Call to C.operator_&
}

记住我们的目标是:

if (c)
  System.Console.WriteLine("in the consequence");

我们如何计算这个? C# 的原因是,如果您在C 上有operator true,那么您应该能够通过简单地调用operator trueif 条件下使用它。所以完成它,最终我们有了语义:

C a = p != null; // Call to P.operator_!=
C c;
bool is_false = C.operator_false(a);
if (is_false)
  c = a;
else
{
  bool t = T(); 
  C b = t; // call to C.operator_implicit_C(bool)
  c = a & b; // Call to C.operator_&
}
bool is_true = C.operator_true(c);
if (is_true) …

但是正如我们在这个疯狂的例子中看到的那样,我们可以输入if 的结果而不调用T 没问题只要operator falseoperator true 都返回true。当我们运行程序时,我们得到:

P !=
C operator false
C operator true
in the consequence

一个明智的人永远不会编写代码,其中C 被认为是同时为真和假,但像我今天这样不明智的人可以,编译器知道因为我们将编译器设计为正确无论程序是否合理。

这就解释了为什么if (p != null && T(out s))s 可以在结果中取消分配。如果pdynamic,那么编译器的原因是“p 可能是运行时这些疯狂的类型之一,在这种情况下,我们不再使用bool 操作数,因此可能不会分配s” .

故事的寓意是:dynamic 使编译器对可能发生的事情非常保守;它必须假设最坏的情况。在这种特殊情况下,它必须假设p != null 可能不是 是空引用检查并且可能不是bool,并且operator true 和@987654404 @ 可能都返回 true

那么,这是一个合法的错误吗(我使用的是 C# 7.0)?

编译器的分析是正确的——相信我,这不是一个容易编写或测试的逻辑。

您的代码有错误;修复它。

我应该如何处理?

如果您想对dynamic 进行空引用检查,最好的选择是:如果这样做会造成伤害,请不要这样做。

丢弃dynamic 并返回object,然后然后进行引用相等检查:if (((object)p) == null && …

或者,另一个不错的解决方案是使其非常明确: if (object.ReferenceEquals((object)p, null) && …

这些是我的首选解决方案。更糟糕的解决方案是将其分解:

if (p != null)
  if (T(out string s))
     consequence

现在即使在最坏的情况下也不会调用operator &。请注意,尽管在这种情况下,我们仍然可能处于p != null 为真而p 为空的情况,因为没有什么可以阻止任何人重载!= 以始终返回真,而不管其操作数如何。

【讨论】:

  • 这再次证明了运算符重载通常是一个不应该存在的坏特性。是否存在通过使用运算符重载可以编写更好、更易读、更易于维护的代码的情况?
  • @LucaCremonesi,听起来像是软件工程的问题......哦等等,已经问过了:softwareengineering.stackexchange.com/a/136531/74174
  • @LucaCremonesi:如果您的问题仅限于用户定义的重载运算符,那么我向您提出重载运算符有许多有效的用途。例如,BigInteger 似乎是拥有+ 运算符的绝佳候选者,如果BigInteger 的开发人员无法提供这样的运算符,那将是不幸的。我对用户定义的运算符重载的问题是,C++ 的设计者做出了一个可怕的决定,第一次介绍大多数开发人员必须在流上使用 <<>> 运算符的功能。
  • @LucaCremonesi:该决定基本上传达了“运算符重载的目的是为现有运算符提供“可爱”的新含义,这些运算符完全脱离了您可能已经拥有的任何现有语义”。所以人们会产生这样的想法,哦,C++中的“正确的做法”是说“让客户加产品等于采购订单”或一些与加法语义无关的废话。不过,该功能可能被滥用的事实并不是用户定义的运算符重载所独有的。
  • @NH.: 啊,我想你可能错过了我的意思。我们赋予dynamic 的基本设计特征是dynamic 类型的表达式的操作在运行时具有完全相同的语义,就好像编译器在编译时正确地知道了这些类型。这对编译器施加了一个约束,即如果包含dynamic 的程序对于任何类型 来说都是非法的,那么它必须在编译时或运行时给出错误。这是这样一个案例,尽管是一个非常不寻常的案例。因为存在这样的类型 P,dynamic 必须表现得像它,不管它到底是什么!
猜你喜欢
  • 2010-11-17
  • 2016-10-09
  • 2016-10-04
  • 1970-01-01
  • 2017-06-17
  • 1970-01-01
  • 2014-05-13
相关资源
最近更新 更多