1- #if NET8_0_OR_GREATER
1+ #if NET9_0_OR_GREATER
2+ // .NET 9 added Delegate.EnumerateInvocationList, which is exactly the allocation-free
3+ // enumerator we want, is runtime-agnostic, and works on NativeAOT; prefer it
4+ #define BCL_INVOCATION_LIST
5+ #elif NET8_0_OR_GREATER
26#define UNSAFE_ACCESSOR // retain ability to disable easily
37#endif
48
711using System . Collections . Generic ;
812using System . Runtime . CompilerServices ;
913
10- #if ! UNSAFE_ACCESSOR
14+ #if ! UNSAFE_ACCESSOR && ! BCL_INVOCATION_LIST
1115using System . Reflection ;
1216using System . Reflection . Emit ;
1317#endif
@@ -25,7 +29,7 @@ internal static class Delegates
2529 /// <typeparam name="T">The type of delegate being enumerated.</typeparam>
2630 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
2731 public static DelegateEnumerator < T > GetEnumerator < T > ( this T ? handler ) where T : MulticastDelegate
28- => handler is null ? default : new ( handler ) ;
32+ => new ( handler ) ;
2933
3034 /// <summary>
3135 /// Iterate over the individual elements of a multicast delegate (without allocation).
@@ -41,8 +45,15 @@ public static DelegateEnumerable<T> AsEnumerable<T>(this T? handler) where T : M
4145 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
4246 public static bool IsSingle ( this MulticastDelegate handler )
4347 {
44- #if UNSAFE_ACCESSOR
45- return s_getArr ( handler ) is null ;
48+ #if BCL_INVOCATION_LIST
49+ var iterator = Delegate . EnumerateInvocationList ( handler ) ;
50+ return iterator . MoveNext ( ) && ! iterator . MoveNext ( ) ;
51+ #elif UNSAFE_ACCESSOR
52+ if ( s_isAvailable )
53+ {
54+ return s_getArr ( handler ) is null ;
55+ }
56+ return handler . GetInvocationList ( ) . Length == 1 ;
4657#else
4758 if ( s_isAvailable )
4859 {
@@ -63,7 +74,11 @@ public static bool IsSingle(this MulticastDelegate handler)
6374 /// </summary>
6475 public static bool IsSupported => s_isAvailable ;
6576
66- #if UNSAFE_ACCESSOR
77+ #if BCL_INVOCATION_LIST
78+ #pragma warning disable SA1303
79+ private const bool s_isAvailable = true ;
80+ #pragma warning restore SA1303
81+ #elif UNSAFE_ACCESSOR
6782#pragma warning disable SA1300
6883 [ UnsafeAccessor ( UnsafeAccessorKind . Field , Name = "_invocationList" ) ]
6984 private static extern ref readonly object ? s_getArr ( MulticastDelegate handler ) ;
@@ -73,10 +88,34 @@ public static bool IsSingle(this MulticastDelegate handler)
7388
7489 // ReSharper disable once InconsistentNaming
7590#pragma warning disable SA1303
76- private const bool s_isAvailable = true ;
91+ private static readonly bool s_isAvailable = IsAvailable ( ) ;
7792#pragma warning restore SA1303
7893
7994#pragma warning restore SA1300
95+
96+ private static bool IsAvailable ( )
97+ {
98+ // these fields are an implementation detail of the CoreCLR/.NET Framework delegate layout;
99+ // other runtimes differ (NativeAOT keeps the invocation list on Delegate, in a different
100+ // shape), in which case the accessors above throw MissingFieldException on first use - so:
101+ // validate before we trust them, and fall back to GetInvocationList() when unsure (see #3157)
102+ try
103+ {
104+ Action probe = Probe ;
105+ if ( s_getArr ( probe ) is not null ) return false ; // expect: single-target => no list
106+
107+ probe += Probe ; // (combine does not de-duplicate, so this is genuinely two targets)
108+ return s_getArr ( probe ) is object [ ] arr && arr . Length >= 2 // (the array can have spare capacity)
109+ && ( int ) s_getCount ( probe ) == 2
110+ && arr [ 0 ] is Action && arr [ 1 ] is Action ;
111+ }
112+ catch
113+ {
114+ return false ;
115+ }
116+
117+ static void Probe ( ) { }
118+ }
80119#else
81120#pragma warning disable SA1300
82121 private static readonly Func < MulticastDelegate , object ? > ? s_getArr ;
@@ -153,8 +192,7 @@ private static Func<MulticastDelegate, T> GetViaReflection<T>(FieldInfo field)
153192 /// Iterate over the individual elements of a multicast delegate (without allocation).
154193 /// </summary>
155194 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
156- public DelegateEnumerator < T > GetEnumerator ( )
157- => _handler is null ? default : new DelegateEnumerator < T > ( _handler ) ;
195+ public DelegateEnumerator < T > GetEnumerator ( ) => new ( _handler ) ;
158196 IEnumerator < T > IEnumerable < T > . GetEnumerator ( ) => GetEnumerator ( ) ;
159197 IEnumerator IEnumerable . GetEnumerator ( ) => GetEnumerator ( ) ;
160198 }
@@ -165,21 +203,33 @@ public DelegateEnumerator<T> GetEnumerator()
165203 /// <typeparam name="T">The type of delegate being enumerated.</typeparam>
166204 public struct DelegateEnumerator < T > : IEnumerator < T > where T : MulticastDelegate
167205 {
168- private readonly T _handler ;
206+ private readonly T ? _handler ;
207+ #if BCL_INVOCATION_LIST
208+ private Delegate . InvocationListEnumerator < T > _iterator ;
209+ #else
169210 private readonly object [ ] ? _arr ;
170211 private readonly int _count ;
171212 private int _index ;
172213 private T ? _current ;
173- internal DelegateEnumerator ( T handler )
214+ #endif
215+
216+ internal DelegateEnumerator ( T ? handler )
174217 {
175- // Debug.Assert(handler is not null);
176218 _handler = handler ;
177- #if UNSAFE_ACCESSOR
178- _arr = s_getArr ( handler ) as object [ ] ;
179- _count = _arr is null ? 1 : ( int ) s_getCount ( handler ) ;
219+ #if BCL_INVOCATION_LIST
220+ _iterator = Delegate . EnumerateInvocationList ( handler ) ;
180221#else
181- if ( s_isAvailable )
222+ if ( handler is null )
223+ {
224+ _arr = null ;
225+ _count = 0 ;
226+ }
227+ else if ( s_isAvailable )
182228 {
229+ #if UNSAFE_ACCESSOR
230+ _arr = s_getArr ( handler ) as object [ ] ;
231+ _count = _arr is null ? 1 : ( int ) s_getCount ( handler ) ;
232+ #else
183233 if ( s_delegates is null )
184234 {
185235 _arr = s_getArr ! ( handler ) as object [ ] ;
@@ -191,16 +241,18 @@ internal DelegateEnumerator(T handler)
191241 _arr = s_delegates ( handler ) ;
192242 _count = _arr ? . Length ?? 1 ;
193243 }
244+ #endif
194245 }
195246 else
196247 {
197248 // ReSharper disable once CoVariantArrayConversion
198249 _arr = handler . GetInvocationList ( ) ;
199250 _count = _arr . Length ;
200251 }
201- #endif
252+
202253 _current = null ;
203254 _index = - 1 ;
255+ #endif
204256 }
205257
206258 /// <summary>
@@ -209,7 +261,11 @@ internal DelegateEnumerator(T handler)
209261 public T Current
210262 {
211263 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
264+ #if BCL_INVOCATION_LIST
265+ get => _iterator . Current ;
266+ #else
212267 get => _current ! ;
268+ #endif
213269 }
214270
215271 object ? IEnumerator . Current => Current ;
@@ -222,24 +278,32 @@ void IDisposable.Dispose() { }
222278 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
223279 public bool MoveNext ( )
224280 {
281+ #if BCL_INVOCATION_LIST
282+ return _iterator . MoveNext ( ) ;
283+ #else
225284 var next = _index + 1 ;
226285 if ( next >= _count )
227286 {
228287 _current = null ;
229288 return false ;
230289 }
231- _current = _arr is null ? _handler : ( T ) _arr [ next ] ;
290+ _current = _arr is null ? _handler ! : ( T ) _arr [ next ] ;
232291 _index = next ;
233292 return true ;
293+ #endif
234294 }
235295
236296 /// <summary>
237297 /// Reset the enumerator, allowing the sequence to be repeated.
238298 /// </summary>
239299 public void Reset ( )
240300 {
301+ #if BCL_INVOCATION_LIST
302+ _iterator = Delegate . EnumerateInvocationList ( _handler ) ;
303+ #else
241304 _current = null ;
242305 _index = - 1 ;
306+ #endif
243307 }
244308 }
245309}
0 commit comments