田敏
返回博客列表
技术

C#中的协变性和你便性

田敏
2024-11-273 分钟阅读
.NETC#Backend

偶然看到迭代器里面的接口

public interface IEnumerable<out T> : IEnumerable
{
    //
    // 摘要:
    //     Returns an enumerator that iterates through the collection.
    //
    // 返回结果:
    //     An enumerator that can be used to iterate through the collection.
    IEnumerator<T> GetEnumerator();
}

自己继承自己,泛型还使用了out;查阅因此了解到了协变性和逆变性这两个概念

在 C# 泛型中,out 是一种 协变性 修饰符,用于泛型接口或委托,表示类型参数可以是指定类型或其派生类型。协变性允许在某些场景中使用更加具体的类型,而不是仅限于声明的类型。


out 的作用

  1. 协变性 (Covariance)

    • 允许将泛型参数类型替换为 更具体 的派生类型。
    • 只能出现在接口或委托中,用于 输出位置 的类型参数。
    • 在实际实现中,out 类型参数 只能作为返回值,不能用作输入参数。
  2. 约束

    • 类型参数标记为 out 后,只能用于返回类型(输出位置),不能用于方法参数(输入位置)。
    • 这是因为协变性保证派生类实例仍然可以用作基类实例,但输入时可能导致类型安全问题。

IEnumerable<out T> 中的作用

对于 IEnumerable<out T>,它的定义:

public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

协变性应用

  • out T 允许 IEnumerable<T> 的派生类在需要 IEnumerable<Base> 的地方使用。
  • 场景
    • 假设有一个基类 Animal 和派生类 Dog
      class Animal { }
      class Dog : Animal { }
      
    • 使用 IEnumerable<Animal> 时,可以将 IEnumerable<Dog> 传递给方法或变量:
      IEnumerable<Dog> dogs = new List<Dog>();
      IEnumerable<Animal> animals = dogs; // 合法,因为 T 被标记为 out
      

为什么可以这样做?

  • 协变性确保返回值(如 IEnumerator<T>)的类型安全:
    • 迭代 dogs 时返回的是 Dog 类型,它是 Animal 类型的子类,完全符合类型安全要求。

协变性的优点

  1. 灵活性
    • 允许在泛型类型之间进行更多的类型转换,而不需要额外的类型转换代码。
  2. 代码复用
    • 同一方法可以操作基类或派生类的泛型集合,而不需要专门为每种类型实现方法。

outin 的对比

| 修饰符 | 作用 | 用途 | 例子 | | ------ | -------------------------- | -------------------------------- | -------------------- | | out | 协变性(允许更具体的类型) | 只能出现在输出位置(如返回值) | IEnumerable<out T> | | in | 逆变性(允许更泛化的类型) | 只能出现在输入位置(如参数类型) | IComparer<in T> |


例子

// 基类和派生类
class Animal { public string Name { get; set; } }
class Dog : Animal { public string Breed { get; set; } }

class Program
{
    static void Main()
    {
        // 泛型集合
        IEnumerable<Dog> dogs = new List<Dog>
        {
            new Dog { Name = "Buddy", Breed = "Labrador" },
            new Dog { Name = "Charlie", Breed = "Beagle" }
        };

        // 协变性:将 IEnumerable<Dog> 转为 IEnumerable<Animal>
        IEnumerable<Animal> animals = dogs;

        foreach (var animal in animals)
        {
            Console.WriteLine(animal.Name); // 输出狗的名字
        }
    }
}

总结

  • out 修饰符 是用于泛型接口和委托的协变性实现。
  • 它使得泛型接口更灵活,支持派生类到基类的类型转换。
  • IEnumerable<out T> 是协变性的经典例子,支持多态的迭代场景。
  • 协变性使代码更简洁,同时保持类型安全性。

在 C# 泛型中,in 是一种 逆变性 修饰符,允许将泛型类型参数替换为 更泛化 的基类类型。逆变性和协变性相反,它主要适用于泛型接口或委托中的 输入位置 类型参数。


逆变性 (Contravariance) 的作用

  1. 更泛化的类型

    • 逆变性允许将泛型类型参数替换为它的 基类 类型。
    • 只能用于输入位置的类型参数。
  2. 场景限制

    • 类型参数标记为 in 后,只能作为方法参数类型(输入位置)。
    • 不能用作返回值(输出位置),因为逆变性意味着允许更泛化的类型,返回值可能会导致类型安全问题。

IComparer<in T> 中的作用

IComparer<in T> 的定义如下:

public interface IComparer<in T>
{
    int Compare(T x, T y);
}

逆变性应用

  • 假设有一个基类 Animal 和派生类 Dog
    class Animal { public string Name { get; set; } }
    class Dog : Animal { public string Breed { get; set; } }
    
  • IComparer<Animal> 可以被用来比较 Dog 对象,因为 DogAnimal 的派生类。
    IComparer<Animal> animalComparer = new AnimalComparer();
    IComparer<Dog> dogComparer = animalComparer; // 合法,因为 T 被标记为 in
    

为什么可以这样做?

  • 逆变性保证了传入方法的类型参数是基类或派生类的实例,输入类型仍然满足类型安全要求。

逆变性的优点

  1. 通用性
    • 允许在泛型接口中接受更广泛的类型输入,而不需要额外的类型转换。
  2. 代码复用
    • 编写针对基类的泛型代码,可以直接应用于派生类,避免重复代码。

inout 的对比

| 修饰符 | 作用 | 用途 | 例子 | | ------ | ------------------------ | ---------------------- | -------------------- | | out | 协变性:允许更具体的类型 | 输出位置(如返回值) | IEnumerable<out T> | | in | 逆变性:允许更泛化的类型 | 输入位置(如参数类型) | IComparer<in T> |


例子

以下代码展示了 IComparer<in T> 中的逆变性:

基类和派生类

class Animal { public string Name { get; set; } }
class Dog : Animal { public string Breed { get; set; } }

泛型比较器

class AnimalComparer : IComparer<Animal>
{
    public int Compare(Animal x, Animal y)
    {
        return string.Compare(x.Name, y.Name);
    }
}

逆变性应用

class Program
{
    static void Main()
    {
        // 基类比较器
        IComparer<Animal> animalComparer = new AnimalComparer();

        // 派生类比较器(逆变性允许)
        IComparer<Dog> dogComparer = animalComparer;

        // 比较两只狗
        Dog dog1 = new Dog { Name = "Buddy", Breed = "Labrador" };
        Dog dog2 = new Dog { Name = "Charlie", Breed = "Beagle" };

        int result = dogComparer.Compare(dog1, dog2);

        Console.WriteLine(result > 0 ? "dog1 comes after dog2" : "dog1 comes before dog2");
    }
}

总结

  • in 修饰符 实现了泛型接口和委托的逆变性。
  • 它允许将派生类类型参数替换为基类类型,从而提升代码通用性。
  • 典型用法
    • IComparer<in T>IEqualityComparer<in T> 等输入为类型参数的泛型接口。
  • 逆变性保证了泛型接口在输入操作上的类型安全,同时让代码更灵活。
版权协议:MIT返回列表