Роздуми щодо визначення методів розширення


77

У C # існує техніка, що використовує відображення, щоб визначити, чи метод був доданий до класу як метод розширення?

З огляду на такий метод розширення, як показано нижче, чи можна визначити, що Reverse () було додано до класу рядків?

public static class StringExtensions
{
    public static string Reverse(this string value)
    {
        char[] cArray = value.ToCharArray();
        Array.Reverse(cArray);
        return new string(cArray);
    }
}

Ми шукаємо механізм, щоб визначити в модульному тестуванні, що метод розширення був належним чином доданий розробником. Однією з причин цього є те, що можливо, що подібний метод буде доданий розробником до фактичного класу, і, якщо він був, компілятор підбере цей метод.

Відповіді:


117

Ви повинні шукати всі збірки, де може бути визначений метод розширення.

Шукайте класи, прикрашені ExtensionAttribute, а потім методи в цьому класі, які також прикрашені ExtensionAttribute. Потім перевірте тип першого параметра, щоб перевірити, чи відповідає він типу, який вас цікавить.

Ось деякий повний код. Це може бути більш суворим (це не перевірка того, чи тип не вкладений, чи є хоча б один параметр), але він повинен допомогти вам.

using System;
using System.Runtime.CompilerServices;
using System.Reflection;
using System.Linq;
using System.Collections.Generic;

public static class FirstExtensions
{
    public static void Foo(this string x) {}
    public static void Bar(string x) {} // Not an ext. method
    public static void Baz(this int x) {} // Not on string
}

public static class SecondExtensions
{
    public static void Quux(this string x) {}
}

public class Test
{
    static void Main()
    {
        Assembly thisAssembly = typeof(Test).Assembly;
        foreach (MethodInfo method in GetExtensionMethods(thisAssembly,
            typeof(string)))
        {
            Console.WriteLine(method);
        }
    }

    static IEnumerable<MethodInfo> GetExtensionMethods(Assembly assembly,
        Type extendedType)
    {
        var query = from type in assembly.GetTypes()
                    where type.IsSealed && !type.IsGenericType && !type.IsNested
                    from method in type.GetMethods(BindingFlags.Static
                        | BindingFlags.Public | BindingFlags.NonPublic)
                    where method.IsDefined(typeof(ExtensionAttribute), false)
                    where method.GetParameters()[0].ParameterType == extendedType
                    select method;
        return query;
    }
}

5
Хороший код. Ви можете виключити купу методів, використовуючи той факт, що: Методи розширення повинні бути визначені в не-загальному статичному класі. де! type.IsGenericType && type.IsSeated
Емі Б

Правда, це досить простий тест. Шкода, що немає еквівалента IsDefined для Type :) Я відредагую відповідь цим кодом.
Джон Скіт,

Не працює, якщо розширений тип є загальним. Методи розширення для IQueryable збігатимуться, але не для IQueryable <>, наприклад. Я думаю, це не вдається на ParameterType.
Себ Нільссон,

4
@Seb: Так, потрібно було б значно більше зусиль, щоб це працювало для загальних методів. Це можливо, але складно.
Джон Скіт,

1
@JonSkeet hmmm Я розумію. Але тоді це залишає можливість, що кожен може застосувати [Extension]атрибут до методу в статичному не-загальному класі верхнього рівня і все одно не бути методом розширення :) Чи не так?
nawfal

12

На основі відповіді Джона Скіта я створив власне розширення типу System.Type.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace System
{
    public static class TypeExtension
    {
        /// <summary>
        /// This Methode extends the System.Type-type to get all extended methods. It searches hereby in all assemblies which are known by the current AppDomain.
        /// </summary>
        /// <remarks>
        /// Insired by Jon Skeet from his answer on http://stackoverflow.com/questions/299515/c-sharp-reflection-to-identify-extension-methods
        /// </remarks>
        /// <returns>returns MethodInfo[] with the extended Method</returns>

        public static MethodInfo[] GetExtensionMethods(this Type t)
        {
            List<Type> AssTypes = new List<Type>();

            foreach (Assembly item in AppDomain.CurrentDomain.GetAssemblies())
            {
                AssTypes.AddRange(item.GetTypes());
            }

            var query = from type in AssTypes
                where type.IsSealed && !type.IsGenericType && !type.IsNested
                from method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
                where method.IsDefined(typeof(ExtensionAttribute), false)
                where method.GetParameters()[0].ParameterType == t
                select method;
            return query.ToArray<MethodInfo>();
        }

        /// <summary>
        /// Extends the System.Type-type to search for a given extended MethodeName.
        /// </summary>
        /// <param name="MethodeName">Name of the Methode</param>
        /// <returns>the found Methode or null</returns>
        public static MethodInfo GetExtensionMethod(this Type t, string MethodeName)
        {
            var mi = from methode in t.GetExtensionMethods()
                where methode.Name == MethodeName
                select methode;
            if (mi.Count<MethodInfo>() <= 0)
                return null;
            else
                return mi.First<MethodInfo>();
        }
    }
}

Він отримує всі збірки з поточного AppDomain та шукає розширені методи.

Використання:

Type t = typeof(Type);
MethodInfo[] extendedMethods = t.GetExtensionMethods();
MethodInfo extendedMethodInfo = t.GetExtensionMethod("GetExtensionMethods");

Наступним кроком буде розширення System.Type методами, що повертає всі Методи (також "звичайні" з розширеними).


5

Це поверне список усіх методів розширення, визначених для певного типу, включаючи загальні:

public static IEnumerable<KeyValuePair<Type, MethodInfo>> GetExtensionMethodsDefinedInType(this Type t)
{
    if (!t.IsSealed || t.IsGenericType || t.IsNested)
        return Enumerable.Empty<KeyValuePair<Type, MethodInfo>>();

    var methods = t.GetMethods(BindingFlags.Public | BindingFlags.Static)
                   .Where(m => m.IsDefined(typeof(ExtensionAttribute), false));

    List<KeyValuePair<Type, MethodInfo>> pairs = new List<KeyValuePair<Type, MethodInfo>>();
    foreach (var m in methods)
    {
        var parameters = m.GetParameters();
        if (parameters.Length > 0)
        {
            if (parameters[0].ParameterType.IsGenericParameter)
            {
                if (m.ContainsGenericParameters)
                {
                    var genericParameters = m.GetGenericArguments();
                    Type genericParam = genericParameters[parameters[0].ParameterType.GenericParameterPosition];
                    foreach (var constraint in genericParam.GetGenericParameterConstraints())
                        pairs.Add(new KeyValuePair<Type, MethodInfo>(parameters[0].ParameterType, m));
                }
            }
            else
                pairs.Add(new KeyValuePair<Type, MethodInfo>(parameters[0].ParameterType, m));
        }
    }

    return pairs;
}

З цим є лише одна проблема: Повернутий тип не той, який ви очікували б від typeof (..), оскільки це загальний тип параметра. Для того, щоб знайти всі методи розширення для даного типу, вам доведеться порівняти GUID усіх базових типів та інтерфейсів типу, наприклад:

public List<MethodInfo> GetExtensionMethodsOf(Type t)
{
    List<MethodInfo> methods = new List<MethodInfo>();
    Type cur = t;
    while (cur != null)
    {

        TypeInfo tInfo;
        if (typeInfo.TryGetValue(cur.GUID, out tInfo))
            methods.AddRange(tInfo.ExtensionMethods);


        foreach (var iface in cur.GetInterfaces())
        {
            if (typeInfo.TryGetValue(iface.GUID, out tInfo))
                methods.AddRange(tInfo.ExtensionMethods);
        }

        cur = cur.BaseType;
    }
    return methods;
}

Щоб бути повним:

Я зберігаю словник інформаційних об’єктів типу, який будую під час ітерації всіх типів усіх збірок:

private Dictionary<Guid, TypeInfo> typeInfo = new Dictionary<Guid, TypeInfo>();

де the TypeInfoвизначається як:

public class TypeInfo
{
    public TypeInfo()
    {
        ExtensionMethods = new List<MethodInfo>();
    }

    public List<ConstructorInfo> Constructors { get; set; }

    public List<FieldInfo> Fields { get; set; }
    public List<PropertyInfo> Properties { get; set; }
    public List<MethodInfo> Methods { get; set; }

    public List<MethodInfo> ExtensionMethods { get; set; }
}

У теперішньому вигляді ваш код додає кілька однакових KVP для декількох обмежень, і "очевидне" виправлення (що насправді є більш правильним для одинарних обмежень) додавання (constraint, m}не є правильним, оскільки обмеження "і" не "або".
Марк Херд,

Насправді, з точки зору компілятора, оскільки він не розглядає обмеження як диференціатори, поточний код має певні переваги: ​​просто опустіть лінію обмеження foreach. Але це не допомагає нам застосовувати методи розширення за допомогою роздумів :-(
Марк Херд,

3

Щоб пояснити питання, яке Джон промовив ... "Додавання" методу розширення до класу жодним чином не змінює клас. Це лише трохи обертання, яке виконує компілятор C #.

Отже, на вашому прикладі ви можете писати

string rev = myStr.Reverse();

але MSIL, записаний до збірки, буде точно таким, як якщо б ви його написали:

string rev = StringExtensions.Reverse(myStr);

Компілятор просто дозволяє вам обдурити себе, думаючи, що ви викликаєте метод String.


3
Так. Я цілком усвідомлюю, що компілятор працює з деякою "магією", щоб приховати деталі. Це одна з причин, через яку ми зацікавлені виявити в модульному тесті, чи є метод розширенням чи ні.
Mike Chess

2

Однією з причин цього є те, що можливо, що подібний метод буде доданий розробником до фактичного класу, і, якщо він був, компілятор підбере цей метод.

  • Припустимо, визначено метод розширення void Foo (цей Клієнт someCustomer) .
  • Припустимо, також, що Клієнт змінено і додано метод void Foo () .
  • Потім новий метод у Замовника охоплює / приховує метод розширення.

Єдиний спосіб викликати старий метод Foo в цей момент:

CustomerExtension.Foo(myCustomer);

0
void Main()
{
    var test = new Test();
    var testWithMethod = new TestWithExtensionMethod();
    Tools.IsExtensionMethodCall(() => test.Method()).Dump();
    Tools.IsExtensionMethodCall(() => testWithMethod.Method()).Dump();
}

public class Test 
{
    public void Method() { }
}

public class TestWithExtensionMethod
{
}

public static class Extensions
{
    public static void Method(this TestWithExtensionMethod test) { }
}

public static class Tools
{
    public static MethodInfo GetCalledMethodInfo(Expression<Action> expr)
    {
        var methodCall = expr.Body as MethodCallExpression;
        return methodCall.Method;
    }

    public static bool IsExtensionMethodCall(Expression<Action> expr)
    {
        var methodInfo = GetCalledMethodInfo(expr);
        return methodInfo.IsStatic;
    }
}

Виходи:

помилковий

Правда

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.