A recent Stack Overflow question piqued my interest. It looked like some inappropriate methods were being considered in overload resolution, when I thought they should have been rejected earlier. Yet again, I thought I’d found a compiler bug, but it turned out to be a case of misreading the spec. I’m arrogant enough to think that if I misread the spec, someone else might too, so I thought I’d dig into it a bit.
It turns out that Eric Lippert has written about the same area of the spec, but in the more complicated scenario where type inference is involved – in other words, where the compiler is working out what generic type arguments should be supplied to a method. While that’s certainly interesting, type inference introduces a whole extra bit of the spec which I prefer to avoid for reasons of health. We can still explore the same key process without involving type inference. I’m effectively revisiting one small part of Eric’s blog post – as much to make sure that I understand it as much as anything else.
The relevant area of the C# 4 spec is section 22.214.171.124, "Method invocations". Broadly speaking, this defines the process for choosing which method to invoke as:
- Find a set of candidate methods
- Remove methods which aren’t in the most derived type. This is one of the strange areas of overloading which can easily cause surprise. I’ve talked about it elsewhere, particularly in my article on overloading.
- If we haven’t found anything else, resort to extension methods
- Pick the best method out of the set
- Validate that the best method is actually sane
Let’s look at this in three different contexts.
Example 1: Ambiguity
Now, for the purposes of this post I’m only interested in where generic type parameter constraints are checked, and the effects of that on the overall process. Let’s look at a concrete example:
static void Foo<T>(string x) where T : struct
static void Foo<T>(Exception e) where T : class
static void Main()
Okay, so we’ve got two methods called
M with different constraints. I’ve given them both parameters of different reference types. Without the parameters, we couldn’t even declare the two methods, as they would have the same signature: the declarations only differ in generic constraints, and constraints aren’t part of the signature. Both parameter types are reference types, so the argument we’re passing (null) is valid for both parameters.
So, what happens when we try to call
Foo<object>(null)? Intuitively we might expect it to call the second overload, with the
Exception parameter – because that has a constraint on
T which is satisfied by the type argument, whereas the first overload has a value type constraint on
T, which clearly isn’t satisfied by
object. But what happens in the spec?
Well, as per the list above, we first need to find the set of candidate methods. This is where I got confused. Working out the method group is easy – both methods are in it. We’ve supplied type arguments, and both methods are generic, so this bit of the spec applies:
If F is generic and M includes a type argument list, F is a candidate when:
- F has the same number of method type parameters as were supplied in the type argument list, and
- Once the type arguments are substituted for the corresponding method type parameters, all constructed types in the parameter list of F satisfy their constraints (§4.4.4), and the parameter list of F is applicable with respect to A (§126.96.36.199).
Note that here, "F" is the method under consideration, and "M" is the invocation expression.
The first bullet is fine: we have a single type argument, and both methods have a single type parameter. So what about the second? It talks about types satisfying constraints, so at first glance you might expect this to rule out the first overload. That’s certainly what I thought initially. But look carefully… it’s talking about the types in the parameter list – checking that they satisfy their constraints. It’s not talking about the type arguments for the method itself, or the constraints there.
Now our two parameters (
Exception) are both nongeneric, so they can’t even have constraints. Both end up being candidate methods, and because neither is better than the other with respect to our invocation, the call ends up being ambiguous:
Test.cs(13,9): error CS0121: The call is ambiguous between the following methods or properties: ‘Test.Foo<object>(string)’ and ‘Test.Foo<object>(System.Exception)’
The next obvious question is what situation the constraint checking here does have an effect. Let’s look at a very slightly different example.
Example 2: Weeding out invalid constructed parameter types
This time I’m going to introduce an extra generic type. It’s called
TItem is constrained to have a parameterless constructor. I’ve called it
TItem rather than just
T for the sake of clarity later on. We’re not going to give it any behaviour, as we’re just interested in the type declaration. Here’s the complete code for the example:
class Factory<TItem> where TItem : new()
static void Foo<T>(Factory<T> factory) where T : struct
static void Foo<T>(Exception e) where T : class
static void Main()
Now, the first thing to note is that just to declare the first method with a parameter of
Factory<T>, we have to ensure that
T will have a parameterless constructor. That’s coverted by the constraint of
T : struct, fortunately.
Now let’s look at the method invocation. This time we’re trying to use string as the type argument. The second method is obviously a candidate – it’s fine in every way. But what about the first?
After type argument substitution, the parameter list for the first method looks like this:
Now the spec says that each type in the parameter list has to be valid with respect to its constraints. In this case, it isn’t:
string doesn’t have a parameterless constructor, so the type
Factory<string> is invalid.
So, the first method is rejected, the second is fine, so it compiles with no problems. The important thing to note is that the compiler hasn’t taken any notice of the
where T : struct part of the first method’s declaration… it’s only rejecting the first method on the basis of the constraint on
Factory<T>. We can demonstrate that by changing the method invocation:
object does have a parameterless constructor, so the method is still part of the candidate set, and we’re back to the same ambiguous position of our first example.
Now we haven’t yet managed to trip the compiler up on the final part of our list – validating the constraints of method after it’s been chosen.
Example 3: Final validation
I could demonstrate this with a single method, so that overload resolution had no choices to make. However, we’ll have a little bit more fun. Here’s our final sample code:
static void Foo<T>(object x) where T : struct
static void Foo<T>(string y) where T : class
static void Main()
Factory<T> – we’re back to simple parameters. The big difference between this example and the first one, however, is that the parameters are
string instead of
Exception. That means that although both methods are in the candidate set, the second one is deemed "better" than the first because the conversion from
string is better (more specific) than the conversion from
So, the compiler resolves everything about the method invocation without paying any attention to the generic constraints involved until the very last moment – where it notices that the chosen method (the second one) isn’t valid when
int, and fails with this error:
Test.cs(13,9): error CS0452: The type ‘int’ must be a reference type in order to use it as parameter ‘T’ in the generic type or method ‘Test.Foo<T>(string)’
There’s no mention of ambiguity, because by this point there isn’t any ambiguity. The cruel irony is that if only the compiler had applied the constraints earlier, it would have found that calling the first method is perfectly valid in every way.
I’m not going to try to argue the pros and cons of the way the language has been designed – Eric has some thoughts on that in the blog post, and obviously he has much more insight into the design process than I do. However, next time you find yourself trying to understand why the compiler is behaving in a particular way, hopefully one of these examples may help you.
8 thoughts on “Overloading and generic constraints”
Awesome read. Thanks!
I find this behaviour quite natural myself, given that overloading applies to value arguments, and constraints apply to type paremeters – one should not directly affect the other. At least, I do now that you clearly explained what it’s doing!
I could see getting confused by the overload resolution being sandwiched between Factory’s T type constraint evaluation and Test.Foo’s, especially if you assumed the reason for the superset requirement on constrained type paremeters was to enable safely evaluating only one type constraint. (I assume it is for the same reason C# requires you to declare ‘overload’, so you are not surprised by ‘viral’ behaviour.)
I personally find this behaviour quite non-sensical. As expressed in comments on Eric’s blog post, in the spec, the generic method type contstraints aren’t part of the method signature, but I (and several other people) think they should be. However, as you say, we don’t know the exact process that led to the behaviour as it currently stands…
So, what I gather from this, in summary, is that C# resolves method overloads based on type arguments first, then type constraints. Is that right? IMHO, looking at generic type parameter constraints first and then arguments would be better.
I haven’t tried this on these exact examples, but one easy way to call the right overload when passing a null parameter is to cast null to something, like this:
@Jason: I think that’s an oversimplification, to be honest. To judge whether or not it’s a *correct* simplification would require a bit more detail.
And yes, I know you can cast null – the point of using null was precisely to allow both overloads to be applicable :)
Just for completeness: I’m unable to reproduce this with dotnet core 3.1.101. Type argument constraints are now being factored in at an earlier stage during overload resolution (see https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-7.3/improved-overload-candidates).
Agreed. Unfortunately editing the post now would be tricky for reasons too long to go into – but it’s a welcome change.
Of course it makes no sense to edit a 10 year old blog post, because of a two year old change to C#. Just wanted to leave the info there for future readers investigating similar issues. By the way, I happen to have stumbled across such a similar issue: https://stackoverflow.com/questions/60754529/how-to-explain-this-call-is-ambiguous-error :)