If you want to minimize GC pressure, I would suggest using interfaces rather than delegates. If you use IDisposable
, you can't avoid having Dispose
called, but you could have the IDisposable implementation use a flag to indicate that the Dispose
method shouldn't do anything. Beyond the fact that delegates have some built-in language support, there isn't really anything they can do that interfaces cannot, but interfaces offer two advantages over delegates:
- Using a delegate which is bound to some data will generally require creating a heap object for the data and a second for the delegate itself. Interfaces don't require that second heap instance.
- In circumstances where one can use generic types which are constrained to an interface, instead of using interface types directly, one may be able to avoid creating any heap instances, as explained below (since back-tick formatting doesn't work in list items). A struct that combines a delegate to a static method along with data to be consumed by that method can behave much like a delegate, without requiring a heap allocation.
One caveat with the second approach: Although avoiding GC pressure is a good thing, the second approach may end up creating a very large number of types at run-time. The number of types created will in most cases be bounded, but there are circumstances where it could increase without bound. I'm not sure if there would any convenient way to determine the full set of types that could be produced by a program in cases where static analysis would be sufficient (in the general case, where static analysis does not suffice, determining whether any particular run-time type would get produced would be equivalent to the Halting Problem, but just as many programs can in practice be statically determined to always halt or never halt, I would expect that in practice one could often identify a closed set of types that a program could produce at run-time).
Edit
The formatting in point #2 above was messed up. Here's the explanation, cleaned up.
Define a type ConditionalCleaner<T> : IDisposable
, which holds an instance of T
and an Action<T>
(both supplied in the constructor--probably with the Action<T>
as the first parameter). In the IDisposable.Dispose()
method, if the Action<T>
is non-null, invoke it on the T
. In a SkipDispose()
method, null out the Action<T>
. For convenience, you may want to also define ConditionalCleaner<T,U>: IDisposable
similarly (perhaps three- and four-argument versions as well), and you may want to define a static class ConditionalCleaner with generic Create<T>
, Create<T,U>
, etc. methods (so one could say e.g. using (var cc = ConditionalCleaner.Create(Console.WriteLine, "ABCDEF") {...}
or ConditionalCleaner.Create((x) => {Console.WriteLine(x);}, "ABCDEF")
to have the indicated action performed when the using block exits. The biggest requirement if one uses a Lambda expression is to ensure that the lambda expression doesn't close over any local variables or parameters from the calling function; anything the calling function wants to pass to the lambda expression must be an explicit parameter thereof. Otherwise the system will define a class object to hold any closed-over variables, as well as a new delegate pointing to it.