1. 程式人生 > 其它 >C#——泛型介面中的變體

C#——泛型介面中的變體

技術標籤:C#c#

C#——泛型介面中的變體

NET Framework 4 引入了對多個現有泛型介面的變體支援。 變體支援允許實現這些介面的類進行隱式轉換。
自 .NET Framework 4 起,以下介面為變體:

IEnumerable(T 是協變)

IEnumerator(T 是協變)

IQueryable(T 是協變)

IGrouping<TKey,TElement>(TKey 和 TElement 都是協變)

IComparer(T 是逆變)

IEqualityComparer(T 是逆變)

IComparable(T 是逆變)

自 .NET Framework 4.5 起,以下介面是變體:

IReadOnlyList(T 是協變)

IReadOnlyCollection(T 是協變)

協變允許方法具有的返回型別比介面的泛型型別引數定義的返回型別的派生程度更大。 若要演示協變功能,請考慮以下泛型介面:IEnumerable 和 IEnumerable。 IEnumerable 介面不繼承 IEnumerable 介面。 但是,String 型別會繼承 Object 型別,在某些情況下,建議為這些介面互相指派彼此的物件。 下面的程式碼示例對此進行了演示。

IEnumerable<String> strings = new List<String>();
IEnumerable<
Object> objects = strings;

在舊版 .NET Framework 中,此程式碼會導致 C# 中出現編譯錯誤;如果啟用 Option Strict 條件,則會導致在 Visual Basic 中出現編譯錯誤。 但現在可使用 strings 代替 objects,如上例所示,因為 IEnumerable 介面是協變介面。

逆變允許方法具有的實參型別比介面的泛型形參定義的型別的派生程度更小。 若要演示逆變,假設已建立了 BaseComparer 類來比較 BaseClass 類的例項。 BaseComparer 類實現 IEqualityComparer 介面。 因為 IEqualityComparer 介面現在是逆變介面,因此可使用 BaseComparer 比較繼承 BaseClass 類的類的例項。 下面的程式碼示例對此進行了演示。

// Simple hierarchy of classes.
class BaseClass { }
class DerivedClass : BaseClass { }

// Comparer class.
class BaseComparer : IEqualityComparer<BaseClass>
{
    public int GetHashCode(BaseClass baseInstance)
    {
        return baseInstance.GetHashCode();
    }
    public bool Equals(BaseClass x, BaseClass y)
    {
        return x == y;
    }
}
class Program
{
    static void Test()
    {
        IEqualityComparer<BaseClass> baseComparer = new BaseComparer();

        // Implicit conversion of IEqualityComparer<BaseClass> to
        // IEqualityComparer<DerivedClass>.
        IEqualityComparer<DerivedClass> childComparer = baseComparer;
    }
}

有關更多示例,請參閱在泛型集合的介面中使用變體 (C#)

只有引用型別才支援使用泛型介面中的變體。 值型別不支援變體。 例如,無法將 IEnumerable 隱式轉換為 IEnumerable,因為整數由值型別表示。

IEnumerable<int> integers = new List<int>();
// The following statement generates a compiler error,
// because int is a value type.
// IEnumerable<Object> objects = integers;

還需記住,實現變體介面的類仍是固定類。 例如,雖然 List 實現協變介面 IEnumerable,但不能將 List 隱式轉換為 List。 以下程式碼示例闡釋了這一點。

// The following line generates a compiler error
// because classes are invariant.
// List<Object> list = new List<String>();

// You can use the interface object instead.
IEnumerable<Object> listObjects = new List<String>();

建立變體泛型介面

介面中的泛型型別引數可以宣告為協變或逆變。 協變允許介面方法具有與泛型型別引數定義的返回型別相比,派生程度更大的返回型別。 逆變允許介面方法具有與泛型形參指定的實參型別相比,派生程度更小的實參型別。 具有協變或逆變泛型型別引數的泛型介面稱為“變體”。

宣告變體泛型介面

可通過對泛型型別引數使用 in 和 out 關鍵字來宣告變體泛型介面。

C# 中的 ref、in 和 out 引數不能為變體。 值型別也不支援變體。

可以使用 out 關鍵字將泛型型別引數宣告為協變。 協變型別必須滿足以下條件:

型別僅用作介面方法的返回型別,不用作方法引數的型別。 下例演示了此要求,其中型別 R 為宣告的協變。

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

}

此規則有一個例外。 如果具有用作方法引數的逆變泛型委託,則可將型別用作該委託的泛型型別引數。 下例中的型別 R 演示了此情形

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 介面和 IExtCovariant 介面擴充套件的是同一個介面,但泛型型別引數 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> { }

避免多義性

實現變體泛型介面時,變體有時可能會導致多義性。 應避免這樣的多義性。

例如,如果在一個類中使用不同的泛型型別引數來顯式實現同一變體泛型介面,便會產生多義性。 在這種情況下,編譯器不會產生錯誤,但未指定將在執行時選擇哪個介面實現。 這種多義性可能導致程式碼中出現小 bug。 請考慮以下程式碼示例。

// 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 之間選擇。 這可能導致程式碼中出現問題。

在泛型集合的介面中使用變體

協變介面允許其方法返回的派生型別多於介面中指定的派生型別。 逆變介面允許其方法接受派生型別少於介面中指定的型別的引數。

在.NET Framework 4 中,多個現有介面已變為協變和逆變介面。 包括 IEnumerable 和 IComparable。 這使你可將對基型別的泛型集合進行操作的那些方法重用於派生型別的集合。

轉換泛型集合

下例闡釋了 IEnumerable 介面中的協變支援的益處。 PrintFullName 方法接受 IEnumerable 型別的集合作為引數。 但可將該方法重用於 IEnumerable 型別的集合,因為 Employee 繼承 Person。

// Simple hierarchy of classes.  
public class Person  
{  
    public string FirstName { get; set; }  
    public string LastName { get; set; }  
}  
  
public class Employee : Person { }  
  
class Program  
{  
    // The method has a parameter of the IEnumerable<Person> type.  
    public static void PrintFullName(IEnumerable<Person> persons)  
    {  
        foreach (Person person in persons)  
        {  
            Console.WriteLine("Name: {0} {1}",  
            person.FirstName, person.LastName);  
        }  
    }  
  
    public static void Test()  
    {  
        IEnumerable<Employee> employees = new List<Employee>();  
  
        // You can pass IEnumerable<Employee>,
        // although the method expects IEnumerable<Person>.  
  
        PrintFullName(employees);  
  
    }  
}

比較泛型集合

下例闡釋了 IComparer 介面中的逆變支援的益處。 PersonComparer 類實現 IComparer 介面。 但可以重用此類來比較 Employee 型別的物件序列,因為 Employee 繼承 Person。

// Simple hierarchy of classes.  
public class Person  
{  
    public string FirstName { get; set; }  
    public string LastName { get; set; }  
}  
  
public class Employee : Person { }  
  
// The custom comparer for the Person type  
// with standard implementations of Equals()  
// and GetHashCode() methods.  
class PersonComparer : IEqualityComparer<Person>  
{  
    public bool Equals(Person x, Person y)  
    {
        if (Object.ReferenceEquals(x, y)) return true;  
        if (Object.ReferenceEquals(x, null) ||  
            Object.ReferenceEquals(y, null))  
            return false;
        return x.FirstName == y.FirstName && x.LastName == y.LastName;  
    }  
    public int GetHashCode(Person person)  
    {  
        if (Object.ReferenceEquals(person, null)) return 0;  
        int hashFirstName = person.FirstName == null  
            ? 0 : person.FirstName.GetHashCode();  
        int hashLastName = person.LastName.GetHashCode();  
        return hashFirstName ^ hashLastName;  
    }  
}  
  
class Program  
{  
  
    public static void Test()  
    {  
        List<Employee> employees = new List<Employee> {  
               new Employee() {FirstName = "Michael", LastName = "Alexander"},  
               new Employee() {FirstName = "Jeff", LastName = "Price"}  
            };  
  
        // You can pass PersonComparer,
        // which implements IEqualityComparer<Person>,  
        // although the method expects IEqualityComparer<Employee>.  
  
        IEnumerable<Employee> noduplicates =  
            employees.Distinct<Employee>(new PersonComparer());  
  
        foreach (var employee in noduplicates)  
            Console.WriteLine(employee.FirstName + " " + employee.LastName);  
    }  
}