ОБОБЩЕНИЯ
Были введены начиная с версии .Net 2.0 в C#. В этот же момент расширилась библиотека базовых классов новым пространством имен System.Collections.Generic.
Что было до: В C# имелась возможность создавать обобщенный код работая ссылками типа object. Класс object является базовым для всех остальных классов, поэтому по ссылке типа object можно обращаться к объекту любого типа.
Проблемы предыдущих подходов: проблемы с типо-безопасностью, для преобразования типа object в конкретный тип данных требовалось приведение типов, которое могло быть выполнено неверно.
Преимущество ООП: возможность многократного использования кода за счет создания производных классов, наследующих все возможности базового класса.
Определение: По существу, параметризированный тип. В общем - механизм, поддерживаемый средой CLR и языками программирования для многократного использования алгоритмов.
Смысл: Позволяют создавать классы, структуры, интерфейсы, методы и делегаты, где обрабатываемые данные указываются в виде параметра. Класс, метод, ... и т.д. называются обобщенными если работают с параметризируемым типом данных.
Преимущества:
- типовая безопасность;
- производительность (избежание распаковки и упаковки типов);
- исключает приведение типов -> процесс упрощается;
- расширение возможности повторного использования двоичного кода (обобщенный класс, определенный однажды, можно использовать для создания экземпляров разных типов);
- код с алгоритмом закрыт для модификаций и открыт для использования.
Недостаток:
- Определение обобщенного класса включается в сборку, создание на его основе специфичных типов не приводит к дублированию кода в IL. НО когда обобщенные методы компилируются в машинный код, для каждого типа значения создается новый класс. Ссылочные типы при этом разделяют общую реализацию одного родного класса - каждый элемент представлен в памяти 4-байтовым адресом (на 32 разрядных системах) и машинные реализации обобщенного класса с различными ссылочными типами-параметрами не отличаются друг от друга. Типы значений содержатся в памяти целиком, каждый их них в основном требует разный объем памяти, поэтому для каждого из них создается свои экземпляры классов на основе обобщенного.
- Не можем написать математический алгоритм для произвольных числовых типов данных (тип данных не известен).
Принцип: разработчик описывает алгоритм (сортировки, поиска, замены ...), но не указывает типы данных, с которыми тот работает. Это позволяет применять алгоритм к объектам разных типов.
Большинство алгоритмов инкапсулировано в типе. CLR поддерживает создание как обобщенных ссылочных, так и обобщенных значимых типов, однако обобщенные перечисляемые типы не поддерживаются (enum). Кроме того, CLR позволяет создавать обобщенные интерфейсы и делегаты. Иногда полезный алгоритм инкапсулирован в одном методе, поэтому CLR поддерживает создание обобщенных методов, определенных в ссылочном типе, в значимом типе или в интерфейсе.
Пояснения:
Ссылочные типы (object, dynamic, string, class, interface, delegate) хранятся в управляемой куче;
Типы значений (struct, enum; bool, byte, char, int, float, double) — в стеке приложения (кроме случая, когда тип значения является полем класса).
Преобразование типа значений к ссылочному типу сопровождается неявной операцией упаковки (boxing) — помещение копии типа значений в класс-обёртку, экземпляр которого сохраняется в куче. Упаковочный тип генерируется CLR и реализует интерфейсы сохраняемого типа значения.
Преобразование ссылочного типа к типу значений вызывает операцию распаковки (unboxing) — извлечение из упаковки копии типа значения и помещение её в стек.
Открытые и закрытые типы
CLR создает внутреннюю структуру данных для каждого типа (объекты-типы), применяемого в приложении.
Типы с обобщенными параметрами типами также считаются типами, для каждого из них CLR создает внутренний объект тип (для ссылочных типов (классов), значимых типов (структур), интерфейсов и делегатов).
Тип с обобщенными параметрами типами - открытые типы, в CLR запрещено конструирование экземпляров открытых типов (как и экземпляров типов интерфейсов).
При ссылке на обобщенный тип в коде можно определить набор обобщенных аргументов типов. Если всем аргументам типам определенного типа передать действительные типы данных, то такой тип будут называть закрытым. CLR разрешает создание экземпляров закрытых типов.
Имена типов заканчиваются левой одиночной кавычкой (‘), за которой следует число - арность типа, число необходимых для него параметров типов.
Например, арность класса Dictionary равна 2, потому что требуется определить типы TKey и TValue. Арность класса DictionaryStringKey — 1, так как требуется указать лишь один тип — TValue.
Замечание: CLR размещает статические поля типа в самом объекте типе, поэтому у каждого закрытого типа есть свои статические поля.
Например, статические поля, определенные в объекте List<T>, не будут совместно использоваться объектами List<DateTime> и List<String>, потому что у каждого объекта закрытого типа есть свои статические поля. Если же в обобщенном типе определен статический конструктор, то последний выполняется для закрытого типа лишь раз.
Обобщенные типы и наследование
!!! Обобщенный тип может быть производным от других типов. При использовании обобщенного типа с указанием аргументов-типов в CLR определяется новый объект-тип, производный от того же типа, что и обобщенный тип.
Например, List<T> является производным от Object, поэтому List<String> и List<Guid> тоже производные от Object.
Зачем: Понимание, что определение аргументов-типов не имеет ничего общего с иерархиями наследования, позволяет разобраться, какие приведения типов допустимы, а какие нет.
Проблемы с идентификацией и тождеством обобщенных типов
Проблема: Синтаксис обобщенных типов часто приводит в замешательство. В исходном коде оказывается слишком много знаков «меньше» (>), что сильно затрудняет его чтение.
Решение: C# позволяет использовать упрощенный синтаксис для ссылки на обобщенный закрытый тип, не влияя на эквивалентность типов. Для этого в начало файла с исходным текстом нужно добавить директиву using:
using DateTimeList = System.Collections.Generic.List;
using определяет символ DateTimeList. При компиляции кода компилятор заменяет все DateTimeList на System.Collections.Generic.List .Таким образом, разработчики могут использовать упрощенный синтаксис, не влияя на смысл кода и тем самым сохраняя идентификацию и тождество типов.
"Распухание" кода
Проблема: При JIT-компиляции метода, в котором используются обобщенные параметры типы, CLR подставляет в IL-код метода указанные аргументы-типы и создает машинный код для данного метода, работающего с конкретными типами данных (одна из основных функций обобщения). НО: CLR генерирует машинный код для каждого сочетания «метод + тип», что приводит к распуханию кода, и в итоге существенно увеличивается рабочий набор приложения и ухудшается производительность.
Решение 1: Если метод вызывается для конкретного аргумента-типа и позже он вызывается опять с тем же аргументом-типом, CLR компилирует код для такого сочетания «метод + тип» только один раз (даже для разных сборок). Это существенно снижает распухание кода.
Решение 2: CLR считает все аргументы ссылочного типа тождественными, что обеспечивает совместное использование кода.
Например, код, скомпилированный в CLR для методов List<String>, может применяться для методов List<Stream>, потому что String и Stream — ссылочные типы.
Однако, если аргументы-типы имеют значимый тип, CLR должна сгенерировать машинный код именно для этого значимого типа (у значимых типов может быть разный размер). Даже если размер одинаковый, CLR не может использовать для них один код, потому что для обработки этих значений могут применяться различные машинные команды.
Обобщенные интерфейсы
Преимущество обобщений: способность определять обобщенные ссылочные и значимые типы.
Что: Поддержка обобщенных интерфейсов.
Зачем: Любая попытка работы со значимым типом через необобщенный интерфейс (например, IComparable) будет приводить к упаковке и потере безопасности типов в процессе компиляции (сильно сузило бы применение обобщенных типов).
Как: CLR поддерживает обобщенные интерфейсы.
Ссылочный и значимый тип реализуют обобщенный интерфейс через задание аргументов-типов;
Любой тип реализует обобщенный интерфейс, не определяя аргументы-типы.
Обобщенные делегаты
Делегат — определение класса с помощью четырех методов: конструктора и методов Invoke, BeginInvoke и EndInvoke.
При определении типа делегата с параметрами-типами, компилятор определяет методы класса делегата, а параметры-типы применяются ко всем методам, параметры и возвращаемые значения которых относятся к указанному параметру-типу.
Зачем:
·Поддержка позволяет передавать методам обратного вызова любые типы объектов, обеспечивая безопасность типов.
·Экземпляры значимого типа могут передаваться методам обратного вызова без упаковки.
!!!!Обобщенные методы
Теория: При определении обобщенного ссылочного и значимого типа или интерфейса все методы, определенные в этих типах, могут ссылаться на любой параметр-тип, заданный этим типом. Параметр-тип может использоваться как параметр метода, возвращаемое значение метода или как заданная внутри него локальная переменная.
Зачем: CLR позволяет методу задавать собственные параметры-типы, которые могут использоваться в качестве параметров, возвращаемых значений или локальных переменных.
Любопытно: Использование обобщенных типов с методами, принимающими параметры out и ref, интересно тем, что переменные, передаваемые в качестве аргумента out/ref, должны быть того же типа, что и параметр метода, чтобы избежать возможных нарушений безопасности типов.
Логический вывод обобщенных методов и типов
Зачем: Для упрощения создания, чтения и работы с кодом в компиляторе С# имеется логический вывод типов (type inference) при вызове обобщенных методов.
Как: Компилятор пытается определить (или логически вывести) тип, который будет автоматически использоваться при вызове обобщенного метода.
Теория: В типе могут определяться несколько методов так, что один из них будет принимать конкретный тип данных, а другой — обобщенный параметр-тип.
Обобщения и другие члены
Проблема: В C# у свойств, индексаторов, событий, методов операторов, конструкторов и деструкторов не может быть параметров-типов.
Решение: Их можно определить в обобщенном типе с тем, чтобы в коде этих членов использовать параметры-типы этого типа.
Верификация и ограничения
Что: В процессе компиляции обобщенного кода компилятор C# анализирует его, убеждаясь, что он будет работать с любыми типами данных — существующими и теми, которые будут определены в будущем.
Проблема: Может показаться, что при использовании обобщений можно лишь объявлять переменные обобщенного типа, назначать переменные, вызывать методы, определенные Object, и все!
Решение: Компиляторы и CLR поддерживают механизм ограничений (constraints), которые позволяют сузить перечень типов, которые можно передать в обобщенном аргументе, и расширяют возможности по работе с этими типами.
Подробнее: Ограничения можно применять к параметрам-типам обобщенных типов и обобщенных методов. CLR не поддерживает перегрузку по именам параметров-типов или ограничений. Перегрузка типов и методов выполняется только по арности.
При переопределении виртуального обобщенного метода в переопределяющем методе нужно задавать то же число параметров-типов, а они, в свою очередь, наследуют ограничения, заданные для них методом базового класса.
Основные ограничения
Что: К параметру-типу могут применяться следующие ограничения: основное, дополнительное и/или ограничение конструктора.
Зачем: В параметре-типе можно задать не более одного основного ограничения (это может быть ссылочный тип, указывающий на неизолированный класс).
Нельзя использовать для этой цели следующие ссылочные типы: System.Object, System.Array, System.Delegate, System.MulticastDelegate, System.ValueType, System.Enum и System.Void.
При задании ограничения ссылочного типа вы обязуетесь перед компилятором, что любой аргумент-тип будет либо того же типа, что и ограничение, либо производного от него типа.
Подробнее о них: два особых основных ограничения: class и struct.
Ограничение class гарантирует компилятору, что указанный аргумент-тип будет ссылочного типа. Этому ограничению удовлетворяют все типы-классы, типы-интерфейсы, типы-делегаты и типы-массивы.
Ограничение struct гарантирует компилятору, что указанный аргумент-тип будет значимого типа. Ограничению удовлетворяют все значимые типы и перечисления.
НО, Компилятор и CLR рассматривают любой значимый тип System.Nullable как особый, а значимые типы с поддержкой значения null не подходят под это ограничение.
Дополнительные ограничения
Что: В параметре-типе можно задать несколько дополнительных ограничений или не задавать их вовсе.
Как: При задании ограничения типа-интерфейса компилятору гарантируется, что указанный аргумент-тип будет типом, реализующим этот интерфейс. Поскольку можно задать несколько ограничений интерфейса, в аргументе-типе должен указываться тип, реализующий все ограничения интерфейса.
Задание ограничения параметра-типа позволяет обобщенному типу / методу указать, что между заданными аргументами-типами должны быть определенные отношения. К параметру-типу может применяться несколько / ни одного ограничения типа.
Ограничения конструктора
Что: В параметре-типе можно задавать не более одного ограничения конструктора.
Зачем: Ограничение конструктора указывает компилятору, что указанный аргумент-тип будет неабстрактного типа, реализующего открытый конструктор без параметров.
Важно: Компилятор С# считает за ошибку одновременное задание ограничения конструктора и ограничения struct, потому что у всех значимых типов неявно присутствует открытый конструктор без параметров.
Другие вопросы верификации
Приведение переменной обобщенного типа
Что: Приведение переменной обобщенного типа к другому типу допускается, лишь если она приводится к типу, разрешенному ограничением.
Пример:
Присвоение переменной обобщенного типа значения по умолчанию
Что: Приравнивание переменной обобщенного типа к null допустимо, только если обобщенный тип ограничен ссылочным типом.
Пример:
Интересно: Ключевое слово default дает команду компилятору C# и JIT-компилятору CLR создать код, приравнивающий temp к null, если T — ссылочного типа, и обнуляющий все биты переменной temp, если Т — значимого типа.
Сравнение переменной обобщенного типа с null
Что: Сравнение переменной обобщенного типа с null с помощью операторов «==» и «!=» допустимо, не зависимо от того, ограничен обобщенный тип или нет.
Пример:
Сравнение двух переменных обобщенного типа
Что: Сравнение двух переменных одинакового обобщенного типа допустимо только в том случае, если обобщенный параметр-тип имеет ссылочный тип.
Пример:
Использование переменных обобщенного типа в качестве операндов
Проблема: Использование операторов с операндами обобщенного типа. C# умеет интерпретировать операторы (например +, , * и /), применяемые к элементарным типам. Но их нельзя использовать с переменными обобщенного типа, потому что во время компиляции компилятор не знает их тип.
Мы не можем написать математический алгоритм для произвольных числовых типов данных.