Вариативность в делегатах (C#)

В платформе .NET Framework 3.5 появилась поддержка вариативности при сопоставлении сигнатур методов с типами делегатов во всех делегатах в C#. Это означает, что делегатам можно назначать не только методы, которые обладают соответствующими сигнатурами, но и методы, которые возвращают более производные типы (ковариация), или принимают параметры, которые имеют менее производные типы (контравариативность), чем указано в типе делегата. Это касается не только универсальных методов-делегатов, но и методов-делегатов, не являющихся универсальными.

Например, рассмотрим следующий код, который содержит два класса и два делегата: универсальный и неуниверсальный.

public class First { }  
public class Second : First { }  
public delegate First SampleDelegate(Second a);  
public delegate R SampleGenericDelegate<A, R>(A a);  

При создании делегатов типов SampleDelegate или SampleGenericDelegate<A, R> им можно назначить любой из следующих методов.

// Matching signature.  
public static First ASecondRFirst(Second second)  
{ return new First(); }  
  
// The return type is more derived.  
public static Second ASecondRSecond(Second second)  
{ return new Second(); }  
  
// The argument type is less derived.  
public static First AFirstRFirst(First first)  
{ return new First(); }  
  
// The return type is more derived
// and the argument type is less derived.  
public static Second AFirstRSecond(First first)  
{ return new Second(); }  

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

// Assigning a method with a matching signature
// to a non-generic delegate. No conversion is necessary.  
SampleDelegate dNonGeneric = ASecondRFirst;  
// Assigning a method with a more derived return type
// and less derived argument type to a non-generic delegate.  
// The implicit conversion is used.  
SampleDelegate dNonGenericConversion = AFirstRSecond;  
  
// Assigning a method with a matching signature to a generic delegate.  
// No conversion is necessary.  
SampleGenericDelegate<Second, First> dGeneric = ASecondRFirst;  
// Assigning a method with a more derived return type
// and less derived argument type to a generic delegate.  
// The implicit conversion is used.  
SampleGenericDelegate<Second, First> dGenericConversion = AFirstRSecond;  

Дополнительные примеры см. в разделах Использование вариативности в делегатах (C#) и Использование вариативности в универсальных методах-делегатах Func и Action (C#).

Вариативность в параметрах универсального типа

В платформе .NET Framework 4 и более поздних версиях можно включить неявное преобразование между делегатами, которое позволит универсальным методам-делегатам, имеющим разные типы, указанные параметрами универсального типа, быть назначенными друг другу, если типы наследуются друг от друга так, как того требует вариативность.

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

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

// Type T is declared covariant by using the out keyword.  
public delegate T SampleGenericDelegate <out T>();  
  
public static void Test()  
{  
    SampleGenericDelegate <String> dString = () => " ";  
  
    // You can assign delegates to each other,  
    // because the type T is declared covariant.  
    SampleGenericDelegate <Object> dObject = dString;
}  

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

В следующем примере кода SampleGenericDelegate<String> нельзя явно преобразовать в SampleGenericDelegate<Object>, хотя String наследует Object. Эту проблему можно устранить, пометив универсальный параметр T ключевым словом out.

public delegate T SampleGenericDelegate<T>();  
  
public static void Test()  
{  
    SampleGenericDelegate<String> dString = () => " ";  
  
    // You can assign the dObject delegate  
    // to the same lambda expression as dString delegate  
    // because of the variance support for
    // matching method signatures with delegate types.  
    SampleGenericDelegate<Object> dObject = () => " ";  
  
    // The following statement generates a compiler error  
    // because the generic type T is not marked as covariant.  
    // SampleGenericDelegate <Object> dObject = dString;  
  
}  

Универсальные методы-делегаты с вариативными параметрами типа в .NET

В платформе .NET Framework 4 появилась поддержка вариативности для параметров универсального типа в нескольких существующих методах-делегатах.

Дополнительные примеры см. в разделе Использование вариативности в универсальных методах-делегатах Func и Action (C#).

Объявление вариативных параметров типа в универсальных методах-делегатах

Если универсальный метод-делегат содержит ковариантные или контравариантные параметры универсального типа, он называется вариативным универсальным методом-делегатом.

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

public delegate R DCovariant<out R>();  

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

public delegate void DContravariant<in A>(A a);  

Внимание

Параметры ref, in и out в C# невозможно пометить как вариативные.

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

public delegate R DVariant<in A, out R>(A a);  

Создание экземпляра и вызов вариативных универсальных методов-делегатов

Создание экземпляра и вызов вариативных делегатов возможен только при создании экземпляра и вызове инвариантных делегатов. В следующем примере создается экземпляр делегата с помощью лямбда-выражения.

DVariant<String, String> dvariant = (String str) => str + " ";  
dvariant("test");  

Объединение вариативных универсальных методов-делегатов

Не объединяйте вариативные делегаты. Метод Combine не поддерживает преобразование вариантных делегатов и ожидает делегаты того же самого типа. Это может вызвать исключение времени выполнения при объединении делегатов с помощью метода Combine или оператора +, как показано в следующем примере кода.

Action<object> actObj = x => Console.WriteLine("object: {0}", x);  
Action<string> actStr = x => Console.WriteLine("string: {0}", x);  
// All of the following statements throw exceptions at run time.  
// Action<string> actCombine = actStr + actObj;  
// actStr += actObj;  
// Delegate.Combine(actStr, actObj);  

Вариативность в параметрах универсального типа для значения и ссылочных типов

Вариативность для параметров универсального типа поддерживается только для ссылочных типов. Например, DVariant<int> нельзя неявно преобразовать в DVariant<Object> или DVariant<long>, поскольку integer является типом значения.

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

// The type T is covariant.  
public delegate T DVariant<out T>();  
  
// The type T is invariant.  
public delegate T DInvariant<T>();  
  
public static void Test()  
{  
    int i = 0;  
    DInvariant<int> dInt = () => i;  
    DVariant<int> dVariantInt = () => i;  
  
    // All of the following statements generate a compiler error  
    // because type variance in generic parameters is not supported  
    // for value types, even if generic type parameters are declared variant.  
    // DInvariant<Object> dObject = dInt;  
    // DInvariant<long> dLong = dInt;  
    // DVariant<Object> dVariantObject = dVariantInt;  
    // DVariant<long> dVariantLong = dVariantInt;
}  

См. также