// 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;
}
}
}
}