16

In the .Net BCL is there a collection data structure similar to list that has a maximum capacity, say configured to 100 items, which when item 101 is added the original first item is popped/removed from the collection thus ensuring the item count never exceeds 100.

I’m using .net 3.5

Thanks in advance

SuperSuperDev1234
  • 4,765
  • 5
  • 26
  • 19

9 Answers9

20

There is no such collection available but one is fairly easy to write. The best way to do this is to create a new collection type that encapsulates an existing collection type.

For example

public class FixedSizeList<T> : IList<T> {
  private List<T> _list = new List<T>();
  private int _capacity = 100;

  public void Add(T value) {
    _list.Add(value);
    while ( _list.Count > _capacity ) {
      _list.RemoveAt(0);
    }
  }

  // Rest omitted for brevity
}

A couple of answers suggested inheritance as a mechanism. This is certainly not a good route especially if you are deriving from one of the generic collections. These collections are not designed to be inherited from and it is very easy to accidentally circumvent any checks you make an capacity as the result of an Add or Remove method.

The primary reason is that these methods are not virtual so they cannot be overridden. You would be forced to either declare an Add method with a different name (thus confusing your users) or re-declare Add with the new syntax. The latter is a very unsafe because as soon as an instance of your class is passed to a reference of the base type, all of your methods will not be called and the list can grow past capacity.

EDIT

As the comment section discussion has indicated, implementing List<T> is not the best approach here. The reason why is that it violates the substitution principle in several cases. The easiest way to display the problem is imagine if my implementation was passed to the following method. This code should pass for any IList<T> implementation but would fail in this case if the list was at capacity.

public void Example<T>(IList<T> list, T value) {
  var count = list.Count;
  list.Add(value);
  var addedValue = list[count];
}

The only collection interface that can be validly implemented for the specified collection is IEnumerable<T>. I've left my implementation up there as an example. But see ShuggyCoUk's answer for an IEnumerable<T> implementation:

Community
  • 1
  • 1
JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
  • 2
    +1 This is an _excellent_ answer! It is nice to hear such a lucid explanation of why you chose to implement `IList` over inheriting from a concrete type. – Andrew Hare Jul 31 '09 at 16:46
  • 1
    Your concept is right, but this is an incredibly inefficient data structure, where every insert once the list is full is O(n) rather than the typical O(1) for a list. A real-world implementation should probably be using a circular buffer. – Greg Beech Jul 31 '09 at 16:53
  • Why implement IList here? It doesn't act like an IList -- all the semantics are wrong -- e.g., items change index during Add(). Doesn't this violate the Liskov Substitution Principle? Won't methods accepting an IList break in strange ways if you pass it one of these? – Ken Jul 31 '09 at 17:15
  • 1
    @Ken - How are the semantics of `IList` not suitable for this implementation? There is nothing about `IList` that precludes the implementation from changing the index of an item based on any of the operations defined in it. What do you think happens to a `List` when you call either of the removal methods? – Andrew Hare Jul 31 '09 at 17:20
  • @Ken, yes it would break the substitution principle. I originally implemented IList because the op said "like a list" and that's the first interface that popped into my head. But really if you want a collection which **strictly** implements the implicit contracts of the collection interfaces and has the behavior described by the OP, you can really only implement IEnumerable. – JaredPar Jul 31 '09 at 17:23
  • @Greg, yes it is less efficient once it passes the capacity. The code is mainly meant to serve as a sample of the encapsulation principle vs. an inherenitance and not necessarily the most efficient implementation. – JaredPar Jul 31 '09 at 17:25
  • Your answer is sounds in all the right parts - feel free to take the window code from mine and just use that – ShuggyCoUk Jul 31 '09 at 17:41
  • @ShuggyCoUk, I went ahead and summarized this comment section in my answer and linked down to your implementation. – JaredPar Jul 31 '09 at 18:12
9

What you are describing is a circular buffer. I use these occasionally and recently ported some older code into a genericised C# class (attached). This code is part of SharpNeat V2 development.

This has O(1) performance on add and remove oprations whereas the solution posted that encapsulates a List is O(n). This is because removing the 0th item in a list causes all other items to be shuffled down to fill the gap.


using System;
using System.Collections.Generic;
using System.Text;

namespace SharpNeat.Utility
{
    /// 
    /// This is a generic circular buffer of items of type T.  A circular buffer must be assigned
    /// a capacity at construction time. Items can be enqueued indefintely, but when the buffer's 
    /// capacity is reached the oldest values in the buffer are overwritten, thus the buffer is best
    /// thought of as a circular array or buffer.
    /// 
    public class CircularBuffer
    {
        /// 
        /// Internal array that stores the circular buffer's values.
        /// 
        protected T[] _buff;

        /// 
        /// The index of the previously enqueued item. -1 if buffer is empty.
        /// 
        protected int _headIdx;

        /// 
        /// The index of the next item to be dequeued. -1 if buffer is empty.
        /// 
        protected int _tailIdx;

        #region Constructors

        /// 
        /// Constructs a circular buffer with the specified capacity.
        /// 
        /// 
        public CircularBuffer(int capacity)
        {
            _buff = new T[capacity];
            _headIdx = _tailIdx = -1;
        }

        #endregion

        #region Properties

        /// 
        /// Gets the number of items in the buffer. Returns the buffer's capacity
        /// if it is full.
        /// 
        public int Length
        {
            get
            {
                if(_headIdx == -1) 
                    return 0;

                if(_headIdx > _tailIdx)
                    return (_headIdx - _tailIdx) + 1;

                if(_tailIdx > _headIdx)
                    return (_buff.Length - _tailIdx) + _headIdx + 1;

                return 1;
            }
        }

        #endregion

        #region Public Methods

        /// 
        /// Clear the buffer.
        /// 
        public virtual void Clear()
        {
            _headIdx = _tailIdx = -1;
        }

        /// 
        /// Enqueue a new item. This overwrites the oldest item in the buffer if the buffer
        /// has reached capacity.
        /// 
        /// 
        public virtual void Enqueue(T item)
        {
            if(_headIdx == -1)
            {   // buffer is currently empty.
                _headIdx = _tailIdx = 0;
                _buff[0] = item;
                return;
            }

            // Determine the index to write to.
            if(++_headIdx == _buff.Length)
            {   // Wrap around.
                _headIdx = 0;
            }

            if(_headIdx == _tailIdx)
            {   // Buffer overflow. Increment tailIdx.
                if(++_tailIdx == _buff.Length) 
                {   // Wrap around.
                    _tailIdx=0;
                }
                _buff[_headIdx] = item;
                return;
            }

            _buff[_headIdx] = item;
            return;
        }

        /// 
        /// Remove the oldest item from the back end of the buffer and return it.
        /// 
        /// 
        public virtual T Dequeue()
        {
            if(_tailIdx == -1)
            {   // buffer is currently empty.
                throw new InvalidOperationException("buffer is empty.");
            }

            T item = _buff[_tailIdx];

            if(_tailIdx == _headIdx)
            {   // The buffer is now empty.
                _headIdx=_tailIdx=-1;
                return item;
            }

            if(++_tailIdx == _buff.Length)
            {   // Wrap around.
                _tailIdx = 0;
            }

            return item;
        }

        /// 
        /// Pop the most recently added item from the front end of the buffer and return it.
        /// 
        /// 
        public virtual T Pop()
        {
            if(_tailIdx == -1)
            {   // buffer is currently empty.
                throw new InvalidOperationException("buffer is empty.");
            }   

            T item = _buff[_headIdx];

            if(_tailIdx == _headIdx)
            {   // The buffer is now empty.
                _headIdx = _tailIdx =- 1;
                return item;
            }

            if(--_headIdx==-1)
            {   // Wrap around.
                _headIdx=_buff.Length-1;
            }

            return item;
        }

        #endregion
    }
}


redcalx
  • 8,177
  • 4
  • 56
  • 105
8

A really simple rolling window

public class RollingWindow<T> : IEnumerable<T>
{
    private readonly T[] data;
    private int head;
    private int nextInsert = 0;

    public RollingWindow(int size)
    {
        if (size < 1)
            throw new Exception();
        this.data = new T[size];
        this.head = -size;
    }

    public void Add(T t)
    {
        data[nextInsert] = t;
        nextInsert = (nextInsert + 1) % data.Length;
        if (head < 0)
            head++;   
    }

    public IEnumerator<T> GetEnumerator()
    {
        if (head < 0)
        {
            for (int i = 0; i < nextInsert; i++)
                yield return data[i];
        }
        else
        {
            for(int i = 0; i < data.Length; i++)
                yield return data[(nextInsert + i) % data.Length];
        }
    }

    System.Collections.IEnumerator 
        System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}
ShuggyCoUk
  • 36,004
  • 6
  • 77
  • 101
2

There's not one available, but it should be easy to write a function to accomplish this with an array or collection.

xpda
  • 15,585
  • 8
  • 51
  • 82
2

you can also just override Collection

public class FixedSizeList<T> : Collection<T>
{
    public int MaxItems {get;set;}

    protected override void InsertItem(int index, T item){
        base.InsertItem(index, item);

        while (base.Count > MaxItems) {
            base.RemoveItem(0);
        }
    }
}
Russ Bradberry
  • 10,705
  • 17
  • 69
  • 85
1

You could inherit from whatever existing collection was most appropriate (Stack, Dequeue, List, CollectionBase, etc.) and implement this feature yourself. Just override or replace the Add() method.

John Fisher
  • 22,355
  • 2
  • 39
  • 64
1

You want a circular buffer. This other SO question already talks about that and it might help provide some ideas for you.

Community
  • 1
  • 1
Erich Mirabal
  • 9,860
  • 3
  • 34
  • 39
0
public class CircularBuffer<T> : ICollection<T>, IEnumerable<T>, ICollection, IEnumerable
{
    private int capacity;
    private int size;
    private int head;
    private int tail;
    private T[] buffer;

    [NonSerialized()]
    private object syncRoot;

    public CircularBuffer(int capacity)
        : this(capacity, false)
    {
    }

    public CircularBuffer(int capacity, bool allowOverflow)
    {
        if (capacity < 0)
            throw new ArgumentException(Properties.Resources.MessageZeroCapacity, "capacity");

        this.capacity = capacity;
        size = 0;
        head = 0;
        tail = 0;
        buffer = new T[capacity];
        AllowOverflow = allowOverflow;
    }

    public bool AllowOverflow
    {
        get;
        set;
    }

    public int Capacity
    {
        get { return capacity; }
        set
        {
            if (value == capacity)
                return;

            if (value < size)
                throw new ArgumentOutOfRangeException("value", Properties.Resources.MessageCapacityTooSmall);

            var dst = new T[value];
            if (size > 0)
                CopyTo(dst);
            buffer = dst;

            capacity = value;
        }
    }

    public int Size
    {
        get { return size; }
    }

    public bool Contains(T item)
    {
        int bufferIndex = head;
        var comparer = EqualityComparer<T>.Default;
        for (int i = 0; i < size; i++, bufferIndex++)
        {
            if (bufferIndex == capacity)
                bufferIndex = 0;

            if (item == null && buffer[bufferIndex] == null)
                return true;
            else if ((buffer[bufferIndex] != null) &&
                comparer.Equals(buffer[bufferIndex], item))
                return true;
        }

        return false;
    }

    public void Clear()
    {
        size = 0;
        head = 0;
        tail = 0;
    }

    public int Put(T[] src)
    {
        return Put(src, 0, src.Length);
    }

    public int Put(T[] src, int offset, int count)
    {
        if (!AllowOverflow &&  count > capacity - size)
            throw new InvalidOperationException(Properties.Resources.MessageBufferOverflow);

        int srcIndex = offset;
        for (int i = 0; i < count; i++, tail++, srcIndex++)
        {
            if (tail == capacity)
                tail = 0;
            buffer[tail] = src[srcIndex];
        }
        size = Math.Min(size + count, capacity);
        return count;
    }

    public void Put(T item)
    {
        if (!AllowOverflow && size == capacity)
            throw new InvalidOperationException(Properties.Resources.MessageBufferOverflow);

        buffer[tail] = item;
        if (++tail == capacity)
            tail = 0;
        size++;
    }

    public void Skip(int count)
    {
        head += count;
        if (head >= capacity)
            head -= capacity;
    }

    public T[] Get(int count)
    {
        var dst = new T[count];
        Get(dst);
        return dst;
    }

    public int Get(T[] dst)
    {
        return Get(dst, 0, dst.Length);
    }

    public int Get(T[] dst, int offset, int count)
    {
        int realCount = Math.Min(count, size);
        int dstIndex = offset;
        for (int i = 0; i < realCount; i++, head++, dstIndex++)
        {
            if (head == capacity)
                head = 0;
            dst[dstIndex] = buffer[head];
        }
        size -= realCount;
        return realCount;
    }

    public T Get()
    {
        if (size == 0)
            throw new InvalidOperationException(Properties.Resources.MessageBufferEmpty);

        var item = buffer[head];
        if (++head == capacity)
            head = 0;
        size--;
        return item;
    }

    public void CopyTo(T[] array)
    {
        CopyTo(array, 0);
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        CopyTo(0, array, arrayIndex, size);
    }

    public void CopyTo(int index, T[] array, int arrayIndex, int count)
    {
        if (count > size)
            throw new ArgumentOutOfRangeException("count", Properties.Resources.MessageReadCountTooLarge);

        int bufferIndex = head;
        for (int i = 0; i < count; i++, bufferIndex++, arrayIndex++)
        {
            if (bufferIndex == capacity)
                bufferIndex = 0;
            array[arrayIndex] = buffer[bufferIndex];
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        int bufferIndex = head;
        for (int i = 0; i < size; i++, bufferIndex++)
        {
            if (bufferIndex == capacity)
                bufferIndex = 0;

            yield return buffer[bufferIndex];
        }
    }

    public T[] GetBuffer()
    {
        return buffer;
    }

    public T[] ToArray()
    {
        var dst = new T[size];
        CopyTo(dst);
        return dst;
    }

    #region ICollection<T> Members

    int ICollection<T>.Count
    {
        get { return Size; }
    }

    bool ICollection<T>.IsReadOnly
    {
        get { return false; }
    }

    void ICollection<T>.Add(T item)
    {
        Put(item);
    }

    bool ICollection<T>.Remove(T item)
    {
        if (size == 0)
            return false;

        Get();
        return true;
    }

    #endregion

    #region IEnumerable<T> Members

    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion

    #region ICollection Members

    int ICollection.Count
    {
        get { return Size; }
    }

    bool ICollection.IsSynchronized
    {
        get { return false; }
    }

    object ICollection.SyncRoot
    {
        get
        {
            if (syncRoot == null)
                Interlocked.CompareExchange(ref syncRoot, new object(), null);
            return syncRoot;
        }
    }

    void ICollection.CopyTo(Array array, int arrayIndex)
    {
        CopyTo((T[])array, arrayIndex);
    }

    #endregion

    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
        return (IEnumerator)GetEnumerator();
    }

    #endregion
}

You can find this implementation of a circular buffer here: http://circularbuffer.codeplex.com

Beachwalker
  • 7,685
  • 6
  • 52
  • 94
0

ArrayList.FixedSize() method.

user142350
  • 222
  • 4
  • 15
  • 4
    I don't believe this meets his needs. He wanted the new item added and an old one dropped, while ArrayList.FixedSize() will prevent additions to the list. – John Fisher Jul 31 '09 at 15:55