Що є причиною використання інтерфейсу проти загальновизнаного типу


15

У об'єктно-орієнтованих мовах, які підтримують параметри загального типу (також відомі як шаблони класів та параметричний поліморфізм, хоча, звичайно, кожне ім’я має різні конотації), часто можна вказати обмеження типу на параметр типу, таким, щоб його зменшити з іншого типу. Наприклад, це синтаксис у C #:

//for classes:
class ExampleClass<T> where T : I1 {

}
//for methods:
S ExampleMethod<S>(S value) where S : I2 {
        ...
}

Які причини використовувати фактичні типи інтерфейсів для типів, обмежених цими інтерфейсами? Наприклад, які причини зробити метод підписом I2 ExampleMethod(I2 value)?


4
шаблони класів (C ++) - це щось зовсім інше і набагато більш потужне, ніж шкідливі дженерики. Навіть незважаючи на те, що мови, що мають дженерики, запозичили для них синтаксис шаблонів.
Дедуплікатор

Методи інтерфейсу - це непрямі дзвінки, тоді як методи типу можуть бути прямими дзвінками. Таким чином, остання може бути швидшою, ніж перша, і у випадку refпараметрів типу значення може фактично змінювати тип значення.
користувач541686

@ Дедуплікатор: Враховуючи, що дженерики старіші за шаблони, я не бачу, як генерики могли взагалі запозичити що-небудь із шаблонів, синтаксис чи інше.
Йорг W Міттаг

3
@ JörgWMittag: Я підозрюю, що під "об'єктно-орієнтованими мовами, які підтримують дженерики", Deduplicator міг би зрозуміти "Java та C #", а не "ML та Ada". Тоді вплив C ++ на колишній ясний, незважаючи на те, що не всі мови, що мають генеричні або параметричні поліморфізм, запозичені з C ++.
Стів Джессоп

2
@SteveJessop: ML, Ada, Eiffel, Haskell предшествуют шаблонам C ++, після цього з'явилися Scala, F #, OCaml, і жоден з них не має синтаксису C ++. (Цікаво, що навіть D, який сильно запозичує C ++, особливо шаблони, не поділяє синтаксис C ++.) "Ява та C #" - це досить вузький погляд на "мови, що мають дженерики", я думаю.
Йорг W Міттаг

Відповіді:


21

Використання параметричної версії дає

  1. Більше інформації для користувачів функції
  2. Обмежує кількість програм, які можна написати (безкоштовна перевірка помилок)

Як випадковий приклад, припустимо, у нас є метод, який обчислює корені квадратичного рівняння

int solve(int a, int b, int c) {
  // My 7th grade math teacher is laughing somewhere
}

І тоді ви хочете, щоб він працював на інших видах, таких як речі int. Можна написати щось на кшталт

Num solve(Num a, Num b, Num c){
  ...
}

Справа в тому, що це не говорить про те, що ви хочете. Він говорить

Дайте мені будь-які 3 речі, подібні до числа (не обов'язково однаково), і я поверну вам якесь число

Ми не можемо зробити щось на кшталт int sol = solve(a, b, c)if a, bі cє ints, тому що ми не знаємо, що метод поверне intв кінці! Це призводить до деяких незграбних танців з придушенням і молитвою, якщо ми хочемо використовувати рішення в більшому виразі.

Всередині функції хтось може подати нам поплавці, бігінти та градуси, і нам доведеться додавати та множувати їх разом. Ми хотіли б статично відкинути це, оскільки операції між цими 3 класами будуть химерними. Градуси є мод 360, тому не буде випадку, a.plus(b) = b.plus(a)і подібні розбіжності виникнуть.

Якщо ми використовуємо параметричний поліморфізм з підтипом, ми можемо виключити все це, оскільки наш тип насправді говорить про те, що ми маємо на увазі

<T : Num> T solve(T a, T b, T c)

Або словами "Якщо ви дасте мені якийсь тип, який є числом, я можу вирішити рівняння з цими коефіцієнтами".

Це з'являється і в багатьох інших місцях. Інший хороший джерело прикладів є функції, абстрактної над яким - то контейнером, ала reverse, sort, mapі т.д.


8
Підсумовуючи, загальна версія гарантує, що всі три входи (і вихідні) будуть однотипними числами .
Математична

Однак це стає недоліком, коли ви не контролюєте відповідний тип (і, отже, не можете додати інтерфейс до нього). Для максимальної загальності вам доведеться прийняти інтерфейс, параметризований за типом аргументу (наприклад Num<int>), як додатковий аргумент. Ви завжди можете реалізувати інтерфейс для будь-якого типу за допомогою делегування. По суті, це типи класів Haskell, за винятком набагато більш втомливих у використанні, оскільки вам доведеться явно проходити по інтерфейсу.
Доваль

16

Які причини використовувати фактичні типи інтерфейсів для типів, обмежених цими інтерфейсами?

Тому що це вам потрібно ...

IFoo Fn(IFoo x);
T Fn<T>(T x) where T: IFoo;

два рішуче різні підписи. Перший приймає будь-який тип, що реалізує інтерфейс, і єдина гарантія, яку він дає, - це те, що повернене значення задовольняє інтерфейс.

Другий приймає будь-який тип, що реалізує інтерфейс, і гарантує, що він поверне принаймні цей тип знову (а не те, що задовольняє менш обмежувальний інтерфейс).

Іноді, ви хочете більш слабку гарантію. Іноді хочеться сильнішого.


Чи можете ви навести приклад того, як ви б використали слабший варіант гарантії?
GregRos

4
@GregRos - Наприклад, у якомусь коді аналізатора я писав. У мене є функція, Orяка бере два Parserоб'єкти (абстрактний базовий клас, але принцип дотримується) і повертає новий Parser(але з іншим типом). Кінцевий користувач не повинен знати або дбати про те, що таке конкретний тип.
Теластин

У C # я уявляю, що повернути T, окрім того, що було передано, майже неможливо (без відбиття болю) без нового обмеження, а також зробить вашу гарантію досить марною самостійно.
NtscCobalt

1
@NtscCobalt: Це корисніше, коли ви поєднуєте параметричне та загальне інтерфейсне програмування. Наприклад, що LINQ робить весь час (приймає IEnumerable<T>, повертає інше IEnumerable<T>, наприклад, насправді OrderedEnumerable<T>)
Ben Voigt

2

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

  1. Метод , який приймає обмежується , як родове refабо outпараметр може бути переданий змінної , яка задовольняє обмеженню; на відміну від цього, негенеричний метод з параметром інтерфейсу обмежується прийняттям змінних такого типу інтерфейсу.

  2. Метод із загальним параметром типу T може приймати загальні колекції T. Метод, який приймає атрибут a IList<T> where T:IAnimal, зможе прийняти a List<SiameseCat>, але метод, який захотів IList<Animal>, не зможе цього зробити.

  3. Обмеження іноді може визначати інтерфейс з точки зору загального типу, наприклад where T:IComparable<T>.

  4. Структура, яка реалізує інтерфейс, може зберігатися як тип значення, коли передається методу, що приймає обмежений загальний параметр, але повинна бути встановлена ​​у вікні, коли передається як тип інтерфейсу. Це може мати величезний вплив на швидкість.

  5. Загальний параметр може мати декілька обмежень, тоді як немає іншого способу вказати параметр "певного типу, який реалізує і IFoo, і IBar". Іноді це може бути двосхилий меч, оскільки код, який отримав параметр типу, IFooбуде дуже важко передати його такому методу, очікуючи на подвійне обмеження загального, навіть якщо відповідний екземпляр задовольняє всі обмеження.

Якщо в певній ситуації не було б переваги використання загального, просто прийміть параметр типу інтерфейсу. Використання generic змусить систему типу та JITter зробити додаткову роботу, тому, якщо немає користі, ніхто не повинен цього робити. З іншого боку, дуже часто зустрічається хоча б одна з перерахованих вище переваг.

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