Подтвердить что ты не робот

Почему не является массивом общего типа?

Array объявлен следующим образом:

public abstract class Array
    : ICloneable, IList, ICollection, IEnumerable {

Мне интересно, почему это не так:

public partial class Array<T>
    : ICloneable, IList<T>, ICollection<T>, IEnumerable<T> {
  1. В чем будет проблема, если он будет объявлен универсальным типом?

  2. Если это был универсальный тип, нужен ли нам не универсальный тип или он может быть получен из Array<T>? Такие как

    public partial class Array: Array<object> { 
    
4b9b3361

Ответ 1

История

Какие проблемы возникнут, если массивы стали универсальным типом?

В С# 1.0 они копировали концепцию массивов в основном из Java. В то время дженерики не существовали, но создатели считали, что они умны и скопировали разбитую семантику семантических ковариантов, которую имеют массивы Java. Это означает, что вы можете отключить такие вещи без ошибки времени компиляции (но вместо этого - ошибки времени выполнения):

Mammoth[] mammoths = new Mammoth[10];
Animal[] animals = mammoths;            // Covariant conversion
animals[1] = new Giraffe();             // Run-time exception

В С# 2.0 были введены дженерики, но не ковариантные/контравариантные общие типы. Если массивы были сделаны родовыми, то вы не могли бы отбрасывать Mammoth[] до Animal[], что-то, что вы могли бы сделать до этого (даже если оно было сломано). Таким образом, создание массивов generic сильно нарушило бы код.

Только в С# 4.0 были ковариантными/контравариантными типичными типами для введенных интерфейсов. Это позволило зафиксировать ковариантность разбитых массивов раз и навсегда. Но опять же, это сломало бы много существующего кода.

Array<Mammoth> mammoths = new Array<Mammoth>(10);
Array<Animal> animals = mammoths;           // Not allowed.
IEnumerable<Animals> animals = mammoths;    // Covariant conversion

Массивы реализуют общие интерфейсы

Почему массивы не реализуют общие интерфейсы IList<T>, ICollection<T> и IEnumerable<T>?

Благодаря трюку во время выполнения каждый массив T[] автоматически реализует IEnumerable<T>, ICollection<T> и IList<T>. 1 Из Array документация классов:

В .NET Framework версии 2.0 класс Array реализует общие интерфейсы IList<T>, ICollection<T> и IEnumerable<T>. Реализации предоставляются массивам во время выполнения и поэтому не видны инструментам сборки документации. В результате общие интерфейсы не отображаются в синтаксисе объявления для класса Array.


Можете ли вы использовать все элементы интерфейсов, реализованные с помощью массивов?

Нет. Документация продолжается с этим замечанием:

Ключевое значение, которое следует учитывать при создании массива на одном из этих интерфейсов, - это элементы, которые добавляют, вставляют или удаляют элементы throw NotSupportedException.

Это потому, что (например) ICollection<T> имеет метод Add, но вы не можете ничего добавить к массиву. Это вызовет исключение. Это еще один пример ранней ошибки проектирования в .NET Framework, которая заставит вас исключить вас во время выполнения:

ICollection<Mammoth> collection = new Mammoth[10];  // Cast to interface type
collection.Add(new Mammoth());                      // Run-time exception

А поскольку ICollection<T> не является ковариантным (по понятным причинам), вы не можете этого сделать:

ICollection<Mammoth> mammoths = new Array<Mammoth>(10);
ICollection<Animal> animals = mammoths;     // Not allowed

Конечно, теперь существует ковариантный IReadOnlyCollection<T> интерфейс, который также реализуется массивами под капотом 1 но он содержит только Count, поэтому он имеет ограниченное использование.


Базовый класс Array

Если массивы были обобщенными, нам все еще нужен не-общий Array класс?

В первые дни мы это сделали. Все массивы реализуют не общий IList, ICollection и IEnumerable через базовый класс Array. Это был единственный разумный способ предоставить всем массивам конкретные методы и интерфейсы и является основным использованием базового класса Array. Вы видите тот же выбор для перечислений: они являются типами значений, но наследуют членов от Enum; и делегаты, которые наследуют от MulticastDelegate.

Можно ли теперь удалить не общий шаблон класса Array, когда поддерживаются общие файлы?

Да, методы и интерфейсы, разделяемые всеми массивами, могут быть определены в общем классе Array<T>, если он когда-либо появился. И тогда вы могли бы написать, например, Copy<T>(T[] source, T[] destination) вместо Copy(Array source, Array destination) с дополнительным преимуществом безопасности определенного типа.

Однако, с точки зрения объектно-ориентированного программирования, хорошо иметь общий базовый класс non-generic Array, который может использоваться для ссылки на любой массив независимо от типа его элементов. Точно так же, как IEnumerable<T> наследует от IEnumerable (который все еще используется в некоторых методах LINQ).

Может ли базовый класс Array получить из Array<object>?

Нет, это создаст циклическую зависимость: Array<T> : Array : Array<object> : Array : .... Кроме того, это означает, что вы можете хранить любой объект в массиве (в конце концов, все массивы в конечном итоге наследуются от типа Array<object>).


Будущее

Может ли новый тип массива Array<T> быть добавлен, не слишком сильно влияя на существующий код?

Нет. Хотя синтаксис можно было бы приспособить, существующую ковариацию массива не удалось использовать.

Массив - это особый тип в .NET. У него даже есть свои собственные инструкции на общем промежуточном языке. Если разработчики .NET и С# когда-либо решили пойти по этой дороге, они могли бы сделать синтаксический сахар синтаксиса T[] для Array<T> (так же, как T? является синтаксическим сахаром для Nullable<T>), и по-прежнему использовать специальные инструкции и поддержку, которые распределяют массивы смежно в памяти.

Однако вы потеряете способность создавать массивы Mammoth[] для одного из своих базовых типов Animal[], подобно тому, как вы не можете использовать List<Mammoth> до List<Animal>. Но ковариантность массива в любом случае сломана, и есть лучшие альтернативы.

Альтернативы ковариации массива?

Все массивы реализуют IList<T>. Если интерфейс IList<T> был превращен в правильный ковариантный интерфейс, вы можете передать любой массив Array<Mammoth> (или любой другой список) на IList<Animal>. Однако для этого требуется, чтобы интерфейс IList<T> был перезаписан для удаления всех методов, которые могут изменить базовый массив:

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

(Обратите внимание, что типы параметров на входных позициях не могут быть T, так как это приведет к поломке ковариации. Однако object достаточно хорош для Contains и IndexOf, которые просто вернут false при передаче объект неправильного типа, а коллекции, реализующие эти интерфейсы, могут предоставлять свои собственные общие IndexOf(T value) и Contains(T value).)

Тогда вы можете сделать это:

Array<Mammoth> mammoths = new Array<Mammoth>(10);
IList<Animals> animals = mammoths;    // Covariant conversion

Существует даже небольшое улучшение производительности, поскольку время выполнения не должно проверять, совместимо ли присвоенное значение с реальным типом элементов массива при установке значения элемента массива.


Мой удар по нему

Я понял, как будет работать такой тип Array<T>, если он будет реализован на С# и .NET, в сочетании с реальными ковариантными интерфейсами IList<T> и ICollection<T>, описанными выше, и он работает очень хорошо. Я также добавил инвариантные интерфейсы IMutableList<T> и IMutableCollection<T>, чтобы предоставить методы мутации, отсутствующие в моих новых интерфейсах IList<T> и ICollection<T>.

Я построил вокруг него простую коллекционную библиотеку, и вы можете загрузить исходный код и скомпилированные двоичные файлы из BitBucket или установить пакет NuGet:

M42.Collections – Специализированные коллекции с большей функциональностью, функциями и простотой использования, чем встроенные классы .NET.


1) Массив T[] в .Net 4.5 реализует через свой базовый класс Array: ICloneable, IList, ICollection, IEnumerable, IStructuralComparable, IStructuralEquatable; и беззвучно через время выполнения: IList<T>, ICollection<T>, IEnumerable<T>, IReadOnlyList<T> и IReadOnlyCollection<T>.

Ответ 2

[Обновление, новые идеи, он чувствовал, что что-то не хватало до сих пор]

Относительно более раннего ответа:

  • Массивы ковариантны, как и другие типы. Вы можете реализовать такие вещи, как "object [] foo = new string [5];" с ковариацией, так что это не причина.
  • Совместимость, вероятно, является причиной не пересмотра дизайна, но я утверждаю, что это также неверный ответ.

Однако, другая причина, о которой я могу думать, состоит в том, что массив является "базовым типом" для линейного набора элементов в памяти. Я думал об использовании Array <T> , где вы также можете задаться вопросом, почему T является объектом и почему этот "объект" даже существует? В этом сценарии T [] - это то, что я считаю другим синтаксисом для Array <T> , который является ковариантным с Array. Поскольку типы действительно различаются, я рассматриваю два подобных случая.

Обратите внимание, что и основной объект, и базовый массив не являются требованиями к языку OO. С++ - прекрасный пример этого. Предостережение об отсутствии базового типа для этих базовых конструкций не в состоянии работать с массивами или объектами с использованием отражения. Для объектов, которые вы использовали для создания предметов Foo, которые делают объект "естественным". В действительности, отсутствие базового класса массива делает невозможным делать Foo, что не так часто используется, но не менее важно для парадигмы.

Следовательно, наличие С# без базового типа массива, но с богатством типов времени выполнения (в частности, отражение) является невозможным IMO.

Итак, больше деталей...

Где используются массивы и почему они являются массивами

Наличие базового типа для чего-то фундаментального, как массив, используется для многих вещей и с полным основанием:

  • Простые массивы

Да, мы уже знали, что люди используют T[], точно так же, как они используют List<T>. Оба реализуют общий набор интерфейсов, а точнее: IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection и IEnumerable.

Вы можете легко создать массив, если вы это знаете. Мы все знаем, что это правда, и это не захватывающе, поэтому мы движемся дальше...

  • Создать коллекции.

Если вы копаете в Списке, в конечном итоге вы получите массив, а точнее: массив T [].

Так почему это? Хотя вы могли бы использовать структуру указателя (LinkedList), это просто не то же самое. Списки представляют собой непрерывные блоки памяти и получают их скорость, будучи непрерывным блоком памяти. В этом есть много причин, но просто поставьте: обработка непрерывной памяти - это самый быстрый способ обработки памяти - есть даже инструкции для этого в вашем процессоре, которые делают это быстрее.

Тщательный читатель может указать на то, что для этого вам не нужен массив, а непрерывный блок элементов типа "Т", который ИЛ понимает и может обрабатывать. Другими словами, вы можете избавиться от типа Array здесь, если вы убедитесь, что есть другой тип, который IL может использовать для выполнения того же самого.

Обратите внимание, что есть значения и типы классов. Чтобы сохранить наилучшую производительность, вам необходимо сохранить их в своем блоке как таковом... но для сортировки его просто необходимо.

  • Ранжирование.

Маршаллинг использует базовые типы, с которыми согласны все языки. Этими базовыми типами являются такие вещи, как byte, int, float, pointer... и array. Наиболее вероятно, что в C/С++ используются массивы, которые выглядят следующим образом:

for (Foo *foo = beginArray; foo != endArray; ++foo) 
{
    // use *foo -> which is the element in the array of Foo
}

В основном это устанавливает указатель в начале массива и увеличивает указатель (с байтами sizeof (Foo)) до тех пор, пока он не достигнет конца массива. Элемент извлекается на * foo - который получает элемент, на который указывает указатель "foo".

Обратите внимание, что существуют типы значений и ссылочные типы. Вы действительно не хотите, чтобы MyArray просто хранил все в штучной упаковке как объект. Внедрение MyArray просто стало намного сложнее.

Некоторые осторожные читатели могут указать на то, что здесь вам действительно не нужен массив, что верно. Вам нужен непрерывный блок элементов с типом Foo - и если он тип значения, он должен быть сохранен в блоке в виде (байтового представления) типа значения.

  • Многомерные массивы

Итак, еще... Как насчет многомерности? По-видимому, правила не такие черные и белые, потому что у нас уже не все базовые классы:

int[,] foo2 = new int[2, 3];
foreach (var type in foo2.GetType().GetInterfaces())
{
    Console.WriteLine("{0}", type.ToString());
}

Сильный тип просто вышел из окна, и вы получите типы коллекций IList, ICollection и IEnumerable. Эй, как мы должны получить размер? При использовании базового класса Array мы могли бы использовать это:

Array array = foo2;
Console.WriteLine("Length = {0},{1}", array.GetLength(0), array.GetLength(1));

... но если мы посмотрим на альтернативы типа IList, то нет эквивалента. Как мы это решаем? Здесь следует ввести IList<int, int>? Конечно, это неправильно, потому что базовый тип - это просто int. Что насчет IMultiDimentionalList<int>? Мы можем сделать это и заполнить его методами, которые в настоящее время находятся в массиве.

  • Массивы имеют фиксированный размер

Вы заметили, что существуют специальные вызовы для перераспределения массивов? Это имеет все, что связано с управлением памятью: массивы настолько низки, что они не понимают, что такое рост или сокращение. В C вы использовали бы "malloc" и "realloc" для этого, и вам действительно нужно реализовать свои собственные "malloc" и "realloc", чтобы понять, почему именно фиксированные размеры важны для всех вещей, которые вы прямо выделяете.

Если вы посмотрите на это, есть только несколько вещей, которые выделяются в "фиксированных" размерах: массивы, все базовые типы значений, указатели и классы. По-видимому, мы обрабатываем массивы по-разному, так же, как мы обрабатываем основные типы по-разному.

Заметка о безопасности типов

Итак, зачем в первую очередь нужны все эти "точки доступа"?

Лучшая практика во всех случаях - предоставить пользователям безопасную точку доступа типа. Это можно проиллюстрировать путем сравнения кода следующим образом:

array.GetType().GetMethod("GetLength").Invoke(array, 0); // don't...

чтобы сделать следующий код:

((Array)someArray).GetLength(0); // do!

Тип безопасности позволяет вам быть неаккуратным при программировании. Если он используется правильно, компилятор обнаружит ошибку, если вы ее сделали, а не обнаружите ее во время выполнения. Я не могу достаточно подчеркнуть, насколько это важно - в конце концов, ваш код может вообще не вызываться в тестовом примере, в то время как компилятор всегда будет его оценивать!

Объединяя все это

Итак... пусть все вместе. Мы хотим:

  • Сильно типизированный блок данных
  • Он постоянно хранит свои данные
  • Поддержка IL, чтобы убедиться, что мы можем использовать классные инструкции процессора, которые быстро истекают кровью
  • Общий интерфейс, который предоставляет все функции
  • Тип безопасности
  • Multi-мерность
  • Мы хотим, чтобы типы значений сохранялись как типы значений
  • И та же структура сортировки, что и любой другой язык там
  • И фиксированный размер, потому что это облегчает распределение памяти

Это довольно немного требований низкого уровня для любой коллекции... для этого требуется, чтобы память была организована определенным образом, а также преобразование в ИЛ/ЦП... Я бы сказал, что есть веская причина, по которой она считалась основной тип.

Ответ 3

Совместимость. Массив - это исторический тип, который восходит к тому времени, когда не было никаких дженериков.

Сегодня было бы разумно иметь Array, затем Array<T>, а затем конкретный класс;)

Ответ 4

Таким образом, я хотел бы знать, почему это не так:

Причина в том, что generics не присутствовали в первой версии С#.

Но я не могу понять, в чем проблема.

Проблема заключается в том, что он разбил бы огромный объем кода, который использует класс Array. С# не поддерживает множественное наследование, поэтому строки вроде этого

Array ary = Array.Copy(.....);
int[] values = (int[])ary;

будет нарушено.

Если MS снова делала С# и .NET с нуля, тогда, вероятно, не возникло бы проблемы при создании Array общего класса, но это не реальность.

Ответ 5

В дополнение к другим проблемам, о которых говорили люди, попытка добавить общий Array<T> создаст еще несколько трудностей:

  • Даже если сегодня функции ковариации существовали с момента введения дженериков, их было бы недостаточно для массивов. Процедура, которая предназначена для сортировки Car[], сможет сортировать Buick[], даже если она должна скопировать элементы из массива в элементы типа Car, а затем скопировать их обратно. Копирование элемента из типа Car обратно в Buick[] не является безопасным по типу, но оно полезно. Можно было бы определить интерфейс одномерного массива ковариантного массива таким образом, чтобы сделать возможной сортировку (например, путем включения метода Swap (int firstIndex, int secondIndex)], но было бы трудно сделать что-то, что было бы гибким, как массивы.

  • В то время как тип Array<T> может хорошо работать для T[], в системе типа generic не было бы средств для определения семейства, которое включало бы T[], T[,], T[,,], T[,,,] и т.д. для произвольного числа индексов.

  • В .net нет средств для выражения понятия, что два типа следует считать одинаковыми, так что переменная типа T1 может быть скопирована в один из типов T2 и наоборот, с обе переменные, содержащие ссылки на один и тот же объект. Кто-то, использующий тип Array<T>, вероятно, захочет передать экземпляры в код, ожидающий T[], и принять экземпляры из кода, который использует T[]. Если массивы старого стиля не могут быть переданы и из кода, использующего новый стиль, то новые массивы будут скорее препятствием, чем функцией.

Могут существовать способы сглаживания системы типов, чтобы допускать тип Array<T>, который вел себя так, как должен, но такой тип будет вести себя разными способами, которые полностью отличались от других типов общего типа, и поскольку уже существует тип, который реализует желаемое поведение (т.е. T[]), неясно, какие выгоды получат от определения другого.

Ответ 6

Как все говорят - оригинал Array является неэквивалентным, потому что не было никаких дженериков, когда оно появилось в v1. Спекуляция ниже...

Чтобы сделать "Массив" общим (что будет иметь смысл сейчас), вы можете либо

  • сохранить существующий Array и добавить общую версию. Это хорошо, но большинство применений "Массив" связаны с его ростом с течением времени, и это, скорее всего, причина того, что вместо этого была реализована более эффективная реализация той же концепции List<T>. На данный момент добавление родовой версии "последовательного списка элементов, которые не растут" выглядит не очень привлекательно.

  • удалить не общий Array и заменить с помощью общей реализации Array<T> на тот же интерфейс. Теперь вам нужно сделать скомпилированный код для более старых версий для работы с новым типом вместо существующего типа Array. Хотя было бы возможно (и, скорее всего, трудно) для каркасного кода поддерживать такую ​​миграцию, всегда есть много кода, написанного другими людьми.

    Поскольку Array является очень базовым типом, почти каждый кусок существующего кода (который включает в себя настраиваемый код с отражением и сортировкой с помощью собственного кода и COM) использует его. В результате цена даже крошечной несовместимости между версиями (1.x → 2.x.Net Framework) была бы очень высокой.

Таким образом, тип Array должен оставаться навсегда. Теперь мы имеем List<T> как общий эквивалент, который следует использовать.

Ответ 7

Возможно, я что-то пропустил, но если экземпляр массива не был выбран или не использовался как ICollection, IEnumerable и т.д., тогда вы ничего не получите с массивом T.

Массивы бывают быстрыми и уже безопасны по типу и не несут накладные расходы в боксе/распаковке.