Skip to content

Instantly share code, notes, and snippets.

@JohannesMP
Last active July 16, 2018 17:52
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JohannesMP/10da899192d8d32216e7f7880a785a85 to your computer and use it in GitHub Desktop.
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.
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); }
}
}
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