Главная · Карта сайта · Поиск · Статьи · Компьютерные курсы · Обучающие программы · Открытые проекты · Веб-программирование · Создание интернет-сайта · Полезные ссылки · Глоссарий · Контакты · Декабрь 09 2016 20:18:29
Последнее опубликованное

Что такое Model-View-Controller
Pattern Model-View-Controller

Как создать свой веб-сайт
Как создать свой сайт в интернете

Разное
Статистика

Программирование на C#. Ковариантность и контрвариантность


[Назад] [Следующая страница]

3.3. Ковариантность и контрвариантность в контексте универсальных типов

Ниже представлен пример кода, который с одной стороны вроде бы не лишен здравого смысла, а с другой - корректным для C# не является. Казалось бы, объекты производных классов (Label) можно приводить к их базовым типам (PointGeometry), но в случае с типами-параметрами не все так просто.

static void Main()
        {
            GeometrySet<Label, LinkedList<Label>> _set = new GeometrySet<Label, LinkedList<Label>>();
            //Ошибка: Cannot implicity convert type...
            GeometrySet<PointGeometry, LinkedList<PointGeometry>> _set2 = _set;
        }

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

Теперь представьте себе, что вы используете SDK некоторого приложения, позволяющего вам настраивать его поведение заменой некоторых компонентов по умолчанию вашими реализациями, выполненными по заданным авторами SDK спецификациям. Как это обычно бывает: один компонент использует функциональность другого компонента НЕ непосредственно, а через определенный для данного вида взаимодействия интерфейс (используется полиморфизм за счет реализации одного и того же интерфейса разными компонентами). В таком случае, что мешает архитекторам приложения обеспечить возможность кастомизации путем замены реализации по умолчанию на вашу собственную реализацию? Ничего, кроме отсутствия необходимости в этом. Такой интерфейс в том числе может быть и универсальным, а его типом-параметром в реализации по умолчанию может являться открытый для переопределения класс (без модификатора sealed). Допустим, вы хотите заменить реализацию по умолчанию этого интерфейса своей реализацией, при этом, типом-параметром у вас будет являться производный от типа-параметра в реализации по умолчанию класс. Чувствую, что словами уже сложно объяснять ситуацию, поэтому лучше я приведу иллюстрацию на примере выдуманной подсистемы импорта данных.

using System;
namespace CSharpQuickGuide
{
    //Настройки импорта по умолчанию
    public class DefaultImportSettings { }
    //Переопределение настроек импорта
    public class MyCustomImportSettings : DefaultImportSettings { }
    //Универсальный интерфейс сервиса импорта, параметризованный настройками
    public interface IDataImport<T> where T : DefaultImportSettings
    {
        object Convert(object _data, T _settings);
    }
    //Реализация сервиса импорта по умолчанию
    public class ImportManager : IDataImport<DefaultImportSettings>
    {
        public object Convert(object _data, DefaultImportSettings _settings)
        { throw new NotImplementedException(); }
    }
    //Новая реализация сервиса импорта
    public class MyCustomImportManager : IDataImport<MyCustomImportSettings>
    {
        public object Convert(object _data, MyCustomImportSettings _settings)
        { throw new NotImplementedException(); }
    }
    //API для вызова операций импорта
    public static class DataManager
    {
        public static void Import(IDataImport<DefaultImportSettings> _settings, object _Data)
        { throw new NotImplementedException(); }
    }
    //Примеры вызова операций импорта
    static class Application
    {
        public static void Import(object _data)
        {
            //Корректный вызов
            DataManager.Import(new ImportManager() as IDataImport<DefaultImportSettings>, _data);
            //Некорректный вызов, хотя компилятор ошибки сразу не обнаружит, но в момент исполнения
            //на вход DataManager.Import в качестве первого параметра будет передан null
            DataManager.Import(new MyCustomImportManager() as IDataImport<DefaultImportSettings>, _data);
            //Здесь уже компилятор ошибку обнаружит на этапе сборки.
            IDataImport<DefaultImportSettings> _settings = new MyCustomImportManager();
            //До этого вызова DataManager.Import дело не дойдет.
            DataManager.Import(_settings, _data);

            //Если изменить определение класса MyCustomImportManager следующим образом:
            //MyCustomImportManager : IDataImport<DefaultImportSettings>
            //,то ошибка компиляции исчезнет.
        }
    }
}

Приведение экземпляра MyCustomImportSettings к типу DefaultImportSettings естественно, является корректным, но приведение IDataImport<MyCustomImportSettings> к типу IDataImport<DefaultImportSettings> не совсем, и вот почему. Допустим, интерфейс IDataImport определяет метод Convert() с аргументом (входным параметром) типа T, а базовая реализация этого интерфейса – ImportManager ее реализует со значением типа-параметра DefaultImportSettings. Передавая на вход метода DataManager.Import свою реализацию сервиса импорта (экземпляр MyImportManager), имеющую дело уже с MyCustomImportSettings, мы, тем самым, сужаем область допустимых значений метода Convert(), поскольку его реализация по умолчанию в качестве второго аргумента ожидает экземпляр DefaultImportSettings или экземпляр любого его производного типа, а переопределенная нами реализация Convert() ожидает на вход экземпляр уже MyCustomImportSettins или экземпляр его производного типа. Таким образом получается, что наша реализация сервиса не знает, что делать с настройками импорта, если они являются экземпляром любого производного от DefaultImportSettings класса, кроме MyCustomImportSettings, что при подмене компонента импорта по умолчанию на нашу версию приведет к ошибкам. Как следствие, подобное приведение типа является некорректным.

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

using System;
namespace CSharpQuickGuide
{
    //Произвольный базовый класс
    public class MyBaseClass
    {
        //Попытка преобразования типов одной иерархии друг к другу в C# запрещено!
        //user-defined conversions to or from a derived class are not allowed        
        /* public static explicit operator MyInheritorClass(MyBaseClass _base)
        {
            return new MyInheritorClass(); 
        }*/
    }
    //Любой производный класс
    public class MyInheritorClass : MyBaseClass { }
    //Интерфейс контейнера для доступа к элементам
    public interface IMyContainer<out T>
    {
        T Item(int Index);

        //Тип T разрешено использовать только в качестве выходного параметра,
        //поэтому добавление следующего метода приведет к ошибке:
        //Invalid variance: The type parameter 'T' must be contravariantly 
        //valid on 'CSharpQuickGuide.IMyContainer<T>.Add(T)'. 'T' is covariant.

        /* void Add(T _newItem); */
    }
    //Интерфейс контейнера для добавления элементов
    public interface IMyContainer2<in T>
    {
        //Тип T разрешено использовать только в качестве входного параметра,
        //поэтому добавление следующего метода приведет к ошибке:
        //Invalid variance: The type parameter 'T' must be covariantly 
        //valid on 'CSharpQuickGuide.IMyContainer2<T>.Item(int)'. 'T' is contravariant.

        /* T Item(int Index); */

        void Add(T _newItem);
    }
    //Реализация универсального контейнера
    public class MyContainer<T> : IMyContainer<T>, IMyContainer2<T>
        where T : class
    {
        public T Item(int Index)
        { throw new NotImplementedException(); }

        public void Add(T _newItem)
        { throw new NotImplementedException(); }
    }
    //Примеры преобразований простых и универсальных типов
    static class Application
    {
        public static void Main()
        {
            //Преобразование производного типа к базовому разрешено.
            MyBaseClass _base = new MyInheritorClass();

            //Ошибка преобразования базового типа к производному!
            //Cannot implicitly convert type 'CSharpQuickGuide.MyBaseClass' 
            //to 'CSharpQuickGuide.MyInheritorClass'. 
            //An explicit conversion exists (are you missing a cast?)
            //Явное определение преобразования типа также не поможет,
            //поскольку оно в данном случае запрещено (см. MyBaseClass).
            MyInheritorClass _inheritor = new MyBaseClass();

            //Ковариантность. Преобразование корректно
            IMyContainer<MyBaseClass> _Container11 = new MyContainer<MyInheritorClass>();

            //Ошибка! Тип-параметр разрешено использовать только в качестве 
            //выходного параметра (модификатор out), а значит при таком преобразовании
            //неизбежны попытки преобразования MyBaseClass к MyInheritorClass
            //, что запрещено (см. пример выше).
            IMyContainer<MyInheritorClass> _Container12 = new MyContainer<MyBaseClass>();

            //Ошибка! Тип-параметр разрешено использовать только в качестве
            //входного параметра (модификатор in), а значит возможны попытки
            //сужения ОДЗ методов, принимающих на вход MyBaseClass.
            IMyContainer2<MyBaseClass> _Container21 = new IMyContainer<MyInheritorClass>();

            //Контрвариантность. Преобразование корректно
            IMyContainer2<MyInheritorClass> _Container22 = new MyContainer<MyBaseClass>();
        }
    }
}

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

using System;
namespace CSharpQuickGuide
{
    public class BaseClassT<T> where T : struct { }

    public class InheritorClassT<T> : BaseClassT<T> where T : struct { }

    //Примеры преобразований простых и универсальных типов
    static class Application
    {
        public static void ConversionSamples()
        {
            //Ошибка! Cannot implicitly convert type 
            //'CSharpQuickGuide.BaseClassT<int>' to 'CSharpQuickGuide.BaseClassT<double>' 
            BaseClassT<double> _bct_double = new BaseClassT<int>();

            //Не смотря на то, что неявное преобразование int в double вполне себе корректно.
            int i = 10; double d = i;

            //Важно: Подобные преобразования корректны
            BaseClassT<int> _bct_int = new InheritorClassT<int>();
            BaseClassT<double> _bct_double2 = new InheritorClassT<double>();

            //А такие, естественно, нет!
            InheritorClassT<int> _ict_int = new BaseClassT<int>();
            InheritorClassT<double> _ict_int = new BaseClassT<double>();
        }
    }
}

Модификаторы in и out могут быть определены только для интерфейса или делегата. О делегатах речь будет идти в следующем разделе.



[Назад] [Следующая страница]

Компьютерные курсы и курсы программирования
Основы программирования

Курс для начинающих программистов на C# и VB.NET.

SQL 25™

Построение SQL запросов и работа с базой данных.

C# Quick Guide™

Программирование на C#. Краткое руководство.

RegEx

Применение регулярных выражений.

Plug-in архитектура

Примеры программной Plug-in архитектуры.

XML и его расширения

Язык разметки XML и его расширения с примерами.

HTML и разметка гипертекста

Языки HTML, XHTML и CSS с примерами разметки.

Основы веб-дизайна

Основы веб-дизайна: решения типовых задач верстки.

Программирование на PHP

Руководство по программированию на PHP для начинающих.

Справочные материалы

Шаблоны проектирования
Каталог шаблонов проектирования программных компонентов.

Рефакторинг кода
Каталог приемов рефакторинга программного кода.

Гость
Имя

Пароль



Забыли пароль?
Запросите новый здесь
.
Coding Craft. Все права защищены © 2011. Проект Инициативного Народного Фронта Образования - ИНФО-проект.