C# generic type inference versus covariance - bug or restriction

浪子不回头ぞ 提交于 2019-12-11 04:54:35

问题


When a generic method with dependent parameters infers the type it gives unexpected results in some cases. If I specify the type explicitly everything works without any further Changes.

IEnumerable<List<string>> someStringGroups = null; // just for demonstration
IEqualityComparer<IEnumerable<string>> someSequenceComparer = null;

var grouped = someStringGroups
  .GroupBy(x => x, someSequenceComparer);

Of course, above code is not intended to be ever executed, but it demonstrates that the result type of grouped is IEnumerable<IEnumerable<string>,List<string>> rather than IEnumerable<List<string>,List<string>> as expected because of x => x.

If I specify the types explicitly everything is fine.

var grouped = someStringGroups
  .GroupBy<List<string>,List<string>>(x => x, someSequenceComparer);

If I do not use an explicit comparer everything works as expeceted too.

I think that the problem is that taking the least common denominator of the supplied argument types (IEnumerable<string>) takes precedence over the covariance of the IEqualityComparer<> interface. I would have expected the opposite, i.e. a generic method should infer the most specific type that is satisfied by the arguments.

The question is: is this a bug or a documented behavior?


回答1:


I would have expected the opposite, i.e. a generic method should infer the most specific type that is satisfied by the arguments.

Based on, what, exactly?

The behavior you see is documented and compliant with the C# specification. As you might imagine, the type inference specification is fairly complex. I won't quote the whole thing here, but you can review it yourself if you are interested. The relevant section is 7.5.2 Type inference.

Based on comments you've written, I think at least part of the confusion stems from you forgetting that there are three parameters to this method, not two (which affects how inference proceeds). In addition, it seems you are expecting the second parameter, the keySelector delegate, to affect type inference, when it does not in this case (at least, not directly…it creates a dependency between the type parameters, but not in a material way).

But I think the main thing is that you are expecting type inference to be more aggressive about type variance than the specification in fact requires.

During type inference, the first thing that happens is described in the specification, under 7.5.2.1 The first phase. The second argument is, for all intents and purposes in this phase, ignored. It does not have explicitly declared types for its parameters (though, it wouldn't matter if it did). During this phase, type inference starts to develop bounds for the type parameters, but does not fix the parameters themselves.

You are calling this overload of GroupBy():

public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    IEqualityComparer<TKey> comparer)

There are two type parameters that need to be inferred, TSource and TKey. During inference, it is true that the compiler determines upper- and lower-bounds for the type parameters. But these are based on the types passed to the method call. The compiler does not search for alternate base or derived types that would satisfy the type requirements.

So, for TSource, a lower-bound of List<string> is identified, while for TKey, an upper-bound of IEnumerable<string> is identified (7.5.2.9 Lower-bound inferences). These types are what you provided to the call, so that's what the compiler uses.

In the second phase, an attempt to fix the types is made. TSource does not depend on any other parameter, so it's fixed first, as List<string>. A second go-around in the second phase fixes TKey. While type variance allows the bounds set for TKey to accommodate List<string>, there's no need, because according to its bounds the type you passed in can be used directly.

Thus, you wind up with IEnumerable<string> instead.

Of course, it would have been legitimate (if not compliant with the specification) for the compiler to use List<string> as TKey instead. We can see this work if the parameter is explicitly cast accordingly:

var grouped2 = someStringGroups
  .GroupBy(x => x, (IEqualityComparer<List<string>>)someSequenceComparer);

This changes the type of the expression used for the call, thus the bounds used, and finally of course the actual type chosen during inference. But in the original call, the compiler had no need during inference to use a type different from what you specified, even though it would have been allowed, and so it didn't.

The C# specification has some fairly hairy parts to it. Type inference is definitely one of them, and frankly I am not an expert on interpreting this section of the specification. It makes my head hurt, and there are definitely some more challenging corner cases that I probably don't understand (i.e. I doubt I could implement this part of the specification, without a lot more study). But I believe the above is a correct interpretation of the parts relevant to your question, and I hope that I've done a reasonable job explaining it.




回答2:


I'd be pretty sure this is expected behaviour.

The method signature we are interested in is:

public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector,
    IEqualityComparer<TKey> comparer
)

source is of type IEnumerable<List<string>> so the natural choice for TSource is List<string>. The comparer is of type IEqualityComparer<IEnumerable<string>> so the natural choice for TKey is IEnumerable<string>.

If we then look at the last parameter keySelector is x=>x. Does this satisfy the type constraints we have so far? Yes it does because x is a List<string>and that can be implicitly converted to an IEnumerable<string>.

At this point why should the compiler go looking for anything more? The natural and obvious choices with no casting needed works so it uses that. If you don't like it you always have the option to do as you did and explicitly state the generic parameters.

Or of course you could make your comparer of type IEqualityComparer<List<string>> in which case your output object will be of the type you expect (and I hope you can see why this is).



来源:https://stackoverflow.com/questions/44777233/c-sharp-generic-type-inference-versus-covariance-bug-or-restriction

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!