¿Hay un propósito específico para las listas heterogéneas?

13

Viniendo de un fondo C # y Java, estoy acostumbrado a que mis listas sean homogéneas, y eso tiene sentido para mí. Cuando comencé a recoger a Lisp, noté que las listas pueden ser heterogéneas. Cuando comencé a jugar con la dynamicpalabra clave en C #, noté que, a partir de C # 4.0, también puede haber listas heterogéneas:

List<dynamic> heterogeneousList

Mi pregunta es ¿cuál es el punto? Parece que una lista heterogénea tendrá mucha más sobrecarga al hacer el procesamiento y que si necesita almacenar diferentes tipos en un lugar, puede necesitar una estructura de datos diferente. ¿Mi ingenuidad está criando su cara fea o hay realmente momentos en que es útil tener una lista heterogénea?

Jetti
fuente
1
¿Querías decir ... me di cuenta de que las listas pueden ser heterogéneas ...?
Ingeniero mundial
¿Cómo es List<dynamic>diferente (para su pregunta) de simplemente hacer List<object>?
Peter K.
@WorldEngineer sí, lo hago. He actualizado mi publicación. ¡Gracias!
Jetti
@PeterK. Supongo que para el uso diario, no hay ninguna diferencia. Sin embargo, no todos los tipos en C # se derivan de System.Object, por lo que habría casos extremos donde hay diferencias.
Jetti

Respuestas:

16

El documento Colecciones heterogéneas fuertemente tipadas de Oleg Kiselyov, Ralf Lämmel y Keean Schupke contiene no solo una implementación de listas heterogéneas en Haskell, sino también un ejemplo motivador de cuándo, por qué y cómo usaría las listas H. En particular, lo están utilizando para acceder a la base de datos verificada en tiempo de compilación de tipo seguro. (Piense en LINQ, de hecho, el papel al que hacen referencia es el papel de Haskell de Erik Meijer et al que condujo a LINQ).

Citando del párrafo introductorio del documento HLists:

Aquí hay una lista abierta de ejemplos típicos que requieren colecciones heterogéneas:

  • Una tabla de símbolos que se supone que almacena entradas de diferentes tipos es heterogénea. Es un mapa finito, donde el tipo de resultado depende del valor del argumento.
  • Un elemento XML se escribe de forma heterogénea. De hecho, los elementos XML son colecciones anidadas que están restringidas por expresiones regulares y la propiedad de 1 ambigüedad.
  • Cada fila devuelta por una consulta SQL es un mapa heterogéneo de nombres de columnas a celdas. El resultado de una consulta es una secuencia homogénea de filas heterogéneas.
  • Agregar un sistema de objetos avanzado a un lenguaje funcional requiere colecciones heterogéneas del tipo que combinan registros extensibles con subtipos y una interfaz de enumeración.

Tenga en cuenta que los ejemplos que dio en su pregunta realmente no son listas heterogéneas en el sentido de que la palabra se usa comúnmente. Son tipos débiles o sin tipo listas. De hecho, en realidad son listas homogéneas , ya que todos los elementos son del mismo tipo: objecto dynamic. A continuación, se ve obligado a realizar lanzamientos o instanceofpruebas no verificadas o algo por el estilo, para poder trabajar de manera significativa con los elementos, lo que los hace tipados débilmente.

Jörg W Mittag
fuente
Gracias por el enlace y su respuesta. Usted señala bien que las listas realmente no son heterogéneas sino que están tipadas débilmente. Tengo muchas ganas de leer ese documento (probablemente mañana, me consiguió un
examen de
5

En pocas palabras, los contenedores heterogéneos intercambian el rendimiento en tiempo de ejecución por flexibilidad. Si desea tener una "lista de cosas" sin tener en cuenta el tipo particular de cosas, la heterogeneidad es el camino a seguir. Los Lisps se caracterizan dinámicamente y, de todos modos, casi todo es una lista en contra de valores encuadrados, por lo que se espera un golpe de rendimiento pequeño. En el mundo de Lisp, la productividad del programador se considera más importante que el rendimiento en tiempo de ejecución.

En un lenguaje de tipo dinámico, los contenedores homogéneos en realidad tendrían una ligera sobrecarga en comparación con los heterogéneos, porque todos los elementos agregados tendrían que ser verificados por tipo.

Su intuición acerca de elegir una mejor estructura de datos es acertada. En términos generales, cuantos más contratos pueda establecer en su código, más sabe sobre cómo funciona y más confiable, fácil de mantener, etc. se vuelve. Sin embargo, a veces realmente desea un contenedor heterogéneo, y se le debe permitir tener uno si lo necesita.

Jon Purdy
fuente
1
"Sin embargo, a veces realmente quieres un contenedor heterogéneo, y se te debería permitir tener uno si lo necesitas". - ¿Por qué sin embargo? Esa es mi pregunta ¿Por qué necesitarías mezclar una gran cantidad de datos en una lista aleatoria?
Jetti
@Jetti: Digamos que tiene una lista de configuraciones ingresadas por el usuario de varios tipos. Podría crear una interfaz IUserSettinge implementarla varias veces, o hacer una genérica UserSetting<T>, pero uno de los problemas con la escritura estática es que está definiendo una interfaz antes de saber exactamente cómo se va a usar. Las cosas que haces con la configuración de enteros son probablemente muy diferentes de las que haces con la configuración de cadenas, entonces, ¿qué operaciones tienen sentido poner en una interfaz común? Hasta que sepa con certeza, es mejor usar juiciosamente la escritura dinámica y concretarlo más tarde.
Jon Purdy
Mira, ahí es donde me encuentro con problemas. Para mí, eso parece un mal diseño, hacer algo antes de que sepas lo que hará / usará. Además, en ese caso, podría hacer que la interfaz con un valor de retorno de objeto. Hace lo mismo que la lista heterogénea, pero es más claro y más fácil de solucionar una vez que sabe con certeza cuáles son los tipos utilizados en la interfaz.
Jetti 01 de
@Jetti: Sin embargo, ese es esencialmente el mismo problema: una clase base universal no debería existir en primer lugar porque, sin importar qué operaciones defina, habrá un tipo para el que esas operaciones no tienen sentido. Pero si C # hace que sea más fácil usar un en objectlugar de un dynamic, entonces seguro, use el primero.
Jon Purdy
1
@Jetti: De esto se trata el polimorfismo. La lista contiene varios objetos "heterogéneos" aunque pueden ser subclases de una sola superclase. Desde un punto de vista de Java, puede obtener las definiciones de clase (o interfaz) correctas. Para otros lenguajes (LISP, Python, etc.) no hay ningún beneficio en obtener todas las declaraciones correctas, ya que no existe una diferencia práctica de implementación.
S.Lott 01 de
2

En lenguajes funcionales (como lisp), utiliza la coincidencia de patrones para determinar qué sucede con un elemento en particular en una lista. El equivalente en C # sería una cadena de instrucciones if ... elseif que verifican el tipo de un elemento y realizan una operación basada en eso. No es necesario decir que la coincidencia de patrones funcionales es más eficiente que la verificación del tipo de tiempo de ejecución.

El uso del polimorfismo sería una coincidencia más cercana a la coincidencia de patrones. Es decir, hacer que los objetos de una lista coincidan con una interfaz particular y llamar a una función en esa interfaz para cada objeto. Otra alternativa sería proporcionar una serie de métodos sobrecargados que toman un tipo de objeto específico como parámetro. El método predeterminado que toma Object como su parámetro.

public class ListVisitor
{
  public void DoSomething(IEnumerable<dynamic> list)
  {
    foreach(dynamic obj in list)
    {
       DoSomething(obj);
    }
  }

  public void DoSomething(SomeClass obj)
  {
    //do something with SomeClass
  }

  public void DoSomething(AnotherClass obj)
  {
    //do something with AnotherClass
  }

  public void DoSomething(Object obj)
  {
    //do something with everything els
  }
}

Este enfoque proporciona una aproximación a la coincidencia de patrones Lisp. El patrón de visitante (como se implementa aquí, es un gran ejemplo de uso para listas heterogéneas). Otro ejemplo sería para el envío de mensajes, donde hay escuchas para ciertos mensajes en una cola de prioridad y utilizando una cadena de responsabilidad, el despachador pasa el mensaje y el primer controlador que coincide con el mensaje lo maneja.

El otro lado está notificando a todos los que se registran para un mensaje (por ejemplo, el patrón de Agregador de eventos comúnmente utilizado para el acoplamiento suelto de ViewModels en el patrón MVVM). Yo uso el siguiente constructo

IDictionary<Type, List<Object>>

La única forma de agregar al diccionario es una función

Register<T>(Action<T> handler)

(y el objeto es en realidad una WeakReference al controlador pasado). Así que aquí TENGO que usar List <Object> porque en el momento de la compilación, no sé cuál será el tipo cerrado. Sin embargo, en Runtime puedo asegurar que será ese tipo la clave del diccionario. Cuando quiero disparar el evento llamo

Send<T>(T message)

y nuevamente resuelvo la lista. No hay ninguna ventaja en usar List <dynamic> porque necesito lanzarlo de todos modos. Como ve, hay ventajas en ambos enfoques. Si va a despachar dinámicamente un objeto utilizando la sobrecarga de métodos, la forma dinámica es hacerlo dinámico. Si estás FORZADO a lanzar independientemente, también podrías usar Object.

Michael Brown
fuente
Con la coincidencia de patrones, los casos son (generalmente, al menos en ML y Haskell) establecidos en piedra por la declaración del tipo de datos coincidente. Las listas que contienen tales tipos tampoco son heterogéneas.
No estoy seguro acerca de ML y Haskell, pero Erlang puede competir contra cualquiera, lo que significa que, si ha llegado hasta aquí porque no hubo otras coincidencias, haga esto.
Michael Brown
@MikeBrown - Esto es bueno pero no cubre POR QUÉ uno usaría listas heterogéneas y no siempre funcionaría con List <dynamic>
Jetti
44
En C #, las sobrecargas se resuelven en tiempo de compilación . Por lo tanto, su código de ejemplo siempre llamará DoSomething(Object)(al menos cuando se usa objecten el foreachbucle; dynamices algo completamente diferente).
Heinzi
@Heinzi, tienes razón ... Tengo sueño hoy: P arreglado
Michael Brown
0

Tiene razón en que la heterogeneidad conlleva una sobrecarga de tiempo de ejecución, pero lo más importante es que debilita las garantías de tiempo de compilación proporcionadas por el typechecker. Aun así, hay algunos problemas en los que las alternativas son aún más costosas.

En mi experiencia, al tratar con bytes sin procesar a través de archivos, sockets de red, etc., a menudo te encuentras con tales problemas.

Para dar un ejemplo real, considere un sistema para computación distribuida usando futuros . Un trabajador en un nodo individual puede generar trabajo de cualquier tipo serializable, produciendo un futuro de ese tipo. Detrás de escena, el sistema envía el trabajo a un compañero y luego guarda un registro que asocia esa unidad de trabajo con el futuro particular que debe completarse una vez que la respuesta a ese trabajo regrese.

¿Dónde se pueden guardar estos registros? Intuitivamente, lo que quieres es algo así como Dictionary<WorkId, Future<TValue>>, pero esto te limita a administrar solo un tipo de futuros en todo el sistema. El tipo más adecuado es Dictionary<WorkId, Future<dynamic>>, ya que el trabajador puede emitir el tipo apropiado cuando fuerza el futuro.

Nota : Este ejemplo proviene del mundo de Haskell donde no tenemos subtipos. No me sorprendería si hay una solución más idiomática para este ejemplo en particular en C #, pero es de esperar que todavía sea ilustrativo.

Adán
fuente
0

ISTR dice que Lisp no tiene ninguna estructura de datos que no sea una lista, por lo que si necesita algún tipo de objeto de datos agregado, tendrá que ser una lista heterogénea. Como otros han señalado, también son útiles para serializar datos para transmisión o almacenamiento. Una buena característica es que también son abiertos, por lo que puede usarlos en un sistema basado en una analogía de tuberías y filtros y tener pasos de procesamiento sucesivos para aumentar o corregir los datos sin requerir un objeto de datos fijo o una topología de flujo de trabajo .

TMN
fuente