31 votes

Problèmes de résolution de surcharge de méthode C # dans Visual Studio 2013

Ces trois méthodes disponibles dans Rx.NET bibliothèque

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) {...}

J'écris l'exemple de code suivant dans MSVS 2013:

var sequence =
  Observable.Create<int>( async ( observer, token ) =>
                          {
                            while ( true )
                            {
                              token.ThrowIfCancellationRequested();
                              await Task.Delay( 100, token );
                              observer.OnNext( 0 );
                            }
                          } );

Ce n'est pas compilé en raison ambigu surcharges. Exacte de sortie du compilateur:

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>)'

Cependant dès que j'ai remplacer while( true ) avec while( false ) ou avec 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 );
                            }
                          } );

l'erreur disparaît et l'appel de la méthode résout à ceci:

public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, CancellationToken, Task> subscribeAsync) {...}

Ce qui se passe là-bas?

31voto

Jon Skeet Points 692016

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.

5voto

armenm Points 692

En plus de la réponse de @Daisy Shipton je tiens à ajouter que le même comportement peut être observé dans le cas suivant, trop:

var sequence = Observable.Create<int>(
    async (observer, token) =>
    {
        throw new NotImplementedException();
    });

essentiellement à cause de la même raison, le compilateur voit que la fonction lambda ne retourne jamais de sorte que tout type de retour serait de match, ce qui rend le lambda correspond à aucun des Observable.Create des surcharges.

Et, enfin, un exemple de solution simple: vous pouvez lancer le lambda souhaité de signature type de soupçon le compilateur qui Rx surcharge de choisir.

var sequence =
    Observable.Create<int>(
        (Func<IObserver<int>, CancellationToken, Task>)(async (observer, token) =>
        {
            throw new NotImplementedException();
        })
      );

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X