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


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параметров типа значения может фактически изменить тип значения.
— 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;

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

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

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


Можете ли вы привести пример того, как бы вы использовали более слабую версию гарантии?
— ГрегРос

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 выполнять дополнительную работу, поэтому, если нет никакой выгоды, никто не должен этого делать. С другой стороны, очень часто применяется, по крайней мере, одно из указанных выше преимуществ.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.