I complain to anyone who will listen about the poor language support in C# for the IDisposable interface. Yeah, we’ve got the using statement, and that’s fine as far as it goes.
But compare that to what C++/CLI has: the most complete support of any .NET language. Not just the equivalent of the using statement, but also automatic implementation of IDisposable that takes care of disposing of nested owned objects, including inherited ones, like magic (relatively speaking, unless you’re a C++ programmer in which case you’ve taken it for granted for a decade or two).
The relationship between deterministic clean-up (i.e. destructors) and garbage collection (i.e. finalizers) was quite vaguely understood until Herb Sutter clarified as part of his work on C++/CLI. But now it’s all very clear how it should work – the only problem is, there doesn’t seem to be any movement towards fixing it in any future version of C#.
Anyway, by now you’re probably bored of the ranting and wondering where the code snippets are. So here’s one:
class FunkyResource : IDisposable
{
public string Label { get; set; }
public void Dispose()
{
Console.WriteLine("Disposing " + Label);
}
}
Not particularly impressive, I grant you, but it serves as a dummy example of a class that represents some resource that needs to be cleaned up. Here we just log the fact that a given instance is being cleaned up, identifying it with a label string.
Here’s where it gets interesting:
[AutoDisposable]
class FunkyOwner
{
[AutoDispose]
private FunkyResource _resource1 = new FunkyResource { Label = "resource1" };
[AutoDispose]
private FunkyResource _resource2 = new FunkyResource { Label = "resource2" };
}
This class owns a couple of FunkyResource instances. By “own” I simply mean that when an instance of this class is disposed of, those two resource instances must also be disposed of.
But wait a second – how does FunkyOwner implement IDisposable? The answer is that it automatically does so, because of that [AutoDisposable] attribute.
This is all thanks to the wonderfully powerful PostSharp, which effectively allows you to extend any CLR-based language through custom attributes. And because PostSharp builds on the CLR in a language-independent way, this means that the extensions you create should work in any CLR-based language that supports attributes.
Very briefly, PostSharp registers an extra step in the compilation process of Visual Studio: after your output assembly is built, a program called PostSharp.exe takes a look at it, and performs additional processing on the raw IL in the assembly. And hey presto, extra features make their way into your assembly.
Before we see how this example works, what about inheritance?
class DerivedOwner : FunkyOwner
{
[AutoDispose]
private FunkyResource _resource3 = new FunkyResource { Label = "resource3" };
}
Note that because we’re deriving from a class that has the [AutoDisposable] attribute, we don’t need to specify it again (it will be ignored if we do). If we call Dispose on an instance of DerivedOwner, we get this output:
Disposing resource3
Disposing resource1
Disposing resource2
That is, the resources owned by DerivedOwner are disposed of first, then the base class’s resources are also disposed of. So inheritance works fine.
And what about a mixed scenario, where I want automatic clean-up of owned resources but I also want to run some custom code of my own during disposal?
class MixedOwner : IDisposable
{
[AutoDispose]
private DerivedOwner _owner2 = new DerivedOwner();
public void Dispose()
{
Console.WriteLine("Disposing MixedOwner");
this.TryDisposeFields();
}
}
Note the call to TryDisposeFields(), an extension method I cooked up (see below). The reason for the prefix ‘Try’ is that you can call it on anything and it will look for fields marked with the [AutoDispose] attribute. If it finds any, it will dispose of them. If it doesn’t find any, that’s okay – nothing happens.
When manually implementing IDisposable like this, consider making it virtual, so that derived classes can override it, although they must of course call the base class’s version after performing their custom cleanup. It might be good practice in some circumstances to follow the pattern where you have a separate virtual method Dispose(bool) instead. However, that is really intended to allow you to implement a finalizer and a Dispose method together, and these days there are almost no circumstances in which it is recommended that you write a finalizer (unfortunately a lot of old books give out-of-date advice here).
Also note that we don’t need the [AutoDisposable] attribute on the class, as we already implement IDisposable (again, it would have been harmless to add it).
Unsurprisingly, the output of disposing an instance of MixedOwner is:
Disposing MixedOwner
Disposing resource3
Disposing resource1
Disposing resource2
All very nice. And extremely easy to implement, using the “easy” mode of PostSharp which is known as Laos. No need to directly manipulate IL, just write classes to represent your attributes, deriving from base classes that take care of the messy details.
My [AutoDispose] attribute is actually completely trivial because it’s just a simple marker on fields that I look for using reflection. So this isn’t inherently anything to do with PostSharp:
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public sealed class AutoDisposeAttribute : Attribute { }
Where PostSharp makes an appearance is in the [AutoDisposable] attribute:
[Serializable]
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
public sealed class AutoDisposableAttribute : CompositionAspect
{
public override object CreateImplementationObject(InstanceBoundLaosEventArgs eventArgs)
{
return new AutoDisposableImpl(eventArgs.Instance);
}
public override Type GetPublicInterface(Type containerType)
{
return typeof(IDisposable);
}
public override CompositionAspectOptions GetOptions()
{
return CompositionAspectOptions.IgnoreIfAlreadyImplemented;
}
}
By inheriting the attribute from PostSharp.Laos.CompositionAspect, I’m saying that I want to add an extra interface to any class marked with my attribute. The implementation is created and returned from the CreateImplementationObject method. The other overrides are pretty self-explanatory.
Here’s what my AutoDisposableImpl class looks like:
public sealed class AutoDisposableImpl : IDisposable
{
private readonly object _instance;
public AutoDisposableImpl(object instance)
{
_instance = instance;
}
public void Dispose()
{
_instance.TryDisposeFields();
}
}
Pretty simple, eh? It just stores a “back pointer” to the object we are extending, so it can call the TryDisposeFields extension method on whatever that object happens to be. In a way, it’s just like MixedOwner except it doesn’t do anything extra.
Actually most of the complicated mess is hidden in that extension method. So here’s the gory detail:
public static void TryDisposeFields(this object instance)
{
instance.GetType()
.Chain(type => type.BaseType)
.SelectMany(type => type.GetFields(BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.DeclaredOnly))
.Where(fieldInfo => fieldInfo.GetCustomAttributes(typeof(AutoDisposeAttribute), false)
.OfType<AutoDisposeAttribute>().Any())
.Select(fieldInfo => fieldInfo.GetValue(instance))
.OfType<IDisposable>()
.ForEach(field => field.Dispose());
}
As you can see, I enjoy chaining a lot of LINQ operators together. The first one, Chain, is a little custom gadget of mine that turns any linked list of T into an IEnumerable<T>. I’ll describe it in a separate post.
After that, I get all the fields of all the types in the inheritance chain into a flat list, using the marvelous SelectMany method. Then I filter them according to whether they have the [AutoDispose] attribute, and select the values of the remaining fields, then filter again based on whether they support IDisposable, and finally I dispose of each one. (That last ForEach method is another one of mine, and I’ve seen a lot of people suggesting the same thing).
So what are the drawbacks of this marvelous scheme? The one major issue with the PostSharp approach is that the Visual Studio IDE does its own compilation of your source to provide auto-completion and other kinds of “intellisense”. It doesn’t look at the assembly on disk, because there might not be one (e.g. if the code doesn’t completely compile without errors yet, as is often the case when you are adding new code, which is precisely the time when you require intellisense features).
This means that the IDE doesn’t know that [AutoDisposable] classes support IDisposable. Nor does the real compilation stage that happens during the build. PostSharp doesn’t kick in until after the compilation has completed. The upshot is that we cannot do this:
using (new FunkyOwner())
{
Console.WriteLine("Using FunkyOwner...");
}
The using statement needs to statically verify that the object supports IDisposable. It won’t try to resolve this at runtime.
This would seem to be not so much a drawback, more a total friggin’ disaster. However, there is a solution, in the form of another generally applicable extension method:
public static void TryUsing<T>(this T instance, Action<T> action)
{
try
{
action(instance);
}
finally
{
IDisposable disp = instance as IDisposable;
if (disp != null)
disp.Dispose();
}
}
Now we can write:
new FunkyOwner().TryUsing(funkyOwner =>
{
Console.WriteLine("Using FunkyOwner...");
});
I arranged the interface of the TryUsing method carefully in order to mimic the characteristic of the using statement. In particular, the object to be disposed of is only given a name by the lambda parameter. This means that it cannot be accidentally used outside of the block where it is “in scope” (i.e. still not yet disposed), unless the programmer makes a special effort to circumvent this protection by storing a reference to the object in a variable declared outside the lambda.
And with that, I have now discussed every single part of the AutoDisposal library. You can download the complete source along with a demonstration program here:
http://www.earwicker.com/AutoDisposalSource.zip
You will of course also need the PostSharp system, which you can get here:
No comments:
Post a Comment