7

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?

Denis
  • 3,653
  • 4
  • 30
  • 43
  • 2
    Eric Lippert has a blog post about overload resolution you may find useful: ["*Closer is better*"](http://ericlippert.com/2013/12/23/closer-is-better/). – Wai Ha Lee Dec 16 '15 at 17:12
  • 3
    As a workaround, I'd suggest renaming your extension method to indicate what it does that the regular `decimal.Floor` method doesn't do. Naming things the same as something built into the framework is not a good idea. – mason Dec 16 '15 at 17:13
  • 1
    Im with @mason. Better name it something like CutDigits or TruncateDigits. – CSharpie Dec 16 '15 at 17:14

3 Answers3

10

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.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
6

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.

Community
  • 1
  • 1
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
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.

René Vogt
  • 43,056
  • 14
  • 77
  • 99
  • 2
    It would also work by specifing the argument like so `(10.1234m).Floor(precision: 2)` – CSharpie Dec 16 '15 at 17:13
  • 2
    @CSharpie's solution is better. But neither answer the question, "*Why compiler is unable to choose correct method?*" – kdbanman Dec 16 '15 at 17:14
  • 1
    @kdbanman That's a red herring. The compiler did choose the correct method according to how the language designers want it to, it's just not the one you might have been expecting. The real question (as Rene said) is "Why did the designers make the language that way?" – mason Dec 16 '15 at 17:25
  • @mason, that's not what red herring [means](http://literarydevices.net/red-herring/). A red herring is a diversion that is both irrelevant and intentional. The original question is not an intentional diversion, and it isn't irrelevant either. It's perfectly answerable by addressing the related question you pose. See dasblinkenlight's answer. – kdbanman Dec 16 '15 at 17:42
  • 1
    @kdbanman It's been a while since I learned all the fallacies in school. But my point is that it's the wrong question to ask, because it presumes that it's not behaving correctly, where "correct" is based on some preconceived notion of how it should work. – mason Dec 16 '15 at 17:43
  • @mason, The question isn't as misguided as you think. The method signatures in the question look perfectly distinguishable. "*Why is the compiler unable to choose correct method?*" is a good question and it has a short answer, "*Because the language designers needed to make choices.*" which has the obvious follow up "*Why did the designers make those choices?*". The follow up is exactly the question you suggest - it isn't unrelated or more correct. It's just a follow up. – kdbanman Dec 16 '15 at 17:56
  • @kdbanman,mason my phone is ringing for each of your comments. This is no chat or forum. I up voted dasblinkenlight's answer since he is more precise and put more effort in it. Of course OP wanted to know why it is like that no matter if it's the compiler or the language or the language designers. – René Vogt Dec 16 '15 at 18:01