Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get best matching overload from set of overloads

Let's say I have a class as follows:

public class AcceptMethods
{
    public int Accept(string s, int k = 1)
    {
        return 1;
    }

    public int Accept(object s)
    {
        return 2;
    }

    public int Accept(IEnumerable<object> s)
    {
        return 7;
    }
    public int Accept(IList<object> s)
    {
        return 4;
    }
}

Now, if I try to consume this in code, I use something like this:

        object[] list = new object[] { "a", new object[0], "c", "d" };
        Assert.AreEqual(7, list.Select((a)=>((int)new AcceptMethods().Accept((dynamic)a))).Sum());

The reason that it's 7, is because overload resolution prefers [IList<object>] over [IEnumerable<object>] and [object], and because [string, int=default] has preference over [object].

In my scenario, I'd like to get the best matching overload using reflection. In other words: 'best' is defined as 'c# overload resolution'. E.g.:

int sum = 0;
foreach (var item in list)
{
    var method = GetBestMatching(typeof(AcceptMethods).GetMethods(), item.GetType());
    sum += (int)method.Invoke(myObject, new object[]{item});
}
Assert.AreEqual(7, sum);

While the scenario I sketch has only 1 parameter, the solution I seek can have multiple parameters.

Update 1:

Because I received a comment that this is too difficult for SO due to the difficulties of overload resolution implementation (which I am well aware of), I feel inclined to send an update. To give my argument some power, this was my first attempt, which uses the default .NET binder that handles the overload resolution:

    private MethodBase GetBestMatching(IEnumerable<MethodInfo> methods, Type[] parameters)
    {
        return Type.DefaultBinder.SelectMethod(BindingFlags.Instance | BindingFlags.Public | BindingFlags.OptionalParamBinding | BindingFlags.InvokeMethod,
                        methods.ToArray(), parameters, null);
    }

This version already seems to do simple overload resolution correctly, but fails to work with optional parameters. Because .NET afaik works with type binding like I show here, I suppose that the solution could be implemented fairly easy.

like image 299
atlaste Avatar asked Jan 14 '13 08:01

atlaste


2 Answers

This is a massive subject, requires quite a lot of work and certainly cannot be wrapped up in an SO answer in my opinion. I suggest you read through the C# spec and read the formal rules defining overload resolution (also, please pay attention to generic methods) and try to implement them up to the point that satisfies your needs.

Update

Optional (i.e. parameters with default values) are not a trivial case - and the Reflection binder makes no attempt at all to fill them in - that's because it's a compiler's job to identify the default values, pull them out and inject them into a call to such a method.

You need a multi-pass approach that's something like this (note - does NOT include generics):

  1. Search manually for a method whose number of parameters and types of those parameters match exactly the number and types of arguments you've got. If you find a match - use it and bunk out.

  2. Now identify the 'candidate list' of methods for your overload selection (generally that's by name - you might also exclude generics here - unless you're going to try and bind those too).

  3. If none of those methods have optional parameters then you might be able to go ahead and use the default binder as per your question to find the match (if not, you need a argument/parameter-ranking algorithm based on the types - if so, skip to 5).

  4. Re-running through the candidate list built in 3), pull out all the optional parameters and incorporate their default values into your own parameter lists (you might need to build a separate set of parameters for each method at this point, including those that you have been supplied, but also the defaults).

  5. Run your ranking algorithm for these methods built in 3) and possibly 4) to identify the best match (you seem to have a good handle on this so I'm not going to go through it all here - it's not a simple algorithm and I frankly can't quote it all verbatim here either!).

  6. Your ranking algorithm should produce a clear winning method - i.e. with a unique high score or similar. If you get a clear winner, then that's the one you bind. Otherwise you have an ambiguity and you have to bunk out.

You might be interested in my own SO at this point - Default parameters and reflection: if ParameterInfo.IsOptional then is DefaultValue always reliable? - which should help you with identifying methods that have parameters with defaults, and how to lift them out.

like image 165
Andras Zoltan Avatar answered Oct 11 '22 01:10

Andras Zoltan


For other people that want to do runtime overload resolution, this is a fairly complete description on how to implement it.

Important side note is that the 'dynamic' trick doesn't work in all scenario's as well (specifically: generics); it seems that the compiler is more flexible than runtime behavior.

Also note that this is not a complete algorithm/implementation (or at least I think that it isn't), but works in most cases including nevertheless. I found this to work in all cases that I encountered so far, including difficult cases like array covariance.

The scoring algorithm works as follows:

  • If parameter type == source type: score = 0
  • If the parameter is a generic type argument that is valid (generic constraints): score = 1
  • If the source type is implicitly convertible to the parameter type: score = 2 (see: http://msdn.microsoft.com/en-us/library/y5b434w4.aspx for all rules)
  • If you need to fill in a default parameter: score = 3
  • Otherwise calculate the score for compatibility score as below

The compatibility score is the most strict conversion between a type type A and type B (including and covariance, contravariance). For example, string[] has 1 conversion to IList (using object[] and then IList) and 2 conversions to IEnumerable (1. by using object[] and then IEnumerable or 2. by IEnumerable). Therefore, IList is the more strict conversion and is therefore selected.

Counting the number of conversions is easy:

            int cnt = CountCompatible(parameter.ParameterType, sourceType.GetInterfaces()) +
                      CountCompatible(parameter.ParameterType, sourceType.GetBaseTypes()) +
                      CountCompatible(parameter.ParameterType, new Type[] { sourceType });
            [...]

    private static int CountCompatible(Type dst, IEnumerable<Type> types)
    {
        int cnt = 0;
        foreach (var t in types)
        {
            if (dst.IsAssignableFrom(t))
            {
                ++cnt;
            }
        }
        return cnt;
    }

To make sure that a better score is selected when a more strict conversion is used, I calculate score as 'score = 5 - 1.0 / (cnt + 2);'. The +2 ensures that you never divide by 0 or 1, leading to a score between 4 and 5.

To do overload resolution, select the method with the minimum score for all arguments. Make sure you enter default method arguments properly when invoking (see the excellent link by Andras above) and make sure you fill in the generic arguments before you return the method. If you encounter a draw for the best method: the best resolution is to throw an exception.

In case you wonder, yes... it's quite a bit of work to get it all working correctly... I plan to make a working version available in my framework once that's done. (You'll see the moment my profile has a working website link :-) )

like image 45
atlaste Avatar answered Oct 11 '22 03:10

atlaste