Подтвердить что ты не робот

Методы расширения, переопределенные классом, не дают предупреждения

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

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

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

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

4b9b3361

Ответ 1

Нет - это известный недостаток методов расширения и что-то очень осторожное. Лично мне жаль, что компилятор С# не предупредил вас, если бы вы объявили метод расширения, который никогда не будет вызываться иначе, чем через обычный статический маршрут (ExtensionClassName.MethodName(target, ...)).

Вероятно, было бы не слишком сложно написать небольшой инструмент для изучения всех методов расширения в сборке и выдачи предупреждений таким образом. Вероятно, не нужно было бы очень точно начинать с: просто предупреждение, если бы уже был метод с тем же именем (без беспокойства о типах параметров), было бы хорошим началом.

EDIT: Хорошо... здесь очень грубый инструмент, чтобы хотя бы дать отправную точку. Похоже, что он работает, по крайней мере, в некоторой степени с помощью общих типов, но он не пытается ничего делать с типами параметров или именами... отчасти потому, что это становится сложным с массивами параметров. Он также загружает сборки "полностью", а не только с отражением, что было бы лучше - я пробовал "правильный" маршрут, но столкнулся с некоторыми проблемами, которые не были сразу тривиальны для решения, поэтому отступили к быстрому и грязному маршруту: )

В любом случае, надеюсь, кому-то это будет полезно.

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

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

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

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

Ответ 3

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

MyMethod_Ext

или

MyMethodExt