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
它甚至没有隐藏在任何地方:)
解决方案很简单:使用明确类型化的 null
、default(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>
今天这段代码让我大吃一惊:
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
它甚至没有隐藏在任何地方:)
解决方案很简单:使用明确类型化的 null
、default(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>