Static method and extension method with same name

2019-04-29 02:44发布

问题:

I created extension method:

public static class XDecimal
{
    public static decimal Floor(
        this decimal value,
        int precision)
    {
        decimal step = (decimal)Math.Pow(10, precision);
        return decimal.Floor(step * value) / step;
    }
}

Now I try to use it:

(10.1234m).Floor(2)

But compiler says Member 'decimal.Floor(decimal)' cannot be accessed with an instance reference; qualify it with a type name instead. I understand there is static decimal.Floor(decimal) method. But it has different signature. Why compiler is unable to choose correct method?

回答1:

You have two good and correct answers here, but I understand that answers which simply quote the specification are not always that illuminating. Let me add some additional details.

You probably have a mental model of overload resolution that goes like this:

  • Put all the possible methods in a big bucket -- extension methods, static methods, instance methods, etc.
  • If there are methods that would be an error to use, eliminate them from the bucket.
  • Of the remaining methods, choose the unique one that has the best match of argument expressions to parameter types.

Though this is many people's mental model of overload resolution, regrettably it is subtly wrong.

The real model -- and I will ignore generic type inference issues here -- is as follows:

  • Put all the instance and static methods in a bucket. Virtual overrides are not counted as instance methods.
  • Eliminate the methods that are inapplicable because the arguments do not match the parameters.

At this point we either have methods in the bucket or we do not. If we have any methods in the bucket at all then extension methods are not checked. This is the important bit right here. The model is not "if normal overload resolution produced an error then we check extension methods". The model is "if normal overload resolution produced no applicable methods whatsoever then we check extension methods".

If there are methods in the bucket then there is some more elimination of base class methods, and finally the best method is chosen based on how well the arguments match the parameters.

If this happens to pick a static method then C# will assume that you meant to use the type name and used an instance by mistake, not that you wish to search for an extension method. Overload resolution has already determined that there is an instance or static method whose parameters match the arguments you gave, and it is going to either pick one of them or give an error; it's not going to say "oh, you probably meant to call this wacky extension method that just happens to be in scope".

I understand that this is vexing from your perspective. You clearly wish the model to be "if overload resolution produces an error, fall back to extension methods". In your example that would be useful, but this behaviour produces bad outcomes in other scenarios. For example, suppose you have something like

mystring.Join(foo, bar);

The error given here is that it should be string.Join. It would be bizarre if the C# compiler said "oh, string.Join is static. The user probably meant to use the extension method that does joins on sequences of characters, let me try that..." and then you got an error message saying that the sequence join operator -- which has nothing whatsoever to do with your code here -- doesn't have the right arguments.

Or worse, if by some miracle you did give it arguments that worked but intended the static method to be called, then your code would be broken in a very bizarre and hard-to-debug way.

Extension methods were added very late in the game and the rules for looking them up make them deliberately prefer giving errors to magically working. This is a safety system to ensure that extension methods are not bound by accident.



回答2:

The process of deciding on which method to call has lots of small details described in the C# language specification. The key point applicable to your scenario is that extension methods are considered for invocation only when the compiler cannot find a method to call among the methods of the receiving type itself (i.e. the decimal).

Here is the relevant portion of the specification:

The set of candidate methods for the method invocation is constructed. For each method F associated with the method group M:

  • If F is non-generic, F is a candidate when:

  • M has no type argument list, and

  • F is applicable with respect to A (§7.5.3.1).

According to the above, double.Floor(decimal) is a valid candidate.

If the resulting set of candidate methods is empty, then further processing along the following steps are abandoned, and instead an attempt is made to process the invocation as an extension method invocation (§7.6.5.2). If this fails, then no applicable methods exist, and a binding-time error occurs.

In your case the set of candidate methods is not empty, so extension methods are not considered.



回答3:

The signature of decimal.Floor is

public static Decimal Floor(Decimal d);

I'm no specialist in type inference, but I guess since there is a implicit conversion from int to Decimal the compiler chooses this as the best fitting method.

If you change your signature to

public static decimal Floor(
    this decimal value,
    double precision)

and call it like

(10.1234m).Floor(2d)

it works. But of course a double as precision is somewhat strange.

EDIT: A quote from Eric Lippert on the alogrithm:

Any method of the receiving type is closer than any extension method.

Floor is a method of the "receiving type" (Decimal). On the why the C# developers implemented it like this I can make no statement.