Approfondimenti sulla programmazione funzionale in C# (11) Covarianza e controvarianza

Approfondimenti sulla programmazione funzionale in C# (11) Covarianza e controvarianza

[LINQ tramite serie C#]

[Serie di approfondimento programmazione funzionale C#]

Ultima versione:https://weblogs.asp.net/dixin/functional-csharp-covariance-and-contravariance

In covarianza e controvarianza, varianza significa la capacità di sostituire un tipo con un tipo più derivato o un tipo meno derivato in un contesto. Quella che segue è una semplice gerarchia di eredità:

internal class Base { }

internal class Derived : Base { }

Base è un tipo meno derivato e Derived è un tipo più derivato. Quindi un'istanza derivata "è una" istanza di base, o in altre parole, un'istanza derivata può sostituire un'istanza di base:

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

Qui covarianza e controvarianza discutono della relazione "è a" o di sostituzione di funzioni e interfacce generiche. C# 2,0 introduce le varianze per le funzioni e C# 4,0 introduce le varianze per i tipi delegati generici e le interfacce generiche. La covarianza/controvarianza in C# si applica solo ai tipi di riferimento, non ai tipi di valore. Quindi i tipi Base e Derivato sopra sono definiti come classi e vengono utilizzati per dimostrare le varianze.

Varianze di tipo di funzione non generica

Utilizzando sopra Base e Derivato come tipo di funzione di input e output, ci sono 4 combinazioni:

// 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();

Sono di 4 diversi tipi di funzioni:

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

Prendi come esempio la seconda funzione DerivedToDerived, naturalmente è del secondo tipo di funzione DerivedToDerived:

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

Da C# 2.0, sembra anche del primo tipo di funzione DerivedToBase:

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());
}

Quindi l'output effettivo dell'istanza della funzione può essere più derivato dell'output richiesto del tipo di funzione. Pertanto, la funzione con un output più derivato "è una" funzione con un output meno derivato, o in altre parole, una funzione con un output più derivato può sostituire la funzione con un output meno derivato. Questo si chiama covarianza. Allo stesso modo, l'input dell'istanza della funzione può essere meno derivato dell'input del tipo di funzione:

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());
}

Pertanto, la funzione con input meno derivato "è una" funzione con input più derivati, o in altre parole, la funzione con input meno derivati ​​può sostituire la funzione con input più derivati. Questo si chiama controvarianza. Covarianza e controvarianza possono verificarsi contemporaneamente:

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());
}

Apparentemente, l'output dell'istanza della funzione non può essere meno derivato dell'output del tipo di funzione e l'input della funzione non può essere più derivato dell'input del tipo di funzione. Non è possibile compilare il seguente codice:

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
}

Varianze del tipo di funzione generico

Con il tipo delegato generico, tutti i tipi di funzione precedenti possono essere rappresentati da:

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

Quindi le varianze di cui sopra possono essere rappresentate come:

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.
}

Per le funzioni di tipo GenericFunc, la covarianza può verificarsi quando TOutput viene sostituito da un tipo più derivato e la controvarianza può verificarsi quando TInput viene sostituito da un tipo meno derivato. Quindi TOutput è chiamato parametro di tipo covariante per questo tipo delegato generico e TInput è chiamato parametro di tipo controvariante. C# 4.0 introduce i modificatori out/in per il parametro di tipo covariante/controvariante:

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

Questi modificatori abilitano la conversione/sostituzione implicita tra funzioni:

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.
}

Come accennato in precedenza, vengono forniti tipi di delegati generici Func e Action unificati per rappresentare tutti i tipi di funzione. Da .NET Framework 4.0, tutti i parametri di tipo hanno i modificatori out/in:

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);

    // ...
}

Il parametro del tipo di variante non è zucchero sintattico. I modificatori out/in vengono compilati in CIL +/– flag in CIL:

.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.
}

Varianze dell'interfaccia generica

Oltre ai tipi delegati generici, C# 4,0 introduce anche le varianze per le interfacce generiche. Un'interfaccia può essere vista come un insieme di firme dei membri di funzione per indicare i loro tipi di funzione, senza implementazioni. Ad esempio:

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

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

    void TypeParameterNotUsed();
}

Nell'interfaccia generica sopra, ci sono 2 membri di funzione che usano il parametro di tipo e il parametro di tipo è covariante per i tipi di funzione di queste 2 funzioni. Pertanto, il parametro type è covariante per l'interfaccia e il modificatore out può essere utilizzato per abilitare la conversione implicita:

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().
}

L'interfaccia IOutput non eredita l'interfaccia IOutput, ma sembra che un'interfaccia IOutput "è un" interfaccia IOutput, o in altre parole, l'interfaccia IOutput con più argomenti di tipo derivato può sostituire IOutput con argomento di tipo derivato meno. Questa è la covarianza dell'interfaccia generica. Allo stesso modo, l'interfaccia generica può anche avere un parametro di tipo controvariante e il modificatore in può abilitare la conversione implicita:

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();
}

L'interfaccia IInput non eredita l'interfaccia IInput, ma sembra che un'interfaccia IInput "sia" un'interfaccia IInput o, in altre parole, l'interfaccia IInput con più argomenti di tipo derivato può sostituire IInput con argomento di tipo derivato meno. Questa è la controvarianza dell'interfaccia generica:

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();
}

Simile al tipo delegato generico, l'interfaccia generica può avere contemporaneamente un parametro di tipo covariante e un parametro di tipo controvariante:

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();
}

L'esempio seguente mostra la covarianza e la controvarianza:

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;
}

Non tutti i parametri di tipo possono essere varianti per l'interfaccia generica. Ad esempio:

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

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

Il parametro di tipo T non è né covariante per tutti i membri di funzione che utilizzano T, né controvariante per tutti i membri di funzione che utilizzano T, quindi T non può essere covariante o controvariante per l'interfaccia.

Varianze di funzioni generiche di ordine superiore

Finora la covarianza e il modificatore out riguardano l'output e la controvarianza e il modificatore in riguardano tutti l'input. Le varianze sono interessanti per tipi di funzione generici di ordine superiore. Ad esempio, il seguente tipo di funzione è di ordine superiore, perché restituisce una funzione:

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

Il parametro type viene utilizzato dal tipo di funzione di output, dove è ancora covariante. L'esempio seguente mostra come funziona:

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();
}

Per i tipi di funzione di ordine superiore, quando il parametro di tipo viene utilizzato nel tipo di funzione di output, è sempre covariante:

// () -> 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.

// ...

Allo stesso modo, il tipo di funzione di ordine superiore può essere definito accettando la funzione come input:

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;
}

Tuttavia, il codice sopra non può essere compilato. Il motivo è che, quando il parametro di tipo viene utilizzato dal tipo di funzione di input, può essere covariante o controvariante. In questo caso, diventa controvariante:

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

Ed ecco come funziona:

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>));
}

Per i tipi di funzione di ordine superiore, quando il parametro type viene utilizzato nel tipo di funzione di input, ecco le sue varianze:

// () -> 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.

// ...

Covarianza dell'array

Come accennato in precedenza, un array T[] implementa 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.
    }
}

Per IList, T non è covariante per il relativo setter dell'indicizzatore e T non è controvariante per il relativo getter dell'indicizzatore. Quindi T dovrebbe essere invariante per IList e array T[]. Tuttavia, il compilatore C# e CLR/CoreCLR supportano in modo imprevisto la covarianza per l'array. L'esempio seguente può essere compilato ma genera ArrayTypeMismatchException in fase di esecuzione, che può essere una fonte di bug:

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.
}

Ecco alcune informazioni di base per la covarianza dell'array:

  • Jonathan Allen ha detto,
  • Nel libro "The Common Language Infrastructure Annotated Standard", ha affermato Jim Miller,
  • Rick Byers ha detto,
  • Anders Hejlsberg (architetto capo di C#) ha detto in questo video,
  • Eric Lippert (membro del team di progettazione di C#) ha inserito la covarianza dell'array tra le prime 1 delle 10 peggiori funzionalità di C#

Questa è una funzionalità del linguaggio C# che non dovrebbe mai essere utilizzata.

Varianze in .NET e LINQ

La query LINQ seguente trova i tipi delegati generici e le interfacce con i parametri di tipo variant nella libreria .NET core:

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]
}

Nello spazio dei nomi System.Linq sono presenti anche numerose interfacce generiche con varianza:IGrouping, IQueryable, IOrderedQueryable. MSDN ha un elenco di tipi di interfaccia generica e delegati, ma è impreciso. Ad esempio, dice TElement è covariante per IOrderedEnumerable, ma in realtà non:

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

Per LINQ sequenziale locale, come accennato in precedenza, T è covariante per IEnumerable. Ecco la storia completa:

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>.
    }
}

Innanzitutto, il parametro di tipo di IEnumerator viene utilizzato solo dal getter della relativa proprietà Current, che può essere visualizzato come una funzione get_Current di tipo () –> T e IEnumerator può essere visualizzato come un wrapper di () –> Funzione T. Poiché T è la covarianza per () –> T funzione, T è anche covariante per IEnumerator wrapper. Quindi, in IEnumerable, T viene utilizzato solo dal metodo GetEnumerator che restituisce IEnumerator. Per quanto riguarda IEnumerator è un semplice wrapper di () –> T funzione, GetEnumerator può essere virtualmente visto come una funzione di ordine superiore che restituisce () –> T funzione, pertanto, il tipo di funzione di GetEnumerator () –> IEnumerator può essere virtualmente visto come tipo di funzione di ordine superiore () –> () –> T. E allo stesso modo, IEnumerable può essere visto come un wrapper di questa funzione () –> () –> T. Poiché T è ancora covariante per () –> () –> T, T è anche covarianza per IEnumerable wrapper. Ciò offre comodità alle query LINQ. Ad esempio, il seguente metodo di query LINQ concatena 2 istanze IEnumerable:

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

Il codice seguente illustra la conversione implicita abilitata dal modificatore out nella definizione IEnumerable:

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

Per Parallel LINQ locale, ParallelQuery è una classe anziché un'interfaccia, quindi T non è una variante. Anche in questo caso, la varianza del parametro di tipo riguarda il tipo di funzione, incluso il tipo delegato non generico, il tipo delegato generico e l'interfaccia generica. La classe può avere l'implementazione della funzione, quindi le varianze non si applicano.

Per LINQ remoto, ecco la definizione di 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 { }
}

Qui T viene utilizzato solo per il membro ereditato da IEnumerable, quindi apparentemente T rimane covariante per IQueryable.