In C #, perché un metodo anonimo non può contenere un'istruzione yield?


87

Ho pensato che sarebbe stato bello fare qualcosa del genere (con il lambda che fa un ritorno di rendimento):

public IList<T> Find<T>(Expression<Func<T, bool>> expression) where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();

    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

Tuttavia, ho scoperto che non posso usare il rendimento nel metodo anonimo. Mi chiedo perché. I documenti di rendimento dicono solo che non è consentito.

Poiché non era consentito, ho semplicemente creato Elenco e aggiunto gli elementi ad esso.


Ora che possiamo avere asynclambda anonimi che consentono l' awaitinterno in C # 5.0, sarei interessato a sapere perché non hanno ancora implementato iteratori anonimi con yielddentro. Più o meno, è lo stesso generatore di macchine a stati.
noseratio

Risposte:


113

Eric Lippert ha recentemente scritto una serie di post sul blog sul perché in alcuni casi la resa non è consentita.

EDIT2:

  • Parte 7 (questa è stata pubblicata più tardi e affronta specificamente questa domanda)

Probabilmente troverai la risposta lì ...


EDIT1: questo è spiegato nei commenti della Parte 5, nella risposta di Eric al commento di Abhijeet Patel:

Q:

Eric,

Puoi anche fornire alcune informazioni sul motivo per cui i "rendimenti" non sono consentiti all'interno di un metodo anonimo o di un'espressione lambda

A:

Buona domanda. Mi piacerebbe avere blocchi iteratori anonimi. Sarebbe assolutamente fantastico poter costruire da soli un piccolo generatore di sequenze sul posto che si chiudesse su variabili locali. Il motivo è semplice: i benefici non superano i costi. La bellezza di creare generatori di sequenze sul posto è in realtà piuttosto piccola nel grande schema delle cose e i metodi nominali fanno il lavoro abbastanza bene nella maggior parte degli scenari. Quindi i vantaggi non sono così convincenti.

I costi sono elevati. La riscrittura dell'iteratore è la trasformazione più complicata nel compilatore e la riscrittura del metodo anonimo è la seconda più complicata. I metodi anonimi possono trovarsi all'interno di altri metodi anonimi e i metodi anonimi possono essere all'interno di blocchi iteratori. Pertanto, ciò che facciamo è prima di tutto riscrivere tutti i metodi anonimi in modo che diventino metodi di una classe di chiusura. Questa è la penultima cosa che il compilatore fa prima di emettere IL per un metodo. Una volta completato questo passaggio, il rewriter iteratore può presumere che non ci siano metodi anonimi nel blocco iteratore; sono già stati riscritti tutti. Pertanto, il rewriter dell'iteratore può concentrarsi sulla riscrittura dell'iteratore, senza preoccuparsi che potrebbe esserci un metodo anonimo non realizzato.

Inoltre, i blocchi iteratori non "annidano" mai, a differenza dei metodi anonimi. Il rewriter iteratore può presumere che tutti i blocchi iteratori siano "di livello superiore".

Se i metodi anonimi possono contenere blocchi iteratori, allora entrambi questi presupposti vengono eliminati dalla finestra. Puoi avere un blocco iteratore che contiene un metodo anonimo che contiene un metodo anonimo che contiene un blocco iteratore che contiene un metodo anonimo e ... schifo. Ora dobbiamo scrivere un passaggio di riscrittura in grado di gestire blocchi iteratori annidati e metodi anonimi annidati allo stesso tempo, unendo i nostri due algoritmi più complicati in un algoritmo molto più complicato. Sarebbe davvero difficile progettare, implementare e testare. Siamo abbastanza intelligenti da farlo, ne sono sicuro. Abbiamo una squadra intelligente qui. Ma non vogliamo assumerci quel grande fardello per una funzione "piacevole da avere ma non necessaria". - Eric


2
Interessante, soprattutto perché ora ci sono funzioni locali.
Mafii

4
Mi chiedo se questa risposta non sia aggiornata perché richiederà un rendimento di rendimento in una funzione locale.
Joshua

2
@Joshua ma una funzione locale non è la stessa di un metodo anonimo ... il rendimento del rendimento non è ancora consentito nei metodi anonimi.
Thomas Levesque

21

Eric Lippert ha scritto un'eccellente serie di articoli sui limiti (e sulle decisioni di progettazione che influenzano tali scelte) sui blocchi iteratori

In particolare, i blocchi iteratori sono implementati da alcune sofisticate trasformazioni del codice del compilatore. Queste trasformazioni avrebbero un impatto con le trasformazioni che avvengono all'interno di funzioni anonime o lambda in modo tale che in determinate circostanze entrambi proverebbero a "convertire" il codice in qualche altro costrutto incompatibile con l'altro.

Di conseguenza è loro vietato interagire.

Il modo in cui i blocchi iteratori funzionano sotto il cofano è trattato bene qui .

Come semplice esempio di incompatibilità:

public IList<T> GreaterThan<T>(T t)
{
    IList<T> list = GetList<T>();
    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

Il compilatore desidera contemporaneamente convertirlo in qualcosa di simile:

// inner class
private class Magic
{
    private T t;
    private IList<T> list;
    private Magic(List<T> list, T t) { this.list = list; this.t = t;}

    public IEnumerable<T> DoIt()
    {
        var items = () => {
            foreach (var item in list)
                if (fun.Invoke(item))
                    yield return item;
        }
    }
}

public IList<T> GreaterThan<T>(T t)
{
    var magic = new Magic(GetList<T>(), t)
    var items = magic.DoIt();
    return items.ToList();
}

e allo stesso tempo l'aspetto dell'iteratore sta cercando di fare il suo lavoro per creare una piccola macchina a stati. Alcuni semplici esempi potrebbero funzionare con una discreta quantità di controllo di integrità (prima occupandosi delle chiusure annidate (possibilmente arbitrariamente)) quindi vedendo se le classi risultanti di livello più basso potrebbero essere trasformate in macchine a stati iteratori.

Tuttavia questo sarebbe

  1. Un bel po 'di lavoro.
  2. Non potrebbe funzionare in tutti i casi senza almeno che l'aspetto del blocco iteratore sia in grado di impedire all'aspetto di chiusura di applicare determinate trasformazioni per l'efficienza (come la promozione di variabili locali in variabili di istanza piuttosto che una classe di chiusura a tutti gli effetti).
    • Se ci fosse anche una minima possibilità di sovrapposizione dove era impossibile o sufficientemente difficile da non implementare, il numero di problemi di supporto risultanti sarebbe probabilmente alto poiché il sottile cambiamento di rottura andrebbe perso per molti utenti.
  3. Può essere facilmente aggirato.

Nel tuo esempio in questo modo:

public IList<T> Find<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    return FindInner(expression).ToList();
}

private IEnumerable<T> FindInner<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();
    foreach (var item in list)
        if (fun.Invoke(item))
            yield return item;
}

2
Non c'è una ragione chiara per cui il compilatore non può, una volta tolte tutte le chiusure, eseguire la consueta trasformazione dell'iteratore. Conoscete un caso che effettivamente presenterebbe qualche difficoltà? A proposito, la tua Magicclasse dovrebbe esserlo Magic<T>.
Qwertie

3

Sfortunatamente non so perché non lo permettessero, dato che ovviamente è del tutto possibile immaginare come funzionerebbe.

Tuttavia, i metodi anonimi sono già un pezzo di "magia del compilatore", nel senso che il metodo verrà estratto o in un metodo nella classe esistente, o anche in una classe completamente nuova, a seconda che si tratti di variabili locali o meno.

Inoltre, i metodi iteratori che usano yieldsono implementati anche usando la magia del compilatore.

La mia ipotesi è che uno di questi due renda il codice non identificabile per l'altro pezzo di magia e che si sia deciso di non perdere tempo a far funzionare questo lavoro per le versioni correnti del compilatore C #. Naturalmente, potrebbe non essere affatto una scelta consapevole e che semplicemente non funziona perché nessuno ha pensato di implementarla.

Per una domanda accurata al 100% ti suggerisco di utilizzare il sito di Microsoft Connect e segnalare una domanda, sono sicuro che otterrai qualcosa di utilizzabile in cambio.


1

Lo farei:

IList<T> list = GetList<T>();
var fun = expression.Compile();

return list.Where(item => fun.Invoke(item)).ToList();

Ovviamente è necessario System.Core.dll a cui fa riferimento .NET 3.5 per il metodo Linq. E includi:

using System.Linq;

Saluti,

Sly


0

Forse è solo una limitazione della sintassi. In Visual Basic .NET, che è molto simile a C #, è perfettamente possibile anche se scomodo da scrivere

Sub Main()
    Console.Write("x: ")
    Dim x = CInt(Console.ReadLine())
    For Each elem In Iterator Function()
                         Dim i = x
                         Do
                             Yield i
                             i += 1
                             x -= 1
                         Loop Until i = x + 20
                     End Function()
        Console.WriteLine($"{elem} to {x}")
    Next
    Console.ReadKey()
End Sub

Notare anche le parentesi ' here; la funzione lambda Iterator Function... End Function restituisce un IEnumerable(Of Integer)ma non è tale oggetto stesso. Deve essere chiamato per ottenere quell'oggetto.

Il codice convertito da [1] genera errori in C # 7.3 (CS0149):

static void Main()
{
    Console.Write("x: ");
    var x = System.Convert.ToInt32(Console.ReadLine());
    // ERROR: CS0149 - Method name expected 
    foreach (var elem in () =>
    {
        var i = x;
        do
        {
            yield return i;
            i += 1;
            x -= 1;
        }
        while (!i == x + 20);
    }())
        Console.WriteLine($"{elem} to {x}");
    Console.ReadKey();
}

Sono fortemente in disaccordo con il motivo fornito nelle altre risposte che è difficile da gestire per il compilatore. Quello Iterator Function()che vedi nell'esempio VB.NET è stato creato appositamente per gli iteratori lambda.

In VB, c'è la Iteratorparola chiave; non ha una controparte C #. IMHO, non vi è alcun motivo reale per questa non è una funzionalità di C #.

Quindi, se vuoi davvero, davvero vuoi funzioni iteratore anonimo, attualmente usa Visual Basic o (non l'ho controllato) F #, come affermato in un commento della Parte # 7 nella risposta di @Thomas Levesque (fai Ctrl + F per F #).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.