Jump to content

Ковариантность и контравариантность (информатика)

(Перенаправлено из Ковариация параметров )

Многие языков программирования системы типов поддерживают подтипирование . Например, если тип Cat является подтипом Animal, то выражение типа Cat должно быть заменяемым везде, где выражение типа Animal используется.

Вариация — это то, как подтипирование между более сложными типами связано с подтипированием между их компонентами. Например, как следует составить список Catотносятся к списку Animalс? Или как должна функция , возвращающая Cat относятся к функции, которая возвращает Animal?

В зависимости от вариации конструктора типа отношение подтипирования простых типов может быть сохранено, отменено или проигнорировано для соответствующих сложных типов. Например, в языке программирования OCaml «список кошек» является подтипом «списка животных», поскольку конструктор типа списка является ковариантным . Это означает, что отношение подтипов простых типов сохраняется для сложных типов.

С другой стороны, «функция из животного в строку» является подтипом «функции из кошки в строку», поскольку конструктор типа функции является контравариантным по типу параметра . Здесь отношение подтипов простых типов меняется на противоположное для сложных типов.

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

Чтобы сохранить простоту системы типов и позволить использовать полезные программы, язык может рассматривать конструктор типа как инвариантный, даже если было бы безопасно считать его вариантом, или рассматривать его как ковариантный, даже если это может нарушить безопасность типов.

Формальное определение

[ редактировать ]

Предполагать A и B являются типами и I<U> обозначает применение конструктора типа I с аргументом типа U. В системе типов языка программирования правило типизации для конструктора типа I является:

  • ковариантным , если он сохраняет порядок типов (≤) , который упорядочивает типы от более конкретных к более общим: Если A ≤ B, затем I<A> ≤ I<B>;
  • контрвариантен , если он меняет этот порядок на противоположный: Если A ≤ B, затем I<B> ≤ I<A>;
  • бивариантным, если применимы оба из них (т. е. если A ≤ B, затем I<A> ≡ I<B>); [1]
  • вариант, если ковариантный, контравариантный или бивариантный;
  • инвариантный или нонвариантный , если не вариант.

В статье рассматривается, как это применимо к некоторым конструкторам распространенных типов.

Примеры С#

[ редактировать ]

Например, в C# , если Cat является подтипом Animal, затем:

  • IEnumerable<Cat> является подтипом IEnumerable<Animal>. Подтип сохраняется, поскольку IEnumerable<T> является ковариантным относительно T.
  • Action<Animal> является подтипом Action<Cat>. Подтипы обратные, потому что Action<T> является контрвариантным относительно T.
  • Ни один IList<Cat> ни IList<Animal> является подтипом другого, потому что IList<T> инвариантен относительно T.

Вариант универсального интерфейса C# объявляется путем размещения out (ковариантный) или in (контравариантный) атрибут (ноль или более) его параметров типа. [2] : 144  Вышеуказанные интерфейсы объявлены как IEnumerable<out T>, Action<in T>, и IList<T>. Типы с более чем одним параметром типа могут указывать разные отклонения для каждого параметра типа. Например, тип делегата Func<in T, out TResult> представляет функцию с контравариантным входным параметром типа T и ковариантное возвращаемое значение типа TResult. [3] [2] : 145  Компилятор проверяет, что все типы определены и используются согласованно с их аннотациями, и в противном случае сигнализирует об ошибке компиляции.

Правила типизации для разнообразия интерфейса обеспечивают безопасность типов. Например, Action<T> представляет функцию первого класса, ожидающую аргумент типа T, [2] : 144  и функцию, которая может обрабатывать любой тип животных, всегда можно использовать вместо функции, которая может обрабатывать только кошек.

Типы данных только для чтения (источники) могут быть ковариантными; Типы данных только для записи (приемники) могут быть контравариантными. Изменяемые типы данных, которые действуют как источники и приемники, должны быть инвариантными. Чтобы проиллюстрировать это общее явление, рассмотрим тип массива . Для типа Animal мы можем сделать тип Animal[], который представляет собой «массив животных». Для целей этого примера этот массив поддерживает элементы как чтения, так и записи.

У нас есть возможность рассматривать это как:

  • ковариант: а Cat[] это Animal[];
  • контрвариант: Animal[] это Cat[];
  • инвариант: Animal[] это не Cat[] и Cat[] не является Animal[].

Если мы хотим избежать ошибок типа, то безопасным является только третий вариант. Понятно, что не каждый Animal[] можно рассматривать так, как если бы это был Cat[], поскольку клиент, читающий из массива, будет ожидать Cat, но Animal[] может содержать, например, Dog. Таким образом, контравариантное правило небезопасно.

И наоборот, Cat[] нельзя рассматривать как Animal[]. Всегда должна быть возможность поставить Dog в Animal[]. При использовании ковариантных массивов нельзя гарантировать безопасность, поскольку резервное хранилище на самом деле может быть массивом кошек. Таким образом, ковариантное правило также небезопасно — конструктор массива должен быть инвариантным . Обратите внимание, что это проблема только для изменяемых массивов; ковариантное правило безопасно для неизменяемых массивов (только для чтения). Аналогично, контравариантное правило будет безопасно для массивов, доступных только для записи.

Ковариантные массивы в Java и C#

[ редактировать ]

Ранние версии Java и C# не включали дженерики, называемые также параметрическим полиморфизмом . В таких условиях инвариантность массивов исключает полезные полиморфные программы.

Например, рассмотрите возможность написания функции для перемешивания массива или функции, которая проверяет два массива на равенство, используя метод Object. equals метод на элементах. Реализация не зависит от точного типа элемента, хранящегося в массиве, поэтому должна быть возможность написать одну функцию, которая будет работать со всеми типами массивов. Легко реализовать функции типа:

boolean equalArrays(Object[] a1, Object[] a2);
void shuffleArray(Object[] a);

Однако если бы типы массивов рассматривались как инвариантные, эти функции можно было бы вызывать только для массива именно того типа Object[]. Например, нельзя было перетасовать массив строк.

Таким образом, и Java, и C# относятся к типам массивов ковариантно. Например, в Java String[] является подтипом Object[]и в С# string[] является подтипом object[].

Как обсуждалось выше, ковариантные массивы приводят к проблемам с записью в массив. Ява [4] : 126  и C# решает эту проблему, маркируя тип каждого объекта массива при его создании. Каждый раз, когда значение сохраняется в массиве, среда выполнения проверяет, что тип времени выполнения значения равен типу времени выполнения массива. Если есть несоответствие, ArrayStoreException (Ява) [4] : 126  или ArrayTypeMismatchException (С#) выбрасывается:

// a is a single-element array of String
String[] a = new String[1];

// b is an array of Object
Object[] b = a;

// Assign an Integer to b. This would be possible if b really were
// an array of Object, but since it really is an array of String,
// we will get a java.lang.ArrayStoreException.
b[0] = 1;

В приведенном выше примере можно безопасно читать данные из массива (b). Это всего лишь попытка записи в массив, что может привести к проблемам.

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

С добавлением дженериков Java [4] : 126–129  и C# теперь предлагают способы написания такого рода полиморфных функций, не полагаясь на ковариацию. Функциям сравнения и перетасовки массивов могут быть присвоены параметризованные типы.

<T> boolean equalArrays(T[] a1, T[] a2);
<T> void shuffleArray(T[] a);

В качестве альтернативы, чтобы обеспечить доступ метода C# к коллекции только для чтения, можно использовать интерфейс IEnumerable<object> вместо того, чтобы передавать ему массив object[].

Типы функций

[ редактировать ]

В языках с первоклассными функциями есть такие типы функций , как «функция, ожидающая кота и возвращающая животное» (написано cat -> animal в синтаксисе OCaml или Func<Cat,Animal> в синтаксисе C# ).

Эти языки также должны указывать, когда один тип функции является подтипом другого, то есть когда безопасно использовать функцию одного типа в контексте, который ожидает функцию другого типа. Безопасно заменить функцию f на функцию g , если f принимает более общий тип аргумента и возвращает более конкретный тип, чем g . Например, функции типа animal -> cat, cat -> cat, и animal -> animal можно использовать везде, где cat -> animal было ожидаемо. (Можно сравнить это с принципом надежности коммуникации: «будьте либеральны в том, что вы принимаете, и консервативны в том, что вы производите».) Общее правило таково:

если и .

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

Другими словами, конструктор типа → контравариантен по типу параметра (входного) и ковариантен по типу возвращаемого (выходного) значения . Это правило было впервые формально сформулировано Джоном Рейнольдсом . [5] и далее популяризирован в статье Луки Карделли . [6]

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

Наследование в объектно-ориентированных языках

[ редактировать ]

Когда подкласс переопределяет метод в суперклассе, компилятор должен проверить, что переопределяющий метод имеет правильный тип. Хотя некоторые языки требуют, чтобы тип точно соответствовал типу в суперклассе (инвариантность), типобезопасно также позволить переопределяющему методу иметь «лучший» тип. Согласно обычному правилу создания подтипов для типов функций это означает, что переопределяющий метод должен возвращать более конкретный тип (ковариация возвращаемого типа) и принимать более общий аргумент (контравариантность типа параметра). В нотации UML возможности следующие (где класс B — это подкласс, расширяющий класс A, который является суперклассом):

В качестве конкретного примера предположим, что мы пишем класс для моделирования приюта для животных . Мы предполагаем, что Cat является подклассом Animalи что у нас есть базовый класс (с использованием синтаксиса Java)

UML-диаграмма
class AnimalShelter {

    Animal getAnimalForAdoption() {
        // ...
    }
    
    void putAnimal(Animal animal) {
        //...
    }
}

Теперь вопрос: если мы создадим подкласс AnimalShelter, какие типы нам разрешено присваивать getAnimalForAdoption и putAnimal?

Тип возвращаемого значения ковариантного метода

[ редактировать ]

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

UML-диаграмма
class CatShelter extends AnimalShelter {

    Cat getAnimalForAdoption() {
        return new Cat();
    }
}

Среди основных объектно-ориентированных языков Java , C++ и C# (начиная с версии 9.0). [7] ) поддерживают ковариантные типы возврата. Добавление ковариантного возвращаемого типа было одной из первых модификаций языка C++, одобренных комитетом по стандартизации в 1998 году. [8] Scala и D также поддерживают ковариантные типы возврата.

Тип параметра контравариантного метода

[ редактировать ]

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

UML-диаграмма
class CatShelter extends AnimalShelter {
    void putAnimal(Object animal) {
        // ...
    }
}

Лишь немногие объектно-ориентированные языки действительно позволяют это (например, Python при проверке типов с помощью mypy). C++, Java и большинство других языков, поддерживающих перегрузку и/или затенение, интерпретируют это как метод с перегруженным или затененным именем.

Однако Сатер поддерживал как ковариацию, так и контравариантность. Соглашения о вызовах для переопределенных методов ковариантны с параметрами out и возвращаемыми значениями и контравариантны с обычными параметрами (с режимом in ).

Тип параметра ковариантного метода

[ редактировать ]

Пара основных языков, Eiffel и Dart. [9] разрешить параметрам переопределяющего метода иметь более конкретный тип, чем метод в суперклассе (ковариация типа параметра). Таким образом, следующий код Dart будет вводить проверку с putAnimal переопределение метода в базовом классе:

UML-диаграмма
class CatShelter extends AnimalShelter {

    void putAnimal(covariant Cat animal) {
        // ...
    }
}

Это не является типобезопасным. Путем повышения CatShelter к AnimalShelter, можно попробовать поместить собаку в кошачий приют. Это не соответствует CatShelter ограничения параметров и приведет к ошибке во время выполнения. Отсутствие безопасности типов (известное в сообществе Eiffel как «проблема свиста», где «cat» или «CAT» означает измененную доступность или тип) является давней проблемой. На протяжении многих лет для решения этой проблемы предлагались различные комбинации глобального статического анализа, локального статического анализа и новых возможностей языка. [10] [11] и они были реализованы в некоторых компиляторах Eiffel.

Несмотря на проблему безопасности типов, разработчики Eiffel считают ковариантные типы параметров решающими для моделирования требований реального мира. [11] Приют для кошек иллюстрирует распространенное явление: это своего рода приют для животных, но имеет дополнительные ограничения , и для моделирования этого кажется разумным использовать наследование и ограниченные типы параметров. Предлагая такое использование наследования, дизайнеры Эйфеля отвергают принцип подстановки Лискова , который гласит, что объекты подклассов всегда должны быть менее ограничены, чем объекты их суперкласса.

Еще одним примером основного языка, допускающего ковариацию параметров метода, является PHP в отношении конструкторов классов. В следующем примере метод __construct() принимается, несмотря на то, что параметр метода ковариантен параметру родительского метода. Если бы этот метод отличался от __construct(), произошла бы ошибка:

interface AnimalInterface {}


interface DogInterface extends AnimalInterface {}


class Dog implements DogInterface {}


class Pet
{
    public function __construct(AnimalInterface $animal) {}
}


class PetDog extends Pet
{
    public function __construct(DogInterface $dog)
    {
        parent::__construct($dog);
    }
}

Другой пример, в котором ковариантные параметры кажутся полезными, — это так называемые бинарные методы, т. е. методы, в которых ожидается, что параметр будет того же типа, что и объект, для которого вызывается метод. Примером является compareTo метод: a.compareTo(b) проверяет, есть ли a приходит до или после b в некотором порядке, но способ сравнения, скажем, двух рациональных чисел будет отличаться от способа сравнения двух строк. Другие распространенные примеры бинарных методов включают проверки на равенство, арифметические операции и операции над множествами, такие как подмножество и объединение.

В старых версиях Java метод сравнения был указан как интерфейс. Comparable:

interface Comparable {

    int compareTo(Object o);
}

Недостатком этого является то, что метод указан для приема аргумента типа Object. Типичная реализация сначала приводит этот аргумент к понижению (выдавая ошибку, если он не ожидаемого типа):

class RationalNumber implements Comparable {
    int numerator;
    int denominator;
    // ...
 
    public int compareTo(Object other) {
        RationalNumber otherNum = (RationalNumber)other;
        return Integer.compare(numerator * otherNum.denominator,
                               otherNum.numerator * denominator);
    }
}

В языке с ковариантными параметрами аргумент compareTo может быть непосредственно задан желаемый тип RationalNumber, скрывая приведение типов. (Конечно, это все равно приведет к ошибке выполнения, если compareTo затем был вызван, например, String.)

Устранение необходимости в ковариантных типах параметров

[ редактировать ]

Другие особенности языка могут обеспечить очевидные преимущества ковариантных параметров, сохраняя при этом заменяемость Лискова.

На языке с дженериками (он же параметрический полиморфизм ) и ограниченной количественной оценкой предыдущие примеры могут быть записаны типобезопасным способом. [12] Вместо определения AnimalShelter, мы определяем параметризованный класс Shelter<T>. (Одним из недостатков этого является то, что разработчик базового класса должен предвидеть, какие типы необходимо будет специализировать в подклассах.)

class Shelter<T extends Animal> {

    T getAnimalForAdoption() {
        // ...
    }

    void putAnimal(T animal) {
        // ...
    }
}

    
class CatShelter extends Shelter<Cat> {

    Cat getAnimalForAdoption() {
        // ...
    }

    void putAnimal(Cat animal) {
        // ...
    }
}

Аналогично, в последних версиях Java Comparable интерфейс был параметризован, что позволяет опускать понижающее приведение типобезопасным способом:

class RationalNumber implements Comparable<RationalNumber> {

    int numerator;
    int denominator;
    // ...
         
    public int compareTo(RationalNumber otherNum) {
        return Integer.compare(numerator * otherNum.denominator, 
                               otherNum.numerator * denominator);
    }
}

Еще одна языковая особенность, которая может помочь, — это множественная отправка . Одна из причин, по которой бинарные методы неудобно писать, заключается в том, что при вызове типа a.compareTo(b), выбирая правильную реализацию compareTo действительно зависит от типа времени выполнения обоих a и b, но в обычном объектно-ориентированном языке только тип времени выполнения a учитывается. В языке с в стиле Common Lisp Object System (CLOS) множественной диспетчеризацией метод сравнения может быть записан как общая функция, где оба аргумента используются для выбора метода.

Джузеппе Кастанья [13] заметил, что в типизированном языке с множественной отправкой универсальная функция может иметь некоторые параметры, управляющие отправкой, и некоторые «остаточные» параметры, которые этого не делают. Поскольку правило выбора метода выбирает наиболее конкретный применимый метод, если метод переопределяет другой метод, то у переопределяющего метода будут более конкретные типы управляющих параметров. С другой стороны, чтобы гарантировать типобезопасность, язык по-прежнему должен требовать, чтобы оставшиеся параметры были как минимум такими же общими. Используя предыдущую терминологию, типы, используемые для выбора метода во время выполнения, являются ковариантными, тогда как типы, не используемые для выбора метода во время выполнения метода, являются контравариантными. Обычные языки с единой диспетчеризацией, такие как Java, также подчиняются этому правилу: для выбора метода используется только один аргумент (объект-получатель, передаваемый методу в качестве скрытого аргумента). this), да и вообще тип this более специализирован внутри переопределяющих методов, чем в суперклассе.

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

Краткое изложение дисперсии и наследования

[ редактировать ]

В следующей таблице приведены правила переопределения методов на языках, обсуждавшихся выше.

Тип параметра Тип возврата
C++ (с 1998 г.), Java (начиная с J2SE 5.0 ), D , C# (начиная с C# 9). Инвариант Ковариантный
С# (до C# 9) Инвариант Инвариант
Сатер Контравариантный Ковариантный
Эйфелева Ковариантный Ковариантный

Общие типы

[ редактировать ]

В языках программирования, поддерживающих дженерики (также известные как параметрический полиморфизм ), программист может расширить систему типов с помощью новых конструкторов. Например, интерфейс C#, такой как IList<T> позволяет создавать новые типы, такие как IList<Animal> или IList<Cat>. Тогда возникает вопрос, какой должна быть дисперсия этих конструкторов типов.

Существует два основных подхода. В языках с аннотациями вариации места объявления (например, C# ) программист аннотирует определение универсального типа с предполагаемой вариацией его параметров типа. При использовании аннотаций вариантов использования (например, Java ) программист вместо этого аннотирует места, где создается экземпляр универсального типа.

Аннотации отклонений на сайте объявления

[ редактировать ]

Наиболее популярными языками с аннотациями отклонений на сайте объявлений являются C# и Kotlin (с использованием ключевых слов out и in), а также Scala и OCaml (с использованием ключевых слов + и -). C# допускает аннотации отклонений только для типов интерфейсов, тогда как Kotlin, Scala и OCaml допускают их как для типов интерфейсов, так и для конкретных типов данных.

Интерфейсы

[ редактировать ]

В C# каждый параметр типа универсального интерфейса может быть помечен как ковариантный ( out), контравариант ( in) или инвариант (без аннотации). Например, мы можем определить интерфейс IEnumerator<T> итераторов только для чтения и объявите его ковариантным (out) в параметре типа.

interface IEnumerator<out T>
{
    T Current { get; }
    bool MoveNext();
}

Этим заявлением IEnumerator будет рассматриваться как ковариантный по параметру типа, например IEnumerator<Cat> является подтипом IEnumerator<Animal>.

Средство проверки типов обеспечивает, чтобы каждое объявление метода в интерфейсе упоминало только параметры типа в соответствии с in/ out аннотации. То есть параметр, объявленный ковариантным, не должен встречаться ни в одной контравариантной позиции (где позиция является контравариантной, если она встречается в нечетном числе конструкторов контравариантного типа). Точное правило [14] [15] заключается в том, что типы возвращаемых значений всех методов в интерфейсе должны быть действительными ковариантно , а все типы параметров метода должны быть действительными контравариантно , где действительный S-ly определяется следующим образом:

  • Необобщенные типы (классы, структуры, перечисления и т. д.) допустимы как ко-, так и контравариантно.
  • Параметр типа T действителен ковариантно, если он не был отмечен inи действителен контрвариантно, если он не был отмечен out.
  • Тип массива A[] действительно S-ly, если A является. (Это потому, что в C# есть ковариантные массивы.)
  • Общий тип G<A1, A2, ..., An> действительно S-ly, если для каждого параметра Ai,
    • Ai является действительным S-лым, а i- й параметр G объявляется ковариантным, или
    • Ai действителен (не S)-ly, и i- й параметр для G объявляется контравариантным, или
    • Ai действителен как ковариантно, так и контравариантно, а i- й параметр для G объявлен инвариантным.

В качестве примера применения этих правил рассмотрим IList<T> интерфейс.

interface IList<T>
{
    void Insert(int index, T item);
    IEnumerator<T> GetEnumerator();
}

Тип параметра T из Insert должен быть допустимым контравариантно, т. е. параметр типа T не должен быть помечен out. Аналогично, тип результата IEnumerator<T> из GetEnumerator должно быть действительным ковариантно, т.е. (поскольку IEnumerator является ковариантным интерфейсом) тип T должен быть действительным ковариантно, т. е. параметр типа T не должен быть помечен in. Это показывает, что интерфейс IList не допускается отмечать ни ко-, ни контрвариантно.

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

C# допускает аннотации отклонений для параметров интерфейсов, но не для параметров классов. Поскольку поля в классах C# всегда изменяемы, классы с вариантной параметризацией в C# не будут очень полезны. Но языки, в которых особое внимание уделяется неизменяемым данным, могут эффективно использовать ковариантные типы данных. Например, во всех Scala , Kotlin и OCaml неизменяемый тип списка является ковариантным: List[Cat] является подтипом List[Animal].

Правила Scala для проверки аннотаций отклонений по существу такие же, как и в C#. Однако есть некоторые идиомы, которые особенно применимы к неизменяемым структурам данных. Они иллюстрируются следующим (выдержкой из) определением List[A] сорт.

sealed abstract class List[+A] extends AbstractSeq[A] {
    def head: A
    def tail: List[A]

    /** Adds an element at the beginning of this list. */
    def ::[B >: A] (x: B): List[B] =
        new scala.collection.immutable.::(x, this)
    /** ... */
}

Во-первых, члены класса, имеющие вариантный тип, должны быть неизменяемыми. Здесь, head имеет тип A, который был объявлен ковариантным ( +), и действительно head был объявлен как метод ( def). Пытаюсь объявить его как изменяемое поле ( var) будет отклонено как ошибка типа.

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

def :: (x: A): List[A]

Однако это было бы ошибкой типа, поскольку ковариантный параметр A появляется в контравариантной позиции (как параметр функции). Но есть хитрость, позволяющая обойти эту проблему. Мы даем :: более общий тип, позволяющий добавлять элемент любого типа B пока B является супертипом A. Обратите внимание, что это зависит от List является ковариантным, поскольку this имеет тип List[A] и мы рассматриваем его как имеющий тип List[B]. На первый взгляд может быть неочевидно, что обобщенный тип верен, но если программист начнет с более простого объявления типа, ошибки типа укажут место, которое необходимо обобщить.

Вывод дисперсии

[ редактировать ]

Можно разработать систему типов, в которой компилятор автоматически выводит наилучшие возможные аннотации отклонений для всех параметров типа данных. [16] Однако анализ может усложниться по нескольким причинам. Во-первых, анализ нелокален, поскольку дисперсия интерфейса I зависит от дисперсии всех интерфейсов, которые I упоминает. Во-вторых, чтобы получить уникальные лучшие решения, система типов должна допускать бивариантные параметры (которые одновременно являются ко- и контравариантными). И, наконец, изменение параметров типа, возможно, должно быть осознанным выбором разработчика интерфейса, а не чем-то, что происходит просто так.

По этим причинам [17] большинство языков делают очень небольшой вывод дисперсии. C# и Scala вообще не выводят никаких аннотаций отклонений. OCaml может определить дисперсию параметризованных конкретных типов данных, но программист должен явно указать дисперсию абстрактных типов (интерфейсов).

Например, рассмотрим тип данных OCaml. T который оборачивает функцию

type ('a, 'b) t = T of ('a -> 'b)

Компилятор автоматически сделает вывод, что T контравариантен по первому параметру и ковариантен по второму. Программист также может предоставить явные аннотации, соответствие которым проверит компилятор. Таким образом, следующее объявление эквивалентно предыдущему:

type (-'a, +'b) t = T of ('a -> 'b)

Явные аннотации в OCaml полезны при указании интерфейсов. Например, интерфейс стандартной библиотеки Map.S для ассоциативных таблиц включите аннотацию о том, что конструктор типа карты является ковариантным по отношению к типу результата.

module type S =
    sig
        type key
        type (+'a) t
        val empty: 'a t
        val mem: key -> 'a t -> bool
        ...
    end

Это гарантирует, что, например, cat IntMap.t является подтипом animal IntMap.t.

Использование аннотаций отклонений на сайте (подстановочные знаки)

[ редактировать ]

Одним из недостатков подхода «сайт объявления» является то, что многие типы интерфейсов необходимо сделать инвариантными. Например, мы видели выше, что IList должен был быть инвариантным, поскольку он содержал оба Insert и GetEnumerator. Чтобы обеспечить большую вариативность, разработчик API может предоставить дополнительные интерфейсы, предоставляющие подмножества доступных методов (например, «список только для вставки», который предоставляет только Insert). Однако это быстро становится громоздким.

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

Java предоставляет аннотации отклонений в зависимости от места использования посредством подстановочных знаков — ограниченной формы ограниченных экзистенциальных типов . Параметризованный тип может быть создан с помощью подстановочного знака. ? вместе с верхней или нижней границей, например List<? extends Animal> или List<? super Animal>. Неограниченный подстановочный знак, например List<?> эквивалентно List<? extends Object>. Такой тип представляет List<X> для какого-то неизвестного типа X что удовлетворяет границе. [4] : 139  Например, если l имеет тип List<? extends Animal>, то программа проверки типов примет

Animal a = l.get(3);

потому что тип X как известно, является подтипом Animal, но

l.add(new Animal());

будет отклонено как ошибка типа, поскольку Animal не обязательно является X. В общем, учитывая некоторый интерфейс I<T>, ссылка на I<? extends T> запрещает использование методов из интерфейса, где T происходит контравариантно в типе метода. И наоборот, если l имел тип List<? super Animal> можно было бы позвонить l.add но не l.get.

Подтипы подстановочных знаков в Java можно визуализировать в виде куба.

Хотя параметризованные типы без подстановочных знаков в Java являются инвариантными (например, между List<Cat> и List<Animal>), типы подстановочных знаков можно сделать более конкретными, указав более жесткую границу. Например, List<? extends Cat> является подтипом List<? extends Animal>. Это показывает, что типы подстановочных знаков ковариантны в своих верхних границах (а также контравариантны в своих нижних границах ). В общей сложности, учитывая тип подстановочного знака, например C<? extends T>, существует три способа формирования подтипа: путем специализации класса C, задав более жесткую границу Tили заменив подстановочный знак ? определенного типа (см. рисунок). [4] : 139 

Применяя две из трех вышеперечисленных форм подтипирования, становится возможным, например, передать аргумент типа List<Cat> к методу, ожидающему List<? extends Animal>. Именно такая выразительность достигается благодаря ковариантным типам интерфейсов. Тип List<? extends Animal> действует как тип интерфейса, содержащий только ковариантные методы List<T>, но разработчик List<T> не нужно было определять это заранее.

В общем случае общей структуры данных IListковариантные параметры используются для методов, извлекающих данные из структуры, а контравариантные параметры — для методов, помещающих данные в структуру. Мнемоника Producer Extends, Consumer Super (PECS) из книги Эффективная Java» « Джошуа Блоха дает простой способ запомнить, когда использовать ковариацию и контравариацию. [4] : 141 

Подстановочные знаки являются гибкими, но есть один недостаток. Хотя вариативность в зависимости от места использования означает, что разработчикам API не нужно учитывать разницу параметров типа в интерфейсах, вместо этого им часто приходится использовать более сложные сигнатуры методов. Типичный пример включает в себя Comparable интерфейс. [4] : 66  Предположим, мы хотим написать функцию, которая находит самый большой элемент в коллекции. Элементы должны реализовать compareTo метод, [4] : 66  так что первая попытка может быть

<T extends Comparable<T>> T max(Collection<T> coll);

Однако этот тип не является достаточно общим — можно найти максимум Collection<Calendar>, но не Collection<GregorianCalendar>. Проблема в том, что GregorianCalendar не реализует Comparable<GregorianCalendar>, но вместо этого (лучший) интерфейс Comparable<Calendar>. В Java, в отличие от C#, Comparable<Calendar> не считается подтипом Comparable<GregorianCalendar>. Вместо этого тип max необходимо изменить:

<T extends Comparable<? super T>> T max(Collection<T> coll);

Ограниченный подстановочный знак ? super T передает информацию, которая max вызывает только контравариантные методы из Comparable интерфейс. Этот конкретный пример разочаровывает, поскольку все методы в Comparable контравариантны, так что условие тривиально истинно. Система сайта объявлений могла бы справиться с этим примером с меньшим беспорядком, аннотируя только определение Comparable.

The max Метод можно изменить еще больше, используя ограниченный сверху подстановочный знак для параметра метода: [18]

<T extends Comparable<? super T>> T max(Collection<? extends T> coll);

Сравнение аннотаций сайта объявления и сайта использования

[ редактировать ]

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

Один из способов оценить, полезна ли дополнительная гибкость, — это посмотреть, используется ли она в существующих программах. Обзор большого набора Java-библиотек [16] обнаружили, что 39% аннотаций с подстановочными знаками можно было напрямую заменить аннотациями сайта объявления. Таким образом, оставшиеся 61% являются показателем мест, где Java получает выгоду от наличия системы использования сайта.

В языке сайта объявлений библиотеки должны либо предоставлять меньшую вариативность, либо определять больше интерфейсов. Например, библиотека Scala Collections определяет три отдельных интерфейса для классов, использующих ковариантность: ковариантный базовый интерфейс, содержащий общие методы, инвариантную изменяемую версию, которая добавляет методы с побочными эффектами, и ковариантную неизменяемую версию, которая может специализировать унаследованные реализации для использования структурных делиться. [19] Этот дизайн хорошо работает с аннотациями сайтов объявлений, но большое количество интерфейсов влечет за собой сложность для клиентов библиотеки. А изменение интерфейса библиотеки может быть невозможным — в частности, одной из целей при добавлении дженериков в Java было обеспечение обратной двоичной совместимости.

С другой стороны, подстановочные знаки Java сами по себе сложны. В презентации на конференции [20] Джошуа Блох раскритиковал их как слишком сложные для понимания и использования, заявив, что при добавлении поддержки замыканий «мы просто не можем позволить себе еще одни подстановочные знаки ». В ранних версиях Scala использовались аннотации вариантов сайта использования, но программистам было трудно использовать их на практике, в то время как аннотации места объявления оказались очень полезными при разработке классов. [21] В более поздних версиях Scala были добавлены экзистенциальные типы и подстановочные знаки в стиле Java; однако, по словам Мартина Одерски , если бы не было необходимости во взаимодействии с Java, они, вероятно, не были бы включены. [22]

Росс Тейт утверждает [23] эта часть сложности подстановочных знаков Java связана с решением кодировать вариацию места использования с использованием формы экзистенциальных типов. Оригинальные предложения [24] [25] использовал синтаксис специального назначения для аннотаций отклонений, записывая List<+Animal> вместо более подробного описания Java List<? extends Animal>.

Поскольку подстановочные знаки являются формой экзистенциальных типов, их можно использовать не только для дисперсии. Тип вроде List<?> ("список неизвестного типа" [26] ) позволяет передавать объекты методам или сохранять их в полях без точного указания параметров их типа. Это особенно ценно для таких классов, как Class где большинство методов не упоминают параметр типа.

Однако вывод типа для экзистенциальных типов представляет собой сложную проблему. Для разработчика компилятора подстановочные знаки Java вызывают проблемы с завершением проверки типов, выводом аргументов типа и неоднозначными программами. [27] В общем, невозможно определить , является ли Java-программа, использующая дженерики, правильно типизированной или нет. [28] поэтому для некоторых программ любая программа проверки типов должна будет перейти в бесконечный цикл или тайм-аут. Для программиста это приводит к сообщениям об ошибках сложного типа. Тип Java проверяет типы подстановочных знаков, заменяя подстановочные знаки новыми переменными типа (так называемое преобразование захвата ). Это может затруднить чтение сообщений об ошибках, поскольку они относятся к переменным типа, которые программист не записывал напрямую. Например, пытаясь добавить Cat к List<? extends Animal> выдаст ошибку типа

method List.add (capture#1) is not applicable
  (actual argument Cat cannot be converted to capture#1 by method invocation conversion)
where capture#1 is a fresh type-variable:
  capture#1 extends Animal from capture of ? extends Animal

Поскольку аннотации как места объявления, так и места использования могут быть полезны, некоторые системы типов предоставляют и то, и другое. [16] [23]

Этимология

[ редактировать ]

Эти термины происходят из понятия ковариантных и контравариантных функторов в теории категорий . Рассмотрим категорию чьи объекты являются типами и чьи морфизмы представляют отношение подтипа ≤. (Это пример того, как любое частично упорядоченное множество можно рассматривать как категорию.) Тогда, например, конструктор типа функции принимает два типа p и r и создает новый тип p r ; поэтому он принимает объекты в к объектам в . По правилу подтипирования для типов функций эта операция меняет порядок ≤ для первого параметра и сохраняет его для второго, поэтому она является контравариантным функтором для первого параметра и ковариантным функтором для второго.

См. также

[ редактировать ]
  1. ^ Это происходит только в патологическом случае. Например, I<T> = int: можно указать любой тип T и результат все еще int.
  2. ^ Перейти обратно: а б с Скит, Джон (23 марта 2019 г.). C# в глубине . Мэннинг. ISBN  978-1617294532 .
  3. ^ Делегат Func<T, TResult> — Документация MSDN
  4. ^ Перейти обратно: а б с д и ж г час Блох, Джошуа (2018). «Эффективная Java: Руководство по языку программирования» (третье изд.). Аддисон-Уэсли. ISBN  978-0134685991 .
  5. ^ Рейнольдс, Джон К. (1981). Сущность Алгола . Симпозиум по алгоритмическим языкам. Северная Голландия.
  6. ^ Карделли, Лука (1984). Семантика множественного наследования (PDF) . Семантика типов данных (Международный симпозиум София-Антиполис, Франция, 27–29 июня 1984 г.). Конспекты лекций по информатике. Том. 173. Спрингер. стр. 51–67. дои : 10.1007/3-540-13346-1_2 . ISBN  3-540-13346-1 .
    Более длинная версия: - (февраль 1988 г.). «Семантика множественного наследования». Информация и вычисления . 76 (2/3): 138–164. CiteSeerX   10.1.1.116.1298 . дои : 10.1016/0890-5401(88)90007-7 .
  7. ^ Торгерсен, Мадс. «C# 9.0 в записи» .
  8. ^ Эллисон, Чак. «Что нового в стандартном C++?» .
  9. ^ «Устранение распространенных проблем типа» . Язык программирования Dart .
  10. ^ Бертран Мейер (октябрь 1995 г.). «Статическая типизация» (PDF) . OOPSLA 95 (Объектно-ориентированное программирование, системы, языки и приложения), Атланта, 1995 г.
  11. ^ Перейти обратно: а б Ховард, Марк; Безо, Эрик; Мейер, Бертран; Колнет, Доминик; Стапф, Эммануэль; Арну, Карин; Келлер, Маркус (апрель 2003 г.). «Типобезопасная ковариация: компетентные компиляторы могут уловить все свисты» (PDF) . Проверено 23 мая 2013 г.
  12. ^ Франц Вебер (1992). «Получение эквивалента правильности класса и правильности системы - как получить правильную ковариацию». ИНСТРУМЕНТЫ 8 (8-я конференция по технологии объектно-ориентированных языков и систем), Дортмунд, 1992 г. CiteSeerX   10.1.1.52.7872 .
  13. ^ Кастанья, Джузеппе (май 1995 г.). «Ковариантность и контравариантность: конфликт без причины». Транзакции ACM в языках и системах программирования . 17 (3): 431–447. CiteSeerX   10.1.1.115.5992 . дои : 10.1145/203095.203096 . S2CID   15402223 .
  14. ^ Липперт, Эрик (3 декабря 2009 г.). «Точные правила достоверности отклонений» . Проверено 16 августа 2016 г.
  15. ^ «Раздел II.9.7». Международный стандарт ECMA ECMA-335 Общеязыковая инфраструктура (CLI) (6-е изд.). Июнь 2012.
  16. ^ Перейти обратно: а б с Альтидор, Джон; Шань, Хуан Шань; Смарагдакис, Яннис (2011). «Укрощение подстановочных знаков: сочетание различий в определениях и местах использования». Материалы 32-й конференции ACM SIGPLAN по проектированию и реализации языков программирования (PLDI'11) . АКМ. стр. 602–613. CiteSeerX   10.1.1.225.8265 . дои : 10.1145/1993316.1993569 . ISBN  9781450306638 .
  17. ^ Липперт, Эрик (29 октября 2007 г.). «Ковариантность и контравариантность в C#, часть седьмая: зачем нам вообще нужен синтаксис?» . Проверено 16 августа 2016 г.
  18. ^ Bloch 2018 , стр. 139–145, Глава §5, пункт 31. Используйте ограниченные подстановочные знаки для повышения гибкости API.
  19. ^ Одерский, Марин; Ложка, Лекс (7 сентября 2010 г.). «API коллекций Scala 2.8» . Проверено 16 августа 2016 г.
  20. ^ Блох, Джошуа (ноябрь 2007 г.). «Спор о закрытиях [видео]» . Презентация на Javapolis'07. Архивировано из оригинала 2 февраля 2014 г. {{cite web}}: CS1 maint: местоположение ( ссылка )
  21. ^ Одерский, Мартин; Зенгер, Матиас (2005). «Масштабируемые абстракции компонентов» (PDF) . Материалы 20-й ежегодной конференции ACM SIGPLAN по объектно-ориентированному программированию, системам, языкам и приложениям (OOPSLA '05) . АКМ. стр. 41–57. CiteSeerX   10.1.1.176.5313 . дои : 10.1145/1094811.1094815 . ISBN  1595930310 .
  22. ^ Веннерс, Билл; Соммерс, Фрэнк (18 мая 2009 г.). «Цель системы типов Scala: разговор с Мартином Одерски, часть III» . Проверено 16 августа 2016 г.
  23. ^ Перейти обратно: а б Тейт, Росс (2013). «Смешанная вариативность» . FOOL '13: Неофициальные материалы 20-го международного семинара по основам объектно-ориентированных языков . CiteSeerX   10.1.1.353.4691 .
  24. ^ Игараси, Ацуши; Вироли, Мирко (2002). «О подтипировании параметрических типов на основе дисперсии». Материалы 16-й Европейской конференции по объектно-ориентированному программированию (ECOOP '02) . Конспекты лекций по информатике. Том. 2374. стр. 441–469. CiteSeerX   10.1.1.66.450 . дои : 10.1007/3-540-47993-7_19 . ISBN  3-540-47993-7 .
  25. ^ Торуп, Крестен Краб; Торгерсен, Мэдс (1999). «Унификация универсальности: сочетание преимуществ виртуальных типов и параметризованных классов». Объектно-ориентированное программирование (ЭКОП '99) . Конспекты лекций по информатике. Том. 1628. Спрингер. стр. 186–204. CiteSeerX   10.1.1.91.9795 . дои : 10.1007/3-540-48743-3_9 . ISBN  3-540-48743-3 .
  26. ^ «Учебные пособия по Java™, универсальные шаблоны (обновленные), неограниченные подстановочные знаки» . Проверено 17 июля 2020 г.
  27. ^ Тейт, Росс; Люнг, Алан; Лернер, Сорин (2011). «Укрощение подстановочных знаков в системе типов Java» . Материалы 32-й конференции ACM SIGPLAN по проектированию и реализации языков программирования (PLDI '11) . стр. 614–627. CiteSeerX   10.1.1.739.5439 . ISBN  9781450306638 .
  28. ^ Григоре, Раду (2017). «Java-дженерики завершены по Тьюрингу». Материалы 44-го симпозиума ACM SIGPLAN по принципам языков программирования (POPL'17) . стр. 73–85. arXiv : 1605.05274 . Бибкод : 2016arXiv160505274G . ISBN  9781450346603 .
[ редактировать ]
Arc.Ask3.Ru: конец переведенного документа.
Arc.Ask3.Ru
Номер скриншота №: eb9d9bc2d9ba6f47c5503bf508415bcd__1719524760
URL1:https://arc.ask3.ru/arc/aa/eb/cd/eb9d9bc2d9ba6f47c5503bf508415bcd.html
Заголовок, (Title) документа по адресу, URL1:
Covariance and contravariance (computer science) - Wikipedia
Данный printscreen веб страницы (снимок веб страницы, скриншот веб страницы), визуально-программная копия документа расположенного по адресу URL1 и сохраненная в файл, имеет: квалифицированную, усовершенствованную (подтверждены: метки времени, валидность сертификата), открепленную ЭЦП (приложена к данному файлу), что может быть использовано для подтверждения содержания и факта существования документа в этот момент времени. Права на данный скриншот принадлежат администрации Ask3.ru, использование в качестве доказательства только с письменного разрешения правообладателя скриншота. Администрация Ask3.ru не несет ответственности за информацию размещенную на данном скриншоте. Права на прочие зарегистрированные элементы любого права, изображенные на снимках принадлежат их владельцам. Качество перевода предоставляется как есть. Любые претензии, иски не могут быть предъявлены. Если вы не согласны с любым пунктом перечисленным выше, вы не можете использовать данный сайт и информация размещенную на нем (сайте/странице), немедленно покиньте данный сайт. В случае нарушения любого пункта перечисленного выше, штраф 55! (Пятьдесят пять факториал, Денежную единицу (имеющую самостоятельную стоимость) можете выбрать самостоятельно, выплаичвается товарами в течение 7 дней с момента нарушения.)