Reflection Helpers¶
Reflection you can afford to call every frame. ReflectionHelpers turns a FieldInfo, PropertyInfo, MethodInfo or ConstructorInfo into a delegate once, caches it, and hands you back something you can call in a loop -- instead of paying GetValue / Invoke (and a boxing allocation) on every access.
Everything below is a static member of ReflectionHelpers. The later samples omit the using directives shown above; add using System.Collections.Generic; and using UnityEngine; where the snippet needs them.
When to use it¶
Reach for it when the same member is read, written, or invoked many times: serialization, inspector and editor tooling, save systems, attribute-driven wiring. Skip it when the reflection happens once (an editor button, a one-shot import step) -- plain FieldInfo.GetValue is simpler and the delegate never pays for itself. The package uses it in Runtime/Core/Serialization/Serializer.cs, Runtime/Core/Attributes/RelationalComponentInitializer.cs and Runtime/Core/Extension/WallstopStudiosLogger.cs.
Fields¶
Use the typed overloads when you know both types at compile time -- they avoid boxing. Use the boxed overloads when the type is only known at runtime (a serializer walking arbitrary fields).
Structs need the ref setter. GetFieldSetter<TInstance, TValue> returns FieldSetter<TInstance, TValue>, a delegate whose first parameter is ref TInstance, so the write lands on your value rather than on a boxed copy:
| C# | |
|---|---|
Properties and indexers¶
Same shape as fields: boxed for runtime-typed work, typed for hot paths. Non-public accessors are supported; a property with no setter throws ArgumentException from GetPropertySetter.
Indexers take the index arguments as an object[], so one delegate serves every index:
Methods¶
GetMethodInvoker and GetStaticMethodInvoker take an object[] of arguments and work with any signature, including private methods. The typed invokers (arities 0-4) skip the array and the boxing entirely, and validate the signature when you build them.
For a single call where caching buys nothing, InvokeMethod(method, instance, parameters) and InvokeStaticMethod(method, parameters) go through the same cached invokers in one line.
For instance invokers, the receiver type may be the declaring type, a derived class, or a type implementing the declaring interface. The cache keeps each requested receiver type separate, so requesting a derived receiver first does not affect a later base or sibling receiver. Struct receivers support inherited object and interface methods; the receiver is passed by value. A broader receiver type such as object cannot invoke a method declared only on a derived class. Parameter and return types must match the method signature exactly; mismatches throw ArgumentException before a delegate enters the cache.
Typed invokers do not support ref or out parameters and throw NotSupportedException for those signatures; use the boxed invoker instead.
Constructors and factories¶
Deserializers create the same type over and over. Build the constructor delegate once:
Collections¶
When a serializer knows an element Type but not a generic parameter, these build the concrete T[], List<T>, HashSet<T> or Dictionary<TKey, TValue> without Activator.CreateInstance on every call:
CreateTypedArray<TSource>(elementType, source, count) copies the first count items of a List<TSource> into a new elementType[] -- the shape a serializer needs when it has been buffering into a pooled list. TSource is constrained to class, so the source list must hold reference types (List<int> will not compile). count is clamped to the list length, a null list or a null elementType yields an empty array, and an item that is not an instance of elementType is written as null rather than throwing.
Types and attributes¶
Scanning loaded assemblies normally means handling ReflectionTypeLoadException yourself. Every *Safe helper here swallows loader errors and returns an empty result instead of throwing, so a single bad assembly cannot take down startup.
Related helpers: GetAllLoadedTypes, GetTypesFromAssembly, GetTypesFromAssemblyName, GetComponentTypes, GetScriptableObjectTypes, GetMethodsWithAttribute, GetFieldsWithAttribute, GetMethodsWithAttributeSafe, GetPropertiesWithAttributeSafe, GetAttributeSafe, GetAllAttributesSafe, HasAnyFieldWithAttribute, HasAnyFieldWithAttributes, IsAttributeDefined, LoadStaticFieldsForType<T> and LoadStaticPropertiesForType<T>.
Component state¶
enabled lives on Behaviour, Collider and Renderer but not on Component, so generic code cannot just read it. These two extension methods handle any UnityEngine.Object and return false for a destroyed one:
| C# | |
|---|---|
API index¶
| Task | Boxed (runtime types) | Typed (compile-time types) |
|---|---|---|
| Read / write a field | GetFieldGetter, GetFieldSetter | GetFieldGetter<TInstance, TValue>, GetFieldSetter<TInstance, TValue> (ref setter) |
| Read / write a static | GetStaticFieldGetter, GetStaticFieldSetter | GetStaticFieldGetter<T>, GetStaticFieldSetter<T> |
| Read / write a property | GetPropertyGetter, GetPropertySetter | GetPropertyGetter<TInstance, TValue>, GetPropertySetter<TInstance, TValue> |
| Static property | -- | GetStaticPropertyGetter<T>, GetStaticPropertySetter<T> |
| Indexer | GetIndexerGetter, GetIndexerSetter | -- |
| Call a method | GetMethodInvoker, InvokeMethod | GetInstanceMethodInvoker<...>, GetInstanceActionInvoker<...> (arities 0-4) |
| Call a static method | GetStaticMethodInvoker, InvokeStaticMethod | GetStaticMethodInvoker<...>, GetStaticActionInvoker<...> (arities 0-4) |
| Construct | GetConstructor, GetParameterlessConstructor(Type), CreateInstance | GetParameterlessConstructor<T>, CreateInstance<T>, CreateGenericInstance<T> |
| Build a collection | CreateArray, CreateList, CreateHashSet, CreateDictionary, GetHashSetAdder, GetHashSetClearer | GetArrayCreator<T>, GetListCreator<T>, GetListWithCapacityCreator<T>, GetHashSetWithCapacityCreator<T>, GetHashSetAdder<T>, GetDictionaryCreator<TKey, TValue> |
| Find types / attributes | GetTypesDerivedFrom, GetTypesWithAttribute, TryResolveType, *Safe attribute helpers | GetTypesDerivedFrom<T>, GetTypesWithAttribute<TAttribute> |
The boxed and Type-keyed helpers cache for you: asking for GetFieldGetter(field) twice returns the same delegate. Not every typed generic overload is cached, so hold on to the delegate you get back rather than re-requesting it inside a loop.
Platform behaviour¶
The helpers pick the fastest delegate the platform allows, and the API you call never changes:
| Platform | Strategy used | Cost |
|---|---|---|
| Editor and Mono players (incl. server) | DynamicMethod IL emit, expression compile as backup | Fastest; typed paths avoid boxing entirely |
| IL2CPP (iOS, Android, console, desktop) | Cached reflection wrappers | No lookup cost; struct setters and boxed invokers box |
| WebGL | Cached reflection wrappers | Same as IL2CPP; runtime codegen is unavailable |
| Burst jobs | Not supported | Burst forbids managed reflection -- pre-bake the data |
IL emit is compiled out by #if !((UNITY_WEBGL && !UNITY_EDITOR) || ENABLE_IL2CPP), and expression compilation is additionally probed at runtime before use. A SINGLE_THREADED build swaps the concurrent caches for plain dictionaries.
Cache entries are keyed by member and strategy, so an expression-compiled delegate and an IL-emitted one never overwrite each other, and a strategy that fails for a member is remembered and skipped next time. The hooks that force a strategy (OverrideReflectionCapabilities, TryGetDelegateStrategy, ClearFieldGetterCache, ClearPropertyCache, ClearMethodCache, ClearConstructorCache) are internal test hooks, not public API.
IL2CPP/WebGL notes¶
Nothing to configure: the same calls work, they just resolve to cached reflection instead of emitted IL. Caching still removes the repeated GetField/GetMethod lookups, which is most of the win.
⚠️ IL2CPP Code Stripping Considerations¶
ReflectionHelpers is IL2CPP-safe, but Unity's managed code stripping can delete the members you are reflecting over. This affects any reflection-based code. Symptoms show up only in non-development IL2CPP builds: FieldInfo or MethodInfo comes back null, Type.GetType returns null, or you get a TypeLoadException for a type that exists in the Editor.
Preserve anything you reach by string name with a link.xml in Assets:
You do not need link.xml when the type is referenced directly in code (typeof(MyClass), a generic argument such as GetFieldGetter<MyClass, int>()), or for Unity's own built-in types.
Thread safety and pitfalls¶
Caches are concurrent dictionaries, so building and calling delegates from worker threads is safe -- except under SINGLE_THREADED, where those same caches are plain dictionaries and calls must be confined to one thread or externally synchronized.
- Passing an instance
FieldInfo/PropertyInfoto aGetStatic*helper throwsArgumentException. GetPropertySetteron a get-only property throwsArgumentException.- Writing a struct's instance field needs
GetFieldSetter<TInstance, TValue>(therefsetter); the boxed setter writes to a copy. - Typed invokers reject
ref/outparameters withNotSupportedException. - Prefer the typed overloads in loops, and hoist the delegate out of the loop.
Benchmarking & Verification¶
Numbers and methodology live in the Reflection Performance benchmarks. Tests/Runtime/Performance/ReflectionPerformanceTests captures getter, setter, invoker and constructor timings, and Tests/Runtime/Helper/ReflectionHelperCapabilityMatrixTests runs every helper with each strategy forced on and off, so the IL2CPP fallback path is covered on desktop. When you refresh timings, record the Unity version, scripting backend and OS alongside them.
See also¶
- Helper Utilities
Runtime/Core/Helper/ReflectionHelpers.cs,Runtime/Core/Helper/ReflectionHelpers.Factory.csandRuntime/Core/Helper/ReflectionHelpers.TypeDiscovery.cs-- the three files of theReflectionHelperspartial class.