Generics vs загальний інтерфейс?


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на мене. Яка річ зроблена для того, щоб виправити.

Вся справа в тому: чи загальні дженери підходять для будь-якого, крім колекцій, і не застосовують обмеження типу, роблять загальні як спеціалізовані, як це застосовує обмеження цього типу замість загального параметра типу всередині класу?

Відповіді:


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 .

Ви не можете запропонувати такий сильний набір тексту від простого успадкування. Запропонований клас репозиторію не робить нічого, що заважає людям плутати сховища для різних видів бізнес-об’єкта або гарантувати, що правильний вид бізнес-об’єкта не повернеться.


Дякую, це має сенс. Але чи весь сенс використання цієї високо оціненої мовної функції настільки простий, як допомога користувачам, які вимкнули IntelliSense? (Я трохи перебільшую, але я впевнений, що ви зрозуміли)
jungle_mole

@zloidooraque: Також IntelliSense не знає, який тип об'єктів зберігається у сховищі. Але так, ви можете зробити що завгодно без дженерики, якщо бажаєте використовувати замість них касти.
гексицид

@gexicide - це справа: я не бачу, де мені потрібно використовувати касти, якщо я використовую загальний інтерфейс. Я ніколи не казав "користуватися Object". Також я розумію, навіщо використовувати дженерики під час написання колекцій (принцип DRY). Можливо, у мого початкового запитання мало бути щось про використання дженеріки поза контекстом колекцій ..
jungle_mole

@zloidooraque: Це не має нічого спільного з довкіллям. Intellisense не може сказати вам, чи IBusinessObjectє a BusinessObject1чи a BusinessObject2. Він не може вирішити перевантаження на основі похідного типу, який він не знає. Він не може відхилити код, який передається неправильного типу. Є мільйон бітів сильнішого набору тексту, який Intellisense не може зробити абсолютно нічого. Краща підтримка інструментів - це приємна користь, але насправді нічого спільного з основними причинами.
DeadMG

@DeadMG, і в цьому моя думка: intellisense не може цього зробити: використовувати generic, щоб це могло? це важливо? коли ви отримуєте об'єкт за його інтерфейсом, навіщо його применшувати? якщо ви це зробите, це поганий дизайн, ні? і чому і що таке "вирішення перевантажень"? Користувач не повинен вирішувати, чи буде метод виклику заснований на похідному типі, якщо він делегує метод виклику правильної системи (який поліморфізм). і це знову призводить мене до питання: чи корисні дженерики поза контейнерами? Я не сперечаюся з тобою, мені дуже потрібно це зрозуміти.
jungle_mole

13

"найімовірніше, клієнти цього Репозиторію захочуть отримувати та використовувати об'єкти через інтерфейс IBusinessObject".

Ні, вони не будуть.

Розглянемо, що IBusinessObject має таке визначення:

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

Він просто визначить Id, оскільки це єдиний спільний функціонал між усіма бізнес-об'єктами. І у вас є два фактичні бізнес-об’єкти: "Особа" та "Адреса" (оскільки у людей немає вулиць, а адреси не мають імен, ви не можете обмежувати їх обом інтерфейсом комунікації з функціональністю обох. Це було б жахливим дизайном, що порушив би Принцип сегментації інтерфейсу , "Я" у SOLID )

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 вниз по дорозі), будете страждати від литих накладних витрат без потреби ... І навіть якщо ви цього не зробите дбайте про те, щоб перевірити час компіляції, ані продуктивність (ви не повинні), а кастинг після цього точно не дасть вам користі в порівнянні з використанням генеричних даних.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.