C# 三元运算符在不应该评估的时候进行评估

C# Ternary Operator evaluating when it shouldn't

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

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();
    }

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

但我的问题是,为什么明明只应该评估左侧,却完全评估右侧? (文档指出 "Only one of the two expressions is evaluated.")

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

编辑:这与 "Why do I need to add a cast before it will compile" 类型的三元运算符问题不同。这里的要点是不需要转换就可以编译 - 只是为了让它正常工作。

问题不在于三元求值的正确参数,显然不是(尝试一下,在隐式运算符中抛出不同的异常,代码仍会抛出 InvalidOperationException 因为 ((Nullable<byte>)(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 实际上是 LookupCode<T> 类型(由于类型推断在三元运算符中的工作方式),并且需要使用隐式运算符转换为字符串;这就是给你例外的原因。

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<object>))不是 string 类型,所以调用隐式运算符。隐式运算符抛出无效运算异常

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

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

你仍然得到一个无效的操作异常,第三个操作数没有被评估。这在生成的 IL 中当然非常明显:两个操作数是两个独立的分支;他们两个都没有办法被处决。第一个分支还有另一个明显的痛苦:

ldnull      
call        LookupCode`1.op_Implicit

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

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

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

The second and third operands of the ?: operator control the type of the conditional expression.

Let X and Y be the types of the second and third operands. Then,

  • If X and Y are the same type, then this is the type of the conditional expression.
  • Otherwise, if an implicit conversion (§13.1) exists from X to Y, but not from Y to X, then Y is the type of the conditional expression.
  • Otherwise, if an implicit conversion (§13.1) exists from Y to X, but not from X to Y, then X is the type of the conditional expression.
  • Otherwise, no expression type can be determined, and a compile-time error occurs.

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

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