【问题标题】:C# Ternary Operator evaluating when it shouldn't [closed]C# 三元运算符评估何时不应该 [关闭]
【发布时间】:2017-03-27 18:43:22
【问题描述】:

今天这段代码让我大吃一惊:

clientFile.ReviewMonth == null ? null : MonthNames.AllValues[clientFile.ReviewMonth.Value]

clientFile.Review 月份是一个字节?在失败的情况下,它的值为空。 预期的结果类型是字符串。

异常在这段代码中

    public static implicit operator string(LookupCode<T> code)
    {
        if (code != null) return code.Description;

        throw new InvalidOperationException();
    }

求值的右侧正在求值,然后隐式转换为字符串。

但我的问题是,当显然只应评估左侧时,为什么要评估右侧? (文档指出“仅计算两个表达式中的一个。”)

顺便说一句,解决方案是将 null 转换为字符串 - 这可行,但 Resharper 告诉我转换是多余的(我同意)

编辑:这与“为什么我需要在编译之前添加强制转换”类型的三元运算符问题不同。这里的要点是不需要强制转换就可以编译 - 只是为了让它正常工作。

【问题讨论】:

标签: c# conditional-operator


【解决方案1】:

问题不在于三元求值的正确参数,显然不是(试试看,在隐式运算符中抛出不同的异常,代码仍然会抛出 InvalidOperationException 因为((Nullable&lt;byte&gt;)(null)).Value

所以问题在于隐式转换何时发生。看来:

clientFile.ReviewMonth == null ? null : MonthNames.AllValues[clientFile.ReviewMonth.Value]

等价于

(string)(clientFile.ReviewMonth == null ? (Nullable<LookupCode<byte>>)null : (Nullable<LookupCode<byte>>)MonthNames.AllValues[clientFile.ReviewMonth.Value]);

而不是

clientFile.ReviewMonth == null ? (string)null : (string)MonthNames.AllValues[clientFile.ReviewMonth.Value]);

所以 resharper 在这里是完全错误的。

【讨论】:

  • 由于null 不能隐式转换为bool,我不明白您如何声称该表达式等同于(string)(null ? ...)null 文字在运算符左侧是不合法的。
【解决方案2】:

您忘记了隐式运算符是在编译时确定的。这意味着您拥有的null 实际上是LookupCode&lt;T&gt; 类型(由于类型推断在三元运算符中的工作方式),并且需要使用隐式运算符将其转换为字符串;这就是你的例外。

void Main()
{
  byte? reviewMonth = null;

  string result = reviewMonth == null 
                  ? null // Exception here, though it's not easy to tell
                  : new LookupCode<object> { Description = "Hi!" };

  result.Dump();
}

class LookupCode<T>
{
  public string Description { get; set; }

  public static implicit operator string(LookupCode<T> code)
  {
      if (code != null) return code.Description;

      throw new InvalidOperationException();
  }
}

无效操作不会发生在第三个操作数上,而是发生在第二个操作数上 - null(实际上是 default(LookupCode&lt;object&gt;))不是 string 类型,因此调用了隐式运算符。隐式操作符抛出无效操作异常。

如果您使用稍作修改的代码,您可以很容易地看出这是真的:

string result = reviewMonth == null 
                ? default(LookupCode<object>) 
                : "Does this get evaluated?".Dump();

你仍然得到一个无效的操作异常,并且第三个操作数没有被计算。这在生成的 IL 中当然非常明显:两个操作数是两个独立的分支;他们两个都没有办法被处决。而第一个分支还有另一个令人痛苦的显而易见的事情:

ldnull      
call        LookupCode`1.op_Implicit

它甚至没有隐藏在任何地方:)

解决方案很简单:使用显式类型的nulldefault(string)。 R# 完全是错误的 - 在这种情况下,(string)nullnull 不同,并且 R# 在这种情况下具有错误的类型推断。

当然,这在 C# 规范(14.13 - 条件运算符)中都有描述:

?: 运算符的第二个和第三个操作数控制条件表达式的类型。

让 X 和 Y 是第二个和第三个操作数的类型。那么,

  • 如果 X 和 Y 是同一类型,则这是条件表达式的类型。
  • 否则,如果存在从 X 到 Y 的隐式转换(第 13.1 节),而不是从 Y 到 X,则 Y 是 条件表达式。
  • 否则,如果存在从 Y 到 X,但不存在从 X 到 Y 的隐式转换(第 13.1 节),则 X 是 条件表达式。
  • 否则无法确定表达式类型,并出现编译时错误。

在您的情况下,存在从LookupCode&lt;T&gt;string 的隐式转换,但反之亦然,因此LookupCode&lt;T&gt; 类型优于string。有趣的是,由于这一切都是在编译时完成的,因此赋值的 LHS 实际上有所作为:

string result = ... // Fails
var result = ... // Works fine, var is of type LookupCode<object>

【讨论】:

  • 谢谢 - 这是有道理的。我也认为 Tamas 描述的是同一件事,但他提到试图在隐式运算符中抛出不同的异常是不正确的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-07-03
  • 2016-02-12
  • 1970-01-01
  • 2015-02-15
  • 2016-04-29
  • 2021-11-04
  • 1970-01-01
相关资源
最近更新 更多