Tworzenie wariantów interfejsów ogólnych (C#)

Parametry typu ogólnego można zadeklarować w interfejsach jako kowariantne lub kontrawariantne. Kowariancja umożliwia metodom interfejsu posiadanie większej liczby pochodnych typów zwracanych niż zdefiniowane przez parametry typu ogólnego. Kontrawariancja umożliwia metodom interfejsu używanie typów argumentów, które są mniej pochodne niż określone przez parametry ogólne. Interfejs ogólny, który ma kowariantne lub kontrawariantne parametry typu ogólnego, jest nazywany wariantem.

Uwaga

Program .NET Framework 4 wprowadził obsługę wariancji dla kilku istniejących interfejsów ogólnych. Aby uzyskać listę interfejsów wariantów na platformie .NET, zobacz Wariancja w interfejsach ogólnych (C#).

Deklarowanie wariantów interfejsów ogólnych

Warianty interfejsów ogólnych można zadeklarować przy użyciu in słów kluczowych i out dla parametrów typu ogólnego.

Ważne

refparametrów , ini out w języku C# nie może być wariantem. Typy wartości nie obsługują również wariancji.

Za pomocą słowa kluczowego out można zadeklarować kowariantny parametr typu ogólnego. Typ kowariantny musi spełniać następujące warunki:

  • Typ jest używany tylko jako zwracany typ metod interfejsu i nie jest używany jako typ argumentów metody. Przedstawiono to w poniższym przykładzie, w którym typ R jest zadeklarowany kowariantnie.

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

    Istnieje jeden wyjątek od tej reguły. Jeśli masz kontrawariantnego delegata ogólnego jako parametru metody, możesz użyć typu jako parametru typu ogólnego dla delegata. Jest to zilustrowane przez typ R w poniższym przykładzie. Aby uzyskać więcej informacji, zobacz Variance in Delegates (C#) (Wariancja w delegatach (C#) i Using Variance for Func and Action Generic Delegates (C#)( Używanie wariancji dla func and action Generic Delegates (C#).

    interface ICovariant<out R>
    {
        void DoSomething(Action<R> callback);
    }
    
  • Typ nie jest używany jako ograniczenie ogólne dla metod interfejsu. Jest to pokazane w poniższym kodzie.

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

Za pomocą słowa kluczowego in można zadeklarować kontrawariant parametru typu ogólnego. Typ kontrawariantny może być używany tylko jako typ argumentów metody, a nie jako zwracany typ metod interfejsu. Typ kontrawariantny może być również używany w przypadku ograniczeń ogólnych. Poniższy kod pokazuje, jak zadeklarować interfejs kontrawariantny i użyć ogólnego ograniczenia dla jednej z jego metod.

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

Istnieje również możliwość obsługi zarówno kowariancji, jak i kontrawariancji w tym samym interfejsie, ale dla różnych parametrów typu, jak pokazano w poniższym przykładzie kodu.

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

Implementowanie wariantów interfejsów ogólnych

Interfejsy ogólne wariantu można zaimplementować w klasach przy użyciu tej samej składni, która jest używana dla niezmiennych interfejsów. Poniższy przykład kodu pokazuje, jak zaimplementować kowariantny interfejs w klasie ogólnej.

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

Klasy implementujące interfejsy wariantów są niezmienne. Rozważmy na przykład następujący kod.

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

Rozszerzanie wariantów interfejsów ogólnych

Podczas rozszerzania wariantu interfejsu ogólnego należy użyć in słów kluczowych i out , aby jawnie określić, czy interfejs pochodny obsługuje wariancję. Kompilator nie wywnioskuje wariancji z interfejsu, który jest rozszerzany. Rozważmy na przykład następujące interfejsy.

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

W interfejsie IInvariant<T> ogólny parametr T typu jest niezmienny, podczas gdy w IExtCovariant<out T> parametrze typu jest kowariantny, chociaż oba interfejsy rozszerzają ten sam interfejs. Ta sama reguła jest stosowana do kontrawariantnych parametrów typu ogólnego.

Można utworzyć interfejs, który rozszerza zarówno interfejs, w którym ogólny parametr T typu jest kowariantny, jak i interfejs, w którym jest kontrawariantny, jeśli w interfejsie rozszerzającym parametr T typu ogólnego jest niezmienny. Jest to pokazane w poniższym przykładzie kodu.

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

Jeśli jednak ogólny parametr T typu jest zadeklarowany kowariantnie w jednym interfejsie, nie można zadeklarować jego kontrawariant w interfejsie rozszerzającym lub na odwrót. Jest to pokazane w poniższym przykładzie kodu.

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

Unikanie niejednoznaczności

Podczas implementowania interfejsów ogólnych wariantów wariancja może czasami prowadzić do niejednoznaczności. Należy unikać takiej niejednoznaczności.

Jeśli na przykład jawnie zaimplementujesz ten sam wariant interfejs ogólny z różnymi parametrami typu ogólnego w jednej klasie, może to spowodować niejednoznaczność. W tym przypadku kompilator nie generuje błędu, ale nie określono, która implementacja interfejsu zostanie wybrana w czasie wykonywania. Ta niejednoznaczność może prowadzić do drobnych usterek w kodzie. Spójrz na poniższy przykład kodu.

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

W tym przykładzie nie określono pets.GetEnumerator sposobu wyboru metody między Cat i Dog. Może to spowodować problemy w kodzie.

Zobacz też