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 dynamic
palabra 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?
List<dynamic>
diferente (para su pregunta) de simplemente hacerList<object>
?Respuestas:
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:
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:
object
odynamic
. A continuación, se ve obligado a realizar lanzamientos oinstanceof
pruebas no verificadas o algo por el estilo, para poder trabajar de manera significativa con los elementos, lo que los hace tipados débilmente.fuente
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.
fuente
IUserSetting
e implementarla varias veces, o hacer una genéricaUserSetting<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.object
lugar de undynamic
, entonces seguro, use el primero.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.
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
La única forma de agregar al diccionario es una función
(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
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.
fuente
DoSomething(Object)
(al menos cuando se usaobject
en elforeach
bucle;dynamic
es algo completamente diferente).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 esDictionary<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.
fuente
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 .
fuente