Вариации типов обобщений в C# и Java

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

С места в карьер. Инвариантность типов обобщений

Начнём с простого примера, который иногда вызывает недоумение у новичков. Так, если мы можем привести ссылку на String к ссылке на Object, то почему не проходит следующее (C#):

List<object> l = new List<string>();  // Error!

Всё дело в том, что List<object> и List<string> никак не связаны в иерархии наследования, несмотря на то, что String — дочерний класс Object. Такое поведение является инвариантным, так, типы обобщений в С# — инвариантны. Это решение разумно. Оно позволяет оградить нас от ошибок времени выполнения. Например, представим, что мы можем поступить так, как показано в предыдущем примере. Тогда мы бы могли нарушить работу нашего списка (С#):

List<string> ls = new List<string>();
ls.Add("Hello");
List<object> lo = ls;  // Представим, что мы можем так сделать
lo[0] = new object();  // Ошибка! lo[0] - ссылка на строку,
                       // нельзя привести object к string.

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

Ковариация массивов

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

object[] a = string[10];  // Всё ок!

Массивы в С# (как и в Java) — ковариантные. И при неправильной работе с ними во время выполнения мы можем получить неожиданное исключение приведения типов. Так, ковариантность в нашем случае позволяет расширить тип аргумента массива до более широкого (базового).

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

Ковариация типов переопределяемых методов в Java

Java (и С++) поддерживают ковариация для типов возвращаемых значений виртуальных методов (Java):

class Animal {
    public Animal getAnimal() {
        return null;
    }
}

class Cat extends Animal {
    @Override public Cat getAnimal() {
        return null;
    }
}

Ковариация и контрвариация делегатов в С#

Чуть ли не единственное место (кроме массивов), где в С# разрешена ковариация — это делегаты. В этом случае ковариантность позволяет методу, возвращающему делегат иметь тип, дочерний по отношению к тому, что определён в делегате (C#):

class A { }
class B : A { }
class Program
{
    public delegate A HandlerMethod();

    public static A FirstHandler() { return null; }
    public static B SecondHandler() { return null; }

    static void Main()
    {
        HandlerMethod handler1 = FirstHandler;
        // Ковариация
        HandlerMethod handler2 = SecondHandler;
    }
}

Далее же мы увидим иллюстрацию ещё одного понятия — контрвариации. Она так же доступна при работе с делегатами. Контрвариация противоположна ковариации. Тогда как ковариация используется для чтения и безопасна в этом отношении, контрвариация используется для записи. Так, контрвариация в нашем случае позволяет методу иметь параметры с типами, которые являются родительскими для тех, что определены в делегате. Так (C#):

class A { }
class B : A { }
class C : B { }

class Program
{
    public delegate void hC(C a);
    public delegate void hB(B b);

    public static void MultiHandler(A a)
    {
        Console.WriteLine(a.GetType());
    }

    static void Main(string[] args)
    {
        hC hc = MultiHandler;
        hB hb = MultiHandler;

        hc(new C());
        hb(new B());
    }
}

Подробнее о вариантности типов делегатов можно почитать в MSDN. А мы вернёмся к обобщениям.

Ковариация и обобщения Java

В было сказано, в Java, как и в С#, типы обобщений инвариантны. Однако в Java мы можем использовать подстановочные типы, чтобы добиться необходимой вариантности.

Вот как можно получить ковариантность в нашем примере со списком (Java):

ArrayList<? extends Object> l = new ArrayList<String>();

Так, мы имеем ковариантность, мы можем считывать значения, поскольку они точно приводимы к Object, однако запись значений нам запрещена по причинам, указанным в первой главе. Мы немного изменим наш пример и поэксперементируем (Java):

class Animal {}
class Cat extends Animal {}

// ... 

    ArrayList<Cat> cats = new ArrayList<Cat>();
    cats.add(new Cat());

    // OK - ковариантность
    ArrayList<? extends Animal> animals = cats;
    Animal a = animals.get(0);

    // Не скомпилируется. Ошибка -- запись не типобезопасна
    a.add(new Animal());

Контрвариация и обобщения Java

Контрвариация в обобщениях Java реализуется путём указания ограничений супертипов. Контрвариация типобезопасна к записи. Компилятор не знает точного типа для метода add, но ему можно передавать любые объекты Cat и его потомков. Однако при вызове метода get неизвестен конкретный тип возвращаемого значения, поэтому здесь его мы можем присвоить только переменной Object (Java):

ArrayList<Animal> animals = new ArrayList<Animal>();
ArrayList<? super Cat> myanimals = animals;

myanimals.add(new Cat()); // Ок - контрвариация

// Не известен конкретный тип, так нельзя...
Cat cat = myanimals.get(0); 

// ... поэтому только так:
Object o = myanimals.get(0);

Вариантности и обобщения в .NET

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

class Animal { }
class Cat : Animal { }

interface ICovariantEnumerator<+T> {  // + означает ковариацию
    T Current { get; }
    bool MoveNext();
}

interface ICovariantEnumerable<+T> {
    ICovariantEnumerator <T> GetEnumerator();
}

// …

ICovariantEnumerable<Cat> cats = ...
ICovariantEnumerable<Animal> animals = cats; // всё ок

Однако С# это не поддерживает и мы можем воспользоваться одним из «обходных путей», о которых подробно рассказано в MSDN.

Заключение

В статье были рассмотрены вопросы вариации типов. Так, Java поддерживает ковариацию и контрвариацию типов обобщений посредством подстановочных типов (wildcards), С# же такой возможности не предоставляет. Однако оба языка поддерживают ковариацию массивов. Использование вариаций может предоставлять большую мощь при разработке, однако эта тема достаточно нетривиальна. Подробнее о практической составляющей использования вариаций в реальных языках программирования вы можете почитать, пройдя по нижеприведённым ссылкам. Теоретическую же часть можно найти в учебниках по теории категорий.

Ссылки

Wikipedia — Covariance and contravariance
MSDN — Variance in Generic Types (C# Programming Guide)
Rick Byers — Generic type parameter variance in the CLR
Rick Byers — More on generic variance
Barry Kelly — Covariance and Contravariance in .NET, Java and C++

About these ads

One thought on “Вариации типов обобщений в C# и Java

  1. Спасибо, статья интересная. Однако мне не понятна польза от практического применения ковариантности обобщений. Если сейчас есть возможность при параметризации дженерика указать некий базовый тип и работать с дженериком (например List) передавая в его методы любые дочерние типы, то разве этого недостаточно?

Добавить комментарий

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

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s