代码之家  ›  专栏  ›  技术社区  ›  simendsjo

类重写的扩展方法未给出警告

  •  19
  • simendsjo  · 技术社区  · 14 年前

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

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

    按预期工作:thirdparty.my method->“我的扩展方法”

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

    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—>“第三方方法”

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

    是否有办法启用此类警告或避免此类警告?

    3 回复  |  直到 6 年前
        1
  •  10
  •   Jon Skeet    14 年前

    ExtensionClassName.MethodName(target, ...) )

    非常 精确开始:如果已经有一个同名的方法(不用担心参数类型),那么警告就是一个好的开始。

    编辑:好的…这里有一个 非常粗糙的 tool to at least give a starting point. 它看起来至少能起作用 一些 extent with generic types - but it's not trying to do anything with parameter types or names... 部分原因是参数数组变得很难处理。它还“完全”加载程序集,而不是只使用反射,这会更好-我尝试了“正确”的路径,但遇到了一些问题,这些问题不是立即解决的,因此返回到快速和肮脏的路径:)

    不管怎样,希望它在某个地方对某人有用。

    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);
        }
    }
    
        2
  •  2
  •   Stephen Cleary    14 年前

    我喜欢乔恩的回答,但还有另一种方法类似于丹尼尔的。如果你有很多扩展方法,你可以定义一个类似的“名称空间”。如果你有一个稳定的工作界面(例如,如果你知道 IThirdParty 不会改变的。然而,在您的案例中,您需要一个包装类。

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

    定义“扩展名称空间”时,需要提供一种输入它的方法和一种离开它的方法,例如:

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

    The method to "leave" the "namespace" may work better as an instance method instead of an extension method. 我的 文件路径 只是隐式转换为 一串 但这并不适用于所有情况。

    如果你想要 MyThirdParty 使当前定义的所有成员 ThirdParty 以及扩展方法(但不是将来定义的 第三方 ,然后您必须将成员实现转发到 第三方 对象。这可能很乏味,但像Resharper这样的工具可以半自动完成。

    Final Note: the "As" prefix on entering/leaving the namespace is a sort of unspoken guideline. LINQ uses this system (e.g., AsEnumerable , AsQueryable , AsParallel 离开当前的“名称空间”并输入另一个名称空间)。

    我写 a blog post 今年年初,关于我所说的“基于扩展的类型”,有更多的陷阱,而不仅仅是无法重写实例方法。然而,这是一个非常有趣的概念。

        3
  •  0
  •   Daniel Dyson    14 年前

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

    MyMethod_Ext
    

    MyMethodExt