Unexpected effect of implicit cast on delegate typ

2019-04-05 18:49发布

问题:

I have a simple Money type with an implicit cast from decimal:

struct Money
{
    decimal innerValue;
    public static implicit operator Money(decimal value)
    {
        return new Money { innerValue = value };
    }
    public static explicit operator decimal(Money value)
    {
        return value.innerValue;
    }

    public static Money Parse(string s)
    {
        return decimal.Parse(s);
    }
}

And I defined a Sum() overload to operate on those values:

static class MoneyExtensions
{
    public static Money Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, Money> selector)
    {
        return source.Select(x => (decimal)selector(x)).Sum();
    }
}

What I didn't expect was for this extension method to interfere with the existing Sum() extension methods:

var source = new[] { "2" };
Money thisWorks = source.Sum(x => Money.Parse(x));
int thisWorksToo = source.Sum(new Func<string, int>(x => int.Parse(x)));
int thisDoesNot = source.Sum(x => int.Parse(x));

The error is "Cannot implicitly convert type 'Money' to 'int'. An explicit conversion exists (are you missing a cast?)". Is it correct that the compiler favors int => decimal => Money implicit conversions over resolving an overload that's an exact match?

回答1:

From the C# 4.0 Specification, section 7.6.5.2:

The preceding rules mean that instance methods take precedence over extension methods, that extension methods available in inner namespace declarations take precedence over extension methods available in outer namespace declarations, and that extension methods declared directly in a namespace take precedence over extension methods imported into that same namespace with a using namespace directive

Probably, this is causing your Money Sum extension method to take precedence over the ones from Linq - that's why you don't get an "ambiguous method call" error.



回答2:

Following on from Rob Siklos's research, (please vote up the research) Putting the extension in a seperate namespace fixes this problem. I seem to recall this as one of the guidelines for extensions.

using System;
using System.Collections.Generic;
using System.Linq;
using Extensions;

namespace Currency
{
    struct Money
    {          
        decimal innerValue;
        public static implicit operator Money(decimal value)
        {
            return new Money { innerValue = value };
        }
        public static explicit operator decimal(Money value)
        {
            return value.innerValue;
        }
        public static Money Parse(string s)
        {
        return decimal.Parse(s);
        }
     }

     class Program
     {
         static void Main()
         {
             var source = new[] { "2" };
             Money thisWorks = source.Sum(x => Money.Parse(x));
             int thisWorksToo = 
                 source.Sum(new Func<string, int>(x => int.Parse(x)));       
             int thisWorksTooNow = source.Sum(x => int.Parse(x));

         }
     }
}
namespace Extensions
{
    static class IEnumerableTExtensions
    {
        public static Currency.Money Sum<TSource>(
                                       this IEnumerable<TSource> source,
                                       Func<TSource, Currency.Money> selector)
        {
            return source.Select(x => (decimal)selector(x)).Sum();
        }
    }
}


回答3:

It's because you are explicitly declaring thisDoesNot as type int. If you use implicit declaration, it works fine:

void Main()
{
    var source = new[] { "2" };
    Money thisWorks = source.Sum(x => Money.Parse(x));
    int thisWorksToo = source.Sum(new Func<string, int>(x => int.Parse(x)));
    var thisDoesNot = source.Sum(x => int.Parse(x));

    Console.Write(thisDoesNot.GetType());
}

From the specification:

The method group identifies the one method to invoke or the set of overloaded methods from which to choose a specific method to invoke. In the latter case, determination of the specific method to invoke is based on the context provided by the types of the arguments in the argument-list.