Resolución de sobrecarga peculiar con while (true)

Estaba implementando sobrecargas sincronizadas / asíncronas cuando me encontré con esta situación peculiar:

Cuando tengo una expresión lambda normal sin parámetros o un valor de retorno, va a la sobrecarga Run con el parámetro Action , que es predecible. Pero cuando esa lambda tiene un while (true) en ella va a la sobrecarga con el parámetro de Func .

 public void Test() { Run(() => { var name = "bar"; }); Run(() => { while (true) ; }); } void Run(Action action) { Console.WriteLine("action"); } void Run(Func func) // Same behavior with Func of any type. { Console.WriteLine("func"); } 

Salida:

acción
Func

Entonces, ¿cómo puede ser eso? ¿Hay alguna razón para ello?

Entonces, para empezar, la primera expresión solo puede llamar a la primera sobrecarga. No es una expresión válida para Func porque hay una ruta de código que devuelve un valor no válido ( void lugar de Task ).

() => while(true) es en realidad un método válido para cualquiera de las firmas. (Esto, junto con implementaciones como () => throw new Expression(); son cuerpos válidos de métodos que devuelven cualquier tipo posible, incluido el void , un punto de trivia interesante, y por qué los métodos autogenerados de un IDE típicamente solo arrojan un excepción; comstackrá independientemente de la firma del método.) Un método que realiza bucles infinitamente es un método en el que no hay rutas de código que no devuelven el valor correcto (y eso es cierto si el “valor correcto” es void , Task o literalmente cualquier otra cosa). Esto es, por supuesto, porque nunca devuelve un valor, y lo hace de una manera que el comstackdor puede probar. (Si lo hizo de una manera que el comstackdor no pudo probar, ya que no ha resuelto el problema de detención después de todo, entonces estaríamos en el mismo barco que A ).

Por lo tanto, para nuestro ciclo infinito, que es mejor, dado que ambas sobrecargas son aplicables. Esto nos lleva a nuestra sección de mejora de las especificaciones de C #.

Si vamos a la sección 7.4.3.3, viñeta 4, vemos:

Si E es una función anónima, T1 y T2 son tipos de delegates o tipos de árbol de expresiones con listas de parámetros idénticas, y existe un tipo de retorno inferido X para E en el contexto de esa lista de parámetros (§7.4.2.11):

[…]

si T1 tiene un tipo de retorno Y, y T2 no tiene retorno, entonces C1 es la mejor conversión.

Por lo tanto, al convertir desde un delegado anónimo, que es lo que estamos haciendo, preferirá la conversión que devuelve un valor sobre uno que es void , por lo que elige Func .