Почему концепции ковариации и контравариантности применимы при реализации методов интерфейса?

Вариант использования примерно такой:

public class SomeClass : ICloneable
{
    // Some Code

    // Implementing interface method
    public object Clone()
    {
        // Some Clonning Code
    }
}

Теперь мой вопрос: Почему невозможно использовать «SomeClass (поскольку он является производным от объекта)» в качестве типа возвращаемого значения метода Clone(), если мы рассматриваем основы ковариации и контравариантности?

Может ли кто-нибудь объяснить мне причину этой реализации Microsoft ????


person Amit    schedule 23.03.2010    source источник
comment
Если интерфейс IClonable был написан до вашего кода, как метод Clone мог возвращать определенные типы? Единственная гарантия, которую мы имеем, заключается в том, что все, что будет возвращено вашей реализацией Clone, будет получено из Object. Поскольку вы должны придерживаться точного определения интерфейса, вам придется вернуть то, что сказал интерфейс, будучи Object.   -  person Marvin Smit    schedule 23.03.2010
comment
Здесь я хочу, чтобы моя реализация метода Clone могла возвращать определенный тип, а не метод Clone() IClonable, когда я внедряю интерфейс IClonable. Но это недопустимо, получаем ошибку компиляции. Итак, я хочу знать причину, по которой концепция ковариации или контравариантности не разрешена для реализации интерфейса.   -  person Amit    schedule 24.03.2010


Ответы (6)


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

Например:

public interface IFoo
{
    object Flurp(Array array);
}

public class GoodFoo : IFoo
{
    public int Flurp(Array array) { ... }
}

public class NiceFoo : IFoo
{
    public object Flurp(IEnumerable enumerable) { ... }
}

Оба законны по «новым» правилам, верно? Но как насчет этого:

public class QuestionableFoo : IFoo
{
    public double Flurp(Array array) { ... }
    public object Flurp(IEnumerable enumerable) { ... }
}

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

И это далеко не самое худшее. Что, если мы сделаем это вместо этого:

public class EvilFoo : IFoo
{
    public object Flurp(ICollection collection) { ... }
    public object Flurp(ICloneable cloneable) { ... }
}

Кто из них получит приз? Это вполне допустимая перегрузка, но ICollection и ICloneable не имеют ничего общего друг с другом, а Array реализует их обе. Я не вижу здесь очевидного решения.

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

public interface ISuck
{
    Stream Munge(ArrayList list);
    Stream Munge(Hashtable ht);
    string Munge(NameValueCollection nvc);
    object Munge(IEnumerable enumerable);
}

public class HateHateHate : ISuck
{
    public FileStream Munge(ICollection collection);
    public NetworkStream Munge(IEnumerable enumerable);
    public MemoryStream Munge(Hashtable ht);
    public Stream Munge(ICloneable cloneable);
    public object Munge(object o);
    public Stream Munge(IDictionary dic);
}

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

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

Я не знаю, является ли это официальной причиной, по которой он сегодня не поддерживается в C#, но он должен служить хорошим примером кода «только для записи», к которому он может привести, и частью дизайна команды C#. философия состоит в том, чтобы не дать разработчикам писать ужасный код.

person Aaronaught    schedule 23.03.2010
comment
Согласен с вами по пунктам Aaronaught. Возможно, именно поэтому Microsoft пошла по этому пути. - person Amit; 24.03.2010
comment
Я не думаю, что большинство людей подумают, что такая половинчатая реализация сломана. C++ поддерживает ковариантность типа возвращаемого значения, но не контравариантность типа формального параметра. Eiffel поддерживает ковариацию возвращаемого типа и тип формального параметра covariance, что довольно странно. (У Эйфеля необычный подход к созданию подтипов.) Сатер поддерживает и то, и другое. В результате я не видел массовой миграции с C++ на Sather. - person Eric Lippert; 26.03.2010
comment
@Eric: Я не могу спорить ни с чем из этого (хотя я могу сказать, что причина, по которой люди не стекаются к Sather, заключается в том, что они никогда о нем не слышали: P), но на таком языке, как C #, который уже поддерживает контравариантность типа аргумента как часть дисперсии преобразования группы методов, я думаю, что отсутствие этой функции для реализации интерфейса определенно будет чувствовать себя сломанным; как правило, C# и .NET Framework очень стараются быть согласованными. Мои два цента! - person Aaronaught; 26.03.2010

Позвольте мне перефразировать вопрос:

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

abstract class Enclosure {}
class Aquarium : Enclosure {}
abstract class Animal 
{
    public virtual Enclosure GetEnclosure();
}

тогда это недопустимо в С#, но эквивалентный код будет допустим в С++:

class Fish : Animal
{
    public override Aquarium GetEnclosure() { ... 

Как называется эта особенность C++?

Эта функция называется «ковариация возвращаемого типа». (Как указывает другой ответ, также можно было бы поддерживать «контравариантность формальных параметров», хотя С++ этого не делает.)

Почему это не поддерживается в С#?

Как я уже неоднократно указывал, нам не нужно указывать причину, по которой функция не поддерживается; состояние по умолчанию всех функций "не поддерживается". Только когда на реализацию тратится огромное количество времени и усилий, функция становится поддерживаемой. Скорее, функции, которые внедряются, должны иметь для этого причины, и чертовски веские причины, учитывая, сколько стоит их создание.

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

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

  2. Андерс считает, что это не очень хорошая языковая особенность. Андерс — главный архитектор, и если он думает, что это плохая фича, велики шансы, что она не будет реализована. (Теперь, заметьте, мы думали, что именованные и необязательные параметры тоже не стоят затрат, но в конечном итоге это было сделано. Иногда становится ясно, что вам нужно стиснуть зубы и реализовать функцию, которая вам не очень нравится эстетика для удовлетворения реального спроса.)

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

person Eric Lippert    schedule 26.03.2010
comment
Итак, очевидный дополнительный вопрос: Почему Андерс считает, что ковариация возвращаемых типов — это плохая функция? - person LBushkin; 26.03.2010
comment
@LBushkin: в следующий раз, когда увидишь его, спроси его. :-) - person Eric Lippert; 26.03.2010

Вы должны реализовать методы интерфейса точно так же, как они есть в интерфейсе. Метод Clone ICloneable возвращает объект, поэтому ваш SomeClass также должен возвращать объект. Однако вы можете без проблем вернуть экземпляр SomeClass в методе SomeClass Clone, но определение метода должно соответствовать интерфейсу:

public class SomeClass: IClonable
 {
     // Some Code

     //Implementing interface method
     Public object Clone()
      {
        SomeClass ret = new SomeClass();
        // copy date from this instance to ret
        return ret;
      }
 }
person Jeremy Bell    schedule 23.03.2010
comment
И это было бы ожидаемо. С новым материалом .NET 4 за углом с списками входов/выходов co/contra variance, возможно ли сделать именно то, что она просит? - person Nick Larsen; 23.03.2010

С точки зрения объяснения причин, лежащих в основе решений C#, Эрик Липперт из Microsoft много написал, объясняя Contra/CoVariance в C#... вот список тегов из его блога: http://blogs.msdn.com/ericlippert/archive/tags/Covariance+and+Contravariance/default.aspx

[Изменить] В зависимости от вашего вопроса, это может быть правильным сообщением. -part-five-interface-variance.aspx" rel="nofollow noreferrer">http://blogs.msdn.com/ericlippert/archive/2007/10/26/covariance-and-contravariance-in-c-part- пять-interface-variance.aspx

person Jim L    schedule 23.03.2010
comment
Не совсем. Эти статьи посвящены дисперсии параметризованного типа. Вопрос о дисперсии возвращаемого типа, которая отличается. - person Eric Lippert; 26.03.2010

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

Об этом говорится здесь:

http://bytes.com/topic/c-sharp/answers/469671-generic-icloneable

По сути, общий интерфейс, который позволит: public class MyClass : IClonable<MyClass>

также позволит: public class MyClass : IClonable<MyOtherClass>

что на самом деле не дает никакой пользы и может все запутать.

person zod    schedule 23.03.2010
comment
Я бы сказал, что IClonable‹out T› был бы действительно хорошим шаблоном, особенно если бы он наследовал ISelf‹out T›, который, в свою очередь, включал свойство self типа T, доступное только для чтения. Метод clone не должен быть наследуемым, но должен иметь идентичный базовый тип, за исключением наличия защищенного метода Clone. Таким образом, и foo, и производныйFoo могут иметь неклонируемые производные, но можно написать подпрограмму, которая будет принимать как CloneableFoo, так и CloneableDerivedFoo (поскольку оба являются ICloneable‹Foo›). - person supercat; 24.08.2011

Согласно спецификации C#, вы должны использовать метод с идентичной сигнатурой при переопределении или реализации метода интерфейса. Имейте в виду, что Microsoft не владеет C#. Их компилятор C# — это просто его реализация. Так почему же спецификация поступит таким образом? Я могу только догадываться, но я подозреваю, что это было сделано для простоты реализации.

person Peter Ruderman    schedule 23.03.2010