Вариантность и делегаты

Обобщенные делегаты следуют тем же правилам, что и обобщенные интерфейсы, применяя декорации вариантности к параметрам обобщений. Библиотека базовых классов .NET (Base Class Library — BCL) включает удобные типы обобщенных делегатов, такие как Actiono и Funco, которые применимы ко многим экземплярам, избавляя от обязанности определять собственные типы делегатов.

Делегаты Actiono можно использовать для хранения методов, принимающих до 16 параметров и не возвращающих значения, а делегаты Funco применять для хранения методов, принимающих до 16 параметров и возвращающих значение.

До появления .NET 4.0 BCL делегаты Actiono и Funco принимали только до четырех параметров, а теперь — до шестнадцати.

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

1
2
public delegate void Action< in Tl, in T2 > ( Tl argl, T2 arg2 );
public delegate TResult Func< in Tl, in T2, out TResult> ( Tl argl, T2 arg2 );

Теперь в качестве примеров вариантности делегатов рассмотрим следующую иерархию типов:

1
2
3
4
5
6
class Animal
{
}
class Dog : Animal
{
}

Предположим, что имеется следующая пара методов, определенных в некотором классе:

1
2
static void SomeFuntion ( Animal animal );
static void AnotherFunction ( Dog dog );

Поскольку сигнатура функции соответствует сигнатуре делегата, возможность приведенного ниже присваивания SomeFunction экземпляру Action имеет смысл:

1
Action<animal> actionl = SomeFunction;

Когда вызывается actionl, ему можно передать Dog или Animal, поскольку Dog неявно преобразуется в Animal. Давайте представим, что позже создан экземпляр Action следующим образом:

1
Action<dog> action2 = AnotherFunction;

При вызове action2 следует передавать экземпляр Dog. Но также обратите внимание, что поскольку экземпляр Dog можно передать и SomeFunction, то можно было бы создать action2 следующим образом:

1
Action<dog> action2 = SomeFunction;

Этот тип вариантного присваивания (в данном случае — контравариантного) группы методов экземпляру делегата молча поддерживается в С# уже некоторое время. Поэтому, если возможно показанное выше присваивание, то имеет смысл, чтобы была возможность и делать приведенное ниже присваивание, что и реализовано в версии С# 4.0:

1
Action<dog> action2 = actionl;

Теперь рассмотрим краткий пример контравариантного присваивания Action с использованием той же иерархии объектов, что и в приведенном выше примере:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
class Animal {
public virtual void ShowAffection ()  {
Console.WriteLine ( "Реакция неизвестна" );
}
class Dog : Animal {
public override void ShowAffection ()  {
Console.WriteLine ( "Виляние хвостом..." );
static class EntryPoint {
static void Main()  {
Action<animal> petAnimal = (Animal a) => {
Console.Write( "Любимое домашнее животное и его реакция: " );
а.ShowAffection();
};
// Правило контравариантности в действии!
//
// Поскольку Dog -> Animal и
// Action<animal> -> Action<dog>,
//то следующее присваивание контравариантно:
Action<dog> petDog = petAnimal;
petDog( new Dog() );
}

В методе Main создается экземпляр Action, который хранит ссылку на функцию, принимающую экземпляр Animal и вызывающую метод ShowAffection на этом экземпляре.

Для присваивания функции экземпляру Action применялся синтаксис лямбда-выражения.

Интересные моменты начинаются в следующей строке Main. Здесь экземпляр Action присваивается ссылке на Action. И поскольку Dog неявно преобразуется к Animal, a Action неявно преобразуется к Action, то такое присваивание является контравариантным.

В случае непонимания, каким образом тип Action оказывается неявно преобразуемым в Action, если Animal не может быть неявно преобразовано к Dog, попробуйте сосредоточиться на действии. Если действие может оперировать экземплярами Animal, оно определенно может работать и с экземплярами Dog.

Давайте пойдем еще дальше. В дисциплинах функционального программирования принято передавать одни функции в качестве параметров другим функциям. За счет использования делегатов делать это всегда было просто.

Функции, которые принимают в качестве параметров другие функции, часто называют функциями высшего порядка или функционалами. Так какой же вид вариантности участвует в присваивании совместимых экземпляров функций высшего порядка друг другу? Давайте рассмотрим новое определение делегата:

1
delegate void Task<t> ( Action<t> action );

Здесь имеем определение делегата Task, который ссылается на функцию, принимающую другой делегат типа Action.

Не путайте тип Task в данном примере с типом Task в библиотеке TPL (Task Parallel Library — библиотека параллельного выполнения задач).

Если бы понадобилось пометить этот делегат как вариантный, каким ключевым словом должен декориваться параметр-тип — in или out? Исследуем этот вопрос, взглянув на следующий пример:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static class EntryPoint
{
static void Main () {
Action<animal> petAnimal = (Animal a) -> {
Console.Write ( "Любимое домашнее животное и его реакция: " );
а.ShowAffection();
};
// Правило контравариантности в действии!
//
// Поскольку Dog -> Animal и
// Action<animal> -> Action<dog>,
//то следующее присваивание контравариантно:
Action<dog> petDog = petAnimal;
petDog ( new Dog() );
Task<dog> doStuf fToADog = BuildTask<dog>() ;
doStuffToADog( petDog );
//Но задача, принимающая действие над dog,
// также может принимать действие над
animal doStuffToADog( petAnimal );
// Следовательно, вполне логично для Task<dog>
// быть неявно преобразуемым в Task<animal>
//
// Ковариантность в действии!
//
// Поскольку Dog -> Animal и
// Task<dog> -> Task<animal>,
//то следующее присваивание ковариантно:
Task<animal> doStuffToAnAnimal = doStuffToADog ;
doStuf fToAnAnimal ( petAnimal ) ;
doS tuff ToADog ( petAnimal ) ;
static Task<t> BuildTask<t>() where T : new()  {
return (Action<t> action) => action( new T() );
}

Обратите внимание, что в коде создан обобщенный вспомогательный метод BuildTask, чтобы сделать его несколько более читабельным. В Main создается экземпляр Task, который затем присваивается переменной doStuffToADog. Переменная doStuffToADog хранит ссылку на делегат, принимающий экземпляр Action в качестве параметра. Затем вызывается doStuffToADog с передачей ему petDOG, представляющего собой экземпляр Action.

Но в предыдущем примере мы выяснили, что тип Action неявно преобразуем в Action, поэтому при втором вызове doStuffToADog можно передать petAnimal.

Теперь давайте последуем тем же путем, что и в предыдущем примере, в котором выяснилось, что тип Action контравариантно присваиваемый Action. В Main создается экземпляр Task, который присваивается переменной doStuffToADog. При вызове doStuffToADog ему определенно можно передать экземпляр Action < Animal >. Но поскольку Action также может быть передан Task, это подразумевает, что экземпляр Task может быть присвоен Task.

В самом деле, именно это и демонстрируется в данном примере. Но что это — контравариантность или ковариантность?

На первый взгляд, поскольку Т используется справа в объявлении делегата Task, можно прийти к заключению, что параметр-тип Т должен декорироваться ключевым словом in. Однако давайте проанализируем ситуацию.

Поскольку Dog неявно преобразуется в Animal, a Task — в Task, присваивание ковариантно, так как направление преобразования в отношении Т в обоих случаях осуществляется в одном направлении. Таким образом, параметр-тип должен быть декорирован ключевым словом out, в результате чего объявление Task будет выглядеть так:

1
delegate void Task<out Т>( Action<t> action );

Здесь следует понять, что выбирать между ключевыми словами in и out только на основе того, на какой стороне объявления делегата используется обобщенный параметр, нельзя. Необходимо анализировать преобразование, чтобы определить, ковари-антное оно или контравариантное, и затем соответственно делать выбор.

Разумеется, если вы ошибетесь с выбором, то компилятор сообщит об этом.

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100