Question

I was inspecting the Jon Skeet's MoreLinq and I became curious about the acquire extension source code

The implementation is as follows

        /// <summary>
        /// Ensures that a source sequence of <see cref="IDisposable"/> 
        /// objects are all acquired successfully. If the acquisition of any 
        /// one <see cref="IDisposable"/> fails then those successfully 
        /// acquired till that point are disposed.
        /// </summary>
        /// <typeparam name="TSource">Type of elements in <paramref name="source"/> sequence.</typeparam>
        /// <param name="source">Source sequence of <see cref="IDisposable"/> objects.</param>
        /// <returns>
        /// Returns an array of all the acquired <see cref="IDisposable"/>
        /// object and in source order.
        /// </returns>
        /// <remarks>
        /// This operator executes immediately.
        /// </remarks>

        public static TSource[] Acquire<TSource>(this IEnumerable<TSource> source)
            where TSource : IDisposable
        {
            if (source == null) throw new ArgumentNullException("source");

            var disposables = new List<TSource>();
            try
            {
                disposables.AddRange(source);
                return disposables.ToArray();
            }
            catch
            {
                foreach (var disposable in disposables)
                    disposable.Dispose();
                throw;
            }
        }

From my understanding it receives a IEnumerable<IDisposable> and it creates a List<IDisposable>.

I can't grasp whatever can go wrong here.

Can anyone explain it to me, and possibly provide an example where this extension can be useful?

Was it helpful?

Solution

The call to AddRange iterates over source. If for any reason it encounters an exception, any that had previously been acquired will be disposed. Consider this example:

var filenames = new[] { "file1.xml", "file2.xml", "doesnotexist.xml" };
var disposables = filenames.Select(fn => File.OpenRead(fn));
var fileStreams = disposables.Acquire();

No exception will be thrown when you're assigning disposables, because of lazy evaluation. However, when the call to AddRange inside Aquire reaches the third element (where it tries to open "doesnotexist.xml"), a FileNotFoundException will be thrown. When this happens, Acquire will safely dispose the previous streams. A simple ToList / ToArray would leave the first two file streams open.

In essence, Acquire is there to ensure that either all the files in filenames is safely opened, or none of them are.

OTHER TIPS

Assume you have code which creates and returns disposable objects one by one:

public IEnumerable<FileStream> GetFiles()
{
    yield return File.OpenRead("file1");
    yield return File.OpenRead("file2"); // does not exist
    yield return File.OpenRead("file3");
}

You need to get all of the disposable objects, but if in the middle of acquisition there is an exception, then the objects which were already yielded will stay in memory and not disposed. So, Acquire either acquires all streams and returns them, or, upon failing, it disposes all already acquired streams and rethrows the exception.

FileStream[] streams = GetFiles().Acquire();

Remember that most of the IEnumerable collections you get using LINQ are evaluated in lazy way, e.g. you get just the recipe how to generate the list. The code gets actually executed only when you iterate over the collection, which in this case happens in disposables.AddRange(source). If this call fails, then you end up with a partial collection of objects that should be disposed, which happens here:

            foreach (var disposable in disposables)
                disposable.Dispose();
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top