MoreLinq Acquire. What does it do?
Asked Answered
B

3

10

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?

Baryon answered 31/1, 2014 at 14:55 Comment(3)
I think that Jon Skeet would be the best suited to answer this question for obvious reasons. As far as examples go, let's just say that I have a sequence/array/list of closed file descriptors and I would like to open them all and do something with them (and I need all of them). If any of the files fails to open, I'd like to close all other descriptors automatically. In that situation, Acquire would do exactly that.Thirsty
A lazy enumerable might throw an exception half way through the iteration. In that case you should Dispose all earlier objects, which this function does.Longlegged
All answers are perfect and I'd like to mark all of them as accepted but I had to pick only one. Looking at the answers made me feel like "D'oh"Baryon
S
16

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.

Studied answered 31/1, 2014 at 15:1 Comment(0)
S
7

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();
Sestertium answered 31/1, 2014 at 15:2 Comment(0)
A
2

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();
Agility answered 31/1, 2014 at 15:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.