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

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

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

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

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

19
задан Chenmunka 18 July 2018 в 15:59
поделиться

3 ответа

Нет - это известный недостаток методов расширения, с которым нужно быть очень осторожным. Лично мне хотелось бы, чтобы компилятор 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);
    }
}
10
ответ дан 30 November 2019 в 05:12
поделиться

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

MyMethod_Ext

или

MyMethodExt
0
ответ дан 30 November 2019 в 05:12
поделиться

Мне нравится ответ Джона, но есть другой подход, похожий на подход Дэниела. Если у вас много методов расширения, вы можете определить своего рода «пространство имен». Это лучше всего работает, если у вас есть стабильный интерфейс для работы (например, если вы знали, что IThirdParty не изменится). Однако в вашем случае вам понадобится класс-оболочка.

Я сделал это, чтобы добавить методы обработки строк как путей к файлам. Я определил тип FileSystemPath , который оборачивает строку и предоставляет такие свойства и методы, как 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 просто имеет неявное преобразование в строку , но это работает не во всех случаях.

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

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

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

2
ответ дан 30 November 2019 в 05:12
поделиться
Другие вопросы по тегам:

Похожие вопросы: