2014-09-17 14 views
48

considerare questo Extensions reattivi snippet (ignorare la praticità di esso):In che modo l'aggiunta di un'interruzione in un ciclo while risolve l'ambiguità di sovraccarico?

return Observable.Create<string>(async observable => 
{ 
    while (true) 
    { 
    } 
}); 

Questo non viene compilato con reattivi Extensions 2.2.5 (utilizzando il pacchetto NuGet Rx-Principale). Viene a mancare con:

Error 1 The call is ambiguous between the following methods or properties: 'System.Reactive.Linq.Observable.Create<string>(System.Func<System.IObserver<string>,System.Threading.Tasks.Task<System.Action>>)' and 'System.Reactive.Linq.Observable.Create<string>(System.Func<System.IObserver<string>,System.Threading.Tasks.Task>)'

Tuttavia, l'aggiunta di un break in qualsiasi parte del ciclo while correzioni l'errore di compilazione:

return Observable.Create<string>(async observable => 
{ 
    while (true) 
    { 
     break; 
    } 
}); 

Il problema può essere riprodotta senza reattivi estensioni a tutti (più facile se si vuole provare senza giocherellare con Rx):

class Program 
{ 
    static void Main(string[] args) 
    { 
     Observable.Create<string>(async blah => 
     { 
      while (true) 
      { 
       Console.WriteLine("foo."); 
       break; //Remove this and the compiler will break 
      } 
     }); 
    } 
} 

public class Observable 
{ 
    public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, Task> subscribeAsync) 
    { 
     throw new Exception("Impl not important."); 
    } 

    public static IObservable<TResult> Create<TResult>(Func<IObserver<TResult>, Task<Action>> subscribeAsync) 
    { 
     throw new Exception("Impl not important."); 
    } 
} 

public interface IObserver<T> 
{ 
} 

Ignorando la parte estensioni reattivi di esso, perché l'aggiunta break aiuto del compilatore C# determinazione l'ambiguità? Come può essere descritto con le regole della risoluzione di sovraccarico dalla specifica C#?

Sto utilizzando Visual Studio 2013 Update 2 targeting 4.5.1.

+11

@ Mic1780: da quando i compilatori non riescono a rilevare un ciclo infinito? Mai visto prima ... – knittl

+1

@ Mic1780 Non ho idea di cosa tu stia parlando. Il compilatore C# (in realtà, la maggior parte dei compilatori) compilerà felicemente un ciclo infinito, anche uno semplice come 'while (true) {}' – tnw

risposta

59

È più semplice estrarre lo async così come i lambda qui, in quanto enfatizza cosa sta succedendo. Entrambi questi metodi sono validi e si elaborano:

public static void Foo() 
{ 
    while (true) { } 
} 
public static Action Foo() 
{ 
    while (true) { } 
} 

Tuttavia, per questi due metodi:

public static void Foo() 
{ 
    while (true) { break; } 
} 
public static Action Foo() 
{ 
    while (true) { break; } 
} 

Il primo compila, e la seconda no. Ha un percorso di codice che non restituisce un valore valido.

Infatti, while(true){} (insieme a throw new Exception();) è una dichiarazione interessante in quanto è il corpo valido di un metodo con qualsiasi tipo di reso.

Poiché il ciclo infinito è un candidato adatto per entrambi i sovraccarichi, e nessuno dei due sovraccarichi è "migliore", provoca un errore di ambiguità. L'implementazione del ciclo non infinito ha solo un candidato adatto nella risoluzione di sovraccarico, quindi compila.

Ovviamente, per rimettere in gioco async, è effettivamente rilevante in un modo. Per i metodi async entrambi restituiscono sempre qualcosa, sia che si tratti di Task o Task<T>. Gli algoritmi "betterness" per la risoluzione del sovraccarico preferiranno i delegati che restituiscono un valore superiore a void delegati quando c'è un lambda che potrebbe uguagliarli entrambi, tuttavia nel tuo caso i due overload hanno entrambi i delegati che restituiscono un valore, il fatto che per i metodi async che ritornano a Task anziché a Task<T> l'equivalente concettuale di non restituire un valore non è incorporato in quell'algoritmo di betterness. Per questo motivo l'equivalente non asincrono non comporterebbe un errore di ambiguità, anche se entrambi i sovraccarichi sono applicabili.

Ovviamente vale la pena notare che scrivere un programma per determinare se un blocco arbitrario di codice sarà mai completato è un problema notoriamente irrisolvibile, tuttavia, mentre il compilatore non può valutare correttamente se ogni singolo snippet verrà completato, può dimostrare, in alcuni casi semplici come questo, che il codice non sarà mai completato.Per questo ci sono modi di scrivere codice che chiaramente (per te e per me) non sarà mai completo, ma che il compilatore considererà come possibilmente completo.

+1

C'è qualcosa di specifico in 'async': dato' void Foo (Action a); void Foo (Func f); ',' Foo (() => {lancia nuova Exception();}); } 'is * not * ambiguo e chiamerà il secondo overload: la specifica della lingua favorisce un delegato con un tipo di ritorno. Ma le funzioni 'async' possono avere un tipo di ritorno anche se in realtà non sembrano restituire un valore concreto: può ancora avere un tipo di ritorno' Task'. – hvd

+0

@hvd Giusto, aggiunto in un paragrafo per andare oltre. – Servy

+1

Interessante. Quindi 'while (this.PropertyIsAlwaysTrue) {}' (o 'MethodReturnsTrue()') si risolverà nuovamente in un metodo con un valore di ritorno fisso (perché il compilatore non può sapere che cosa restituirà la proprietà). – knittl

24

Lasciando async su questo per cominciare ...

Con la rottura, la fine dell'espressione lambda è raggiungibile, quindi il tipo di ritorno del lambda deve esserevoid.

Senza interruzione, la fine dell'espressione lambda è irraggiungibile, quindi qualsiasi tipo di ritorno sarebbe valido. Ad esempio, questo va bene:

Func<string> foo =() => { while(true); }; 

che tale non è:

Func<string> foo =() => { while(true) { break; } }; 

Quindi, senza l'break, l'espressione lambda sarebbe convertibile in qualsiasi tipo delegato con un singolo parametro. Con break, l'espressione lambda è solo convertibile in un tipo delegato con un singolo parametro e un tipo di ritorno void.

aggiungere la parte async e void diventa void o Task, vs void, Task o Task<T> per qualsiasi T dove in precedenza si poteva avere alcun tipo di ritorno. Ad esempio:

// Valid 
Func<Task<string>> foo = async() => { while(true); }; 
// Invalid (it doesn't actually return a string) 
Func<Task<string>> foo = async() => { while(true) { break; } }; 
// Valid 
Func<Task> foo = async() => { while(true) { break; } }; 
// Valid 
Action foo = async() => { while(true) { break; } };