Detectando IEnumerable "Máquinas de Estado"

17

Acabo de leer un artículo interesante llamado Cómo ser demasiado lindo con retorno de rendimiento de C #

Me hizo preguntarme cuál es la mejor manera de detectar si un IEnumerable es una colección real enumerable, o si es una máquina de estado generada con la palabra clave de rendimiento.

Por ejemplo, puede modificar DoubleXValue (del artículo) a algo como:

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

Pregunta 1) ¿Hay una mejor manera de hacer esto?

Pregunta 2) ¿Es esto algo de lo que debería preocuparme al crear una API?

CondiciónRacer
fuente
1
Debería usar una interfaz en lugar de un concreto y también lanzarlo hacia abajo, ICollection<T>sería una mejor opción ya que no todas las colecciones lo son List<T>. Por ejemplo, las matrices se Point[]implementan IList<T>pero no lo son List<T>.
Trevor Pilley
3
Bienvenido al problema con los tipos mutables. Ienumerable se supone implícitamente que es un tipo de datos inmutable, por lo que las implementaciones mutantes son una violación de LSP. Este artículo es una explicación perfecta de los peligros de los efectos secundarios, así que practique escribir sus tipos de C # para estar lo más libre posible de efectos secundarios y nunca tendrá que preocuparse por esto.
Jimmy Hoffa
1
@JimmyHoffa IEnumerablees inmutable en el sentido de que no puede modificar una colección (agregar / eliminar) mientras la enumera, sin embargo, si los objetos en la lista son mutables, es perfectamente razonable modificarlos mientras enumera la lista.
Trevor Pilley
@TrevorPilley No estoy de acuerdo. Si se espera que la colección sea inmutable, la expectativa de un consumidor es que los miembros no serán mutados por actores externos, eso se llamaría un efecto secundario y viola la expectativa de inmutabilidad. Viola POLA e implícitamente LSP.
Jimmy Hoffa
¿Qué hay de usar ToList? msdn.microsoft.com/en-us/library/bb342261.aspx No detecta si se generó por rendimiento, sino que lo hace irrelevante.
luiscubal

Respuestas:

22

Su pregunta, según tengo entendido, parece estar basada en una premisa incorrecta. Déjame ver si puedo reconstruir el razonamiento:

  • El artículo vinculado describe cómo las secuencias generadas automáticamente exhiben un comportamiento "perezoso" y muestra cómo esto puede conducir a un resultado contrario a la intuición.
  • Por lo tanto, puedo detectar si una instancia dada de IEnumerable exhibirá este comportamiento perezoso al verificar si se genera automáticamente.
  • ¿Cómo puedo hacer eso?

El problema es que la segunda premisa es falsa. Incluso si pudiera detectar si un IEnumerable dado fue el resultado de una transformación de bloque iterador (y sí, hay formas de hacerlo) no ayudaría porque la suposición es incorrecta. Vamos a ilustrar por qué.

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;
  }
}

Muy bien, tenemos cuatro métodos. S1 y S2 son secuencias generadas automáticamente; S3 y S4 son secuencias generadas manualmente. Ahora supongamos que tenemos:

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

El resultado para S1 y S4 será 0; cada vez que enumera la secuencia, obtiene una nueva referencia a una M creada. El resultado para S2 y S3 será 100; cada vez que enumera la secuencia, obtiene la misma referencia a M que obtuvo la última vez. Si el código de secuencia se genera automáticamente o no es ortogonal a la pregunta de si los objetos enumerados tienen identidad referencial o no. Esas dos propiedades, la generación automática y la identidad referencial, en realidad no tienen nada que ver entre sí. El artículo que vinculaste los combina de alguna manera.

A menos que un proveedor de secuencias esté documentado como siempre ofreciendo objetos que tienen identidad referencial , no es prudente suponer que lo hace.

Eric Lippert
fuente
2
Todos sabemos que aquí tendrá la respuesta correcta, eso es un hecho; pero si pudiera poner un poco más de definición en el término "identidad referencial", podría ser bueno, lo estoy leyendo como "inmutable", pero eso se debe a que combino inmutable en C # con el nuevo objeto devuelto cada tipo de valor o obtención, lo que creo es lo mismo a lo que te refieres. Aunque la inmutabilidad garantizada se puede obtener de varias otras maneras, es la única forma racional en C # (que yo sepa, es muy posible que conozca formas en las que no estoy al tanto).
Jimmy Hoffa
2
@JimmyHoffa: Dos referencias de objeto x e y tienen "identidad referencial" si Object.ReferenceEquals (x, y) devuelve verdadero.
Eric Lippert el
Siempre es bueno recibir una respuesta directamente de la fuente. Gracias eric
ConditionRacer
10

Creo que el concepto clave es que debes tratar cualquier IEnumerable<T>colección como inmutable : no debes modificar los objetos a partir de ella, debes crear una nueva colección con nuevos objetos:

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

(O escriba lo mismo de manera más sucinta usando LINQ Select()).

Si realmente desea modificar los elementos de la colección, no use IEnumerable<T>, use IList<T>(o posiblemente IReadOnlyList<T>si no desea modificar la colección en sí, solo los elementos que contiene y está en .Net 4.5):

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

De esta manera, si alguien intenta usar su método IEnumerable<T>, fallará en el momento de la compilación.

svick
fuente
1
La clave real es, como dijiste, devolver un nuevo número con las modificaciones cuando quieras cambiar algo. Ienumerable como tipo es perfectamente viable y no tiene ninguna razón para ser cambiado, es solo que la técnica para usarlo debe entenderse como usted dijo. +1 por mencionar cómo trabajar con tipos inmutables.
Jimmy Hoffa
1
Algo relacionado: también debe tratar cada IEnumerable como si se realizara mediante ejecución diferida. Lo que puede ser cierto en el momento en que recupera su referencia IEnumerable puede no ser cierto cuando realmente lo enumera. Por ejemplo, devolver una instrucción LINQ dentro de un candado puede generar algunos resultados interesantes e inesperados.
Dan Lyons
@ Jimmy Jimmy: Estoy de acuerdo. Voy un paso más allá y trato de evitar iterar más de una IEnumerablevez. La necesidad de hacerlo más de una vez puede obviarse llamando ToList()e iterando sobre la lista, aunque en el caso específico mostrado por el OP, prefiero que la solución de svick de tener DoubleXValue también devuelva un IEnumerable. Por supuesto, en código real probablemente estaría usando LINQpara generar este tipo de IEnumerable. Sin embargo, la elección de svick yieldtiene sentido en el contexto de la pregunta del OP.
Brian
@Brian Sí, no quería cambiar demasiado el código para hacer mi punto. En código real, lo habría usado Select(). Y con respecto a múltiples iteraciones de IEnumerable: ReSharper es útil con eso, porque puede advertirte cuando lo haces.
svick
5

Personalmente, creo que el problema resaltado en ese artículo proviene del uso excesivo de la IEnumerableinterfaz por parte de muchos desarrolladores de C # en estos días. Parece que se ha convertido en el tipo predeterminado cuando se requiere una colección de objetos, pero hay mucha información que IEnumerablesimplemente no te dice ... crucialmente: si se ha reificado o no *.

Si encuentra que necesita detectar si un IEnumerablees realmente un IEnumerableo algo más, la abstracción se ha filtrado y probablemente se encuentre en una situación en la que su tipo de parámetro es demasiado flojo. Si necesita la información adicional que IEnumerablesolo no proporciona, explique ese requisito eligiendo una de las otras interfaces como ICollectiono IList.

Editar para los downvoters Por favor regrese y lea la pregunta cuidadosamente. El OP está pidiendo una manera de garantizar que las IEnumerableinstancias se hayan materializado antes de pasar a su método API. Mi respuesta aborda esa pregunta. Hay un problema más amplio con respecto a la forma correcta de usar IEnumerable, y ciertamente las expectativas del código que el OP pegó se basan en un malentendido de ese problema más amplio, pero el OP no preguntó cómo usar correctamente IEnumerableo cómo trabajar con tipos inmutables . No es justo rechazarme por no responder una pregunta que no se hizo.

* Yo uso el término reify, otros usan stabilizeo materialize. No creo que la comunidad haya adoptado una convención común todavía.

MattDavey
fuente
2
Un problema con ICollectiony IListes que son mutables (o al menos se ven de esa manera). IReadOnlyCollectiony IReadOnlyListde .Net 4.5 resuelven algunos de esos problemas.
svick
Por favor explique el voto negativo?
MattDavey
1
No estoy de acuerdo con que debas cambiar los tipos que estás usando. Ienumerable es un gran tipo y a menudo puede tener el beneficio de la inmutabilidad. Creo que el verdadero problema es que las personas no entienden cómo trabajar con tipos inmutables en C #. No es sorprendente, es un lenguaje extremadamente dinámico, por lo que es un concepto nuevo para muchos desarrolladores de C #.
Jimmy Hoffa
Solo ienumerable permite la ejecución diferida, por lo que no permitirlo como parámetro elimina una característica completa de C #
Jimmy Hoffa
2
Lo rechacé porque creo que está mal. Creo que eso está en el espíritu de SE, para garantizar que las personas no acumulen información incorrecta, depende de todos nosotros rechazar esas respuestas. Tal vez estoy equivocado, totalmente posible que esté equivocado y que tengas razón. Eso depende de la comunidad en general decidir mediante votación. Lo siento, lo estás tomando personalmente, si te sirve de consuelo, he recibido muchas respuestas negativas que he escrito y borrado sumariamente porque acepto que la comunidad cree que está mal, así que es poco probable que tenga razón.
Jimmy Hoffa