Методы расширения, переопределенные классом, не дают предупреждения - PullRequest
19 голосов
/ 25 июня 2010

У меня была дискуссия в другом потоке, и я обнаружил, что методы класса имеют приоритет над методами расширения с тем же именем и параметрами. Это хорошо, поскольку методы расширения не будут перехватывать методы, но предполагается, что вы добавили некоторые методы расширения в стороннюю библиотеку:

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 -> «Сторонний метод»

Теперь неожиданно код будет вести себя по-другому во время выполнения, так как сторонний метод «похитил» ваш метод расширения! Компилятор не выдает никаких предупреждений.

Есть ли способ включить такие предупреждения или иным образом избежать этого?

Ответы [ 3 ]

10 голосов
/ 25 июня 2010

Нет - это известный недостаток методов расширения, с которым нужно быть очень осторожным. Лично я хотел бы, чтобы компилятор 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);
    }
}
2 голосов
/ 25 июня 2010

Мне нравится ответ Джона, но есть другой подход, похожий на подход Дэниела.Если у вас много методов расширения, вы можете определить «пространство имен».Это работает лучше всего, если у вас есть стабильный интерфейс для работы (т. Е. Если вы знали, что 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) { ... }

Метод «выхода» из «пространства имен» может работать лучше как метод экземпляра, а не как метод расширения.Мой FileSystemPath просто имеет неявное преобразование в string, но это работает не во всех случаях.

Если вы хотите, чтобы MyThirdParty имел все определенные в настоящее время члены ThirdParty, а такжеметоды расширения (но не определяемые в будущем членами ThirdParty), тогда вам придется перенаправлять реализации элементов в обернутый объект ThirdParty.Это может быть утомительно, но такие инструменты, как ReSharper, могут делать это полуавтоматически.

Последнее замечание: префикс «As» при входе / выходе из пространства имен является своего рода невысказанным руководством.LINQ использует эту систему (например, AsEnumerable, AsQueryable, AsParallel покидают текущее «пространство имен» и вводят другое).

Я написал пост в блоге в начале этого годао том, что я называю «типы на основе расширений».Есть больше подводных камней, чем просто невозможность переопределить методы экземпляра.Тем не менее, это очень интересная концепция.

0 голосов
/ 25 июня 2010

Один из возможных способов уменьшить вероятность этого - включить сокращение Ext в конец имени метода расширения.Я знаю, это очень ужасно, но это уменьшает шансы на это.

MyMethod_Ext

или

MyMethodExt
...