В чем причина использования интерфейса по сравнению с типом с общими ограничениями?

15

В объектно-ориентированных языках, которые поддерживают параметры универсального типа (также называемые шаблонами классов и параметрическим полиморфизмом, хотя, конечно, каждое имя несет в себе различные коннотации), часто можно указать ограничение типа для параметра типа, например, для его наследования. из другого типа. Например, это синтаксис в C #:

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

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

Каковы причины использовать фактические типы интерфейсов над типами, ограниченными этими интерфейсами? Например, каковы причины создания подписи метода I2 ExampleMethod(I2 value)?

GregRos
источник
4
шаблоны классов (C ++) - это нечто совершенно другое и гораздо более мощное, чем жалкие обобщения. Хотя языки, имеющие дженерики, заимствовали шаблонный синтаксис для них.
дедупликатор
Методы интерфейса являются косвенными вызовами, а методы типа могут быть прямыми вызовами. Таким образом, последний может быть быстрее первого, а в случае refпараметров типа значения может фактически изменить тип значения.
user541686
@Deduplicator: Учитывая, что генерики старше шаблонов, я не вижу, как генерики вообще могли позаимствовать что-либо из шаблонов, синтаксиса или иным образом.
Йорг Миттаг
3
@ JörgWMittag: Я подозреваю, что под «объектно-ориентированными языками, которые поддерживают дженерики», Deduplicator мог бы понимать «Java и C #», а не «ML и Ada». Тогда влияние C ++ на первое очевидно, несмотря на то, что не все языки, имеющие дженерики или параметрический полиморфизм, заимствованы из C ++.
Стив Джессоп
2
@SteveJessop: ML, Ada, Eiffel, Haskell предшествовали шаблонам C ++, появились Scala, F #, OCaml, и ни один из них не разделяет синтаксис C ++. (Интересно, что даже D, который в значительной степени заимствует из C ++, особенно из шаблонов, не разделяет синтаксис C ++.) Я думаю, что «Java и C #» - это довольно узкое представление о «языках, имеющих дженерики».
Йорг Миттаг

Ответы:

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)если a, bи cэто intпотому , что мы не знаем , что этот метод будет возвращать 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;

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

Второй принимает любой тип, реализующий интерфейс, и гарантирует, что он снова вернет хотя бы этот тип (а не что-то, что удовлетворяет менее ограничивающему интерфейсу).

Иногда вам нужна более слабая гарантия. Иногда хочется более сильного.

Telastyn
источник
Можете ли вы привести пример того, как бы вы использовали более слабую версию гарантии?
ГрегРос
4
@GregRos - Например, в некотором коде парсера, который я написал. У меня есть функция, Orкоторая принимает два Parserобъекта (абстрактный базовый класс, но принцип работает) и возвращает новый Parser(но с другим типом). Конечный пользователь не должен знать или заботиться о том, какой конкретно тип.
Теластин
В C # я представляю, что возвращение T, отличного от того, который был передан, почти невозможно (без отражения) без нового ограничения, а также делает вашу сильную гарантию довольно бесполезной.
NtscCobalt
1
@NtscCobalt: это более полезно, когда вы комбинируете параметрическое и интерфейсное программирование. Например, что LINQ делает все время (принимает IEnumerable<T>, возвращает другое, IEnumerable<T>которое, например, фактически является OrderedEnumerable<T>)
Бен Фойгт
2

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

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

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

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

  4. Структура, которая реализует интерфейс, может быть сохранена как тип значения при передаче в метод, принимающий ограниченный универсальный параметр, но должна быть помещена в коробку при передаче как тип интерфейса. Это может оказать огромное влияние на скорость.

  5. Универсальный параметр может иметь несколько ограничений, в то время как нет другого способа указать параметр «некоторого типа, который реализует как IFoo, так и IBar». Иногда это может быть обоюдоострый меч, так как код, получивший параметр типа, IFooбудет очень трудно передать его такому методу, ожидающему универсальный тип с двойным ограничением, даже если рассматриваемый экземпляр будет удовлетворять всем ограничениям.

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

Supercat
источник