被类覆盖的扩展方法不会给出警告

发布于 2024-09-07 04:07:51 字数 835 浏览 7 评论 0原文

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

public class ThirdParty
{
}

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

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

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

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

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

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

I had a discussion in another thread, and found out that class methods takes precedence over extension methods with the same name and parameters. This is good as extension methods won't hijack methods, but assume you have added some extension methods to a third party library:

public class ThirdParty
{
}

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

Works as expected: ThirdParty.MyMethod -> "My extension method"

But then ThirdParty updates it's library and adds a method exactly like your extension 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 -> "Third party method"

Now suddenly code will behave different at runtime as the third party method has "hijacked" your extension method! The compiler doesn't give any warnings.

Is there a way to enable such warnings or otherwise avoid this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

归途 2024-09-14 04:08:07

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

MyMethod_Ext

或者

MyMethodExt

One possible way to reduce the chance of this happening is to include the abbreviation Ext at the end of the extension method name. I know, this is very ugly, but it reduces the chances of this happening.

MyMethod_Ext

or

MyMethodExt
断肠人 2024-09-14 04:08:04

我喜欢乔恩的回答,但还有另一种类似于丹尼尔的方法。如果您有很多扩展方法,您可以定义某种“命名空间”。如果您有一个稳定的工作界面(即,如果您知道 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 离开当前“命名空间”并输入另一个)。

今年年初,我写了一篇博客文章,内容涉及我所说的内容“基于扩展的类型。”除了无法重写实例方法之外,还有更多的陷阱。然而,这是一个非常有趣的概念。

I like Jon's answer, but there's another approach similar to Daniel's. If you have a lot of extension methods, you can define a "namespace" of sorts. This works best if you have a stable interface to work from (i.e., if you knew IThirdParty wasn't going to change). In your case, however, you'd need a wrapper class.

I've done this to add methods for treating strings as file paths. I defined a FileSystemPath type that wraps a string and provides properties and methods such as IsAbsolute and ChangeExtension.

When defining an "extension namespace," you need to provide a way to enter it and a way to leave it, as such:

// 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. My FileSystemPath just has an implicit conversion to string, but this does not work in all cases.

If you want MyThirdParty to have all the currently-defined members of ThirdParty as well as the extension methods (but not future-defined members of ThirdParty), then you'd have to forward member implementations to the wrapped ThirdParty object. This can be tedious, but tools like ReSharper can do it semi-automatically.

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 leave the current "namespace" and enter another one).

I wrote a blog post early this year about what I call "extension-based types." There are more pitfalls than just the inability to override instance methods. However, it is a very interesting concept.

紫瑟鸿黎 2024-09-14 04:08:01

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

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

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

无论如何,希望它对某个地方的某人有用。

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

Nope - this is a known downside of extension methods, and something to be very careful about. Personally I wish that the C# compiler would warn you if you declared an extension method which would never be called other than via the normal static route (ExtensionClassName.MethodName(target, ...)).

It probably wouldn't be too hard to write a little tool to examine all the extension methods in an assembly and issue warnings that way. It probably wouldn't need to be very precise to start with: just warning if there's already a method with the same name (without worrying about parameter types) would be a good start.

EDIT: Okay... here's a very crude tool to at least give a starting point. It appears to work at least to some extent with generic types - but it's not trying to do anything with parameter types or names... partly because that becomes tricky with parameter arrays. It also loads assemblies "fully" instead of with reflection only, which would be nicer - I tried the "proper" route, but ran into some problems which weren't immediately trivial to resolve, so fell back to the quick and dirty route :)

Anyway, hopefully it'll be useful to someone, somewhere.

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);
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文