C# 関数型プログラミングの詳細 (11) 共分散と反分散

[C# シリーズ経由の LINQ]

[C# 関数型プログラミングの詳細シリーズ]

最新バージョン:https://weblogs.asp.net/dixin/functional-csharp-covariance-and-contravariance

共変性と反変性において、分散とは、コンテキスト内で型をより派生した型またはより派生した型に置き換える機能を意味します。以下は単純な継承階層です:

internal class Base { }

internal class Derived : Base { }

Base は下位派生型であり、Derived は上位派生型です。したがって、派生インスタンスは「ベース インスタンス」です。つまり、派生インスタンスはベース インスタンスを置き換えることができます。

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

ここで、共分散と反分散は、関数とジェネリック インターフェイスの「is a」または置換関係について説明します。 C# 2.0 では関数の差異が導入され、C# 4.0 ではジェネリック デリゲート型とジェネリック インターフェイスの差異が導入されています。 C# の共分散/反分散は、値型ではなく、参照型にのみ適用されます。したがって、上記の Base 型と Derived 型はクラスとして定義され、差異を示すために使用されます。

非ジェネリック関数型の差異

上記の Base と Derived を関数の入出力タイプとして使用すると、次の 4 つの組み合わせがあります。

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

それらは 4 つの異なる機能タイプです:

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

例として 2 番目の関数 DerivedToDerived を取り上げます。当然、これは 2 番目の関数タイプ DerivedToDerived です。

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

C# 2.0 以降、最初の関数型 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());
}

したがって、関数インスタンスの実際の出力は、関数タイプの必要な出力よりも派生する可能性があります。したがって、派生出力が多い関数は、派生出力が少ない関数である、つまり、派生出力が多い関数は、派生出力が少ない関数を置き換えることができます。これを共分散と呼びます。同様に、関数インスタンスの入力は、関数タイプの入力よりも派生度が低い場合があります:

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

したがって、派生入力が少ない関数は、派生入力が多い関数である、つまり、派生入力が少ない関数は、関数を派生入力が多い関数に置き換えることができます。これを反変性と呼びます。共分散と反分散は同時に発生する可能性があります:

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

明らかに、関数インスタンスの出力は関数型の出力よりも派生することはできず、関数の入力は関数型の入力よりも派生することはできません。次のコードはコンパイルできません:

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
}

汎用関数型の違い

ジェネリック デリゲート型を使用すると、上記のすべての関数型を次のように表すことができます:

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

次に、上記の分散は次のように表すことができます:

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

GenericFunc 型の関数の場合、TOutput がより派生した型に置き換えられたときに共分散が発生し、TInput がより少ない派生型に置き換えられたときに反変性が発生する可能性があります。したがって、TOutput はこのジェネリック デリゲート型の共変型パラメーターと呼ばれ、TInput は反変型パラメーターと呼ばれます。 C# 4.0 では、共変/反変型パラメーターの out/in 修飾子が導入されています:

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

これらの修飾子は、関数間の暗黙的な変換/置換を有効にします:

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

前述のように、すべての関数型を表すために、統合された Func および Action ジェネリック デリゲート型が提供されます。 .NET Framework 4.0 以降、すべての型パラメーターには 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);

    // ...
}

バリアント型パラメーターはシンタックス シュガーではありません。 out/in 修飾子は CIL の 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.
}

ジェネリック インターフェイスの違い

ジェネリック デリゲート型に加えて、C# 4.0 ではジェネリック インターフェイスの差異も導入されています。インターフェイスは、実装なしで、関数の型を示す関数メンバーのシグネチャのセットと見なすことができます。例:

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

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

    void TypeParameterNotUsed();
}

上記のジェネリック インターフェイスでは、型パラメーターを使用する 2 つの関数メンバーがあり、型パラメーターはこれら 2 つの関数の関数型に対して共変です。したがって、型パラメーターはインターフェイスに対して共変であり、out 修飾子を使用して暗黙的な変換を有効にすることができます:

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

IOutput インターフェイスは IOutput インターフェイスを継承していませんが、IOutput インターフェイスは IOutput インターフェイス「である」ようです。つまり、より多くの派生型引数を持つ IOutput インターフェイスを代用できます。派生型引数が少ない IOutput。これはジェネリック インターフェイスの共分散です。同様に、ジェネリック インターフェイスも反変型パラメーターを持つことができ、in 修飾子は暗黙的な変換を有効にすることができます:

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

IInput インターフェイスは IInput インターフェイスを継承していませんが、IInput インターフェイスは IInput インターフェイス「である」ようです。つまり、より多くの派生型引数を持つ IInput インターフェイスを代用できます。派生型引数が少ない IInput。これはジェネリック インターフェイスの反変性です:

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

ジェネリック デリゲート型と同様に、ジェネリック インターフェイスは共変型パラメーターと反変型パラメーターを同時に持つことができます。

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

次の例は、共分散と反分散を示しています:

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

すべての型パラメーターがジェネリック インターフェイスのバリアントになるわけではありません。例:

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

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

型パラメーター T は、T を使用するすべての関数メンバーに対して共変でも、T を使用するすべての関数メンバーに対して反変でもないため、T はインターフェイスに対して共変または反変することはできません。

一般的な高階関数の分散

これまでのところ、共変性と out 修飾子はすべて出力に関するものであり、反変性と in 修飾子はすべて入力に関するものです。分散は、一般的な高階関数型にとって興味深いものです。たとえば、次の関数型は関数を返すため、高次です:

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

型パラメーターは出力関数の型で使用されますが、共変のままです。次の例は、これがどのように機能するかを示しています:

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

高階関数型の場合、型パラメーターが出力関数型で使用される場合、常に共変です:

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

// ...

同様に、入力として関数を受け入れることで、高階関数型を定義できます:

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

ただし、上記のコードはコンパイルできません。その理由は、型パラメーターが入力関数型で使用される場合、共変または反変になる可能性があるためです。この場合、反変になります:

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

そして、これがどのように機能するかです:

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

高階関数型の場合、型パラメーターが入力関数型で使用される場合、その差異は次のとおりです:

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

// ...

配列の共分散

前述のように、配列 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.
    }
}

IList の場合、T はそのインデクサー セッターに対して共変ではなく、T はそのインデクサー ゲッターに対して反変ではありません。したがって、T は IList と配列 T[] に対して不変でなければなりません。ただし、C# コンパイラと CLR/CoreCLR は予期せず配列の共分散をサポートします。次の例はコンパイルできますが、実行時に ArrayTypeMismatchException をスローするため、バグの原因となる可能性があります:

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

配列の共分散に関する背景情報は次のとおりです:

  • ジョナサン・アレンはこう言いました
  • 「The Common Language Infrastructure Annotated Standard」という本の中で、Jim Miller は次のように述べています。
  • リック・バイヤーズは言いました、
  • Anders Hejlsberg (C# のチーフ アーキテクト) は、このビデオで次のように述べています。
  • Eric Lippert (C# 設計チームのメンバー) は、配列の共分散を C# の最悪の 10 の機能のトップ 1 に挙げました

これは決して使用すべきではない C# 言語機能です。

.NET と LINQ の違い

次の LINQ クエリは、一般的なデリゲート型と、.NET コア ライブラリ内のバリアント型パラメーターを持つインターフェイスを検索します:

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

System.Linq 名前空間には、IGrouping、IQueryable、IOrderedQueryable など、さまざまな汎用インターフェイスもいくつかあります。 MSDN にはバリアント ジェネリック インターフェイスとデリゲート タイプのリストがありますが、正確ではありません。たとえば、TElement は IOrderedEnumerable に対して共変であると書かれていますが、実際にはそうではありません:

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

前述のように、ローカル順次 LINQ の場合、T は IEnumerable に対して共変です。全文はこちら:

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

まず、IEnumerator の型パラメーターは、Current プロパティの getter によってのみ使用されます。これは、() –> T 型の get_Current 関数と見なすことができ、IEnumerator は () – のラッパーと見なすことができます。> T 関数。 T は () –> T 関数の共分散であるため、T は IEnumerator ラッパーの共変でもあります。次に、IEnumerable では、T は IEnumerator を返す GetEnumerator メソッドによってのみ使用されます。 IEnumerator は () –> T 関数の単純なラッパーであることに注意して、GetEnumerator は仮想的に () –> T 関数を返す高階関数と見なすことができます。 したがって、GetEnumerator の関数型 () –> IEnumerator は、同様に、IEnumerable は、この () –> () –> T 関数のラッパーと見なすことができます。 T は () –> () –> T に対しても共変であるため、T は IEnumerable ラッパーの共分散でもあります。これにより、LINQ クエリが便利になります。たとえば、次の LINQ クエリ メソッドは 2 つの IEnumerable インスタンスを連結します:

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

次のコードは、IEnumerable 定義の out 修飾子によって有効になる暗黙的な変換を示しています。

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

ローカル Parallel LINQ の場合、ParallelQuery はインターフェイスではなくクラスであるため、T はバリアントではありません。繰り返しになりますが、型パラメーターの差異は、非ジェネリック デリゲート型、ジェネリック デリゲート型、およびジェネリック インターフェイスを含む関数型に関するものです。クラスは関数を実装できるため、差異は適用されません。

リモート LINQ の場合、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 { }
}

ここで、T は IEnumerable から継承されたメンバーに対してのみ使用されるため、明らかに T は IQueryable に対して共変のままです。