C#では、リフレクションを使用して、メソッドが拡張メソッドとしてクラスに追加されているかどうかを判断する手法がありますか?
以下に示すような拡張メソッドが与えられた場合、Reverse()が文字列クラスに追加されたことを判断できますか?
public static class StringExtensions
{
public static string Reverse(this string value)
{
char[] cArray = value.ToCharArray();
Array.Reverse(cArray);
return new string(cArray);
}
}
拡張メソッドが開発者によって適切に追加されたことを単体テストで判断するメカニズムを探しています。これを試みる理由の1つは、開発者によって実際のクラスに同様のメソッドが追加される可能性があり、その場合、コンパイラがそのメソッドを選択する可能性があることです。
拡張メソッドを定義できるすべてのアセンブリを調べる必要があります。
ExtensionAttribute
で装飾されたクラスを探し、そのクラス内でExtensionAttribute
で装飾されたalsoメソッドを探します。次に、最初のパラメーターの型を確認して、目的の型と一致するかどうかを確認します。
完全なコードを次に示します。より厳密な場合もあります(型がネストされていないことや、少なくとも1つのパラメーターがあることはチェックしません)が、助けになるはずです。
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;
}
}
John Skeetの回答に基づいて、System.Type-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をメソッドで拡張し、すべてのメソッド(拡張されたメソッドを持つ「通常の」メソッドも)を返すことです。
これは、一般的なものを含む、特定のタイプで定義されたすべての拡張メソッドのリストを返します。
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;
}
これには1つだけ問題があります。返されるTypeは、一般的なパラメーター型であるため、typeof(..)で期待するものとは異なります。特定の型のすべての拡張メソッドを見つけるには、次のようなすべての基本型とTypeのインターフェイスの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>();
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; }
}
ジョンがつやを消した点を明確にするために...拡張メソッドをクラスに「追加」しても、クラスはまったく変更されません。 C#コンパイラーによって実行されるほんの少しのスピンです。
したがって、例を使用して、次のように記述できます。
string rev = myStr.Reverse();
しかし、アセンブリに書き込まれたMSILは、まさにあなたがそれを書いたかのようになります。
string rev = StringExtensions.Reverse(myStr);
コンパイラは、単にあなたをだまして、Stringのメソッドを呼び出していると思わせるだけです。
これを試みる理由の1つは、開発者によって実際のクラスに同様のメソッドが追加される可能性があり、その場合、コンパイラがそのメソッドを選択する可能性があることです。
その時点で古いFooメソッドを呼び出す唯一の方法は次のとおりです。
CustomerExtension.Foo(myCustomer);
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;
}
}
出力:
False
本当だ