C#中的接口冲突解决 [英] Interface conflict resolution in C#

查看:96
本文介绍了C#中的接口冲突解决的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这是一个基于我想知道为什么C#语言在以下特定情况下无法检测到正确的接口成员.我不是在寻求反馈意见,以这种方式设计课程是否被视为最佳实践.

I would like to know why the C# language is designed not being able to detect the correct interface member in the following specific case. I am not looking on feedback whether designing a class this way is considered best practice.

class Turtle { }
class Giraffe { }

class Ark : IEnumerable<Turtle>, IEnumerable<Giraffe>
{
    public IEnumerator<Turtle> GetEnumerator()
    {
        yield break;
    }

    // explicit interface member 'IEnumerable.GetEnumerator'
    IEnumerator IEnumerable.GetEnumerator()
    {
        yield break;
    }

    // explicit interface member 'IEnumerable<Giraffe>.GetEnumerator'
    IEnumerator<Giraffe> IEnumerable<Giraffe>.GetEnumerator()
    {
        yield break;
    }
}

在上面的代码中,Ark具有GetEnumerator()的3个冲突实现.通过将IEnumerator<Turtle>的实现视为默认实现,并要求对两者进行特定的强制转换,可以解决此冲突.

In the code above, Ark has 3 conflicting implementation of GetEnumerator(). This conflict is resolved by treating IEnumerator<Turtle>'s implementation as default, and requiring specific casts for both others.

检索枚举符的工作就像一种魅力:

Retrieving the enumerators works like a charm:

var ark = new Ark();

var e1 = ((IEnumerable<Turtle>)ark).GetEnumerator();  // turtle
var e2 = ((IEnumerable<Giraffe>)ark).GetEnumerator(); // giraffe
var e3 = ((IEnumerable)ark).GetEnumerator();          // object

// since IEnumerable<Turtle> is the default implementation, we don't need
// a specific cast to be able to get its enumerator
var e4 = ark.GetEnumerator();                         // turtle

为什么LINQ的Select扩展方法没有类似的分辨率?是否存在适当的设计决策,以允许解决前者之间的矛盾,而不能解决后者?

Why isn't there a similar resolution for LINQ's Select extension method? Is there a proper design decision to allow the inconsistency between resolving the former, but not the latter?

 // This is not allowed, but I don't see any reason why ..
 // ark.Select(x => x);                                // turtle expected

 // these are allowed
 ark.Select<Turtle, Turtle>(x => x);
 ark.Select<Giraffe, Giraffe>(x => x);

推荐答案

首先了解使用哪种机制来解决对扩展方法Select的调用很重要. C#使用通用类型推断算法,该算法相当复杂;有关详细信息,请参见C#规范. (我真的应该写一篇博客文章来解释这一切;我在2006年录制了一段有关它的视频,但不幸的是它已经消失了.)

It's important to first understand what mechanism is being used to resolve the call to the extension method Select. C# uses a generic type inference algorithm which is fairly complex; see the C# specification for the details. (I really should write a blog article explaining it all; I recorded a video about it in 2006 but unfortunately it has disappeared.)

但是基本上,Select上的泛型类型推断的思想是:我们有:

But basically, the idea of generic type inference on Select is: we have:

public static IEnumerable<R> Select<A, R>(
  this IEnumerable<A> items,
  Func<A, R> projection)

通话中

ark.Select(x => x)

我们必须推断出AR的意图.

we must deduce what A and R was intended.

由于R依赖于A,并且实际上等于A,因此问题减少了,找到了A.我们仅有的信息是ark type .我们知道ark:

Since R depends on A, and in fact is equal to A, the problem reduces to finding A. The only information we have is the type of ark. We know that ark:

  • Ark
  • 扩展object
  • 实现IEnumerable<Giraffe>
  • 实现IEnumerable<Turtle>
  • IEnumerable<T>扩展了IEnumerable并且是协变的.
  • TurtleGiraffe扩展了Animal,后者扩展了object.
  • Is Ark
  • Extends object
  • Implements IEnumerable<Giraffe>
  • Implements IEnumerable<Turtle>
  • IEnumerable<T> extends IEnumerable and is covariant.
  • Turtle and Giraffe extend Animal which extends object.

现在,如果您只知道这些 东西,并且您知道我们正在寻找IEnumerable<A>,那么您对A可以得出什么结论?

Now, if those are the only things you know, and you know that we're looking for IEnumerable<A>, what conclusions can you reach about A?

有很多可能性:

  • 选择Animalobject.
  • 通过抢七局选择TurtleGiraffe.
  • 确定情况不明确,并给出错误信息.
  • Choose Animal, or object.
  • Choose Turtle or Giraffe by some tiebreaker.
  • Decide that the situation is ambiguous, and give an error.

我们可以拒绝第一种选择. C#的设计原则是:面对选项之间的选择时,请始终选择其中一个选项,否则会产生错误. C#从未说过您在AppleCake之间给了我一个选择,所以我选择了Food".它总是从您给出的选择中进行选择,或者说它没有做出选择的依据.

We can reject the first option. A design principle of C# is: when faced with a choice between options, always choose one of the options or produce an error. C# never says "you gave me a choice between Apple and Cake so I choose Food". It always chooses from the choices you gave it, or it says that it has no basis on which to make a choice.

此外,如果我们选择Animal,那只会使情况变得更糟.请参阅本文结尾处的练习.

Moreover, if we chose Animal, that just makes the situation worse. See the exercise at the end of this post.

您提议第二个选项,并且您提议的决胜局是隐式实现的接口比显式实现的接口具有优先权".

You propose the second option, and your proposed tiebreaker is "an implicitly implemented interface gets priority over an explicitly implemented interface".

这个提议的决胜局有一些问题,从开始,没有隐式实现的接口.让您的情况稍微复杂一些:

This proposed tiebreaker has some problems, starting with there is no such thing as an implicitly implemented interface. Let's make your situation slightly more complicated:

interface I<T>
{
  void M();
  void N();
}
class C : I<Turtle>, I<Giraffe>
{
  void I<Turtle>.M() {} 
  public M() {} // Used for I<Giraffe>.M
  void I<Giraffe>.N() {}
  public N() {}
  public static DoIt<T>(I<T> i) {i.M(); i.N();}
}

当我们呼叫C.DoIt(new C())时会发生什么?这两个接口均未明确实现".这两个接口都不是隐式实现的". 接口成员是隐式或显式实现的,而不是接口.

When we call C.DoIt(new C()) what happens? Neither interface is "explicitly implemented". Neither interface is "implicitly implemented". Interface members are implicitly or explicitly implemented, not interfaces.

现在我们可以说所有成员都隐式实现的接口是隐式实现的接口".有帮助吗?没有.因为在您的示例中,IEnumerable<Turtle>有一个隐式实现的成员和一个显式实现的成员:返回IEnumeratorGetEnumerator的重载是IEnumerable<Turtle>的成员,因此您已显式实现了.

Now we could say "an interface that has all of its members implicitly implemented is an implicitly implemented interface". Does that help? Nope. Because in your example, IEnumerable<Turtle> has one member implicitly implemented and one member explicitly implemented: the overload of GetEnumerator that returns IEnumerator is a member of IEnumerable<Turtle> and you've explicitly implemented it.

(旁白:一个评论者指出,上面的措词不雅;从规范中还不能完全清楚是从基本"接口继承"的成员是派生"接口的成员",还是仅仅是接口之间的派生"关系只是要求派生"接口的任何实现者也必须实现基数"的要求的陈述,这一点在历史上尚不清楚,因此可以进行论证无论如何,我的观点是派生接口需要来实现一组特定的成员,并且其中一些成员可以隐式实现,而某些成员可以显式实现,我们可以算一下我们应该选择每个都有很多.)

(ASIDE: A commenter notes that the above is inelegantly worded; it is not entirely clear from the specification whether members "inherited" from "base" interfaces are "members" of the "derived" interface, or whether it is simply the case that a "derivation" relationship between interfaces is simply the statement of a requirement that any implementor of the "derived" interface must also implement the "base". The specification has historically been unclear on this point and it is possible to make arguments either way. Regardless, my point is that the derived interface requires you to implement a certain set of members, and some of those members can be implicitly implemented and some can be explicitly implemented, and we can count how many there are of each should we choose to.)

所以现在提议的决胜局可能是计数成员,而明确实现成员最少的接口是获胜者".

So now maybe the proposed tiebreaker is "count the members, and the interface that has the least members explicitly implemented is the winner".

因此,让我们退后一步,问一个问题:您将如何记录此功能?您将如何解释它?假设有一位顾客来找您,并说:为什么在这里选择乌龟而不是长颈鹿?"您将如何解释?

So let's take a step back here and ask the question: how on earth would you document this feature? How would you explain it? Suppose a customer comes to you and says "why are turtles being chosen over giraffes here?" How would you explain it?

现在,假设客户问:我如何预测编写代码时编译器将执行的操作?"请记住,该客户可能没有Ark的源代码.它可能是第三方库中的一种类型. 您的建议将第三方对用户的不可见实施决策变成控制其他人的代码是否正确的相关因素.开发人员通常会反对使其无法理解其代码功能的功能,除非相应地增强了功能.

Now suppose the customer asks "how can I make a prediction about what the compiler will do when I write the code?" Remember, that customer might not have the source code to Ark; it might be a type in a third-party library. Your proposal makes the invisible-to-users implementation decisions of third parties into relevant factors that control whether other people's code is correct or not. Developers generally are opposed to features that make it impossible for them to understand what their code does, unless there is a corresponding boost in power.

(例如:虚方法使您无法知道代码的作用,但是它们非常有用;没有人认为这种提议的功能具有类似的有用性.)

(For example: virtual methods make it impossible to know what your code does, but they are very useful; no one has made the argument that this proposed feature has a similar usefulness bonus.)

假设第三方更改了一个库,以便以您依赖的类型显式实现不同数量的成员.现在会发生什么? 第三方更改是否明确实现成员可能导致他人代码中的编译错误.

Suppose that third party changes a library so that a different number of members are explicitly implemented in a type you depend on. Now what happens? A third party changing whether or not a member is explicitly implemented can cause compilation errors in other people's code.

更糟糕的是,它可能不会导致编译错误;想象一下这样的情况,某人仅在隐式实现的方法数量上进行更改,而这些方法甚至都不是您调用的方法,而是默默地导致更改一系列的乌龟变成了一系列的长颈鹿.

Even worse, it can not cause a compilation error; imagine a situation in which someone makes a change just in the number of methods that are implicitly implemented, and those methods are not even methods that you call, but that change silently causes a sequence of turtles to become a sequence of giraffes.

这些情况真的非常糟糕. C#经过精心设计,可以防止此类脆性基类"失败.

Those scenarios are really, really bad. C# was carefully designed to prevent this kind of "brittle base class" failure.

哦,但是情况变得更糟.假设我们确实喜欢这个决胜局;我们甚至可以可靠地实施它吗?

Oh, but it gets worse. Suppose we did like this tiebreaker; could we even implement it reliably?

我们怎么知道成员是否被明确实现?程序集中的元数据具有一个表,该表列出了哪些类成员显式映射到了哪些接口成员,但是这是C#源代码中的内容的可靠反映吗??

How can we even tell if a member is explicitly implemented? The metadata in the assembly has a table that lists what class members are explicitly mapped to what interface members, but is that a reliable reflection of what is in the C# source code?

不,不是!在某些情况下,C#编译器必须秘密地代表您生成显式实现的接口,以满足验证者的要求(描述它们将完全是题外话).因此,您实际上不能很容易地分辨出类型的实现者决定显式实现多少个接口成员.

No, it is not! There are situations in which the C# compiler must secretly generate explicitly implemented interfaces on your behalf in order to satisfy the verifier (describing them would be quite off topic). So you cannot actually tell very easily how many interface members the type's implementor decided to implement explicitly.

情况变得更糟:假设该类甚至没有在C#中实现?某些语言总是填充在显式接口表中,实际上,我认为Visual Basic可能是其中的一种.因此,您的建议是使VB中编写的类的类型推断规则与C#中编写的等效类型可能不同.

It gets worse still: suppose the class is not even implemented in C#? Some languages always fill in the explicit interface table, and in fact I think Visual Basic might be one of those languages. So your proposal is to make the type inference rules possibly different for classes authored in VB than an equivalent type authored in C#.

尝试向刚刚将类从VB移植到C#的人解释,以使其具有相同的公共接口,现在他们的测试停止编译.

Try explaining that to someone who just ported a class from VB to C# to have an identical public interface, and now their tests stop compiling.

或者,从实现类Ark的人员的角度考虑它.如果该人希望表达其意图,这种类型既可以用作海龟也可以用作长颈鹿,但是如果有歧义,请选择海龟".您是否相信任何希望表达这种信念的开发人员都会轻松自然地得出结论,即这样做的方法是使其中一个接口更加隐式地实现比另一个?

Or, consider it from the perspective of the person implementing class Ark. If that person wishes to express the intention "this type can be used as both a sequence of turtles and giraffes, but if there is an ambiguity, choose turtles". Do you believe that any developer who wished to express that belief would naturally and easily come to the conclusion that the way to do that is to make one of the interfaces more implicitly implemented than the other?

如果这是开发人员需要消除歧义的事情,那么应该有一个精心设计,清晰,可发现的功能,具有这些语义.像这样:

If that were the sort of thing that developers needed to be able to disambiguate, then there should be a well-designed, clear, discoverable feature with those semantics. Something like:

class Ark : default IEnumerable<Turtle>, IEnumerable<Giraffe> ...

例如.也就是说,该功能应该显而易见可搜索,而不是偶然地从有关该类型的公共表面区域的无关决定中浮现出来.

for example. That is, the feature should be obvious and searchable, rather than emerging by accident from an unrelated decision about what the public surface area of the type should be.

简而言之:明确实现的接口成员的数量不是.NET类型系统的一部分.这是一个私有的实施策略决策,而不是编译器应该用来制定决策的公开表面.

In short: The number of interface members that are explicitly implemented is not a part of the .NET type system. It's a private implementation strategy decision, not a public surface that the compiler should use to make decisions.

最后,我把最重要的原因留在了最后.你说:

Finally, I've left the most important reason for last. You said:

我不是在寻求反馈意见,以这种方式设计课程是否被视为最佳实践.

I am not looking on feedback whether designing a class this way is considered best practice.

但这是一个非常重要的因素! C#的规则并非旨在就糟糕的代码做出正确的决策;他们旨在将糟糕的代码变成无法编译的残破代码,并且这种情况已经发生.该系统正常工作!

But that is an extremely important factor! The rules of C# are not designed to make good decisions about crappy code; they're designed to make crappy code into broken code that does not compile, and that has happened. The system works!

创建一个类来实现同一泛型接口的两个不同版本是一个很糟糕的主意,您不应该这样做. 因为您不应该这样做,所以C#编译器团队没有动力花一点时间弄清楚如何帮助您做得更好.此代码为您提供一条错误消息. 很好.它应该!该错误消息告诉您您做错了,所以请别再做错了,然后再正确地做.如果这样做时很痛,请停止这样做!

Making a class that implements two different versions of the same generic interface is a terrible idea and you should not do it. Because you should not do it, there is no incentive for the C# compiler team to spend even a minute figuring out how to help you do it better. This code gives you an error message. That is good. It should! That error message is telling you you're doing it wrong, so stop doing it wrong and start doing it right. If it hurts when you do that, stop doing that!

(可以肯定地指出,错误消息在诊断问题方面做得很差;这导致了另一堆微妙的设计决策.我打算针对这些情况改进该错误消息,但是这些情况是太罕见了,因此无法将它们作为优先事项,而且我在2012年离开微软之前就没有提到它.显然,在随后的几年中,没有其他人将其列为优先事项.)

(One can certainly point out that the error message does a poor job of diagnosing the problem; this leads to another whole bunch of subtle design decisions. It was my intention to improve that error message for these scenarios, but the scenarios were too rare to make them a high priority and I did not get to it before I left Microsoft in 2012. Apparently no one else has made it a priority in the years that followed either.)

更新:您问为什么对ark.GetEnumerator的调用可以自动执行正确的操作.这是一个容易得多的问题.这里的原理很简单:

UPDATE: You ask why a call to ark.GetEnumerator can do the right thing automatically. That is a much easier question. The principle here is a simple one:

重载分辨率选择可同时访问 适用的最佳成员.

Overload resolution chooses the best member that is both accessible and applicable.

可访问"表示调用者有权访问该成员,因为它足够公开",而适用"表示所有参数都与它们的形式参数类型匹配".

"Accessible" means that the caller has access to the member because it is "public enough", and "applicable" means "all the arguments match their formal parameter types".

当您致电ark.GetEnumerator()时,问题不是 我应该选择IEnumerable<T>的哪个实现"?这根本不是问题.问题是哪个GetEnumerator()既可访问又适用?"

When you call ark.GetEnumerator() the question is not "which implementation of IEnumerable<T> should I choose"? That's not the question at all. The question is "which GetEnumerator() is both accessible and applicable?"

只有一个,因为明确实现的接口成员不是Ark 的可访问成员.只有一个可访问成员,并且该成员恰好适用. C#重载解析的明智规则之一是如果只有一个可访问的适用成员,请选择它!

锻炼:将ark转换为IEnumerable<Animal>会发生什么?做出预测:

Exercise: What happens when you cast ark to IEnumerable<Animal>? Make a prediction:

  • 我会得到一系列乌龟
  • 我会得到一系列的长颈鹿
  • 我会得到一系列的长颈鹿和乌龟
  • 我会收到一个编译错误
  • 我还会得到其他东西-什么?

现在尝试您的预测,看看会发生什么.得出结论,编写具有相同泛型接口的多个结构的类型是好是坏.

Now try out your prediction and see what really happens. Draw conclusions as to whether it is a good or bad idea to write types that have multiple constructions of the same generic interface.

这篇关于C#中的接口冲突解决的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆