如何为ICollection/IEnumerable元素创建Getter委托?动态环境类实现
Absolutely, you can use DynamicMethod to build efficient getter delegates that access collection elements directly—no more slow ElementAt() calls that traverse the entire collection every time. This is a perfect fit for your real-time simulation environment, and I’ll walk you through how to implement it, plus some alternative optimizations that might simplify your code.
The core idea is to generate IL at runtime that directly accesses the collection’s underlying indexer or lookup mechanism, rather than relying on generic enumeration. Let’s break this down for different collection types:
1. Indexable Collections (IList, IReadOnlyList)
For collections with an indexer (like List<T> or arrays), we can create a delegate that calls the indexer directly:
public class Environment { private delegate object GetterDelegate(); private Dictionary<string, GetterDelegate> environmentVariables_; public IEnumerable<string> EnvironmentVariables => environmentVariables_.Keys; public object this[string name] => environmentVariables_[name](); public Environment() { environmentVariables_ = new Dictionary<string, GetterDelegate>(); } public void Register(string name, ICollection collection) { if (collection is IList list) { // Use DynamicMethod for index-based access for (int i = 0; i < list.Count; i++) { int index = i; // Capture the current index to avoid closure issues environmentVariables_.Add($"{name}_{index}", CreateListElementGetter(list, index)); } } else { // Fallback for non-indexable collections (e.g., HashSet) // Note: This still requires enumeration, but it's unavoidable for unindexed collections int i = 0; foreach (var element in collection) { int elementIndex = i++; environmentVariables_.Add($"{name}_{elementIndex}", () => collection.Cast<object>().ElementAt(elementIndex)); } } } private GetterDelegate CreateListElementGetter(IList list, int index) { // Create a DynamicMethod that returns the element at the given index var method = new DynamicMethod( $"GetListElement_{index}", typeof(object), new[] { typeof(IList) }, typeof(Environment).Module); var il = method.GetILGenerator(); // Load the list instance onto the stack il.Emit(OpCodes.Ldarg_0); // Load the index value il.Emit(OpCodes.Ldc_I4, index); // Call the IList indexer property getter il.Emit(OpCodes.Callvirt, typeof(IList).GetProperty("Item").GetGetMethod()); // Return the result (non-generic IList returns object, no boxing needed) il.Emit(OpCodes.Ret); // Return a delegate that captures the list instance return () => method.Invoke(null, new object[] { list }); } }
2. Dictionary Collections
For Dictionary<TKey, TValue>, we can generate delegates that use the dictionary’s fast key lookup instead of converting keys to strings upfront:
public void Register<TKey, TValue>(string name, Dictionary<TKey, TValue> dictionary) { foreach (var pair in dictionary) { TKey key = pair.Key; environmentVariables_.Add($"{name}_{key}", CreateDictionaryValueGetter(dictionary, key)); } } private GetterDelegate CreateDictionaryValueGetter<TKey, TValue>(Dictionary<TKey, TValue> dictionary, TKey key) { var method = new DynamicMethod( $"GetDictionaryValue_{key}", typeof(object), new[] { typeof(Dictionary<TKey, TValue>), typeof(TKey) }, typeof(Environment).Module); var il = method.GetILGenerator(); var valueLocal = il.DeclareLocal(typeof(TValue)); // Load dictionary and key onto the stack il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); // Load address of the local value variable il.Emit(OpCodes.Ldloca_S, valueLocal); // Call Dictionary.TryGetValue to safely look up the key il.Emit(OpCodes.Callvirt, typeof(Dictionary<TKey, TValue>).GetMethod("TryGetValue")); // If TryGetValue returns true, return the value; else return null var notFoundLabel = il.DefineLabel(); il.Emit(OpCodes.Brfalse_S, notFoundLabel); il.Emit(OpCodes.Ldloc_S, valueLocal); if (typeof(TValue).IsValueType) { il.Emit(OpCodes.Box, typeof(TValue)); // Box value types to object } il.Emit(OpCodes.Ret); il.MarkLabel(notFoundLabel); il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Ret); return () => method.Invoke(null, new object[] { dictionary, key }); }
3. Field Access
For FieldInfo, we can generate delegates that directly access the field (instance or static) without reflection overhead:
public void Register(string name, FieldInfo field, object target = null) { environmentVariables_.Add(name, CreateFieldGetter(field, target)); } private GetterDelegate CreateFieldGetter(FieldInfo field, object target) { var method = new DynamicMethod( $"GetField_{field.Name}", typeof(object), target == null ? Type.EmptyTypes : new[] { typeof(object) }, typeof(Environment).Module); var il = method.GetILGenerator(); if (target != null) { // Load the target instance and cast it to the field's declaring type il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Castclass, field.DeclaringType); } // Load the field value il.Emit(field.IsStatic ? OpCodes.Ldsfld : OpCodes.Ldfld, field); // Box value types if needed if (field.FieldType.IsValueType) { il.Emit(OpCodes.Box, field.FieldType); } il.Emit(OpCodes.Ret); // Return a delegate that captures the target (if needed) return target == null ? () => method.Invoke(null, null) : () => method.Invoke(null, new[] { target }); }
If you find writing IL with DynamicMethod too low-level, Expression Trees offer a more readable alternative that compiles to the same efficient IL. For example, here’s how to rewrite the list element getter:
private GetterDelegate CreateListElementGetter(IList list, int index) { var listParam = Expression.Parameter(typeof(IList)); var indexExpr = Expression.Constant(index); var propertyAccess = Expression.Property(listParam, "Item", indexExpr); var lambda = Expression.Lambda<Func<IList, object>>(propertyAccess, listParam); var func = lambda.Compile(); return () => func(list); }
This achieves the same performance as DynamicMethod but is much easier to write and debug.
One critical issue in your initial code is that it captures elements at registration time—if the collection changes later, the getter will return the old value. The approaches above fix this by accessing the collection directly when the getter is called, ensuring you always get real-time data.
- Check for Indexable Interfaces First: Always prioritize
IList,IReadOnlyList, orIDictionarywhen registering collections—these offer the fastest access patterns. - Handle Collection Changes: For dynamic collections that update frequently, consider using
INotifyCollectionChangedto automatically refresh your environment variables when the collection changes. - Cache Compiled Delegates: If you register many instances of the same collection type, cache the compiled delegates to avoid re-generating IL/Expressions every time.
内容的提问来源于stack exchange,提问作者Haus

