【问题标题】:Extension methods overridden by class gives no warning被类覆盖的扩展方法不给出警告
【发布时间】:2011-03-08 04:38:18
【问题描述】:

我在另一个线程中进行了讨论,发现类方法优先于具有相同名称和参数的扩展方法。这很好,因为扩展方法不会劫持方法,但假设您已将一些扩展方法添加到第三方库:

public class ThirdParty
{
}

public static class ThirdPartyExtensions
{
    public static void MyMethod(this ThirdParty test)
    {
        Console.WriteLine("My extension method");
    }
}

按预期工作:ThirdParty.MyMethod ->“我的扩展方法”

但随后第三方更新了它的库并添加了一个与您的扩展方法完全相同的方法:

public class ThirdParty
{
    public void MyMethod()
    {
        Console.WriteLine("Third party method");
    }
}

public static class ThirdPartyExtensions
{
    public static void MyMethod(this ThirdParty test)
    {
        Console.WriteLine("My extension method");
    }
}

ThirdPart.MyMethod -> “第三方方法”

现在,由于第三方方法“劫持”了您的扩展方法,突然代码在运行时的行为会有所不同!编译器不会给出任何警告。

有没有办法启用此类警告或以其他方式避免这种情况?

【问题讨论】:

    标签: c# extension-methods


    【解决方案1】:

    不——这是扩展方法的一个已知缺点,需要非常小心。就我个人而言,我希望 C# 编译器会在您声明一个扩展方法时发出警告,该方法除了通过正常的静态路由 (ExtensionClassName.MethodName(target, ...)) 之外永远不会被调用。

    编写一个小工具来检查程序集中的所有扩展方法并以这种方式发出警告可能不会太难。一开始可能不需要非常精确:如果已经有同名的方法发出警告(不用担心参数类型)将是一个好的开始。

    编辑:好的...这是一个非常粗略的工具,至少可以提供一个起点。它似乎至少在某种程度上适用于泛型类型 - 但它并没有尝试对参数类型或名称做任何事情......部分原因是参数数组变得棘手。它还“完全”加载程序集而不是仅使用反射,这会更好 - 我尝试了“正确”的路线,但遇到了一些无法立即解决的问题,因此退回到快速而肮脏的路线: )

    无论如何,希望它对某人、某处有用。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.CompilerServices;
    
    public class ExtensionCollisionDetector
    {
        private static void Main(string[] args)
        {
            if (args.Length == 0)
            {
                Console.WriteLine
                    ("Usage: ExtensionCollisionDetector <assembly file> [...]");
                return;
            }
            foreach (string file in args)
            {
                Console.WriteLine("Testing {0}...", file);
                DetectCollisions(file);
            }
        }
    
        private static void DetectCollisions(string file)
        {
            try
            {
                Assembly assembly = Assembly.LoadFrom(file);
                foreach (var method in FindExtensionMethods(assembly))
                {
                    DetectCollisions(method);
                }
            }
            catch (Exception e)
            {
                // Yes, I know catching exception is generally bad. But hey,
                // "something's" gone wrong. It's not going to do any harm to
                // just go onto the next file.
                Console.WriteLine("Error detecting collisions: {0}", e.Message);
            }
        }
    
        private static IEnumerable<MethodBase> FindExtensionMethods
            (Assembly assembly)
        {
            return from type in assembly.GetTypes()
                   from method in type.GetMethods(BindingFlags.Static |
                                                  BindingFlags.Public |
                                                  BindingFlags.NonPublic)
                   where method.IsDefined(typeof(ExtensionAttribute), false)
                   select method;
        }
    
    
        private static void DetectCollisions(MethodBase method)
        {
            Console.WriteLine("  Testing {0}.{1}", 
                              method.DeclaringType.Name, method.Name);
            Type extendedType = method.GetParameters()[0].ParameterType;
            foreach (var type in GetTypeAndAncestors(extendedType).Distinct())
            {
                foreach (var collision in DetectCollidingMethods(method, type))
                {
                    Console.WriteLine("    Possible collision in {0}: {1}",
                                      collision.DeclaringType.Name, collision);
                }
            }
        }
    
        private static IEnumerable<Type> GetTypeAndAncestors(Type type)
        {
            yield return type;
            if (type.BaseType != null)
            {
                // I want yield foreach!
                foreach (var t in GetTypeAndAncestors(type.BaseType))
                {
                    yield return t;
                }
            }
            foreach (var t in type.GetInterfaces()
                                  .SelectMany(iface => GetTypeAndAncestors(iface)))
            {
                yield return t;
            }        
        }
    
        private static IEnumerable<MethodBase>
            DetectCollidingMethods(MethodBase extensionMethod, Type type)
        {
            // Very, very crude to start with
            return type.GetMethods(BindingFlags.Instance |
                                   BindingFlags.Public |
                                   BindingFlags.NonPublic)
                       .Where(candidate => candidate.Name == extensionMethod.Name);
        }
    }
    

    【讨论】:

    • @Dan:问题是编译器没有给出任何警告。它只是很高兴地让您的代码调用第三方库而不是扩展方法
    • @David:关键是其他人正在破坏你的课程。例如,假设我在Stream 上有一个CopyTo 扩展方法。这在 .NET 3.5 中是完全合理的 - 但在 .NET 4.0 中没有用。我的方法将不再被调用。现在,您是说我应该检查 .NET 4.0 中引入的每一个新方法,看看它是否会破坏我的扩展方法? 我的 代码没有改变...但它的行为已经改变。 sealed 在这里无关紧要。我以什么方式射中了自己的脚?
    • @Eric:我认为关闭警告只是#pragma 的情况,就像任何其他警告(在声明站点)一样。这是残酷的,但它不太可能被忽略,IMO :) “更好的匹配”问题是一个更有趣的问题......但我不确定情况是否完全相同,因为那是 你 i> 在已经是基类之一的方法之后添加方法。那是你的选择。在这种情况下,它是其他人的类的变化,它正在改变你的代码的行为。这就是为什么它更难拿起。或者我可能遗漏了一些关于你的场景的东西。
    • @Eric - 我会通过删除最近过时的扩展方法(如果行为相同)或使用新名称重构它(如果行为不同)来关闭警告。这听起来可能很激烈,但毕竟它是一个扩展方法,不能再被称为扩展方法。这对于保持旧代码保持原样是一个非常严重的标志。
    • @Eric:在这种假设的情况下,我们是否仍在构建这段我们不拥有的代码?如果是这样的话,我们对 any 警告是否也有同样的问题?例如,假设我们收到“添加新的或覆盖”警告..​​....我们的情况不完全一样吗?
    【解决方案2】:

    减少发生这种情况的一种可能方法是在扩展方法名称的末尾包含缩写 Ext。我知道,这非常丑陋,但它减少了发生这种情况的机会。

    MyMethod_Ext
    

    MyMethodExt
    

    【讨论】:

    • 是的,很丑。它几乎和调用扩展类本身一样难看:ThirdPartyExtensions.MyMethod(instance)
    • 是的,非常非常难看。我只是想我会把它放在那里作为一种可能性。首先发生这种情况的可能性非常低。这只会进一步减少机会。我喜欢 Jon Skeet 关于检查这些可能性的实用程序的想法。
    【解决方案3】:

    我喜欢 Jon 的回答,但还有另一种类似于 Daniel 的方法。如果你有很多扩展方法,你可以定义一个“命名空间”。如果您有一个稳定的界面可供使用(即,如果您知道 IThirdParty 不会改变),这将最有效。但是,在您的情况下,您需要一个包装类。

    我这样做是为了添加将字符串视为文件路径的方法。我定义了一个FileSystemPath 类型,它包装了一个string,并提供了IsAbsoluteChangeExtension 等属性和方法。

    在定义“扩展命名空间”时,您需要提供一种进入和离开的方式,例如:

    // Enter my special namespace
    public static MyThirdParty AsMyThirdParty(this ThirdParty source) { ... }
    
    // Leave my special namespace
    public static ThirdParty AsThirdParty(this MyThirdParty source) { ... }
    

    “离开”“命名空间”的方法作为实例方法而不是扩展方法可能效果更好。我的FileSystemPath 只是隐式转换为string,但这并不适用于所有情况。

    如果您希望 MyThirdParty 拥有 ThirdParty 的所有当前定义成员以及扩展方法(但不是 ThirdParty 的未来定义成员),那么您必须将成员实现转发到包装的ThirdParty 对象。这可能很乏味,但像 ReSharper 这样的工具可以半自动完成。

    最后提示:进入/离开命名空间的“As”前缀是一种不言而喻的准则。 LINQ 使用这个系统(例如,AsEnumerableAsQueryableAsParallel 离开当前的“命名空间”并输入另一个)。

    我在今年年初写了a blog post 我所谓的“基于扩展的类型”。除了无法覆盖实例方法之外,还有更多的陷阱。然而,这是一个非常有趣的概念。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-01
      • 1970-01-01
      相关资源
      最近更新 更多