Давайте сначала поговорим о чисто параметрическом полиморфизме, а позже перейдем к ограниченному полиморфизму.
Что означает параметрический полиморфизм? Ну, это означает, что тип, или, скорее, конструктор типа параметризован типом. Поскольку тип передается как параметр, вы не можете заранее знать, что это может быть. Вы не можете делать какие-либо предположения на основе этого. Теперь, если вы не знаете, что это может быть, то какая польза? Что ты можешь сделать с этим?
Ну, вы можете сохранить и получить его, например. Это тот случай, который вы уже упомянули: коллекции. Чтобы сохранить элемент в списке или массиве, мне ничего не нужно знать об этом элементе. Список или массив могут быть полностью забыты о типе.
Но как насчет Maybe
типа? Если вы не знакомы с ним, Maybe
это тип, который может иметь значение, а может и нет. Где бы вы использовали это? Ну, например, при извлечении элемента из словаря: тот факт, что элемент может отсутствовать в словаре, не является исключительной ситуацией, поэтому вам действительно не следует создавать исключение, если элемент отсутствует. Вместо этого вы возвращаете экземпляр подтипа Maybe<T>
, который имеет ровно два подтипа: None
и Some<T>
. int.Parse
другой кандидат чего-то, что действительно должно вернуть Maybe<int>
вместо того, чтобы бросить исключение или весь int.TryParse(out bla)
танец.
Теперь вы можете утверждать, что Maybe
это своего рода список, который может иметь только ноль или один элемент. И, таким образом, своего рода коллекция.
Тогда как насчет Task<T>
? Это тип, который обещает вернуть значение в какой-то момент в будущем, но не обязательно имеет значение прямо сейчас.
Или что Func<T, …>
? Как бы вы представили концепцию функции от одного типа к другому, если не можете абстрагироваться от типов?
Или, в более общем плане: учитывая, что абстракция и повторное использование являются двумя основными операциями разработки программного обеспечения, почему вы не хотите иметь возможность абстрагироваться над типами?
Итак, давайте теперь поговорим об ограниченном полиморфизме. Ограниченный полиморфизм - это, в основном, место, где встречаются параметрический полиморфизм и полиморфизм подтипа: вместо того, чтобы конструктор типа полностью не обращал внимания на его параметр типа, вы можете связать (или ограничить) тип, чтобы он был подтипом некоторого указанного типа.
Вернемся к коллекциям. Возьми хеш-таблицу. Выше мы говорили, что списку не нужно ничего знать о его элементах. Ну, хеш-таблица делает: она должна знать, что она может их хешировать. (Примечание: в C # все объекты являются хешируемыми, точно так же, как все объекты можно сравнивать на равенство. Однако это не так для всех языков, и иногда считается ошибкой проектирования даже в C #.)
Итак, вы хотите ограничить ваш параметр типа для типа ключа в хеш-таблице, который будет экземпляром IHashable
:
class HashTable<K, V> where K : IHashable
{
Maybe<V> Get(K key);
bool Add(K key, V value);
}
Представьте, если бы вместо этого у вас было это:
class HashTable
{
object Get(IHashable key);
bool Add(IHashable key, object value);
}
Что бы вы сделали с value
тем, чтобы выбраться оттуда? Вы ничего не можете сделать с этим, вы только знаете, что это объект. И если вы итерируете по нему, все, что вы получите, это пара чего-то, что, как вы знаете, является IHashable
(которое не очень вам помогает, потому что у него есть только одно свойство Hash
), а что-то, что вы знаете - это object
(что помогает вам еще меньше).
Или что-то на основе вашего примера:
class Repository<T> where T : ISerializable
{
T Get(int id);
void Save(T obj);
void Delete(T obj);
}
Элемент должен быть сериализуемым, потому что он будет храниться на диске. Но что, если у вас есть это вместо этого:
class Repository
{
ISerializable Get(int id);
void Save(ISerializable obj);
void Delete(ISerializable obj);
}
С родовым случае, если вы поставите BankAccount
, вы получите BankAccount
обратно, с методами и свойствами , как Owner
, AccountNumber
, Balance
, Deposit
, Withdraw
и т.д. Что - то вы можете работать с. Теперь другой случай? Вы кладете в , BankAccount
но вы получите обратно Serializable
, который имеет только одно свойство: AsString
. Что ты собираешься делать с этим?
Есть также несколько хитрых трюков, которые вы можете сделать с ограниченным полиморфизмом:
F-ограниченная квантификация - это, в основном, переменная типа, которая снова появляется в ограничении. Это может быть полезно в некоторых обстоятельствах. Например, как вы пишете ICloneable
интерфейс? Как вы пишете метод, где возвращаемый тип является типом реализующего класса? На языке с функцией MyType это легко:
interface ICloneable
{
public this Clone(); // syntax I invented for a MyType feature
}
На языке с ограниченным полиморфизмом вы можете сделать что-то вроде этого:
interface ICloneable<T> where T : ICloneable<T>
{
public T Clone();
}
class Foo : ICloneable<Foo>
{
public Foo Clone()
{
// …
}
}
Обратите внимание, что это не так безопасно, как версия MyType, потому что ничто не мешает кому-то просто передать «неправильный» класс конструктору типов:
class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
public SomethingTotallyUnrelatedToBar Clone()
{
// …
}
}
Абстрактный тип Члены
Как выясняется, если у вас есть абстрактные члены типа и подтипы, вы можете фактически обойтись без параметрического полиморфизма и все же делать все то же самое. Scala движется в этом направлении, будучи первым основным языком, который начинал с дженериков, а затем пытался удалить их, а это совсем наоборот, например, с Java и C #.
По сути, в Scala, подобно тому, как вы можете иметь поля, свойства и методы в качестве членов, вы также можете иметь типы. И точно так же, как поля, свойства и методы могут быть оставлены абстрактными для последующей реализации в подклассе, члены типа также могут быть оставлены абстрактными. Давайте вернемся к простым коллекциям, List
которые выглядели бы примерно так, если бы они поддерживались в C #:
class List
{
T; // syntax I invented for an abstract type member
T Get(int index) { /* … */ }
void Add(T obj) { /* … */ }
}
class IntList : List
{
T = int;
}
// this is equivalent to saying `List<int>` with generics
interface IFoo<T> where T : IFoo<T>
тоже помнил . это, очевидно, приложение в реальной жизни. пример отличный но по какой-то причине я не чувствую себя удовлетворенным. я скорее хочу обдумать, когда это уместно, а когда нет. Ответы здесь имеют некоторый вклад в этот процесс, но я все еще чувствую себя некомфортно из-за всего этого. это странно, потому что проблемы на уровне языка меня уже давно не беспокоят.Нет . Вы слишком много думать о том
Repository
, где она находится в значительной степени то же самое. Но это не то, для чего существуют дженерики. Они там для пользователей .Ключевой момент здесь не в том, что сам репозиторий является более общим. Дело в том, что пользователи более специализированы - то есть,
Repository<BusinessObject1>
иRepository<BusinessObject2>
это разные типы, и, кроме того, что, если я возьмуRepository<BusinessObject1>
, я знаю, что яBusinessObject1
вернусьGet
.Вы не можете предложить эту строгую типизацию из простого наследования. Предложенный вами класс Repository не делает ничего, чтобы предотвратить путаницу в репозиториях для различных типов бизнес-объектов или гарантировать возвращение правильного типа бизнес-объектов.
источник
Object
». Также я понимаю, зачем использовать дженерики при написании коллекций (принцип DRY). Возможно, мой первоначальный вопрос должен былIBusinessObject
является ли этоBusinessObject1
илиBusinessObject2
. Он не может разрешить перегрузки на основе производного типа, который он не знает. Он не может отклонить код, который передает неправильный тип. Есть миллион битов более сильной типизации, с которой Intellisense ничего не может поделать. Лучшая поддержка инструментов - это хорошее преимущество, но оно не имеет ничего общего с основными причинами.«Скорее всего, клиенты этого репозитория захотят получать и использовать объекты через интерфейс IBusinessObject».
Нет, они не будут.
Давайте рассмотрим, что IBusinessObject имеет следующее определение:
Он просто определяет идентификатор, потому что это единственная общая функциональность между всеми бизнес-объектами. И у вас есть два фактических бизнес-объекта: Person и Address (поскольку у Persons нет улиц, а адреса не имеют имен, вы не можете ограничить их оба интерфейсом commomial с функциональностью обоих. Это было бы ужасным дизайном, нарушающим Принцип сегрегации интерфейса «Я» в ТВЕРДОМ )
Теперь, что происходит, когда вы используете универсальную версию хранилища:
Когда вы вызываете метод Get в универсальном репозитории, возвращаемый объект будет строго типизирован, что позволит вам получить доступ ко всем членам класса.
С другой стороны, когда вы используете неуниверсальный репозиторий:
Вы сможете получить доступ к членам только из интерфейса IBusinessObject:
Итак, предыдущий код не будет компилироваться из-за следующих строк:
Конечно, вы можете привести IBussinesObject к реальному классу, но вы потеряете всю магию времени компиляции, которую позволяют дженерики (приводящие к InvalidCastExceptions в будущем), излишне пострадают от приведения ... И даже если вы этого не сделаете позаботьтесь о том, чтобы во время компиляции не проверялась ни производительность (вы должны это делать), но приведение после определенно не даст вам никакой выгоды по сравнению с использованием обобщенных элементов.
источник