No problem.
Another way would to build a so called ring buffer.
You add elements one by one until you reach the maximum, for example 4.
first, second, third, fourth
But when you introduce the fifth, you actually wrap around the buffer and overwrite the first one (which is simultaneously the oldest entry).
fifth, second, third, fourth
And similarly the sixth
fifth, sixth, third, fourth
This way the actual buffer stays in place, but the “header pointer” moves around in a circle.
This means that if you’d ask for the oldest entry (at index 0), you’d correctly receive “third” as the answer.
Here’s how to implement this.
using System;
using System.Collections.Generic;
public class RingBuffer<T> {
T[] _buffer;
int _head = 0;
int _tail = 0;
public RingBuffer(int maxSize) {
if(maxSize < 0) throw new ArgumentException(nameof(maxSize));
_buffer = new T[maxSize];
}
public int MaxSize => _buffer.Length;
public int Count => _tail;
// returns true when something was dropped
public bool Enqueue(T item) {
if(MaxSize == 0) return false;
_buffer[windex(_head + _tail)] = item;
_tail++;
if(_tail <= MaxSize) return false;
_head = windex(_head + 1);
_tail = MaxSize;
return true;
}
// removes the first (oldest) element and returns it
// will return default(T) when there are no elements
public T Dequeue() {
if(MaxSize == 0 || _tail == 0) return default;
int w = windex(_head);
T result = _buffer[w];
_buffer[w] = default;
_head = windex(_head + 1);
_tail--;
return result;
}
// Same as enqueue, but in the context of a stack
public bool Push(T item) => Enqueue(item);
// removes the last (youngest) element and returns it
// will return default(T) when there are no elements
public T Pop() {
if(MaxSize == 0 || _tail == 0) return default;
int w = windex(_head + _tail - 1);
T result = _buffer[w];
_buffer[w] = default;
_tail--;
return result;
}
// Same as dequeue but doesn't remove anything
public T Peek() => this[0];
public void Clear() {
_head = 0;
_tail = 0;
}
// a custom indexer allows for random access
public T this[int index] {
get {
if(MaxSize == 0 || index < 0 || index > _tail) throw new ArgumentOutOfRangeException();
return _buffer[windex(_head + index)];
}
set {
if(MaxSize == 0 || index < 0 || index > _tail) throw new ArgumentOutOfRangeException();
_buffer[windex(_head + index)] = value;
}
}
public IEnumerable<T> Items { get {
for(int i = 0; i < MaxSize; i++) {
yield return _buffer[windex(_head + i)];
}
} }
// utility for wrapping the index around the ring via "clock arithmetic"
int windex(int index) => index % MaxSize;
// a general purpose solution which got optimized away for this particular implementation
// this is because index and MaxSize are guaranteed to be >= 0
// int windex(int index) => mod(index, MaxSize);
// static int mod(int n, int m) => m <= 0? 0 : (n %= m) < 0? n + m : n;
}
Test
var buf = new RingBuffer<string>(5); // 5 is the maximum capacity
for(int i = 0; i < 8; i++) {
buf.Enqueue($"string_{i}");
Debug.Log($"Count: {buf.Count}"); // 1,2,3,4,5,5,5,5
}
buf.Dequeue();
Debug.Log(buf.Dequeue()); // "string_4"
Debug.Log($"Count: {buf.Count}"); // 3
buf.Enqueue("test1");
buf.Dequeue();
Debug.Log($"Count: {buf.Count}"); // 3
buf.Enqueue("string_8");
Debug.Log(buf.Pop()); // "string_8"
Debug.Log($"Count: {buf.Count}"); // 3
buf.Enqueue("string_9");
buf.Enqueue("test2");
Debug.Log($"Count: {buf.Count}"); // 5
(buf[0], buf[1]) = (buf[1], buf[0]); // swaps the first two elements
buf.Enqueue("string_12");
Console.WriteLine($"Count: {buf.Count}"); // 5
foreach(var x in buf.Items) {
Debug.Log(x);
}
// string_6
// test1
// string_9
// test2
// string_12
Edit:
Bunny was faster 
Edit2:
Added Peek, Push, indexer setter and a test.
Edit3:
Peek had a typo, return yield → yield return, also Count was incorrect, added swap (and counting) to the test.