Rilevamento di "macchine statali" IEnumerable


17

Ho appena letto un articolo interessante intitolato Come diventare troppo carino con il rendimento in c #

Mi sono chiesto quale sia il modo migliore per rilevare se un IEnumerable è una raccolta enumerabile effettiva o se si tratta di una macchina a stati generata con la parola chiave yield.

Ad esempio, puoi modificare DoubleXValue (dall'articolo) in qualcosa del tipo:

private void DoubleXValue(IEnumerable<Point> points)
{
    if(points is List<Point>)
      foreach (var point in points)
        point.X *= 2;
    else throw YouCantDoThatException();
}

Domanda 1) C'è un modo migliore per farlo?

Domanda 2) È qualcosa di cui dovrei preoccuparmi quando creo un'API?


1
Dovresti usare un'interfaccia piuttosto che una concreta e anche lanciarla più in basso, ICollection<T>sarebbe una scelta migliore in quanto non tutte le collezioni lo sono List<T>. Ad esempio, le matrici Point[]implementano IList<T>ma non lo sono List<T>.
Trevor Pilley,

3
Benvenuti nel problema con i tipi mutabili. Ienumerable dovrebbe implicitamente essere un tipo di dati immutabile, quindi le implementazioni mutanti sono una violazione di LSP. Questo articolo è una spiegazione perfetta dei pericoli degli effetti collaterali, quindi fai pratica scrivendo i tuoi tipi di C # per essere il più libero possibile dagli effetti collaterali e non dovrai mai preoccuparti di questo.
Jimmy Hoffa,

1
@JimmyHoffa IEnumerableè immutabile nel senso che non è possibile modificare una raccolta (aggiungi / rimuovi) durante l'enumerazione, tuttavia se gli oggetti nell'elenco sono mutabili, è perfettamente ragionevole modificarli durante l'enumerazione dell'elenco.
Trevor Pilley,

@TrevorPilley Non sono d'accordo. Se ci si aspetta che la collezione sia immutabile, l'aspettativa da parte di un consumatore è che i membri non saranno mutati da attori esterni, questo sarebbe chiamato un effetto collaterale e viola le aspettative di immutabilità. Viola POLA e implicitamente LSP.
Jimmy Hoffa,

Che ne dici di usare ToList? msdn.microsoft.com/en-us/library/bb342261.aspx Non rileva se è stato generato dal rendimento, ma invece lo rende irrilevante.
luiscubal

Risposte:


22

La tua domanda, a quanto ho capito, sembra essere basata su una premessa errata. Fammi vedere se riesco a ricostruire il ragionamento:

  • L'articolo collegato a descrive come le sequenze generate automaticamente mostrano un comportamento "pigro" e mostra come ciò può portare a un risultato contro-intuitivo.
  • Pertanto posso rilevare se una determinata istanza di IEnumerable mostrerà questo comportamento pigro verificando se viene generata automaticamente.
  • Come lo faccio?

Il problema è che la seconda premessa è falsa. Anche se fosse in grado di rilevare se un determinato IEnumerable fosse o meno il risultato di una trasformazione del blocco iteratore (e sì, ci sono modi per farlo) non sarebbe utile perché l'ipotesi è errata. Illustriamo il perché.

class M { public int P { get; set; } }
class C
{
  public static IEnumerable<M> S1()
  {
    for (int i = 0; i < 3; ++i) 
      yield return new M { P = i };
  }

  private static M[] ems = new M[] 
  { new M { P = 0 }, new M { P = 1 }, new M { P = 2 } };
  public static IEnumerable<M> S2()
  {
    for (int i = 0; i < 3; ++i)
      yield return ems[i];
  }

  public static IEnumerable<M> S3()
  {
    return new M[] 
    { new M { P = 0 }, new M { P = 1 }, new M { P = 2 } };
  }

  private class X : IEnumerable<M>
  {
    public IEnumerator<X> GetEnumerator()
    {
      return new XEnum();
    }
    // Omitted: non generic version
    private class XEnum : IEnumerator<X>
    {
      int i = 0;
      M current;
      public bool MoveNext()
      {
        current = new M() { P = i; }
        i += 1;
        return true;
      }
      public M Current { get { return current; } }
      // Omitted: other stuff.
    }
  }

  public static IEnumerable<M> S4()
  {
    return new X();
  }

  public static void Add100(IEnumerable<M> items)
  {
    foreach(M item in items) item.P += 100;
  }
}

Bene, abbiamo quattro metodi. S1 e S2 sono sequenze generate automaticamente; S3 e S4 sono sequenze generate manualmente. Supponiamo ora di avere:

var items = C.Sn(); // S1, S2, S3, S4
S.Add100(items);
Console.WriteLine(items.First().P);

Il risultato per S1 e S4 sarà 0; ogni volta che si enumera la sequenza, si ottiene un nuovo riferimento a una M creata. Il risultato per S2 e S3 sarà 100; ogni volta che si enumera la sequenza, si ottiene lo stesso riferimento a M ottenuto l'ultima volta. Se il codice di sequenza viene generato automaticamente o meno è ortogonale alla domanda se gli oggetti elencati abbiano o meno identità referenziale. Queste due proprietà - generazione automatica e identità referenziale - in realtà non hanno nulla a che fare l'una con l'altra. L'articolo che hai collegato li confonde in qualche modo.

A meno che un provider di sequenze non sia documentato come fornisca sempre oggetti con identità referenziale , non è saggio supporre che lo faccia.


2
Sappiamo tutti che avrai la risposta corretta qui, questo è un dato; ma se potessi mettere un po 'più di definizione nel termine "identità referenziale", potrebbe essere buono, lo sto leggendo come "immutabile", ma è perché confondo immutabile in C # con un nuovo oggetto restituito ogni tipo di valore o di valore, che penso è la stessa cosa a cui ti riferisci. Sebbene l'immutabilità concessa possa essere ottenuta in molti altri modi, è l'unico modo razionale in C # (di cui sono a conoscenza, potresti ben conoscere modi di cui non sono a conoscenza).
Jimmy Hoffa,

2
@JimmyHoffa: due riferimenti a oggetti xey hanno "identità referenziale" se Object.ReferenceEquals (x, y) restituisce true.
Eric Lippert,

Sempre bello ottenere una risposta direttamente dalla fonte. Grazie Eric.
ConditionRacer

10

Penso che il concetto chiave sia che dovresti considerare qualsiasi IEnumerable<T>collezione come immutabile : non devi modificarne gli oggetti, devi creare una nuova collezione con nuovi oggetti:

private IEnumerable<Point> DoubleXValue(IEnumerable<Point> points)
{
    foreach (var point in points)
        yield return new Point(point.X * 2, point.Y);
}

(O scrivi lo stesso in modo più succinto usando LINQ Select().)

Se desideri effettivamente modificare gli elementi nella raccolta, non utilizzare IEnumerable<T>, utilizzare IList<T>(o eventualmente IReadOnlyList<T>se non desideri modificare la raccolta stessa, solo gli elementi in essa contenuti e sei su .Net 4.5):

private void DoubleXValue(IList<Point> points)
{
    foreach (var point in points)
        point.X *= 2;
}

In questo modo, se qualcuno tenta di utilizzare il metodo con IEnumerable<T>, fallirà in fase di compilazione.


1
La vera chiave è come hai detto tu, restituisci un nuovo ienumerable con le modifiche quando vuoi cambiare qualcosa. Ienumerable come type è perfettamente praticabile e non ha motivo di essere modificato, è solo la tecnica per usarlo che deve essere compresa come hai detto. +1 per menzionare come lavorare con tipi immutabili.
Jimmy Hoffa,

1
In qualche modo correlato - dovresti anche trattare ogni IEnumerable come se fosse fatto tramite l'esecuzione differita. Ciò che potrebbe essere vero nel momento in cui recuperi il tuo riferimento IEnumerable potrebbe non essere vero quando lo enumeri effettivamente attraverso di esso. Ad esempio, la restituzione di un'istruzione LINQ all'interno di un blocco può comportare risultati interessanti e inaspettati.
Dan Lyons,

@JimmyHoffa: sono d'accordo. Faccio un ulteriore passo avanti e cerco di evitare di ripetere IEnumerablepiù di una volta. La necessità di farlo più di una volta può essere evocata chiamando ToList()e ripetendo l'elenco, anche se nel caso specifico mostrato dall'OP preferisco che la soluzione svick di avere DoubleXValue restituisca anche un IEnumerable. Naturalmente, nel codice reale probabilmente userei LINQper generare questo tipo di IEnumerable. Tuttavia, la scelta di svick yieldha senso nel contesto della domanda del PO.
Brian,

@Brian Sì, non volevo cambiare troppo il codice per esprimere il mio punto. Nel vero codice, avrei usato Select(). E riguardo a più iterazioni di IEnumerable: ReSharper è utile in questo, perché può avvisarti quando lo fai.
svick

5

Personalmente penso che il problema evidenziato in quell'articolo derivi da un uso eccessivo IEnumerabledell'interfaccia da parte di molti sviluppatori C # in questi giorni. Sembra che sia diventato il tipo predefinito quando hai bisogno di una raccolta di oggetti - ma ci sono molte informazioni che IEnumerablesemplicemente non ti dicono ... fondamentale: se sono state o meno reificate *.

Se si scopre che si ha bisogno per rilevare se una IEnumerableè veramente una IEnumerableo qualcos'altro, l'astrazione è trapelato e probabilmente siete in una situazione in cui il tipo di parametro è un po 'troppo lento. Se si richiedono le informazioni extra che IEnumerableda sole non forniscono, rendere esplicito tale requisito scegliendo una delle altre interfacce come ICollectiono IList.

Modifica per i downvoter Si prega di tornare indietro e leggere attentamente la domanda. L'OP chiede un modo per garantire che le IEnumerableistanze siano state materializzate prima di essere passate al suo metodo API. La mia risposta risponde a questa domanda. Esiste un problema più ampio relativo al modo corretto di utilizzare IEnumerablee certamente le aspettative del codice incollato dall'OP si basano su un fraintendimento di tale problema più ampio, ma l'OP non ha chiesto come utilizzare correttamente IEnumerableo come lavorare con tipi immutabili . Non è giusto votarmi per non aver risposto a una domanda che non è stata posta.

* Uso il termine reify, altri usano stabilizeo materialize. Non credo che la comunità abbia ancora adottato una convenzione comune.


2
Un problema con ICollectioned IListè che sono mutabili (o almeno guardano in quel modo). IReadOnlyCollectione IReadOnlyListda .Net 4.5 risolvono alcuni di questi problemi.
svick

Per favore, spiega il downvote?
MattDavey,

1
Non sono d'accordo sul fatto che dovresti cambiare i tipi che stai utilizzando. Ienumerable è un tipo eccezionale e spesso può avere il vantaggio dell'immutabilità. Penso che il vero problema sia che le persone non capiscono come lavorare con tipi immutabili in C #. Non sorprende, è un linguaggio estremamente di stato, quindi è un nuovo concetto per molti sviluppatori C #.
Jimmy Hoffa,

Solo l'enumerabile consente l'esecuzione differita, quindi la sua disattivazione come parametro rimuove un'intera funzionalità di C #
Jimmy Hoffa

2
L'ho votato perché penso che sia sbagliato. Penso che sia nello spirito di SE, per garantire che le persone non mettano in magazzino informazioni errate, spetta a tutti noi votare le risposte. Forse mi sbaglio, totalmente possibile che mi sbagli e tu abbia ragione. Spetta alla comunità più ampia decidere votando. Mi dispiace che tu lo prenda sul personale, se è una consolazione ho avuto molte risposte a voto negativo che ho scritto e sommariamente cancellato perché accetto la community pensa che sia sbagliato, quindi è improbabile che abbia ragione.
Jimmy Hoffa,
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.