Ce n'est un plaisir :) Il y a plusieurs aspects. Pour commencer, nous allons simplifier de manière très significative par la suppression des Rx et réelle surcharge de la résolution de l'image. Résolution de surcharge est traitée à la fin de la réponse.
Anonyme en fonction de délégué des conversions et de l'accessibilité
La différence ici est de savoir si le point final de l'expression lambda est accessible. Si elle l'est, alors que la lambda expression ne retourne rien, et l'expression lambda ne peut être converti en Func<Task>
. Si le point final de l'expression lambda n'est pas accessible, alors il peut être converti à tout Func<Task<T>>
.
La forme de l' while
déclaration fait une différence parce que de cette partie de la spécification C#. (Ce qui est de l'ECMA C# 5 standard; d'autres versions peuvent avoir formulation légèrement différente pour le même concept.)
Le point de fin d'un while
déclaration est accessible si au moins une des conditions suivantes est remplie:
- L'
while
déclaration contient un accessible instruction break qui sort de l'instruction while.
- L'
while
déclaration est accessible et l'expression Booléenne n'a pas la valeur de la constante true
.
Lorsque vous avez un while (true)
boucle sans break
des déclarations, ni balle est vrai, de sorte que le point de fin de l' while
(et donc l'expression lambda dans votre cas) n'est pas accessible.
Voici un bref exemple complet sans Rx impliqués:
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); };
}
}
Nous pouvons simplifier encore plus loin en supprimant asynchrone à partir de l'équation. Si nous avons un synchrone sans paramètre lambda expression sans retour des déclarations, c'est toujours convertibles Action
, mais il est également convertible en Func<T>
pour toute T
si la fin de l'expression lambda n'est pas joignable. Légère modification du code ci-dessus:
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); };
}
}
Nous pouvons voir cela dans une manière légèrement différente par la suppression des délégués et des expressions lambda à partir de la combinaison. Tenir compte de ces méthodes:
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);
}
Bien que la méthode d'erreur de Method4
"n'est pas de tous les chemins de code de retour d'une valeur" à la manière de ce qui est détecté est "la fin de la méthode est accessible". Maintenant, imaginez ces corps de méthode sont les expressions lambda d'essayer de satisfaire un délégué ayant la même signature que la signature de la méthode, et nous sommes de retour pour le second exemple...
Plaisir avec résolution de surcharge
Comme Panagiotis Kanavos noté, l'erreur d'origine autour de la résolution de surcharge n'est pas reproductible dans Visual Studio 2017. Alors que ce passe? Encore une fois, nous n'avons pas besoin Rx participé à ce test. Mais on peut voir quelques très étrange comportement. Réfléchissez à ceci:
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) ; });
}
}
Qui émet un avertissement (pas de attendent des opérateurs), mais il compile avec le C# 7 compilateur. La sortie m'a surpris:
Foo1
Bar2
Donc, la résolution de Foo
est de déterminer que la conversion en Func<Task>
est mieux que la conversion en Func<Task<int>>
, alors que la résolution de Bar
est de déterminer que la conversion en Func<int>
est mieux que la conversion en Action
. Toutes les conversions sont valables, si vous commentez l' Foo1
et Bar2
méthodes, il est encore compile, mais donne à la sortie de l' Foo2
, Bar1
.
Avec le C# 5 compilateur, l' Foo
appel est ambigu par l' Bar
d'appel décide d' Bar2
, tout comme avec le C# 7 compilateur.
Avec un peu plus de recherche, la machine synchrone formulaire est spécifié dans 12.6.4.4 de l'ECMA C# 5 spécifications:
C1 est un meilleur taux de conversion que C2 si au moins une des conditions suivantes est satisfaite:
- ...
- E est une fonction anonyme, le T1 est un délégué de type D1 ou une expression du type d'arbre d'Expression, T2 est un délégué de type D2 ou une expression du type d'arbre d'Expression et l'une des opérations suivantes est satisfaite:
- D1 est un meilleur taux de conversion que la cible de D2 (inutiles pour nous)
- D1 et D2 sont identiques listes de paramètres et l'une des opérations suivantes est satisfaite:
- D1 a un type de retour Y1, et D2 a un type de retour Y2, déduit le type de retour de X existe pour E dans le contexte de la liste des paramètres (§12.6.3.13), et la conversion de X à Y1 est mieux que la conversion de X à Y2
- E est asynchrone, D1 a un type de retour
Task<Y1>
, et D2 a un type de retour Task<Y2>
, déduit le type de retour Task<X>
existe pour E dans le contexte de la liste des paramètres (§12.6.3.13), et la conversion de X à Y1 est mieux que la conversion de X à Y2
- D1 a un type de retour Y et D2 est nulle retour
Ce qui fait du sens pour les non-async cas - et il est également bon pour la façon dont le C# 5 compilateur n'est pas capable de résoudre l'ambiguïté, car ces règles ne sont pas les départager.
Nous n'avons pas un plein C# 6 ou C# 7 spécification encore, mais il y a un projet de l'un de disponible. Sa résolution de surcharge règles sont exprimées différemment, et le changement est peut-être là quelque part.
Si c'est pour les compiler pour tout, je m'attends à de la Foo
de surcharge d'accepter un Func<Task<int>>
à être choisi au cours de la surcharge en acceptant Func<Task>
- parce que c'est un plus spécifique. (Il y en a une conversion de référence à partir d' Func<Task<int>>
de Func<Task>
, mais pas vice-versa.)
Notez que le déduit le type de retour de l'expression lambda serait juste Func<Task>
dans le C# 5 et le projet de C# 6 cahier des charges.
En fin de compte, la résolution de surcharge et de l'inférence de type sont vraiment dur de bits de la spécification. Cette réponse explique pourquoi l' while(true)
boucle fait une différence (parce que sans elle, la surcharge d'accepter un func retour d'un Task<T>
n'est même pas applicable), mais j'ai atteint la fin de ce que je peux travailler sur le choix du C# 7 compilateur fait.