了解 C# 中的协变和逆变接口

发布于 2024-08-30 05:48:37 字数 252 浏览 1 评论 0原文

我在一本有关 C# 的教科书中遇到过这些内容,但我很难理解它们,可能是由于缺乏上下文。

对于它们是什么以及它们有什么用处,是否有一个很好的简洁解释?

编辑澄清:

协变接口:

interface IBibble<out T>
.
.

逆变接口:

interface IBibble<in T>
.
.

I've come across these in a textbook I am reading on C#, but I am having difficulty understanding them, probably due to lack of context.

Is there a good concise explanation of what they are and what they are useful for out there?

Edit for clarification:

Covariant interface:

interface IBibble<out T>
.
.

Contravariant interface:

interface IBibble<in T>
.
.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

萌化 2024-09-06 05:48:37

使用,您可以将接口引用视为层次结构中向上的一个。

使用,您可以将接口引用视为层次结构中向下的一个。

让我尝试用更英语的术语来解释它。

假设您正在从动物园检索动物列表,并且打算处理它们。所有动物(在您的动物园中)都有一个名字和一个唯一的 ID。有些动物是哺乳动物,有些是爬行动物,有些是两栖动物,有些是鱼类等等,但它们都是动物。

因此,根据您的动物列表(其中包含不同类型的动物),您可以说所有动物都有一个名称,因此显然获得所有动物的名称是安全的。

但是,如果您只有鱼类清单,但需要像对待动物一样对待它们,这行得通吗?直观上,它应该可以工作,但在 C# 3.0 及之前的版本中,这段代码将无法编译:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>

原因是编译器不“知道”您的意图,或者可以,做什么取回动物收藏后。据我们所知,可能有一种方法可以通过 IEnumerable将对象放回列表中,并且这可能允许您将不是鱼的动物放入列表中。应该只包含鱼的集合。

换句话说,编译器不能保证这是不允许的:

animals.Add(new Mammal("Zebra"));

因此编译器直接拒绝编译您的代码。这就是协方差。

我们来看看逆变。

既然我们的动物园可以饲养所有动物,那么它当然也可以饲养鱼类,所以让我们尝试在我们的动物园中添加一些鱼。

在 C# 3.0 及之前的版本中,此方法无法编译:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
fishes.Add(new Fish("Guppy"));

此处,编译器可以允许这段代码,即使该方法仅返回 List 因为所有鱼都是动物,所以如果我们只是将类型更改为:

List<Animal> fishes = GetAccessToFishes();
fishes.Add(new Fish("Guppy"));

那么它会起作用,但编译器无法确定您没有尝试执行此操作:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
Fish firstFist = fishes[0];

由于列表实际上是动物列表,因此这是不允许的。

因此,逆变和协变是您处理对象引用的方式以及您可以对它们执行的操作。

C# 4.0 中的 inout 关键字专门将接口标记为其中之一。使用in,您可以将泛型类型(通常为 T)放置在输入位置中,这意味着方法参数和只写属性。

使用out,您可以将泛型类型放置在输出位置中,即方法返回值、只读属性和out方法参数。

这将允许您执行代码的预期操作:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>
// since we can only get animals *out* of the collection, every fish is an animal
// so this is safe

List 在 T 上既有入方向又有出方向,因此它既不是协变也不是逆变,而是一个允许您添加对象的接口,如下所示:

interface IWriteOnlyList<in T>
{
    void Add(T value);
}

将允许您执行此操作:

IWriteOnlyList<Fish> fishes = GetWriteAccessToAnimals(); // still returns
                                                            IWriteOnlyList<Animal>
fishes.Add(new Fish("Guppy")); <-- this is now safe

这是一个示例:

namespace SO2719954
{
    class Base { }
    class Descendant : Base { }

    interface IBibbleOut<out T> { }
    interface IBibbleIn<in T> { }

    class Program
    {
        static void Main(string[] args)
        {
            // We can do this since every Descendant is also a Base
            // and there is no chance we can put Base objects into
            // the returned object, since T is "out"
            // We can not, however, put Base objects into b, since all
            // Base objects might not be Descendant.
            IBibbleOut<Base> b = GetOutDescendant();

            // We can do this since every Descendant is also a Base
            // and we can now put Descendant objects into Base
            // We can not, however, retrieve Descendant objects out
            // of d, since all Base objects might not be Descendant
            IBibbleIn<Descendant> d = GetInBase();
        }

        static IBibbleOut<Descendant> GetOutDescendant()
        {
            return null;
        }

        static IBibbleIn<Base> GetInBase()
        {
            return null;
        }
    }
}

如果没有这些标记,则可以编译以下内容:

public List<Descendant> GetDescendants() ...
List<Base> bases = GetDescendants();
bases.Add(new Base()); <-- uh-oh, we try to add a Base to a Descendant

或这样:

public List<Base> GetBases() ...
List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases
                                               as Descendants

With <out T>, you can treat the interface reference as one upwards in the hierarchy.

With <in T>, you can treat the interface reference as one downwards in the hiearchy.

Let me try to explain it in more english terms.

Let's say you are retrieving a list of animals from your zoo, and you intend to process them. All animals (in your zoo) have a name, and a unique ID. Some animals are mammals, some are reptiles, some are amphibians, some are fish, etc. but they're all animals.

So, with your list of animals (which contains animals of different types), you can say that all the animals have a name, so obviously it would be safe to get the name of all the animals.

However, what if you have a list of fishes only, but need to treat them like animals, does that work? Intuitively, it should work, but in C# 3.0 and before, this piece of code will not compile:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>

The reason for this is that the compiler doesn't "know" what you intend, or can, do with the animals collection after you've retrieved it. For all it knows, there could be a way through IEnumerable<T> to put an object back into the list, and that would potentially allow you to put an animal that isn't a fish, into a collection that is supposed to contain only fish.

In other words, the compiler cannot guarantee that this is not allowed:

animals.Add(new Mammal("Zebra"));

So the compiler just outright refuses to compile your code. This is covariance.

Let's look at contravariance.

Since our zoo can handle all animals, it can certainly handle fish, so let's try to add some fish to our zoo.

In C# 3.0 and before, this does not compile:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
fishes.Add(new Fish("Guppy"));

Here, the compiler could allow this piece of code, even though the method returns List<Animal> simply because all fishes are animals, so if we just changed the types to this:

List<Animal> fishes = GetAccessToFishes();
fishes.Add(new Fish("Guppy"));

Then it would work, but the compiler cannot determine that you're not trying to do this:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
Fish firstFist = fishes[0];

Since the list is actually a list of animals, this is not allowed.

So contra- and co-variance is how you treat object references and what you're allowed to do with them.

The in and out keywords in C# 4.0 specifically marks the interface as one or the other. With in, you're allowed to place the generic type (usually T) in input-positions, which means method arguments, and write-only properties.

With out, you're allowed to place the generic type in output-positions, which is method return values, read-only properties, and out method parameters.

This will allow you to do what intended to do with the code:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>
// since we can only get animals *out* of the collection, every fish is an animal
// so this is safe

List<T> has both in- and out-directions on T, so it is neither co-variant nor contra-variant, but an interface that allowed you to add objects, like this:

interface IWriteOnlyList<in T>
{
    void Add(T value);
}

would allow you to do this:

IWriteOnlyList<Fish> fishes = GetWriteAccessToAnimals(); // still returns
                                                            IWriteOnlyList<Animal>
fishes.Add(new Fish("Guppy")); <-- this is now safe

Here's an example:

namespace SO2719954
{
    class Base { }
    class Descendant : Base { }

    interface IBibbleOut<out T> { }
    interface IBibbleIn<in T> { }

    class Program
    {
        static void Main(string[] args)
        {
            // We can do this since every Descendant is also a Base
            // and there is no chance we can put Base objects into
            // the returned object, since T is "out"
            // We can not, however, put Base objects into b, since all
            // Base objects might not be Descendant.
            IBibbleOut<Base> b = GetOutDescendant();

            // We can do this since every Descendant is also a Base
            // and we can now put Descendant objects into Base
            // We can not, however, retrieve Descendant objects out
            // of d, since all Base objects might not be Descendant
            IBibbleIn<Descendant> d = GetInBase();
        }

        static IBibbleOut<Descendant> GetOutDescendant()
        {
            return null;
        }

        static IBibbleIn<Base> GetInBase()
        {
            return null;
        }
    }
}

Without these marks, the following could compile:

public List<Descendant> GetDescendants() ...
List<Base> bases = GetDescendants();
bases.Add(new Base()); <-- uh-oh, we try to add a Base to a Descendant

or this:

public List<Base> GetBases() ...
List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases
                                               as Descendants
旧城空念 2024-09-06 05:48:37

这篇文章是我最好的文章简而言之

,协变/逆变/不变处理自动类型转换(从基类到派生类,反之亦然)。仅当在对转换对象执行的读/写操作方面遵守某些保证时,这些类型转换才是可能的。
阅读帖子了解更多详情。

This post is the best I've read on the subject

In short, covariance / contravariance /invariance deals with automatic type casting (from base to derived and vice-versa). Those type casts are possible only if some guarantees are respected in terms of read / write actions performed on the casted objects.
Read the post for more details.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文