C# Funktionale Programmierung im Detail (11) Kovarianz und Kontravarianz

C# Funktionale Programmierung im Detail (11) Kovarianz und Kontravarianz

[LINQ via C#-Reihe]

[Eingehende Serie zur funktionalen Programmierung in C#]

Neueste Version:https://weblogs.asp.net/dixin/functional-csharp-covariance-and-contravariance

In Kovarianz und Kontravarianz bedeutet Varianz die Fähigkeit, einen Typ in einem Kontext durch einen stärker abgeleiteten oder weniger abgeleiteten Typ zu ersetzen. Das Folgende ist eine einfache Vererbungshierarchie:

internal class Base { }

internal class Derived : Base { }

Base ist ein weniger abgeleiteter Typ und Derived ist ein stärker abgeleiteter Typ. Eine abgeleitete Instanz „ist also eine“ Basisinstanz, oder mit anderen Worten, eine abgeleitete Instanz kann eine Basisinstanz ersetzen:

internal static partial class Variances
{
    internal static void Substitute()
    {
        Base @base = new Base();
        @base = new Derived();
    }
}

Hier diskutieren Kovarianz und Kontravarianz die „ist ein“- oder Substitutionsbeziehung von Funktionen und generischen Schnittstellen. C# 2.0 führt Abweichungen für Funktionen ein, und C# 4.0 führt Abweichungen für generische Delegattypen und generische Schnittstellen ein. C#-Kovarianz/Kontravarianz gilt nur für Referenztypen, nicht für Werttypen. Die obigen Basis- und abgeleiteten Typen sind also als Klassen definiert und werden verwendet, um die Abweichungen zu demonstrieren.

Variationen des nicht generischen Funktionstyps

Durch Verwendung der obigen Basis und Abgeleitet als Eingabe- und Ausgabetyp der Funktion gibt es 4 Kombinationen:

// Derived -> Base
internal static Base DerivedToBase(Derived input) => new Base();

// Derived -> Derived
internal static Derived DerivedToDerived(Derived input) => new Derived();

// Base -> Base
internal static Base BaseToBase(Base input) => new Base();

// Base -> Derived
internal static Derived BaseToDerived(Base input) => new Derived();

Es gibt 4 verschiedene Funktionstypen:

internal delegate Base DerivedToBase(Derived input); // Derived -> Base

internal delegate Derived DerivedToDerived(Derived input); // Derived -> Derived

internal delegate Base BaseToBase(Base input); // Base -> Base

internal delegate Derived BaseToDerived(Base input); // Base -> Derived

Nehmen Sie als Beispiel die zweite Funktion DerivedToDerived, natürlich ist sie vom zweiten Funktionstyp DerivedToDerived:

internal static void NonGeneric()
{
    DerivedToDerived derivedToDerived = DerivedToDerived;
    Derived output = derivedToDerived(input: new Derived());
}

Seit C# 2.0 scheint es auch vom ersten Funktionstyp DerivedToBase zu sein:

internal static void NonGenericCovariance()
{
    DerivedToBase derivedToBase = DerivedToBase; // Derived -> Base

    // Covariance: Derived is Base, so that DerivedToDerived is DerivedToBase.
    derivedToBase = DerivedToDerived; // Derived -> Derived

    // When calling derivedToBase, DerivedToDerived executes.
    // derivedToBase should output Base, while DerivedToDerived outputs Derived.
    // The actual Derived output is the required Base output. This always works.
    Base output = derivedToBase(input: new Derived());
}

Daher kann die tatsächliche Ausgabe der Funktionsinstanz stärker abgeleitet werden als die erforderliche Ausgabe des Funktionstyps. Daher „ist eine Funktion mit stärker abgeleiteter Ausgabe eine“ Funktion mit weniger abgeleiteter Ausgabe, oder mit anderen Worten, eine Funktion mit stärker abgeleiteter Ausgabe kann eine Funktion mit weniger abgeleiteter Ausgabe ersetzen. Dies wird als Kovarianz bezeichnet. Ebenso kann die Eingabe der Funktionsinstanz weniger abgeleitet sein als die Eingabe des Funktionstyps:

internal static void NonGenericContravariance()
{
    DerivedToBase derivedToBase = DerivedToBase; // Derived -> Base

    // Contravariance: Derived is Base, so that BaseToBase is DerivedToBase.
    derivedToBase = BaseToBase; // Base -> Base

    // When calling derivedToBase, BaseToBase executes.
    // derivedToBase should accept Derived input, while BaseToBase accepts Base input.
    // The required Derived input is the accepted Base input. This always works.
    Base output = derivedToBase(input: new Derived());
}

Daher „ist eine Funktion mit weniger abgeleiteter Eingabe eine“ Funktion mit stärker abgeleiteter Eingabe, oder mit anderen Worten, eine Funktion mit weniger abgeleiteter Eingabe kann eine Funktion durch eine stärker abgeleitete Eingabe ersetzen. Das nennt man Kontravarianz. Kovarianz und Kontravarianz können gleichzeitig auftreten:

internal static void NonGenericeCovarianceAndContravariance()
{
    DerivedToBase derivedToBase = DerivedToBase; // Derived -> Base

    // Covariance and contravariance: Derived is Base, so that BaseToDerived is DerivedToBase. 
    derivedToBase = BaseToDerived; // Base -> Derived

    // When calling derivedToBase, BaseToDerived executes.
    // derivedToBase should accept Derived input, while BaseToDerived accepts Base input.
    // The required Derived input is the accepted Base input.
    // derivedToBase should output Base, while BaseToDerived outputs Derived.
    // The actual Derived output is the required Base output. This always works.
    Base output = derivedToBase(input: new Derived());
}

Anscheinend kann die Ausgabe einer Funktionsinstanz nicht weniger abgeleitet sein als die Ausgabe eines Funktionstyps, und eine Funktionseingabe kann nicht stärker abgeleitet sein als eine Eingabe des Funktionstyps. Der folgende Code kann nicht kompiliert werden:

internal static void NonGenericInvalidVariance()
{
    // baseToDerived should output Derived, while BaseToBase outputs Base. 
    // The actual Base output is not the required Derived output. This cannot be compiled.
    BaseToDerived baseToDerived = BaseToBase; // Base -> Derived

    // baseToDerived should accept Base input, while DerivedToDerived accepts Derived input.
    // The required Base input is not the accepted Derived input. This cannot be compiled.
    baseToDerived = DerivedToDerived; // Derived -> Derived

    // baseToDerived should accept Base input, while DerivedToBase accepts Derived input.
    // The required Base input is not the expected Derived input.
    // baseToDerived should output Derived, while DerivedToBase outputs Base.
    // The actual Base output is not the required Derived output. This cannot be compiled.
    baseToDerived = DerivedToBase; // Derived -> Base
}

Variationen des generischen Funktionstyps

Mit dem generischen Delegattyp können alle oben genannten Funktionstypen dargestellt werden durch:

internal delegate TOutput GenericFunc<TInput, TOutput>(TInput input);

Dann können die obigen Abweichungen dargestellt werden als:

internal static void Generic()
{
    GenericFunc<Derived, Base> derivedToBase = DerivedToBase; // GenericFunc<Derived, Base>: no variances.
    derivedToBase = DerivedToDerived; // GenericFunc<Derived, Derived>: covariance.
    derivedToBase = BaseToBase; // GenericFunc<Base, Base>: contravariance.
    derivedToBase = BaseToDerived; // GenericFunc<Base, Derived>: covariance and contravariance.
}

Bei Funktionen vom Typ GenericFunc kann Kovarianz auftreten, wenn TOutput durch einen stärker abgeleiteten Typ ersetzt wird, und Kontravarianz kann auftreten, wenn TInput durch einen weniger abgeleiteten Typ ersetzt wird. Daher wird TOutput für diesen generischen Delegattyp als kovarianter Typparameter bezeichnet, und TInput wird als kontravarianter Typparameter bezeichnet. C# 4.0 führt die out/in-Modifikatoren für den kovarianten/kontravarianten Typparameter ein:

internal delegate TOutput GenericFuncWithVariances<in TInput, out TOutput>(TInput input);

Diese Modifikatoren ermöglichen die implizite Konvertierung/Substitution zwischen Funktionen:

internal static void FunctionImplicitConversion()
{
    GenericFuncWithVariances<Derived, Base> derivedToBase = DerivedToBase; // Derived -> Base
    GenericFuncWithVariances<Derived, Derived> derivedToDerived = DerivedToDerived; // Derived -> Derived
    GenericFuncWithVariances<Base, Base> baseToBase = BaseToBase; // Base -> Base
    GenericFuncWithVariances<Base, Derived> baseToDerived = BaseToDerived; // Base -> Derived

    // Cannot be compiled without the out/in modifiers.
    derivedToBase = derivedToDerived; // Covariance.
    derivedToBase = baseToBase; // Contravariance.
    derivedToBase = baseToDerived; // Covariance and contravariance.
}

Wie bereits erwähnt, werden einheitliche generische Delegattypen Func und Action bereitgestellt, um alle Funktionstypen darzustellen. Seit .NET Framework 4.0 haben alle ihre Typparameter die Out/In-Modifikatoren:

namespace System
{
    public delegate TResult Func<out TResult>();

    public delegate TResult Func<in T, out TResult>(T arg);

    public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);

    // ...

    public delegate void Action();

    public delegate void Action<in T>(T obj);

    public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);

    // ...
}

Der Variant-Typ-Parameter ist kein syntaktischer Zucker. Die Out/In-Modifikatoren werden in CIL +/–-Flags in CIL:

kompiliert
.class public auto ansi sealed Func<-T, +TResult> extends System.MulticastDelegate
{
    .method public hidebysig newslot virtual instance !TResult Invoke(!T arg) runtime managed
    {
    }

    // Other members.
}

Varianten der generischen Schnittstelle

Neben generischen Delegattypen führt C# 4.0 auch Varianten für generische Schnittstellen ein. Eine Schnittstelle kann als ein Satz von Signaturen von Funktionsmitgliedern angesehen werden, um ihre Funktionstypen ohne Implementierungen anzugeben. Zum Beispiel:

internal interface IOutput<out TOutput> // TOutput is covariant for all members using TOutput.
{
    TOutput ToOutput(); // () -> TOutput

    TOutput Output { get; } // get_Output: () -> TOutput

    void TypeParameterNotUsed();
}

In der obigen generischen Schnittstelle gibt es 2 Funktionsmitglieder, die den Typparameter verwenden, und der Typparameter ist kovariant für die Funktionstypen dieser 2 Funktionen. Daher ist der Typparameter für die Schnittstelle kovariant, und der Modifizierer out kann verwendet werden, um die implizite Konvertierung zu aktivieren:

internal static void GenericInterfaceCovariance(IOutput<Base> outputBase, IOutput<Derived> outputDerived)
{
    // Covariance: Derived is Base, so that IOutput<Derived> is IOutput<Base>.
    outputBase = outputDerived;

    // When calling outputBase.ToOutput, outputDerived.ToOutput executes.
    // outputBase.ToOutput should output Base, outputDerived.ToOutput outputs Derived.
    // The actual Derived output is the required Base output. This always works.
    Base output1 = outputBase.ToOutput();

    Base output2 = outputBase.Output; // outputBase.get_Output().
}

Die IOutput-Schnittstelle erbt die IOutput-Schnittstelle nicht, aber es scheint, dass eine IOutput-Schnittstelle „eine“ IOutput-Schnittstelle ist, oder mit anderen Worten, eine IOutput-Schnittstelle mit einem abgeleiteteren Typargument ersetzen kann IOutput mit weniger abgeleitetem Typargument. Dies ist die Kovarianz der generischen Schnittstelle. Ebenso kann die generische Schnittstelle auch kontravariante Typparameter haben, und der Modifizierer in kann die implizite Konvertierung aktivieren:

internal interface IInput<in TInput> // TInput is contravariant for all members using TInput.
{
    void InputToVoid(TInput input); // TInput -> void

    TInput Input { set; } // set_Input: TInput -> void

    void TypeParameterNotUsed();
}

Die IInput-Schnittstelle erbt die IInput-Schnittstelle nicht, aber es scheint, dass eine IInput-Schnittstelle „eine“ IInput-Schnittstelle ist, oder mit anderen Worten, die IInput-Schnittstelle kann durch ein abgeleiteteres Typargument ersetzt werden IInput mit weniger abgeleitetem Typargument. Dies ist die Kontravarianz der generischen Schnittstelle:

internal static void GenericInterfaceContravariance(IInput<Derived> inputDerived, IInput<Base> inputBase)
{
    // Contravariance: Derived is Base, so that IInput<Base> is IInput<Derived>.
    inputDerived = inputBase;

    // When calling inputDerived.Input, inputBase.Input executes.
    // inputDerived.Input should accept Derived input, while inputBase.Input accepts Base input.
    // The required Derived output is the accepted Base input. This always works.
    inputDerived.InputToVoid(input: new Derived());

    inputDerived.Input = new Derived();
}

Ähnlich wie beim generischen Delegattyp kann die generische Schnittstelle gleichzeitig einen kovarianten Typparameter und einen kontravarianten Typparameter haben:

internal interface IInputOutput<in TInput, out TOutput> // TInput/TOutput is contravariant/covariant for all members using TInput/TOutput.
{
    void InputToVoid(TInput input); // TInput -> void

    TInput Input { set; } // set_Input: TInput -> void

    TOutput ToOutput(); // () -> TOutput

    TOutput Output { get; } // get_Output: () -> TOutput

    void TypeParameterNotUsed();
}

Das folgende Beispiel demonstriert die Kovarianz und Kontravarianz:

internal static void GenericInterfaceCovarianceAndContravariance(
    IInputOutput<Derived, Base> inputDerivedOutputBase, IInputOutput<Base, Derived> inputBaseOutputDerived)
{
    // Covariance and contravariance: Derived is Base, so that IInputOutput<Base, Derived> is IInputOutput<Derived, Base>.
    inputDerivedOutputBase = inputBaseOutputDerived;

    inputDerivedOutputBase.InputToVoid(new Derived());
    inputDerivedOutputBase.Input = new Derived();
    Base output1 = inputDerivedOutputBase.ToOutput();
    Base output2 = inputDerivedOutputBase.Output;
}

Nicht alle Typparameter können Varianten für generische Schnittstellen sein. Zum Beispiel:

internal interface IInvariant<T>
{
    T Output(); // T is covariant for Output: () -> T.

    void Input(T input); // T is contravariant for Input: T -> void.
}

Der Typparameter T ist weder kovariant für alle Funktionsmitglieder, die T verwenden, noch kontravariant für alle Funktionsmitglieder, die T verwenden, daher kann T für die Schnittstelle nicht kovariant oder kontravariant sein.

Variationen der generischen Funktion höherer Ordnung

Bisher geht es bei der Kovarianz und dem Out-Modifikator nur um die Ausgabe, und bei der Kontravarianz und dem In-Modifikator um die Eingabe. Die Varianzen sind für generische Funktionstypen höherer Ordnung interessant. Der folgende Funktionstyp ist beispielsweise höherwertiger, weil er eine Funktion zurückgibt:

internal delegate Func<TOutput> ToFunc<out TOutput>(); // Covariant output type.

Der Typparameter wird vom Ausgabefunktionstyp verwendet, wo er immer noch kovariant ist. Das folgende Beispiel demonstriert, wie das funktioniert:

internal static void OutputVariance()
{
    // First order functions.
    Func<Base> toBase = () => new Base();
    Func<Derived> toDerived = () => new Derived();

    // Higher-order functions.
    ToFunc<Base> toToBase = () => toBase;
    ToFunc<Derived> toToDerived = () => toDerived;

    // Covariance: Derived is Base, so that ToFunc<Derived> is ToFunc<Base>.
    toToBase = toToDerived;

    // When calling toToBase, toToDerived executes.
    // toToBase should output Func<Base>, while toToDerived outputs Func<Derived>.
    // The actual Func<Derived> output is the required Func<Base> output. This always works.
    Func<Base> output = toToBase();
}

Wenn bei Funktionstypen höherer Ordnung der Typparameter im Ausgabefunktionstyp verwendet wird, ist er immer kovariant:

// () -> T:
internal delegate TOutput Func<out TOutput>(); // Covariant output type.

// () -> () -> T, equivalent to Func<Func<T>>:
internal delegate Func<TOutput> ToFunc<out TOutput>(); // Covariant output type.

// () -> () -> () -> T: Equivalent to Func<Func<Func<T>>>:
internal delegate ToFunc<TOutput> ToToFunc<out TOutput>(); // Covariant output type.

// () -> () -> () -> () -> T: Equivalent to Func<Func<Func<Func<T>>>>:
internal delegate ToToFunc<TOutput> ToToToFunc<out TOutput>(); // Covariant output type.

// ...

Auf ähnliche Weise kann ein Funktionstyp höherer Ordnung definiert werden, indem die Funktion als Eingabe akzeptiert wird:

internal delegate void ActionToVoid<in TTInput>(Action<TTInput> action); // Cannot be compiled.

internal static void InputVariance()
{
    ActionToVoid<Derived> derivedToVoidToVoid = (Action<Derived> derivedToVoid) => { };
    ActionToVoid<Base> baseToVoidToVoid = (Action<Base> baseToVoid) => { };
    derivedToVoidToVoid = baseToVoidToVoid;
}

Der obige Code kann jedoch nicht kompiliert werden. Der Grund dafür ist, dass der Typparameter, wenn er vom Eingabefunktionstyp verwendet wird, kovariant oder kontravariant sein kann. In diesem Fall wird es kontravariant:

internal delegate void ActionToVoid<out TInput>(Action<TInput> action);

Und so funktioniert es:

internal static void InputVariance()
{
    // Higher-order functions.
    ActionToVoid<Derived> derivedToVoidToVoid = (Action<Derived> derivedToVoid) => { };
    ActionToVoid<Base> baseToVoidToVoid = (Action<Base> baseToVoid) => { };

    // Covariance: Derived is Base, so that ActionToVoid<Derived> is ActionToVoid<Base>.
    baseToVoidToVoid = derivedToVoidToVoid;

    // When calling baseToVoidToVoid, derivedToVoidToVoid executes.
    // baseToVoidToVoid should accept Action<Base> input, while derivedToVoidToVoid accepts Action<Derived> input.
    // The required Action<Derived> input is the accepted Action<Base> input. This always works.
    baseToVoidToVoid(default(Action<Base>));
}

Für Funktionstypen höherer Ordnung, wenn Typparameter im Eingabefunktionstyp verwendet werden, sind hier die Abweichungen:

// () -> void:
internal delegate void Action<in TInput>(TInput input); // Contravariant input type.

// (() -> void) -> void, equivalent to Action<Action<T>>:
internal delegate void ActionToVoid<out TTInput>(Action<TTInput> action); // Covariant input type.

// ((() -> void) -> void) -> void, equivalent to Action<Action<Action<T>>>:
internal delegate void ActionToVoidToVoid<in TTInput>(ActionToVoid<TTInput> actionToVoid); // Contravariant input type.

// (((() -> void) -> void) -> void) -> void, equivalent to Action<Action<Action<Action<T>>>>:
internal delegate void ActionToVoidToVoidToVoid<out TTInput>(ActionToVoidToVoid<TTInput> actionToVoidToVoid); // Covariant input type.

// ...

Kovarianz des Arrays

Wie bereits erwähnt, implementiert ein Array T[] IList:

namespace System.Collections.Generic
{
    public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
    {
        T this[int index] { get; set; }
        // T is covariant for get_Item: int -> T.
        // T is contravariant for set_Item: (int, T) -> void.

        // Other members.
    }
}

Für IList ist T nicht kovariant für seinen Indexer-Setter und T ist nicht kontravariant für seinen Indexer-Getter. Daher sollte T für IList und Array T[] unveränderlich sein. C#-Compiler und CLR/CoreCLR unterstützen jedoch unerwartet Kovarianz für Arrays. Das folgende Beispiel kann kompiliert werden, löst aber zur Laufzeit eine ArrayTypeMismatchException aus, die eine Fehlerquelle sein kann:

internal static void ArrayCovariance()
{
    Base[] baseArray = new Base[3];
    Derived[] derivedArray = new Derived[3];

    baseArray = derivedArray; // Array covariance at compile time, baseArray refers to a Derived array at runtime.
    Base value = baseArray[0];
    baseArray[1] = new Derived();
    baseArray[2] = new Base(); // ArrayTypeMismatchException at runtime, Base cannot be in Derived array.
}

Hier sind einige Hintergrundinformationen zur Array-Kovarianz:

  • Jonathan Allen sagte:
  • In dem Buch „The Common Language Infrastructure Annotated Standard“ sagte Jim Miller,
  • Rick Byers sagte:
  • Anders Hejlsberg (Chefarchitekt von C#) sagte in diesem Video
  • Eric Lippert (Mitglied des C#-Designteams) hat die Array-Kovarianz an die Spitze der 10 schlechtesten C#-Features gesetzt

Dies ist eine Funktion der C#-Sprache, die niemals verwendet werden sollte.

Variationen in .NET und LINQ

Die folgende LINQ-Abfrage findet die generischen Delegattypen und Schnittstellen mit Variantentypparametern in der .NET Core-Bibliothek:

internal static void TypesWithVariance()
{
    Assembly coreLibrary = typeof(object).Assembly;
    coreLibrary.GetExportedTypes()
        .Where(type => type.GetGenericArguments().Any(typeArgument =>
        {
            GenericParameterAttributes attributes = typeArgument.GenericParameterAttributes;
            return attributes.HasFlag(GenericParameterAttributes.Covariant)
                || attributes.HasFlag(GenericParameterAttributes.Contravariant);
        }))
        .OrderBy(type => type.FullName)
        .WriteLines();
        // System.Action`1[T]
        // System.Action`2[T1,T2]
        // System.Action`3[T1,T2,T3]
        // System.Action`4[T1,T2,T3,T4]
        // System.Action`5[T1,T2,T3,T4,T5]
        // System.Action`6[T1,T2,T3,T4,T5,T6]
        // System.Action`7[T1,T2,T3,T4,T5,T6,T7]
        // System.Action`8[T1,T2,T3,T4,T5,T6,T7,T8]
        // System.Collections.Generic.IComparer`1[T]
        // System.Collections.Generic.IEnumerable`1[T]
        // System.Collections.Generic.IEnumerator`1[T]
        // System.Collections.Generic.IEqualityComparer`1[T]
        // System.Collections.Generic.IReadOnlyCollection`1[T]
        // System.Collections.Generic.IReadOnlyList`1[T]
        // System.Comparison`1[T]
        // System.Converter`2[TInput,TOutput]
        // System.Func`1[TResult]
        // System.Func`2[T,TResult]
        // System.Func`3[T1,T2,TResult]
        // System.Func`4[T1,T2,T3,TResult]
        // System.Func`5[T1,T2,T3,T4,TResult]
        // System.Func`6[T1,T2,T3,T4,T5,TResult]
        // System.Func`7[T1,T2,T3,T4,T5,T6,TResult]
        // System.Func`8[T1,T2,T3,T4,T5,T6,T7,TResult]
        // System.Func`9[T1,T2,T3,T4,T5,T6,T7,T8,TResult]
        // System.IComparable`1[T]
        // System.IObservable`1[T]
        // System.IObserver`1[T]
        // System.IProgress`1[T]
        // System.Predicate`1[T]
}

Unter System.Linq-Namespace gibt es auch eine Reihe generischer Schnittstellen mit Varianz:IGrouping, IQueryable, IOrderedQueryable. MSDN hat eine Liste mit Varianten generischer Schnittstellen- und Delegate-Typen, aber sie ist ungenau. Zum Beispiel heißt es, dass TElement kovariant für IOrderedEnumerable ist, aber eigentlich nicht:

namespace System.Linq
{
    public interface IOrderedEnumerable<TElement> : IEnumerable<TElement>, IEnumerable
    {
        IOrderedEnumerable<TElement> CreateOrderedEnumerable<TKey>(Func<TElement, TKey> keySelector, IComparer<TKey> comparer, bool descending);
    }
}

Für lokales sequentielles LINQ ist T, wie oben erwähnt, kovariant für IEnumerable. Hier ist die ganze Geschichte:

namespace System.Collections.Generic
{
    /// <summary>Exposes the enumerator, which supports a simple iteration over a collection of a specified type.</summary>
    /// <typeparam name="T">The type of objects to enumerate.This type parameter is covariant. That is, you can use either the type you specified or any type that is more derived. For more information about covariance and contravariance, see Covariance and Contravariance in Generics.</typeparam>
    public interface IEnumerator<out T> : IDisposable, IEnumerator
    {
        T Current { get; } // T is covariant for get_Current: () –> T.
    }

    /// <summary>Exposes the enumerator, which supports a simple iteration over a collection of a specified type.</summary>
    /// <typeparam name="T">The type of objects to enumerate.This type parameter is covariant. That is, you can use either the type you specified or any type that is more derived. For more information about covariance and contravariance, see Covariance and Contravariance in Generics.</typeparam>
    public interface IEnumerable<out T> : IEnumerable
    {
        IEnumerator<T> GetEnumerator(); // T is covariant for IEnumerator<T>, so T is covariant for () -> IEnumerator<T>.
    }
}

Erstens wird der Typparameter von IEnumerator nur vom Getter seiner Current-Eigenschaft verwendet, der als get_Current-Funktion vom Typ () –> T angesehen werden kann, und IEnumerator kann als Wrapper von () – angesehen werden.> T-Funktion. Da T Kovarianz für () –> T-Funktion ist, ist T auch Kovarianz für IEnumerator-Wrapper. Dann wird T in IEnumerable nur von der GetEnumerator-Methode verwendet, die IEnumerator zurückgibt. In Bezug auf IEnumerator ist ein einfacher Wrapper von () –> T-Funktion, GetEnumerator kann praktisch als eine Funktion höherer Ordnung angesehen werden, die () –> T-Funktion zurückgibt. Daher kann GetEnumerators Funktionstyp () –> IEnumerator praktisch als Funktionstyp höherer Ordnung () –> () –> T angesehen werden. Und ähnlich kann IEnumerable als Wrapper dieser () –> () –> T-Funktion betrachtet werden. Da T immer noch kovariant für () –> () –> T ist, ist T auch kovariant für IEnumerable Wrapper. Dies erleichtert LINQ-Abfragen. Beispielsweise verkettet die folgende LINQ-Abfragemethode zwei IEnumerable-Instanzen:

namespace System.Linq
{
    public static class Enumerable
    {
        public static IEnumerable<TSource> Concat<TSource>(this IEnumerable<TSource> first, IEnumerable<TSource> second);
    }
}

Der folgende Code veranschaulicht die implizite Konvertierung, die durch den out-Modifizierer in der IEnumerable-Definition aktiviert wird:

internal static void LinqToObjects(IEnumerable<Base> enumerableOfBase, IEnumerable<Derived> enumerableOfDerived)
{
    enumerableOfBase = enumerableOfBase.Concat(enumerableOfDerived);
}

Für lokales paralleles LINQ ist ParallelQuery eine Klasse anstelle einer Schnittstelle, daher ist T keine Variante. Auch hier gilt die Varianz des Typparameters für den Funktionstyp, einschließlich des nicht generischen Delegattyps, des generischen Delegattyps und der generischen Schnittstelle. Die Klasse kann eine Funktionsimplementierung haben, daher gelten keine Abweichungen.

Für Remote-LINQ ist hier die Definition von IQueryable:

namespace System.Linq
{
    /// <summary>Provides functionality to evaluate queries against a specific data source wherein the type of the data is known.</summary>
    /// <typeparam name="T">The type of objects to enumerate.This type parameter is covariant. That is, you can use either the type you specified or any type that is more derived. For more information about covariance and contravariance, see Covariance and Contravariance in Generics.</typeparam>
    public interface IQueryable<out T> : IEnumerable<T>, IEnumerable, IQueryable { }
}

Hier wird T nur für das von IEnumerable geerbte Mitglied verwendet, also bleibt T anscheinend kovariant für IQueryable.