【问题标题】:Get best matching overload from set of overloads从重载集中获得最佳匹配重载
【发布时间】:2013-01-14 08:56:11
【问题描述】:

假设我有一个类如下:

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;
    }
}

现在,如果我尝试在代码中使用它,我会使用这样的东西:

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

之所以为 7,是因为重载决议更喜欢 [IList&lt;object&gt;] 而不是 [IEnumerable&lt;object&gt;] 和 [object],并且因为 [string, int=default] 优先于 [object ].

在我的场景中,我想使用反射获得最佳匹配重载。换句话说:“最佳”被定义为“c#重载决议”。例如:

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);

虽然我绘制的场景只有 1 个参数,但我寻求的解决方案可以有多个参数。

更新 1

因为我收到一条评论,由于重载解决方案实现的困难(我很清楚),这对 SO 来说太难了,我倾向于发送更新。为了给我的论点一些力量,这是我的第一次尝试,它使用处理重载解决方案的默认 .NET 绑定器:

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

这个版本似乎已经正确地进行了简单的重载解析,但是无法使用可选参数。因为 .NET afaik 可以像我在这里展示的那样使用类型绑定,所以我认为该解决方案可以相当容易地实现。

【问题讨论】:

    标签: c# .net overload-resolution


    【解决方案1】:

    这是一个 大量 主题,需要大量工作,而且在我看来,当然不能包含在 SO 答案中。我建议您通读 C# 规范并阅读定义重载解析的正式规则(另外,请注意泛型方法)并尝试将它们实现到满足您需求的程度。

    更新

    可选(即具有默认值的参数)不是一个简单的情况 - 反射绑定器根本不会尝试填充它们 - 这是因为编译器的工作是识别默认值,将它们取出并将它们注入调用这种方法。

    您需要类似这样的多通道方法(注意 - 不包括泛型):

    1. 手动搜索一个方法,其参数数量和这些参数的类型完全与您获得的参数的数量和类型匹配。如果您找到了匹配项 - 使用它并下铺。

    2. 现在为您的重载选择确定方法的“候选列表”(通常是按名称 - 您也可以在此处排除泛型 - 除非您也打算尝试绑定它们)。

    3. 如果这些方法都没有可选参数,那么您可以继续并根据您的问题使用默认绑定器来查找匹配项(如果没有,您需要一个基于参数/参数排名算法类型 - 如果是,请跳至 5)。

    4. 重新运行3中内置的候选列表),拉出所有可选参数并将它们的默认值合并到您自己的参数列表中(您可能需要在此为每个方法构建单独的参数集点,包括您提供的那些,但也包括默认值)。

    5. 对 3) 和可能的 4) 中内置的这些方法运行您的排名算法,以确定最佳匹配(您似乎对此掌握得很好,所以我不会在这里全部介绍 - 它是这不是一个简单的算法,坦率地说,我也不能在这里逐字引用它!)。

    6. 您的排名算法应该产生一个明确的获胜方法 - 即具有独特的高分或类似的。如果你得到一个明显的赢家,那么这就是你绑定的那个。否则你有一个模棱两可的,你必须卧床不起。

    此时您可能对我自己的 SO 感兴趣 - Default parameters and reflection: if ParameterInfo.IsOptional then is DefaultValue always reliable? - 它应该可以帮助您识别具有默认参数的方法,以及如何将它们取出。

    【讨论】:

    • @StefandeBruijn 也刚刚更新了我的答案 - 恐怕我没有提供代码,但我已经概述了您需要的基本方法。我希望这足以让你走上自己的道路
    • 感谢您的评论,但不幸的是您有部分错误。对于“访问者”实现,您使用编译时解析,但“动态”关键字是运行时构造,而不是编译时构造。它似乎使用 Microsoft.CSharp.RuntimeBinder.Binder 和 System.Runtime.CompilerServices.CallSiteBinder - 虽然我不知道具体如何。无论哪种方式,都应该可以在代码中使用这些构造,而无需经历实现完整绑定器的麻烦。
    • 是的 - 这是特定于 C# 语言的,您可以搭载它。发出呼叫站点可能是其中最难的部分。它们通常是静态的,编译器会为每个动态调用发出一个。然后,它们保存已通过它们的所有动态绑定的缓存。
    • 感谢您的帮助,我将您的答案标记为正确。最终,我根据标准中描述的规则实现了自己的重载解决算法,并根据测试填补了所有空白。详情见下文。
    【解决方案2】:

    对于其他想要进行运行时重载解析的人,这是关于如何实现它的相当完整的描述。

    重要的旁注是“动态”技巧也不适用于所有场景(特别是:泛型);似乎编译器比运行时行为更灵活。

    另请注意,这不是一个完整的算法/实现(或者至少我认为不是),但在大多数情况下都有效,包括不过。我发现这适用于我迄今为止遇到的所有情况,包括数组协方差等困难情况。

    评分算法的工作原理如下:

    • 如果参数类型 == 源类型:分数 = 0
    • 如果参数是有效的泛型类型参数(泛型约束):分数 = 1
    • 如果源类型可隐式转换为参数类型:score = 2(请参阅:http://msdn.microsoft.com/en-us/library/y5b434w4.aspx 了解所有规则)
    • 如果需要填写默认参数:score = 3
    • 否则按照以下方式计算兼容性分数

    兼容性分数是类型A和类型B之间最严格的转换(包括和协变、逆变)。例如,string[] 有 1 次到 IList 的转换(使用 object[] 然后 IList)和 2 次到 IEnumerable 的转换(1. 通过使用 object[] 然后 IEnumerable 或 2. 通过 IEnumerable)。因此,IList 是更严格的转换,因此被选中。

    计算转化次数很容易:

                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;
        }
    

    为了确保在使用更严格的转换时选择更好的分数,我将分数计算为'score = 5 - 1.0 / (cnt + 2);'。 +2 确保您永远不会除以 0 或 1,从而得出 4 到 5 之间的分数。

    要进行重载决策,请选择所有参数得分最低的方法。确保在调用时正确输入默认方法参数(参见上面 Andras 的优秀链接),并确保在返回方法之前填写通用参数。如果遇到最佳方法的平局:最好的解决办法是抛出异常。

    如果您想知道,是的...要使其正常工作需要做很多工作...一旦完成,我计划在我的框架中提供一个可用的工作版本。 (你会看到我的个人资料有一个有效的网站链接:-))

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-07-27
      • 2015-10-14
      • 2018-07-14
      • 1970-01-01
      • 2013-01-07
      • 2014-05-17
      • 1970-01-01
      相关资源
      最近更新 更多