c# - type - ¿Por qué Array no es un tipo genérico?
use generic type c# (7)
Array
está declarado como el siguiente:
public abstract class Array
: ICloneable, IList, ICollection, IEnumerable {
Me pregunto por qué no es así:
public partial class Array<T>
: ICloneable, IList<T>, ICollection<T>, IEnumerable<T> {
¿Cuál sería el problema si se declarase un tipo genérico?
Si fuera un tipo genérico, ¿aún necesitaremos el genérico? Y si lo hiciéramos, ¿podría derivarse de
Array<T>
(como se supone arriba)?public partial class Array: Array<object> {
actualizar:
El segundo punto también es importante para la suposición de que ya no hará que la Array
no genérica sea un tipo básico.
Por lo tanto, me gustaría saber por qué no lo es:
La razón es que los genéricos no estaban presentes en la primera versión de C #.
Pero no puedo entender cuál sería el problema yo mismo.
El problema es que rompería una gran cantidad de código que usa la clase Array
. C # no es compatible con herencia múltiple, por lo que las líneas como esta
Array ary = Array.Copy(.....);
int[] values = (int[])ary;
estaría roto.
Si los MS volvieran a crear C # y .NET desde cero, probablemente no habría problema en hacer de Array
una clase genérica, pero esa no es la realidad.
Historia
¿Qué problemas surgirían si las matrices se convirtieran en un tipo genérico?
De vuelta en C # 1.0, copiaron el concepto de matrices principalmente de Java. Los genéricos no existían en aquel entonces, pero los creadores pensaron que eran inteligentes y copiaron la semántica de matrices covariantes rotas que tienen las matrices de Java. Esto significa que puede realizar cosas como esta sin un error de tiempo de compilación (pero en su lugar un error de tiempo de ejecución):
Mammoth[] mammoths = new Mammoth[10];
Animal[] animals = mammoths; // Covariant conversion
animals[1] = new Giraffe(); // Run-time exception
En C # 2.0 se introdujeron genéricos, pero no genéricos covariantes / contravariantes. Si las matrices se hicieron genéricas, entonces no se podía convertir a Mammoth[]
en Animal[]
, algo que se podía hacer antes (aunque estuviera roto). Así que hacer matrices genéricas habría roto mucho código.
Solo en C # 4.0 se introdujeron tipos genéricos covariantes / contravariantes para las interfaces. Esto hizo posible arreglar la covarianza de matriz rota de una vez por todas. Pero, de nuevo, esto habría roto un montón de código existente.
Array<Mammoth> mammoths = new Array<Mammoth>(10);
Array<Animal> animals = mammoths; // Not allowed.
IEnumerable<Animals> animals = mammoths; // Covariant conversion
Las matrices implementan interfaces genéricas
¿Por qué las matrices no implementan las
IList<T>
genéricasIList<T>
,ICollection<T>
eIEnumerable<T>
?
Gracias a un truco de tiempo de ejecución, cada matriz T[]
implementa IEnumerable<T>
, ICollection<T>
e IList<T>
automáticamente. 1 De la documentación de la clase Array
:
En .NET Framework versión 2.0, la clase Array implementa las interfaces genéricas
IList<T>
,ICollection<T>
eIEnumerable<T>
. Las implementaciones se proporcionan a las matrices en tiempo de ejecución y, por lo tanto, no son visibles para las herramientas de creación de documentación. Como resultado, las interfaces genéricas no aparecen en la sintaxis de declaración para la clase Array.
¿Puedes usar todos los miembros de las interfaces implementadas por las matrices?
No. La documentación continúa con esta observación:
La clave a tener en cuenta cuando se lanza una matriz a una de estas interfaces es que los miembros que agregan, insertan o eliminan elementos lanzan
NotSupportedException
.
Eso es porque (por ejemplo) ICollection<T>
tiene un método Add
, pero no puede agregar nada a una matriz. Lanzará una excepción. Este es otro ejemplo de un error de diseño anticipado en .NET Framework que le generará excepciones en el tiempo de ejecución:
ICollection<Mammoth> collection = new Mammoth[10]; // Cast to interface type
collection.Add(new Mammoth()); // Run-time exception
Y dado que ICollection<T>
no es covariante (por razones obvias), no puede hacer esto:
ICollection<Mammoth> mammoths = new Array<Mammoth>(10);
ICollection<Animal> animals = mammoths; // Not allowed
Por supuesto, ahora existe la IReadOnlyCollection<T>
covariante IReadOnlyCollection<T>
que también se implementa mediante matrices bajo el capó 1 , pero contiene solo Count
por lo que tiene usos limitados.
La clase base Array
Si las matrices fueran genéricas, ¿aún necesitaríamos la clase
Array
no genérica?
En los primeros días lo hicimos. Todas las matrices implementan las interfaces IList
, ICollection
e IEnumerable
no genéricas a través de su Array
clase base. Esta era la única forma razonable de dar a todos los arreglos de métodos e interfaces específicos, y es el uso principal de la clase base Array
. Usted ve la misma opción para las enumeraciones: son tipos de valor pero heredan miembros de Enum
; y delegados que heredan de MulticastDelegate
.
¿Se podría eliminar el
Array
clase base no genérica ahora que los genéricos son compatibles?
Sí, los métodos e interfaces compartidos por todas las matrices podrían definirse en la clase genérica Array<T>
si alguna vez existiera. Y luego podría escribir, por ejemplo, Copy<T>(T[] source, T[] destination)
lugar de Copy(Array source, Array destination)
con el beneficio adicional de algún tipo de seguridad.
Sin embargo, desde un punto de vista de Programación Orientada a Objetos, es agradable tener una Array
clase base no genérica común que pueda usarse para referirse a cualquier matriz, independientemente del tipo de sus elementos. Así como IEnumerable<T>
hereda de IEnumerable
(que todavía se usa en algunos métodos LINQ).
¿Podría la clase base
Array
derivar deArray<object>
?
No, eso crearía una dependencia circular: Array<T> : Array : Array<object> : Array : ...
Además, eso implicaría que podría almacenar cualquier objeto en una matriz (después de todo, todas las matrices heredarían finalmente de tipo Array<object>
).
El futuro
¿Se podría agregar el nuevo array genérico
Array<T>
sin impactar demasiado el código existente?
No. Aunque la sintaxis podría ajustarse, la covarianza de matriz existente no podría utilizarse.
Una matriz es un tipo especial en .NET. Incluso tiene sus propias instrucciones en el Lenguaje Intermedio Común. Si los diseñadores de .NET y C # alguna vez deciden seguir por este camino, podrían hacer que el azúcar sintáctico de sintaxis T[]
para Array<T>
(al igual que T?
Es azúcar sintáctico para Nullable<T>
), y aún así usar el instrucciones especiales y soporte que asigna matrices contiguamente en la memoria.
Sin embargo, perdería la capacidad de lanzar matrices de Mammoth[]
a uno de sus tipos básicos Animal[]
, de forma similar a como no se puede incluir la List<Mammoth>
en la List<Animal>
. Pero la covarianza de matriz se rompe de todos modos, y hay mejores alternativas.
¿Alternativas a la matriz de covarianza?
Todas las matrices implementan IList<T>
. Si la interfaz IList<T>
se convirtiera en una interfaz covariante adecuada, entonces podría convertir cualquier arreglo Array<Mammoth>
(o cualquier lista para ese asunto) en un IList<Animal>
. Sin embargo, esto requiere que la interfaz IList<T>
se reescriba para eliminar todos los métodos que puedan cambiar la matriz subyacente:
interface IList<out T> : ICollection<T>
{
T this[int index] { get; }
int IndexOf(object value);
}
interface ICollection<out T> : IEnumerable<T>
{
int Count { get; }
bool Contains(object value);
}
(Tenga en cuenta que los tipos de parámetros en las posiciones de entrada no pueden ser T
ya que esto rompería la covarianza. Sin embargo, el object
es lo suficientemente bueno para Contains
e IndexOf
, que simplemente devolvería false
cuando pasó un objeto de un tipo incorrecto. proporcionar su propio IndexOf(T value)
genérico IndexOf(T value)
y Contains(T value)
.)
Entonces podrías hacer esto:
Array<Mammoth> mammoths = new Array<Mammoth>(10);
IList<Animals> animals = mammoths; // Covariant conversion
Incluso hay una pequeña mejora en el rendimiento porque el tiempo de ejecución no debería verificar si un valor asignado es de tipo compatible con el tipo real de los elementos de la matriz al establecer el valor de un elemento de una matriz.
Mi puñalada
Aprendí cómo funcionaría un tipo Array<T>
si se implementara en C # y .NET, combinado con las interfaces covariantes IList<T>
e ICollection<T>
descritas anteriormente, y funciona muy bien. También agregué las IMutableList<T>
invariables IMutableList<T>
e IMutableCollection<T>
para proporcionar los métodos de mutación que carecen mis nuevas interfaces IList<T>
e ICollection<T>
.
Construí una biblioteca de colección simple a su alrededor, y puedes descargar el código fuente y los binarios compilados de BitBucket , o instalar el paquete NuGet:
M42.Collections : colecciones especializadas con más funcionalidad, características y facilidad de uso que las clases de colección .NET incorporadas.
1 ) Una matriz T[]
en .Net 4.5 implementa a través de su clase base Array
: ICloneable
, IList
, ICollection
, IEnumerable
, IStructuralComparable
, IStructuralEquatable
; y silenciosamente durante el tiempo de ejecución: IList<T>
, ICollection<T>
, IEnumerable<T>
, IReadOnlyList<T>
, e IReadOnlyCollection<T>
.
Además de los otros problemas que las personas han mencionado, intentar agregar un Array<T>
genérico plantearía algunas otras dificultades:
Incluso si las características de covarianza de hoy hubieran existido desde el momento en que se introdujeron los genéricos, no habrían sido suficientes para las matrices. Una rutina que está diseñada para ordenar un
Car[]
podrá ordenar unBuick[]
, incluso si tiene que copiar elementos de la matriz en elementos de tipoCar
y luego copiarlos de nuevo. La copia del elemento del tipoCar
a unBuick[]
no es realmente segura, pero es útil. Se podría definir una matriz de matriz covariante como una interfaz de matriz unidimensional de tal manera que sea posible la clasificación [por ejemplo, incluyendo un método `Swap (int firstIndex, int secondIndex)], pero sería difícil hacer algo tan flexible como matrices son.Mientras que un tipo
Array<T>
podría funcionar bien para unT[]
, no habría medios dentro del sistema de tipo genérico para definir una familia que incluiríaT[]
,T[,]
,T[,,]
,T[,,,]
, etc. para una cantidad arbitraria de subíndices.No hay medios en .net para expresar la noción de que dos tipos se deben considerar idénticos, de modo que una variable de tipo
T1
se puede copiar a uno de tipoT2
, y viceversa, con ambas variables con referencias al mismo objeto. Alguien que use un tipoArray<T>
probablemente quiera pasar instancias al código que esperaT[]
, y aceptar instancias del código que usaT[]
. Si las matrices de estilo antiguo no se pueden pasar desde y hacia el código que usa el nuevo estilo, entonces las matrices de estilo nuevo serían más un obstáculo que una característica.
Puede haber formas de hacer jinxing el sistema de tipo para permitir que un tipo Array<T>
comporte como debería, pero dicho tipo se comportaría de muchas formas que eran totalmente diferentes de otros tipos genéricos, y ya que hay un tipo que implementa el comportamiento deseado (es decir, T[]
), no está claro qué beneficios se acumularían al definir otro.
Como todos dicen, el Array
original no es genérico porque no había genéricos cuando apareció en v1. Especulación abajo ...
Para hacer que "Array" sea genérico (lo cual tendría sentido ahora) puedes
mantener la
Array
existente y agregar una versión genérica. Esto es bueno, pero la mayoría de los usos de "Matriz" implican crecer con el tiempo y lo más probable es que la razón sea que se implementó mejor el mismo concepto deList<T>
. En este punto, agregar una versión genérica de "lista secuencial de elementos que no crece" no parece muy atractiva.elimine la
Array
no genérica y reemplácela con la implementación genéricaArray<T>
con la misma interfaz. Ahora debe hacer que el código compilado para versiones anteriores funcione con un tipo nuevo en lugar del tipo deArray
existente. Si bien sería posible (también muy probable) que el código del framework apoye dicha migración, siempre hay mucho código escrito por otras personas.Como
Array
es un tipo muy básico, casi todas las partes del código existente (que incluye código personalizado con reflexión y clasificación con código nativo y COM) lo usan. Como resultado, el precio de incluso la pequeña incompatibilidad entre las versiones (1.x -> 2.x de .Net Framework) sería muy alta.
Entonces, como resultado, el tipo de Array
está allí para quedarse para siempre. Ahora tenemos List<T>
como genérico equivalente para ser utilizado.
Compatibilidad. Array es un tipo histórico que se remonta a la época en que no existían los genéricos.
Hoy tendría sentido tener Array
, luego Array<T>
, luego la clase específica;)
Tal vez me falta algo, pero a menos que la instancia de la matriz se transfiera o se use como ICollection, IEnumerable, etc., entonces no se gana nada con una matriz de T.
Las matrices son rápidas y ya son seguras y no incurren en gastos generales de boxeo / desempaquetado.
[Actualización, nuevas ideas, sentía que faltaba algo hasta ahora]
En cuanto a la respuesta anterior:
- Las matrices son covariantes como otros tipos pueden ser. Puede implementar cosas como ''object [] foo = new string [5];'' con covarianza, por lo que esa no es la razón.
- La compatibilidad es probablemente la razón para no reconsiderar el diseño, pero yo sostengo que esta tampoco es la respuesta correcta.
Sin embargo, la otra razón por la que puedo pensar es porque una matriz es el "tipo básico" para un conjunto lineal de elementos en la memoria. He estado pensando en usar Array <T>, que es donde también podría preguntarse por qué T es un Objeto y por qué este "Objeto" existe. En este escenario, T [] es justo lo que considero otra sintaxis para Array <T> que es covariante con Array. Dado que los tipos realmente difieren, considero que los dos casos son similares.
Tenga en cuenta que tanto un objeto básico como un conjunto básico no son requisitos para un idioma OO. C ++ es el ejemplo perfecto para esto. La advertencia de no tener un tipo básico para estas construcciones básicas es no poder trabajar con matrices u objetos usando la reflexión. Para los objetos, estás acostumbrado a hacer cosas Foo que hacen que un "objeto" se sienta natural. En realidad, no tener una clase base de matriz hace que sea igualmente imposible hacer Foo, que no se utiliza con tanta frecuencia, pero es igualmente importante para el paradigma.
Por lo tanto, tener C # sin un tipo base de matriz, pero con la riqueza de los tipos de tiempo de ejecución (particularmente la reflexión) es IMO imposible.
Así que más en los detalles ...
Dónde se utilizan matrices y por qué son matrices
Tener un tipo básico para algo tan fundamental como una matriz se usa para muchas cosas y con una buena razón:
- Arrays simples
Sí, bueno, ya sabíamos que las personas usan T[]
, al igual que usan List<T>
. Ambos implementan un conjunto común de interfaces, para ser exactos: IList<T>
, ICollection<T>
, IEnumerable<T>
, IList
, ICollection
e IEnumerable
.
Puedes crear fácilmente una matriz si sabes esto. También todos sabemos que esto es cierto, y no es emocionante, así que estamos avanzando ...
- Crear colecciones
Si profundiza en List, finalmente terminará con una matriz, para ser exactos: una matriz T [].
¿Entonces por qué? Si bien podría haber usado una estructura de puntero (LinkedList), simplemente no es lo mismo. Las listas son bloques continuos de memoria y obtienen su velocidad al ser un bloque continuo de memoria. Hay muchas razones al respecto, pero en pocas palabras: procesar la memoria continua es la manera más rápida de procesar la memoria; incluso hay instrucciones para eso en la CPU que la hacen más rápida.
Un lector cuidadoso podría señalar el hecho de que no necesita una matriz para esto, sino un bloque continuo de elementos de tipo ''T'' que IL comprende y puede procesar. En otras palabras, puede deshacerse del tipo de matriz aquí, siempre y cuando se asegure de que haya otro tipo que IL pueda usar para hacer lo mismo.
Tenga en cuenta que hay tipos de valor y clase. Para conservar el mejor rendimiento posible, debes almacenarlos en tu bloque como tal ... pero para organizarlos es simplemente un requisito.
- Marshalling.
Marshalling usa tipos básicos que todos los idiomas aceptan comunicar. Estos tipos básicos son cosas como byte, int, float, puntero ... y array. Lo más notable es la forma en que las matrices se usan en C / C ++, que es así:
for (Foo *foo = beginArray; foo != endArray; ++foo)
{
// use *foo -> which is the element in the array of Foo
}
Básicamente, esto establece un puntero al comienzo de la matriz e incrementa el puntero (con tamaño de (Foo) bytes) hasta que llega al final de la matriz. El elemento se recupera en * foo - que obtiene el elemento al que apunta el puntero ''foo''.
Note nuevamente que hay tipos de valores y tipos de referencia. Realmente no quieres un MyArray que simplemente almacene todo en caja como un objeto. Implementar MyArray ahora es mucho más complicado.
Algunos lectores cuidadosos pueden señalar el hecho de que realmente no se necesita una matriz aquí, lo cual es cierto. Necesita un bloque continuo de elementos con el tipo Foo, y si es un tipo de valor, debe almacenarse en el bloque como el tipo de valor (representación del byte).
- Arrays multidimensionales
Entonces más ... ¿Qué hay de la multidimensionalidad? Aparentemente las reglas no son tan blancas y negras, porque de repente ya no tenemos todas las clases base:
int[,] foo2 = new int[2, 3];
foreach (var type in foo2.GetType().GetInterfaces())
{
Console.WriteLine("{0}", type.ToString());
}
El tipo fuerte acaba de salir de la ventana, y terminas con los tipos de colección IList
, ICollection
e IEnumerable
. Oye, ¿cómo se supone que obtendremos el tamaño entonces? Al usar la clase base Array, podríamos haber usado esto:
Array array = foo2;
Console.WriteLine("Length = {0},{1}", array.GetLength(0), array.GetLength(1));
... pero si miramos las alternativas como IList
, no hay equivalente. ¿Cómo vamos a resolver esto? ¿Debería introducir un IList<int, int>
aquí? Sin duda, esto es incorrecto, porque el tipo básico es simplemente int
. ¿Qué pasa con IMultiDimentionalList<int>
? Podemos hacer eso y llenarlo con los métodos que están actualmente en Array.
- Las matrices tienen un tamaño fijo
¿Has notado que hay llamadas especiales para reasignar matrices? Esto tiene mucho que ver con la administración de la memoria: las matrices tienen un nivel tan bajo que no comprenden qué crecimiento o contracción tienen. En C usarías ''malloc'' y ''realloc'' para esto, y realmente deberías implementar tu propio ''malloc'' y ''realloc'' para entender por qué exactamente tener tamaños fijos es importante para todas las cosas que asignas directamente.
Si lo miras, solo hay un par de cosas que se asignan en tamaños ''fijos'': matrices, todos los tipos de valores básicos, punteros y clases. Aparentemente manejamos arreglos de manera diferente, al igual que manejamos los tipos básicos de manera diferente.
Una nota al margen sobre la seguridad del tipo
Entonces, ¿por qué necesitan estas todas estas interfaces de ''punto de acceso'' en primer lugar?
La mejor práctica en todos los casos es proporcionar a los usuarios un punto de acceso seguro. Esto puede ilustrarse comparando un código como este:
array.GetType().GetMethod("GetLength").Invoke(array, 0); // don''t...
para codificar de esta manera:
((Array)someArray).GetLength(0); // do!
La seguridad de tipo le permite ser descuidado cuando programa. Si se usa correctamente, el compilador encontrará el error si lo hizo, en lugar de encontrarlo en tiempo de ejecución. No puedo enfatizar lo importante que es esto: después de todo, su código podría no ser llamado en ningún caso de prueba, ¡mientras que el compilador siempre lo evaluará!
Poniendolo todo junto
Entonces ... pongámoslo todo junto. Queremos:
- Un bloque de datos fuertemente tipado
- Eso tiene sus datos almacenados continuamente
- Soporte de IL para asegurarnos de que podamos usar las instrucciones geniales de la CPU que lo hacen sangrar rápidamente
- Una interfaz común que expone toda la funcionalidad
- Tipo de seguridad
- Multidimensionalidad
- Queremos que los tipos de valores se almacenen como tipos de valor
- Y la misma estructura de clasificación que cualquier otro idioma por ahí
- Y un tamaño fijo porque eso facilita la asignación de memoria
Son bastantes requisitos de bajo nivel para cualquier colección ... requiere que la memoria se organice de cierta manera, así como la conversión a IL / CPU ... Diría que hay una buena razón por la que se considera un tipo básico.