Дженерикс против общего интерфейса?

20

Я не помню, когда я писал общий класс в прошлый раз. Каждый раз, когда я думаю, что мне это нужно, после некоторого размышления я делаю вывод, что нет.

Второй ответ на этот вопрос заставил меня попросить разъяснений (поскольку я пока не могу комментировать, я сделал новый вопрос).

Итак, давайте возьмем данный код в качестве примера случая, когда нужны дженерики:

public class Repository<T> where T : class, IBusinessOBject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

Он имеет ограничения типа: IBusinessObject

Мой обычный способ мышления: класс ограничен в использовании IBusinessObject, так же как и классы, которые его используют Repository. Хранилище этих хранилищ IBusinessObject, наиболее вероятно, что клиенты этого Repositoryзахотят получать и использовать объекты через IBusinessObjectинтерфейс. Так почему бы не просто

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

То есть пример не очень хороший, поскольку это просто другой тип коллекции, а универсальная коллекция - это классика. В этом случае ограничение типа тоже выглядит странно.

На самом деле пример class Repository<T> where T : class, IBusinessbBjectвыглядит очень похоже class BusinessObjectRepositoryна меня. Это то, что дженерики сделаны, чтобы исправить.

Суть в том, что обобщенные элементы хороши для всего, кроме коллекций, и не делают ли ограничения типов универсальными как специализированные, как делает использование этого ограничения типа вместо универсального параметра типа внутри класса?

jungle_mole
источник

Ответы:

24

Давайте сначала поговорим о чисто параметрическом полиморфизме, а позже перейдем к ограниченному полиморфизму.

Параметрический Полиморфизм

Что означает параметрический полиморфизм? Ну, это означает, что тип, или, скорее, конструктор типа параметризован типом. Поскольку тип передается как параметр, вы не можете заранее знать, что это может быть. Вы не можете делать какие-либо предположения на основе этого. Теперь, если вы не знаете, что это может быть, то какая польза? Что ты можешь сделать с этим?

Ну, вы можете сохранить и получить его, например. Это тот случай, который вы уже упомянули: коллекции. Чтобы сохранить элемент в списке или массиве, мне ничего не нужно знать об этом элементе. Список или массив могут быть полностью забыты о типе.

Но как насчет 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-ограниченный полиморфизм

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
Йорг Миттаг
источник
я понимаю, что абстракция над типами полезна. я просто не вижу его использования в "реальной жизни". Func <> и Task <> и Action <> являются типами библиотек. и спасибо, что я interface IFoo<T> where T : IFoo<T>тоже помнил . это, очевидно, приложение в реальной жизни. пример отличный но по какой-то причине я не чувствую себя удовлетворенным. я скорее хочу обдумать, когда это уместно, а когда нет. Ответы здесь имеют некоторый вклад в этот процесс, но я все еще чувствую себя некомфортно из-за всего этого. это странно, потому что проблемы на уровне языка меня уже давно не беспокоят.
jungle_mole
Отличный пример. Я чувствовал, что вернулся в классную комнату. +1
Chef_Code
1
@Chef_Code: Надеюсь, это комплимент :-P
Jörg W Mittag
Да, это так. Позже я подумал о том, как это может быть воспринято после того, как я уже прокомментировал. Так что, чтобы подтвердить искренность ... Да, это комплимент больше ничего.
Chef_Code
14

Суть в том, что обобщенные элементы хороши для всего, кроме коллекций, и не делают ли ограничения типов универсальными как специализированные, как делает использование этого ограничения типа вместо универсального параметра типа внутри класса?

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

Ключевой момент здесь не в том, что сам репозиторий является более общим. Дело в том, что пользователи более специализированы - то есть, Repository<BusinessObject1>и Repository<BusinessObject2>это разные типы, и, кроме того, что, если я возьму Repository<BusinessObject1>, я знаю, что я BusinessObject1вернусь Get.

Вы не можете предложить эту строгую типизацию из простого наследования. Предложенный вами класс Repository не делает ничего, чтобы предотвратить путаницу в репозиториях для различных типов бизнес-объектов или гарантировать возвращение правильного типа бизнес-объектов.

DeadMG
источник
Спасибо, это имеет смысл. Но неужели весь смысл использования этой высоко оцененной языковой функции так же прост, как помощь пользователям, у которых отключен IntelliSense? (Я немного преувеличиваю, но я уверен, что вы
поняли
@zloidooraque: IntelliSense также не знает, какие объекты хранятся в хранилище. Но да, вы можете делать что-либо без генериков, если вместо этого хотите использовать приведение типов.
гексицид
@gexicide в том-то и дело: я не вижу, где мне нужно использовать приведение, если я использую общий интерфейс. Я никогда не говорил «использовать Object». Также я понимаю, зачем использовать дженерики при написании коллекций (принцип DRY). Возможно, мой первоначальный вопрос должен был
касаться
@zloidooraque: Это не имеет ничего общего с окружающей средой. Intellisense не может сказать вам, IBusinessObjectявляется ли это BusinessObject1или BusinessObject2. Он не может разрешить перегрузки на основе производного типа, который он не знает. Он не может отклонить код, который передает неправильный тип. Есть миллион битов более сильной типизации, с которой Intellisense ничего не может поделать. Лучшая поддержка инструментов - это хорошее преимущество, но оно не имеет ничего общего с основными причинами.
DeadMG
@DeadMG, и это моя точка зрения: intellisense не может этого сделать: использовать общий, так что может? это имеет значение? когда вы получаете объект по его интерфейсу, зачем убивать его? если ты это сделаешь, это плохой дизайн, нет? а почему и что такое "разрешать перегрузки"? пользователь не должен решать, будет ли вызывать метод или нет на основе производного типа, если он делегирует вызов правильного метода системе (что такое полиморфизм). и это снова приводит меня к вопросу: полезны ли дженерики вне контейнеров? я не спорю с тобой, мне действительно нужно это понять.
jungle_mole
13

«Скорее всего, клиенты этого репозитория захотят получать и использовать объекты через интерфейс IBusinessObject».

Нет, они не будут.

Давайте рассмотрим, что IBusinessObject имеет следующее определение:

public interface IBusinessObject
{
  public int Id { get; }
}

Он просто определяет идентификатор, потому что это единственная общая функциональность между всеми бизнес-объектами. И у вас есть два фактических бизнес-объекта: Person и Address (поскольку у Persons нет улиц, а адреса не имеют имен, вы не можете ограничить их оба интерфейсом commomial с функциональностью обоих. Это было бы ужасным дизайном, нарушающим Принцип сегрегации интерфейса «Я» в ТВЕРДОМ )

public class Person : IBusinessObject
{
  public int Id { get; private set; }
  public string Name { get; private set; }
}

public class Address : IBusinessObject
{
  public int Id { get; private set; }
  public string City { get; private set; }
  public string StreetName { get; private set; }
  public int Number { get; private set; }
}

Теперь, что происходит, когда вы используете универсальную версию хранилища:

public class Repository<T> where T : class, IBusinessObject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

Когда вы вызываете метод Get в универсальном репозитории, возвращаемый объект будет строго типизирован, что позволит вам получить доступ ко всем членам класса.

Person p = new Repository<Person>().Get(1);
int id = p.Id;
string name = p.Name;

Address a = new Repository<Address>().Get(1);
int id = a.Id;
string cityName = a.City;
int houseNumber = a.Number;

С другой стороны, когда вы используете неуниверсальный репозиторий:

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

Вы сможете получить доступ к членам только из интерфейса IBusinessObject:

IBussinesObject p = new Repository().Get(1);
int id = p.Id; //OK
string name = p.Name; //Oooops, you dont have "Name" defined on the IBussinesObject interface.

Итак, предыдущий код не будет компилироваться из-за следующих строк:

string name = p.Name;
string cityName = a.City;
int houseNumber = a.Number;

Конечно, вы можете привести IBussinesObject к реальному классу, но вы потеряете всю магию времени компиляции, которую позволяют дженерики (приводящие к InvalidCastExceptions в будущем), излишне пострадают от приведения ... И даже если вы этого не сделаете позаботьтесь о том, чтобы во время компиляции не проверялась ни производительность (вы должны это делать), но приведение после определенно не даст вам никакой выгоды по сравнению с использованием обобщенных элементов.

Лукас Корсалетти
источник