三元条件下的隐式转换问题

可能重复:
条件运算符不能隐式转换?
为什么null需要显式类型转换?

我有一个搜索,并没有找到一个很好的解释为什么以下发生。
我有两个具有共同接口的类,我尝试使用三元运算符初始化此接口类型的实例,如下所示,但无法编译错误“无法确定条件表达式的类型,因为之间没有隐式转换’xxx.Class1’和’xxx.Class2’:

public ConsoleLogger : ILogger { .... } public SuppressLogger : ILogger { .... } static void Main(string[] args) { ..... // The following creates the compile error ILogger logger = suppressLogging ? new SuppressLogger() : new ConsoleLogger(); } 

如果我明确地将第一个conditioin强制转换为我的界面,这是有效的:

  ILogger logger = suppressLogging ? ((ILogger)new SuppressLogger()) : new ConsoleLogger(); 

显然我总能做到这一点:

  ILogger logger; if (suppressLogging) { logger = new SuppressLogger(); } else { logger = new ConsoleLogger(); } 

替代方案很好,但我不能完全理解为什么第一个选项因隐式转换错误而失败,因为在我看来,这两个类都是ILogger类型,我不是真的想要进行转换(隐式或显式) )。 我敢肯定这可能是一个静态语言编译问题,但我想了解发生了什么。

这是C#的两个特征汇合的结果。

首先,C#永远不会“魔法化”为你的类型。 如果C#必须从给定的一组类型中确定“最佳”类型,它总是选择您给出的类型之一。 它永远不会说“你给我的类型都不是最好的类型;因为你给我的选择都很糟糕,我会选择一些你没有给我选择的随机事物。”

第二个是C#从外的原因。 我们没有说“哦,我看到你试图将条件运算符结果分配给ILogger;让我确保两个分支都有效。” 相反的情况发生了:C#说“让我确定两个分支返回的最佳类型,并validation最佳类型是否可以转换为目标类型。”

第二条规则是明智的,因为目标类型可能是我们试图确定的。 当你说D d = b ? c : a; D d = b ? c : a; 很清楚目标类型是什么。 但是假设你是在调用M(b?c:a) ? 可能有一百个不同的M重载,每个具有不同类型的forms参数! 我们必须确定参数的类型是什么,然后丢弃不适用的M的重载,因为参数类型与forms参数类型不兼容; 我们不会走另一条路。

考虑如果我们走另一条路会发生什么:

 M1( b1 ? M2( b3 ? M4( ) : M5 ( ) ) : M6 ( b7 ? M8() : M9() ) ); 

假设M1,M2和M6各有一百个重载。 你是做什么? 你说,好吧,如果这是M1(Foo)那么M2(…)和M6(……)必须都可以转换为Foo。 是吗? 让我们来看看。 什么是M2的超载? 有一百种可能性。 让我们看看它们是否可以从M4和M5的返回类型转换……好吧,我们已经尝试了所有这些,所以我们找到了一个有效的M2。 那么M6呢? 如果我们找到的“最佳”M2与“最佳”M6不兼容怎么办? 我们是否应该回溯并继续重新尝试所有100 x 100种可能性,直到找到兼容的对? 问题变得越来越糟。

我们以这种方式对lambdas 进行推理,因此涉及lambda的重载决策在C#中至少是NP-HARD。 那是不好的; 我们宁愿不为编译器添加更多NP-HARD问题来解决。

您也可以在该语言的其他位置查看第一条规则。 例如,如果你说: ILogger[] loggers = new[] { consoleLogger, suppressLogger }; 你会得到类似的错误; 推断的数组元素类型必须是给定的类型表达式的最佳类型 。 如果没有最佳类型可以确定,我们不会尝试找到您没有给我们的类型。

类型推断也是如此。 如果你说:

 void M(T t1, T t2) { ... } ... M(consoleLogger, suppressLogger); 

那么T不会被推断为ILogger; 这将是一个错误。 T被推断为提供的参数类型中的最佳类型,并且其中没有最佳类型。

有关此设计决策如何影响条件运算符行为的更多详细信息,请参阅我关于该主题的系列文章 。

如果您对为什么“从外到内”工作的重载分辨率为NP-HARD感兴趣,请参阅此文章 。

你可以这样做:

 ILogger logger = suppressLogging ? (ILogger)(new SuppressLogger()) : (ILogger)(new ConsoleLogger()); 

当你有一个像condition ? a : b的表达condition ? a : b condition ? a : b ,必须有一个隐式转换,从a的类型到b的类型,或者反过来,否则编译器无法确定表达式的类型。 在您的情况下, SuppressLoggerConsoleLogger之间没有转换……

(有关详细信息,请参阅C#4语言规范中的第7.14节)

问题是语句的右侧是在不查看分配给它的变量类型的情况下进行评估的。

编译器无法查看

 suppressLogging ? new SuppressLogger() : new ConsoleLogger(); 

并确定返回类型应该是什么,因为它们之间没有隐式转换。 它不寻找共同的祖先,即使它确实如此,它怎么知道选择哪一个。

每当您将一种类型的变量更改为另一种类型的变量时,这就是转换。 将类的实例分配给除该类之外的任何类型的变量都需要转换。 这个说法:

 ILogger a = new ConsoleLogger(); 

将执行从ConsoleLogger到ILogger的隐式转换,这是合法的,因为ConsoleLogger实现了ILogger。 同样,这将工作:

 ILogger a = new ConsoleLogger(); ILogger b = suppress ? new SuppressLogger() : a; 

因为SuppressLogger和ILogger之间存在隐式转换。 但是,这不起作用:

 ILogger c = suppress ? new SuppressLogger() : new ConsoleLogger(); 

因为第三级操作员只会非常努力地找出你想要的结果类型。 它基本上是这样的:

  1. 如果操作数2和3的类型相同,则第三个运算符返回该类型并跳过其余的这些步骤。
  2. 如果操作数2可以隐式转换为与操作数3相同的类型,则它可能返回该类型。
  3. 如果操作数3可以隐式转换为与操作数2相同的类型,则可能返回该类型。
  4. 如果#2和#3都为真,或者#2或#3都不为真,则会产生错误。
  5. 否则,它返回#2或#3中的任何一个的类型。

特别是,它不会开始搜索它所知道的关于寻找“最小公分母”类型的所有类型,例如共同的接口。 此外,对第三运算符进行求值,并确定其返回类型,与您将结果存储到的变量类型无关 。 这是一个两步过程:

  1. 确定?:表达式的类型并计算它。
  2. 将#1的结果存储到变量中,根据需要执行任何隐式转换。

如果这是你需要的,那么对你的一个或两个操作数进行类型转换是执行此操作的正确方法。