// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading; namespace System.Reactive.Disposables { /// /// Represents a group of disposable resources that are disposed together. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "Backward compat + ideally want to get rid of the ICollection nature of the type.")] public sealed class CompositeDisposable : ICollection, ICancelable { private readonly object _gate = new object(); private bool _disposed; private List _disposables; private int _count; private const int SHRINK_THRESHOLD = 64; // Default initial capacity of the _disposables list in case // The number of items is not known upfront private const int DEFAULT_CAPACITY = 16; /// /// Initializes a new instance of the class with no disposables contained by it initially. /// public CompositeDisposable() { _disposables = new List(); } /// /// Initializes a new instance of the class with the specified number of disposables. /// /// The number of disposables that the new CompositeDisposable can initially store. /// is less than zero. public CompositeDisposable(int capacity) { if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity)); _disposables = new List(capacity); } /// /// Initializes a new instance of the class from a group of disposables. /// /// Disposables that will be disposed together. /// is null. /// Any of the disposables in the collection is null. public CompositeDisposable(params IDisposable[] disposables) { if (disposables == null) { throw new ArgumentNullException(nameof(disposables)); } Init(disposables, disposables.Length); } /// /// Initializes a new instance of the class from a group of disposables. /// /// Disposables that will be disposed together. /// is null. /// Any of the disposables in the collection is null. public CompositeDisposable(IEnumerable disposables) { if (disposables == null) throw new ArgumentNullException(nameof(disposables)); // If the disposables is a collection, get its size // and use it as a capacity hint for the copy. if (disposables is ICollection c) { Init(disposables, c.Count); } else { // Unknown sized disposables, use the default capacity hint Init(disposables, DEFAULT_CAPACITY); } } /// /// Initialize the inner disposable list and count fields. /// /// The enumerable sequence of disposables. /// The number of items expected from private void Init(IEnumerable disposables, int capacityHint) { var list = new List(capacityHint); // do the copy and null-check in one step to avoid a // second loop for just checking for null items foreach (var d in disposables) { if (d == null) { throw new ArgumentException(Strings_Core.DISPOSABLES_CANT_CONTAIN_NULL, nameof(disposables)); } list.Add(d); } _disposables = list; // _count can be read by other threads and thus should be properly visible // also releases the _disposables contents so it becomes thread-safe Volatile.Write(ref _count, _disposables.Count); } /// /// Gets the number of disposables contained in the . /// public int Count => Volatile.Read(ref _count); /// /// Adds a disposable to the or disposes the disposable if the is disposed. /// /// Disposable to add. /// is null. public void Add(IDisposable item) { if (item == null) throw new ArgumentNullException(nameof(item)); lock (_gate) { if (!_disposed) { _disposables.Add(item); // If read atomically outside the lock, it should be written atomically inside // the plain read on _count is fine here because manipulation always happens // from inside a lock. Volatile.Write(ref _count, _count + 1); return; } } item.Dispose(); } /// /// Removes and disposes the first occurrence of a disposable from the . /// /// Disposable to remove. /// true if found; false otherwise. /// is null. public bool Remove(IDisposable item) { if (item == null) throw new ArgumentNullException(nameof(item)); lock (_gate) { // this composite was already disposed and if the item was in there // it has been already removed/disposed if (_disposed) { return false; } // // List doesn't shrink the size of the underlying array but does collapse the array // by copying the tail one position to the left of the removal index. We don't need // index-based lookup but only ordering for sequential disposal. So, instead of spending // cycles on the Array.Copy imposed by Remove, we use a null sentinel value. We also // do manual Swiss cheese detection to shrink the list if there's a lot of holes in it. // // read fields as infrequently as possible var current = _disposables; var i = current.IndexOf(item); if (i < 0) { // not found, just return return false; } current[i] = null; if (current.Capacity > SHRINK_THRESHOLD && _count < current.Capacity / 2) { var fresh = new List(current.Capacity / 2); foreach (var d in current) { if (d != null) { fresh.Add(d); } } _disposables = fresh; } // make sure the Count property sees an atomic update Volatile.Write(ref _count, _count - 1); } // if we get here, the item was found and removed from the list // just dispose it and report success item.Dispose(); return true; } /// /// Disposes all disposables in the group and removes them from the group. /// public void Dispose() { var currentDisposables = default(List); lock (_gate) { if (!_disposed) { currentDisposables = _disposables; // nulling out the reference is faster no risk to // future Add/Remove because _disposed will be true // and thus _disposables won't be touched again. _disposables = null; Volatile.Write(ref _count, 0); Volatile.Write(ref _disposed, true); } } if (currentDisposables != null) { foreach (var d in currentDisposables) { d?.Dispose(); } } } /// /// Removes and disposes all disposables from the , but does not dispose the . /// public void Clear() { var previousDisposables = default(IDisposable[]); lock (_gate) { // disposed composites are always clear if (_disposed) { return; } var current = _disposables; previousDisposables = current.ToArray(); current.Clear(); Volatile.Write(ref _count, 0); } foreach (var d in previousDisposables) { d?.Dispose(); } } /// /// Determines whether the contains a specific disposable. /// /// Disposable to search for. /// true if the disposable was found; otherwise, false. /// is null. public bool Contains(IDisposable item) { if (item == null) throw new ArgumentNullException(nameof(item)); lock (_gate) { if (_disposed) { return false; } return _disposables.Contains(item); } } /// /// Copies the disposables contained in the to an array, starting at a particular array index. /// /// Array to copy the contained disposables to. /// Target index at which to copy the first disposable of the group. /// is null. /// is less than zero. -or - is larger than or equal to the array length. public void CopyTo(IDisposable[] array, int arrayIndex) { if (array == null) throw new ArgumentNullException(nameof(array)); if (arrayIndex < 0 || arrayIndex >= array.Length) throw new ArgumentOutOfRangeException(nameof(arrayIndex)); lock (_gate) { // disposed composites are always empty if (_disposed) { return; } if (arrayIndex + _count > array.Length) { // there is not enough space beyond arrayIndex // to accommodate all _count disposables in this composite throw new ArgumentOutOfRangeException(nameof(arrayIndex)); } var i = arrayIndex; foreach (var d in _disposables) { if (d != null) { array[i++] = d; } } } } /// /// Always returns false. /// public bool IsReadOnly => false; /// /// Returns an enumerator that iterates through the . /// /// An enumerator to iterate over the disposables. public IEnumerator GetEnumerator() { lock (_gate) { if (_disposed || _count == 0) { return EMPTY_ENUMERATOR; } // the copy is unavoidable but the creation // of an outer IEnumerable is avoidable return new CompositeEnumerator(_disposables.ToArray()); } } /// /// Returns an enumerator that iterates through the . /// /// An enumerator to iterate over the disposables. IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// /// Gets a value that indicates whether the object is disposed. /// public bool IsDisposed => Volatile.Read(ref _disposed); /// /// An empty enumerator for the /// method to avoid allocation on disposed or empty composites. /// static readonly CompositeEnumerator EMPTY_ENUMERATOR = new CompositeEnumerator(new IDisposable[0]); /// /// An enumerator for an array of disposables. /// sealed class CompositeEnumerator : IEnumerator { readonly IDisposable[] disposables; int index; public CompositeEnumerator(IDisposable[] disposables) { this.disposables = disposables; this.index = -1; } public IDisposable Current => disposables[index]; object IEnumerator.Current => disposables[index]; public void Dispose() { // Avoid retention of the referenced disposables // beyond the lifecycle of the enumerator. // Not sure if this happens by default to // generic array enumerators though. var disposables = this.disposables; Array.Clear(disposables, 0, disposables.Length); } public bool MoveNext() { var disposables = this.disposables; for (; ; ) { var idx = ++index; if (idx >= disposables.Length) { return false; } // inlined that filter for null elements if (disposables[idx] != null) { return true; } } } public void Reset() { index = -1; } } } }