una - Problemas de resolución de sobrecarga del método C#en Visual Studio 2013
metodos y funciones en c# (2)
Además de la respuesta de @Daisy Shipton, me gustaría agregar que también se puede observar el mismo comportamiento en el siguiente caso:
var sequence = Observable.Create<int>(
async (observer, token) =>
{
throw new NotImplementedException();
});
básicamente por la misma razón: el compilador ve que la función lambda nunca regresa, por lo que cualquier tipo de retorno coincidirá, lo que a su vez hace que la lambda coincida con cualquiera de las sobrecargas de Observable.Create
.
Y, finalmente, un ejemplo de solución simple: puede convertir la lambda al tipo de firma deseado para sugerir al compilador la sobrecarga de Rx que elija.
var sequence =
Observable.Create<int>(
(Func<IObserver<int>, CancellationToken, Task>)(async (observer, token) =>
{
throw new NotImplementedException();
})
);
Tener estos tres métodos disponibles en la biblioteca Rx.NET
public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, CancellationToken, Task> subscribeAsync) {...}
public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, CancellationToken, Task<IDisposable>> subscribeAsync) {...}
public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, CancellationToken, Task<Action>> subscribeAsync) {...}
Escribo el siguiente código de muestra en MSVS 2013 :
var sequence =
Observable.Create<int>( async ( observer, token ) =>
{
while ( true )
{
token.ThrowIfCancellationRequested();
await Task.Delay( 100, token );
observer.OnNext( 0 );
}
} );
Esto no se compila debido a sobrecargas ambiguas. La salida exacta del compilador es:
Error 1 The call is ambiguous between the following methods or properties:
''System.Reactive.Linq.Observable.Create<int>(System.Func<System.IObserver<int>,System.Threading.CancellationToken,System.Threading.Tasks.Task<System.Action>>)''
and
''System.Reactive.Linq.Observable.Create<int>(System.Func<System.IObserver<int>,System.Threading.CancellationToken,System.Threading.Tasks.Task>)''
Sin embargo, tan pronto como reemplace while( true )
con while( false )
o con var condition = true; while( condition )...
var condition = true; while( condition )...
var sequence =
Observable.Create<int>( async ( observer, token ) =>
{
while ( false ) // It''s the only difference
{
token.ThrowIfCancellationRequested();
await Task.Delay( 100, token );
observer.OnNext( 0 );
}
} );
el error desaparece y la llamada al método se resuelve a esto:
public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, CancellationToken, Task> subscribeAsync) {...}
¿Que esta pasando ahí?
Esta es una divertida :) Hay múltiples aspectos a esto. Para empezar, simplifiquémoslo significativamente eliminando Rx y la resolución de sobrecarga real de la imagen. La resolución de sobrecarga se maneja al final de la respuesta.
Función anónima para delegar conversiones y accesibilidad.
La diferencia aquí es si el punto final de la expresión lambda es alcanzable. Si es así, entonces esa expresión lambda no devuelve nada, y la expresión lambda solo se puede convertir a una función Func<Task>
. Si no se puede alcanzar el punto final de la expresión lambda, entonces se puede convertir a cualquier Func<Task<T>>
.
La forma de la instrucción while
hace una diferencia debido a esta parte de la especificación C #. (Esto es del estándar ECMA C # 5; otras versiones pueden tener una redacción ligeramente diferente para el mismo concepto).
El punto final de una instrucción
while
es alcanzable si al menos uno de los siguientes es verdadero:
- La instrucción
while
contiene una instrucción break accesible que sale de la instrucción while.- La instrucción
while
es accesible y la expresión booleana no tiene el valor constantetrue
.
Cuando tiene un bucle while (true)
sin declaraciones de break
, ninguna viñeta es verdadera, por lo que el punto final de la declaración while
(y, por lo tanto, la expresión lambda en su caso) no es alcanzable.
Aquí hay un ejemplo corto pero completo sin ningún Rx involucrado:
using System;
using System.Threading.Tasks;
public class Test
{
static void Main()
{
// Valid
Func<Task> t1 = async () => { while(true); };
// Valid: end of lambda is unreachable, so it''s fine to say
// it''ll return an int when it gets to that end point.
Func<Task<int>> t2 = async () => { while(true); };
// Valid
Func<Task> t3 = async () => { while(false); };
// Invalid
Func<Task<int>> t4 = async () => { while(false); };
}
}
Podemos simplificar aún más eliminando async de la ecuación. Si tenemos una expresión lambda sin parámetros sincrónica sin declaraciones de retorno, eso siempre se puede convertir a Action
, pero también se puede convertir a Func<T>
para cualquier T
si no se puede alcanzar el final de la expresión lambda. Un ligero cambio en el código anterior:
using System;
public class Test
{
static void Main()
{
// Valid
Action t1 = () => { while(true); };
// Valid: end of lambda is unreachable, so it''s fine to say
// it''ll return an int when it gets to that end point.
Func<int> t2 = () => { while(true); };
// Valid
Action t3 = () => { while(false); };
// Invalid
Func<int> t4 = () => { while(false); };
}
}
Podemos ver esto de una manera ligeramente diferente al eliminar delegados y expresiones lambda de la mezcla. Considere estos métodos:
void Method1()
{
while (true);
}
// Valid: end point is unreachable
int Method2()
{
while (true);
}
void Method3()
{
while (false);
}
// Invalid: end point is reachable
int Method4()
{
while (false);
}
Aunque el método de error para el Método 4 es "no todas las rutas de código devuelven un valor", la forma en que esto se detecta es "se puede alcanzar el final del método". Ahora imagine que los cuerpos de los métodos son expresiones lambda que tratan de satisfacer a un delegado con la misma firma que la firma del método, y volvemos al segundo ejemplo ...
Diversión con resolución de sobrecarga.
Como señaló Panagiotis Kanavos, el error original acerca de la resolución de sobrecarga no se puede reproducir en Visual Studio 2017. Entonces, ¿qué está pasando? De nuevo, no necesitamos que Rx esté involucrado para probar esto. Pero podemos ver un comportamiento muy extraño. Considera esto:
using System;
using System.Threading.Tasks;
class Program
{
static void Foo(Func<Task> func) => Console.WriteLine("Foo1");
static void Foo(Func<Task<int>> func) => Console.WriteLine("Foo2");
static void Bar(Action action) => Console.WriteLine("Bar1");
static void Bar(Func<int> action) => Console.WriteLine("Bar2");
static void Main(string[] args)
{
Foo(async () => { while (true); });
Bar(() => { while (true) ; });
}
}
Eso emite una advertencia (sin esperar operadores) pero se compila con el compilador C # 7. La salida me sorprendió:
Foo1
Bar2
Entonces, la resolución para Foo
es determinar que la conversión a Func<Task>
es mejor que la conversión a Func<Task<int>>
, mientras que la resolución para Bar
determina que la conversión a Func<int>
es mejor que la conversión a Action
Todas las conversiones son válidas: si Foo1
métodos Foo1
y Bar2
, aún se compila, pero da salida de Foo2
, Bar1
.
Con el compilador C # 5, la llamada de Foo
es ambigua, ya que la llamada Bar
resuelve en Bar2
, al igual que con el compilador C # 7.
Con un poco más de investigación, la forma síncrona se especifica en 12.6.4.4 de la especificación ECMA C # 5:
C1 es una mejor conversión que C2 si se cumple al menos uno de los siguientes:
- ...
- E es una función anónima, T1 es un tipo de delegado D1 o una expresión de tipo de árbol de expresiones, T2 es un tipo de delegado D2 o una expresión de tipo de árbol de expresiones y se cumple una de las siguientes condiciones:
- D1 es un objetivo de conversión mejor que D2 (irrelevante para nosotros)
- D1 y D2 tienen listas de parámetros idénticas, y una de las siguientes afirmaciones:
- D1 tiene un tipo de retorno Y1, y D2 tiene un tipo de retorno Y2, existe un tipo de retorno inferido X para E en el contexto de esa lista de parámetros (§12.6.13), y la conversión de X a Y1 es mejor que la conversión de X a Y2
- E es asíncrono, D1 tiene un tipo de retorno
Task<Y1>
y D2 tiene un tipo de retornoTask<Y2>
, existe un tipo de retorno inferidoTask<X>
para E en el contexto de esa lista de parámetros (§12.6.3.13), y la conversión de X a Y1 es mejor que la conversión de X a Y2- D1 tiene un tipo de retorno Y, y D2 está vacío.
Así que eso tiene sentido para el caso no asíncrono, y también tiene sentido para la forma en que el compilador C # 5 no puede resolver la ambigüedad, porque esas reglas no rompen el empate.
Aún no tenemos una especificación completa de C # 6 o C # 7, pero hay un borrador disponible . Sus reglas de resolución de sobrecarga se expresan de manera algo diferente, y el cambio puede estar en alguna parte.
Sin embargo, si se va a compilar con cualquier cosa, esperaría que la sobrecarga de Foo
acepte una función Func<Task<int>>
sobre la función de sobrecarga que acepta la función Func<Task>
, porque es un tipo más específico. (Hay una conversión de referencia de Func<Task<int>>
a Func<Task>
, pero no al revés).
Tenga en cuenta que el tipo de retorno inferido de la expresión lambda solo sería Func<Task>
en las especificaciones C # 5 y C # 6 del borrador.
En última instancia, la resolución de sobrecarga y la inferencia de tipo son partes realmente difíciles de la especificación. Esta respuesta explica por qué el ciclo while(true)
hace una diferencia (porque sin ella, la sobrecarga que acepta una función que devuelve una Task<T>
ni siquiera es aplicable) pero he llegado al final de lo que puedo resolver sobre el Elección que hace el compilador C # 7.