7
\$\begingroup\$

I often write small classes to make undisposable things disposable so that .NET can handle something automatically, like stopwatches or logs. I thought why not write a small helper that can be used instead of implementing a full class for simple things.

The Usingifier requries two delegates, one for the initialization and the other one for cleaning-up. (null checks removed for brevity)

class Usingifier<T> : IDisposable
{
    private readonly Func<T> _initialize;
    private readonly Action<T> _cleanUp;
    private T _state;

    public Usingifier(Func<T> initialize, Action<T> cleanUp)
    {
        _initialize = initialize;
        _cleanUp = cleanUp;
    }

    public Usingifier<T> Initialize()
    {
        _state = _initialize();
        return this;
    }
    public virtual void Dispose()
    {
        if (_state != null)
        {
            _cleanUp(_state);
        }
    }
}

and here's an example with a Stopwatch

using (var autoStopwatch = new Usingifier<Stopwatch>(() => Stopwatch.StartNew(), sw => sw.Elapsed.Dump()))
{
    autoStopwatch.Initialize();
    Thread.Sleep(2000);
}
\$\endgroup\$
2
  • \$\begingroup\$ null checks removed for brevety - why? \$\endgroup\$ Commented Mar 13, 2017 at 14:31
  • \$\begingroup\$ @Mat'sMug I find code without null checks more readable. I always have to cry when I add them or logging and a simple function with just a few lines becomes a huge null check and logging monster. \$\endgroup\$
    – t3chb0t
    Commented Mar 13, 2017 at 14:35

1 Answer 1

7
\$\begingroup\$

It's a little difficult to give a full review without the removed null checks, but I'll give an observation:

In Dispose(), you depend on null to be the value of _state to judge whether or not to call the cleanup action. That's all well and good, but you should likely then constrain the generic parameter T to be class

class Usingifier<T> : IDisposable where T : class

You also have an issue with multiple-disposals calling the cleanup action with the state - both probably being stale after the fact. The class should be considered unusable after the cleanup. Or at least unusable until the initialize function has been called again. Simple boolean should take care of that:

class Usingifier<T> : IDisposable where T : class
{
    private readonly Func<T> _initialize;

    private readonly Action<T> _cleanUp;

    private bool _disposed;

    private T _state;

    public Usingifier(Func<T> initialize, Action<T> cleanUp)
    {
        _initialize = initialize;
        _cleanUp = cleanUp;
    }

    public Usingifier<T> Initialize()
    {
        _state = _initialize();
        _disposed = false;
        return this;
    }

    public virtual void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _disposed = true;

        if (_state != null)
        {
            _cleanUp(_state);
        }
    }
}

You may want to also put in some sort of nice (optional) automatic disposal if T itself happens to be IDisposable. Here's a quick take on that:

class Usingifier<T> : IDisposable where T : class
{
    private readonly Func<T> _initialize;

    private readonly Action<T> _cleanUp;

    private readonly bool _autoDispose;

    private bool _disposed;

    private T _state;

    public Usingifier(Func<T> initialize, Action<T> cleanUp, bool autoDispose = false)
    {
        _initialize = initialize;
        _cleanUp = cleanUp;
        _autoDispose = autoDispose;
    }

    public Usingifier<T> Initialize()
    {
        _state = _initialize();
        _disposed = false;
        return this;
    }

    public virtual void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _disposed = true;

        if (_state != null)
        {
            _cleanUp(_state);
        }

        if (_autoDispose && _state is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }

I had a couple more thoughts: perhaps expose _state via a property (can then be accessed within your using scope) and then perhaps extracting an interface for the whole caboodle for mocking purposes during unit testing. Behold:

interface IUsingifier<T> : IDisposable where T : class
{
    T State { get; }

    Usingifier<T> Initialize();
}

class Usingifier<T> : IUsingifier<T> where T : class
{
    private readonly Func<T> _initialize;

    private readonly Action<T> _cleanUp;

    private readonly bool _autoDispose;

    private bool _disposed;

    public Usingifier(Func<T> initialize, Action<T> cleanUp, bool autoDispose = false)
    {
        _initialize = initialize;
        _cleanUp = cleanUp;
        _autoDispose = autoDispose;
    }

    public T State { get; private set; }

    public Usingifier<T> Initialize()
    {
        State = _initialize();
        _disposed = false;
        return this;
    }

    public virtual void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _disposed = true;

        if (State != null)
        {
            _cleanUp(State);
        }

        if (_autoDispose && State is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}
\$\endgroup\$
7
  • 3
    \$\begingroup\$ Oooh nice catch with the multiple-disposal problem - that's a violation of IDisposable implementation guidelines =) \$\endgroup\$ Commented Mar 13, 2017 at 14:42
  • 1
    \$\begingroup\$ I was going to use it with a Mutex that is an IDisposable so this is definitely a good catch. \$\endgroup\$
    – t3chb0t
    Commented Mar 13, 2017 at 14:46
  • 1
    \$\begingroup\$ I did it with C# 7 like this if (_state is IDisposable disposable) disposable.Dispose(); but I like the autoDispose flag. \$\endgroup\$
    – t3chb0t
    Commented Mar 13, 2017 at 14:56
  • \$\begingroup\$ I was wondering if I could load this up with some C#7 goodness :) \$\endgroup\$ Commented Mar 13, 2017 at 14:57
  • 3
    \$\begingroup\$ Don't throw an exception when an object is disposed multiple times. IDisposable.Dispose is specifically documented to be callable more than once. \$\endgroup\$
    – Kyle
    Commented Mar 13, 2017 at 18:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.