Last active
July 16, 2018 17:52
-
-
Save JohannesMP/10da899192d8d32216e7f7880a785a85 to your computer and use it in GitHub Desktop.
Conveniently extracting data of members on an object that were tagged with a given Attribute, with Reflection caching.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
using UnityEditor; | |
using Utils.Reflection; | |
using System.Linq; | |
public class AttributeDemo | |
{ | |
/// <summary> | |
/// A quick demo for grabing fields on an object that were tagged with a given Attribute. | |
/// In this case an ‘Order’ attribute is used to display the tagged fields in a specific order. | |
/// </summary> | |
[MenuItem("Test/Test Attribute...")] | |
static void RunDemo() | |
{ | |
// Several fields in this object are marked with the 'Order' attribute. | |
// We want to see what the values of those marked fields are right now | |
DemoClass instance = new DemoClass(); | |
// This helper uses lazy reflection caching to quickly extract this information | |
var filteredMembers = AttributeFilter.GetMembersWithAttribute<OrderAttribute>(instance); | |
// Each member that was marked with 'Order' is now printed, ordered based on the value specified in the attribute. | |
foreach (var member in filteredMembers.OrderBy(m => m.attribute.index)) | |
{ | |
Debug.LogFormat("[{0}] {1} = {2}", member.attribute, member.name, member.value); | |
} | |
} | |
/// <summary> | |
/// Example of a class with members marked by tags, in this case indicating their respective order when printed | |
/// </summary> | |
class DemoClass | |
{ | |
public enum DemoEnum { A, B, C }; | |
[Order(0)] | |
public int fieldYes = 1; | |
public int fieldNo = 5; | |
[Order(20)] | |
public static int fieldStaticYes = 2; | |
public static int fieldStaticNo = 123; | |
[Order(1)] | |
public const int fieldConstYes = 111; | |
public const int fieldConstNo = -123; | |
[Order(3)] | |
public string propertyYes { get { return "prop: " + fieldYes; } } | |
public string propertyNo { get { return "prop: " + fieldNo; } } | |
[Order(4)] | |
public DemoEnum enumYes = DemoEnum.A; | |
public DemoEnum enumNo = DemoEnum.C; | |
[Order(-5)] | |
public string FuncMemberYes() { return "method: " + fieldYes; } | |
public string FuncMemberNo() { return "method: " + fieldYes; } | |
[Order(62)] | |
public static string FuncStaticYes() { return "static method: " + fieldStaticYes; } | |
public static string FuncStaticNo() { return "static method: " + fieldStaticYes; } | |
[Order(-39)] | |
public string FuncDefaults(string a = "one", string b = "two") { return "method defaults: " + a + ", " + b; } | |
[Order(-38)] | |
public static string FuncStaticDefaults(string a = "red", string b = "blue") { return "static method defaults: " + a + ", " + b; } | |
[Order(700)] | |
public DemoClass refSelfYes; | |
[Order(8)] | |
public DemoClass RefSelfPropertyYes | |
{ | |
get { return refSelfYes; } | |
} | |
public DemoClass() { refSelfYes = this; } | |
} | |
/// <summary> | |
/// An example Attribute used to mark Members of a class for extraction with some metadata | |
/// </summary> | |
[System.AttributeUsage(validOn: System.AttributeTargets.Field | System.AttributeTargets.Property | System.AttributeTargets.Method, | |
Inherited = true, AllowMultiple = false)] | |
class OrderAttribute : System.Attribute | |
{ | |
public int index { get; private set; } | |
public OrderAttribute(int orderIndex) { this.index = orderIndex; } | |
public override string ToString() { return string.Format("OrderAttribute({0})", index); } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using System.Reflection; | |
namespace Utils.Reflection | |
{ | |
/// <summary> | |
/// Extract members of an object that are marked with a given attribute | |
/// </summary> | |
/// Attempts to speed up reflection with lazy caching | |
/// | |
/// Written by JohannesMP 2018-07-11, released under the Public Domain - No Rights Reserved. | |
/// | |
public static class AttributeFilter | |
{ | |
/// <summary> | |
/// For a member of a class instance that was tagged with an attribute, | |
/// this contains the attribute, member name and value of the member on the instance | |
/// </summary> | |
public struct MemberWithAttribute<TAttrib> | |
{ | |
/// <summary> | |
/// The attribute object this member was tagged with | |
/// </summary> | |
public TAttrib attribute; | |
/// <summary> | |
/// The name of the member field itself | |
/// </summary> | |
public string name; | |
/// <summary> | |
/// The value that the member on the given instance had | |
/// </summary> | |
public object value; | |
} | |
/// <summary> | |
/// The parts of member info we care about | |
/// </summary> | |
private struct CachedMemberData | |
{ | |
readonly public MemberInfo memberInfo; | |
readonly public System.Attribute attribData; | |
readonly public object[] defaultArgs; // Only used if memberInfo is a method that takes default args | |
public CachedMemberData(MemberInfo member, System.Attribute attrib, object[] args) | |
{ | |
memberInfo = member; | |
attribData = attrib; | |
defaultArgs = args; | |
} | |
} | |
/// <summary> | |
/// Reflection is expensive and slow, so we cache it if possible | |
/// </summary> | |
private static Dictionary<int, CachedMemberData[]> TypeMemberInfoCache = new Dictionary<int, CachedMemberData[]>(); | |
/// <summary> | |
/// A quick Berstein hash for arbitrary numbers of objects. Any standard hashing function would do. | |
/// </summary> | |
private static int QuickHash(params object[] objs) | |
{ | |
int hash = 17; | |
foreach (object obj in objs) | |
hash = hash * 31 + obj.GetHashCode(); | |
return hash; | |
} | |
/// <summary> | |
/// For all members in the instance's type marked with TAttrib, evaluate the name/value/attribute of those members. | |
/// - Members can be Fields, Properties and Methods without arguments that return a value (like System.Action) | |
/// </summary> | |
public static MemberWithAttribute<TAttrib>[] GetMembersWithAttribute<TAttrib>( | |
object instance, | |
BindingFlags bindingAttr = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static, | |
bool inherit = true | |
) where TAttrib : System.Attribute | |
{ | |
if (instance == null) | |
return null; | |
// 1. Identify the members of the instance's Type that have the attribute | |
// We check if we've already cached this type information (Reflection is slow) | |
int typeMemberHash = QuickHash(instance.GetType(), typeof(TAttrib), bindingAttr, inherit); | |
CachedMemberData[] memberCache; | |
// Generate attribute type info for cache if it wasn't cached yet | |
if (!TypeMemberInfoCache.TryGetValue(typeMemberHash, out memberCache)) | |
{ | |
List<CachedMemberData> toCache = new List<CachedMemberData>(); | |
foreach (MemberInfo memberInfo in instance.GetType().GetMembers(bindingAttr)) | |
{ | |
object[] methodArgs = null; | |
switch (memberInfo.MemberType) | |
{ | |
// Accept all Fields and Properties | |
case MemberTypes.Field: | |
case MemberTypes.Property: | |
break; | |
// Check Methods | |
case MemberTypes.Method: | |
MethodInfo methodInfo = (MethodInfo)memberInfo; | |
// Reject if not callable or no return | |
if (methodInfo.IsAbstract || methodInfo.ReturnType == typeof(void)) | |
continue; | |
// Reject if cannot call with only default arguments | |
ParameterInfo[] paramInfos = methodInfo.GetParameters(); | |
int paramCount = paramInfos.Length; | |
if (paramCount != 0 && paramInfos[0].IsOptional == false) | |
continue; | |
// It's a method with only optional args (or none). We construct the args to use when calling later | |
methodArgs = new object[paramCount]; | |
for (int i = 0; i < paramCount; ++i) | |
methodArgs[i] = paramInfos[i].DefaultValue; | |
// Accept | |
break; | |
// Skip otherwise | |
default: | |
continue; | |
} | |
object[] attribs = memberInfo.GetCustomAttributes(typeof(TAttrib), inherit); | |
// Take only the first insance of an attribute (the return container expects only one attribute per member) | |
if (attribs.Length != 0) | |
toCache.Add(new CachedMemberData(memberInfo, (System.Attribute)attribs[0], methodArgs)); | |
} | |
memberCache = toCache.ToArray(); | |
// Cache the type | |
TypeMemberInfoCache[typeMemberHash] = memberCache; | |
} | |
// 2. Extract the corresponding member data from the instance using the information about the object's type. | |
List<MemberWithAttribute<TAttrib>> ret = new List<MemberWithAttribute<TAttrib>>(); | |
foreach (CachedMemberData data in memberCache) | |
{ | |
object value = null; | |
switch (data.memberInfo.MemberType) | |
{ | |
case MemberTypes.Field: | |
value = ((FieldInfo)data.memberInfo).GetValue(instance); | |
break; | |
case MemberTypes.Property: | |
value = ((PropertyInfo)data.memberInfo).GetValue(instance, null); | |
break; | |
case MemberTypes.Method: | |
MethodInfo methodInfo = ((MethodInfo)data.memberInfo); | |
value = methodInfo.Invoke(methodInfo.IsStatic ? null : instance, data.defaultArgs); | |
break; | |
// Skip | |
default: | |
continue; | |
} | |
ret.Add(new MemberWithAttribute<TAttrib> | |
{ | |
attribute = (TAttrib)data.attribData, | |
name = data.memberInfo.Name, | |
value = value | |
}); | |
} | |
return ret.ToArray(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment