Поделиться через


Создание вариативных универсальных интерфейсов (C#)

Параметры универсального типа можно объявить в интерфейсах как ковариантный или контравариантный. Ковариация позволяет методам интерфейса иметь более производные типы возвращаемых значений, чем определенные параметрами универсального типа. Контравариантность позволяет методам интерфейса иметь типы аргументов, которые являются менее производными, чем указанные универсальными параметрами. Универсальный интерфейс с ковариантными или контравариантными параметрами универсального типа называется вариантом.

Замечание

Платформа .NET Framework 4 представила поддержку дисперсии для нескольких существующих универсальных интерфейсов. Список вариантов интерфейсов в .NET см. в разделе "Вариативность" в универсальных интерфейсах (C#).

Объявление универсальных интерфейсов variant

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

Это важно

ref, inи out параметры в C# не могут быть вариантами. Типы значений также не поддерживают дисперсию.

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

  • Тип используется только в качестве возвращаемого типа методов интерфейса и не используется в качестве типа аргументов метода. Это показано в следующем примере, в котором тип R объявлен ковариантным.

    interface ICovariant<out R>
    {
        R GetSomething();
        // The following statement generates a compiler error.
        // void SetSomething(R sampleArg);
    
    }
    

    Существует одно исключение из этого правила. Если у вас есть контравариантный универсальный делегат в качестве параметра метода, вы можете использовать этот тип в качестве параметра универсального типа для делегата. Это иллюстрируется типом R в следующем примере. Дополнительные сведения см. в разделе "Вариативность в делегатах (C#)" и "Использование вариативности для Func и Action в обобщенных делегатах (C#)".

    interface ICovariant<out R>
    {
        void DoSomething(Action<R> callback);
    }
    
  • Тип не используется в качестве универсального ограничения для методов интерфейса. Это показано в следующем коде.

    interface ICovariant<out R>
    {
        // The following statement generates a compiler error
        // because you can use only contravariant or invariant types
        // in generic constraints.
        // void DoSomething<T>() where T : R;
    }
    

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

interface IContravariant<in A>
{
    void SetSomething(A sampleArg);
    void DoSomething<T>() where T : A;
    // The following statement generates a compiler error.
    // A GetSomething();
}

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

interface IVariant<out R, in A>
{
    R GetSomething();
    void SetSomething(A sampleArg);
    R GetSetSomethings(A sampleArg);
}

реализация универсальных интерфейсов с вариантными параметрами

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

interface ICovariant<out R>
{
    R GetSomething();
}
class SampleImplementation<R> : ICovariant<R>
{
    public R GetSomething()
    {
        // Some code.
        return default(R);
    }
}

Классы, реализующие вариантные интерфейсы, являются инвариантными. Например, рассмотрим следующий код.

// The interface is covariant.
ICovariant<Button> ibutton = new SampleImplementation<Button>();
ICovariant<Object> iobj = ibutton;

// The class is invariant.
SampleImplementation<Button> button = new SampleImplementation<Button>();
// The following statement generates a compiler error
// because classes are invariant.
// SampleImplementation<Object> obj = button;

Расширение универсальных интерфейсов с вариативностями

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

interface ICovariant<out T> { }
interface IInvariant<T> : ICovariant<T> { }
interface IExtCovariant<out T> : ICovariant<T> { }

В интерфейсе IInvariant<T> параметр T универсального типа является инвариантным, в то время как в IExtCovariant<out T> параметре типа является ковариантным, хотя оба интерфейса расширяют один и тот же интерфейс. Это же правило применяется к параметрам контравариантного универсального типа.

Можно создать интерфейс, который расширяет и интерфейс, в котором параметр универсального типа T является ковариантным, и интерфейс, где он является контравариантным, если в интерфейсе расширения параметр универсального типа T является инвариантным. Это показано в следующем примере кода.

interface ICovariant<out T> { }
interface IContravariant<in T> { }
interface IInvariant<T> : ICovariant<T>, IContravariant<T> { }

Однако если параметр T универсального типа объявлен ковариантным в одном интерфейсе, его нельзя объявить контравариантным в интерфейсе расширения или наоборот. Это показано в следующем примере кода.

interface ICovariant<out T> { }
// The following statement generates a compiler error.
// interface ICoContraVariant<in T> : ICovariant<T> { }

Избегание неоднозначности

При реализации вариантных универсальных интерфейсов вариативность иногда может привести к неоднозначности. Следует избежать такой неоднозначности.

Например, если вы явно реализуете один и тот же вариант универсального интерфейса с разными параметрами универсального типа в одном классе, это может создать неоднозначность. Компилятор не создает ошибку в этом случае, но не указывается, какая реализация интерфейса будет выбрана во время выполнения. Эта неоднозначность может привести к тонким ошибкам в коде. Рассмотрим следующий пример кода.

// Simple class hierarchy.
class Animal { }
class Cat : Animal { }
class Dog : Animal { }

// This class introduces ambiguity
// because IEnumerable<out T> is covariant.
class Pets : IEnumerable<Cat>, IEnumerable<Dog>
{
    IEnumerator<Cat> IEnumerable<Cat>.GetEnumerator()
    {
        Console.WriteLine("Cat");
        // Some code.
        return null;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        // Some code.
        return null;
    }

    IEnumerator<Dog> IEnumerable<Dog>.GetEnumerator()
    {
        Console.WriteLine("Dog");
        // Some code.
        return null;
    }
}
class Program
{
    public static void Test()
    {
        IEnumerable<Animal> pets = new Pets();
        pets.GetEnumerator();
    }
}

В этом примере не указано, как метод pets.GetEnumerator делает выбор между Cat и Dog. Это может привести к проблемам в коде.

См. также