The first thing to be implemented is the Add
method.
The Interface
The following snippet shows the result interface of creating the Add
method and some
arbitrary methods for better testing.
The relevant merge request is this one.
namespace AndrasCsanyi.DataStructuresAndAlgs.DynamicSizeArray;
/// <summary>
/// Dynamic size array, short name, List.
/// The array is typed meaning it can handle only a single type.
/// The list is based on an array meaning this type in contiguous. The size of this list is dynamically managed and
/// doesn't require any act from the user.
/// </summary>
/// <typeparam name="T">The type of the items in the list.</typeparam>
public interface IDynamicSizeArray<T>
{
// add method snippet start
/// <summary>
/// Adds a type T elem to the list. The place of the item is after the last item in the list.
/// </summary>
/// <param name="item">The item to be added.</param>
void Add(T item);
/// <summary>
/// Returns the elements at the defined index value.
/// </summary>
/// <param name="index">The index value.</param>
/// <returns>Element of {T}.</returns>
T Get(int index);
// add method snippet end
/// <summary>
/// Returns the amount of elements in the Dynamic Size Array. The amount of elements does not equal to the allocated
/// size of the storage array of the implementation. For that <see cref="DynamicSizeArray{T}.StorageSize()" />
/// test method.
/// </summary>
/// <returns>The amount of elements in the Dynamic Size Array.</returns>
int Count();
}
First we need the previously mentioned Add
method which returns void
when it is called.
As I mentioned previously nothing fancy here just the bare functionality and when needed we
will add further interfaces and comfort things like you can find in what Dotnet team provides.
The Get
method is needed for testing and to have access to a particular index in the
underlying array.
The Count
method provides the information about how many elements are in the list.
As the documentation says it is not the allocated size of the underlying array.
The Implementation
At this moment two files, both included below, contains the implementation. The testmethods one includes methods let us to take a look under the hood and test the inner workings of the implementation too. These methods should be stripped if this code would be a production code.
Initialization
When a DynamicSizeArray
is initialized by DynamicSizeArray<int> a = new();
the following happens:
- a new typed array will be created and its default size is
8
. Why 8? Because I like this number. We don't have information about with how big arrays our users, they don't exist, are working so we just need a number. - the default size of the storage (the underlying array) is also will be set up. This number is needed when the array size will be increased and it is an implementation details our users should not know.
- at this point the array is empty, meaning
0
elements are in it, the_amountOfElementsInTheStorage
is default 0.
At this point worth to take a look at the Defaults.cs
test file where the default values are
tested.
The explanation for the tests are at the bottom of this page.
Adding an element
When at element is added to the array two scenario can happen:
- the size of the underlying array provides enough space and the item can be added
- the size of the underlying array is not enough so its size needs to be increased
The whole logic is based on knowing how many elements are in the _storage
, this one has to be
tracked manually, and how big the allocated size of the array, this information is there and
can be accessed at any time. The _amountOfElementsInTheStorage
is basically a pointer to the
last not default value item in the _storage
array.
In the case of when the size of _storage
doesn't have to be changed the logic is pretty
straightforward. First the new item should be added after the last item item in the array. The
_amountOfElementsInTheStorage
pointer plays a dual role here. Its value tells us how many
elements are in the array like 1, 2 or 3, it also tells us the next available place in the
array due to the zero indexing nature of the whole shebang. It means that when we have an item
on the index 0
it means we have 1
item in the array and that the slot under the 1
is
available.
The other scenario is when array size management is needed. The first step here
is creating a new array with the increased size. The default strategy is
doubling the size of the previous array. At this point we need the
_storageAllocatedSize
to know what is the size we are looking for. The next
step is copying all items from the old _storage
to the new one. Here we have
to pay attention to that the index values must be the same. However, just simply
iterating through the array is fine. The next step is setting the variable
values controlling the inner workings of the DynamicSizeArray
like
_amountOfElementsInTheStorage
, _storageAllocatedSize
and overwriting the old
_storage
variable with the new one. At this point the DynamicSizeArray
increased its size and behaves like nothing happened.
DynamicSizeArray.cs
file
namespace AndrasCsanyi.DataStructuresAndAlgs.DynamicSizeArray;
public partial class DynamicSizeArray<T> : IDynamicSizeArray<T>
{
private readonly int _storageDefaultSize = 8;
private int _amountOfElementsInTheStorage;
private T[] _storage;
private int _storageAllocatedSize;
public DynamicSizeArray()
{
_storage = new T[_storageDefaultSize];
_storageAllocatedSize = _storageDefaultSize;
}
public void Add(T item)
{
if (_amountOfElementsInTheStorage == _storageAllocatedSize)
{
T[] newStorage = CreateIncreasedStorageWithContent(_storage);
_amountOfElementsInTheStorage = _storageAllocatedSize + 1;
_storageAllocatedSize = newStorage.Length;
_storage = newStorage;
return;
}
_storage[_amountOfElementsInTheStorage] = item;
_amountOfElementsInTheStorage++;
}
public T Get(int index)
{
if (index > _amountOfElementsInTheStorage - 1)
{
throw new ArgumentOutOfRangeException(
$"There is no item at the {index} in this array."
);
}
return _storage[index];
}
public int Count() => _amountOfElementsInTheStorage;
private T[] CreateIncreasedStorageWithContent(T[] storage)
{
T[] newStorage = new T[_storageAllocatedSize * 2];
for (int i = 0; i < storage.Length; i++)
{
newStorage[i] = _storage[i];
}
return newStorage;
}
}
DynamicSizeArray.TestMethods.cs
file.
namespace AndrasCsanyi.DataStructuresAndAlgs.DynamicSizeArray;
public partial class DynamicSizeArray<T>
{
public int StorageSize()
{
return _storage.Length;
}
public int StorageDefaultSize()
{
return _storageDefaultSize;
}
}
Get method
In order to make the DynamicSizeArray
easy to test I added the Get(int index)
method. It returns the element under the provided index value.
The input is validated in order to throw a controlled exception and not either
returning the given T
type default
value or IndexOutOfBoundException
. For
the users this DynamicSizeArray
is not bigger then all the items they put into
it. They don't have to know about that the array is actually bigger.
Tests
Defaults
The code below tests if the DynamicSizeArray
underlying array got created with
the correct allocation size. The default size is an implementation detail, but
made available for testing purposes.
[Fact]
public void DefaultStorageSize()
{
DynamicSizeArray<int> dsa = new();
dsa.StorageSize().Should().Be(dsa.StorageDefaultSize());
}
The following test checks if the volume of elements in the array (what the user put into it) is zero right after instantiation.
[Fact]
public void DefaultElemSize()
{
DynamicSizeArray<int> dsa = new();
dsa.Count().Should().Be(0);
}
Add method
The following test checks what happens when an element is added to the array,
but it doesn't trigger size management. Once the element is added the Count()
and StorageSize()
values are checked if the expected behaviour is there.
The for
loop is for checking if the element indeed in the array.
[Fact]
public void AddElem()
{
DynamicSizeArray<int> su = new();
su.Add(11);
su.Count().Should().Be(1);
su.StorageSize().Should().Be(8);
for (int i = 0; i < su.Count(); i++)
{
su.Get(i).Should().Be(11);
}
}
The test below tests the scenario where a size management event happens. First,
a new DynamicSizeArray
is created and it is filled up with elements to the
point when size management is not triggered yet. The inner workings are checked
at this point. As a next step and new item is added and right after the
StorageSize
(this has to be doubled compared to the previous one) and
Count()
checked. The Count()
value must show that a new item has been added
to the array, so its value got increased by one.
Get method
The following test is a quite simple one. The functionality of adding an element and getting it back is checked.
[Fact]
public void ReturnItem()
{
int testValue = 11;
DynamicSizeArray<int> su = new();
su.Add(testValue);
su.Get(0).Should().Be(testValue);
}
The following test is about testing the validation of the Get
method. If the
requested index is higher than the elements in the array an exception will be
thrown.
[Fact]
public void Throw_WhenIndexOutOfRange()
{
DynamicSizeArray<int> su = new();
su.Add(11);
su.Get(0).Should().Be(11);
Action action = () => su.Get(1);
action.Should().ThrowExactly<ArgumentOutOfRangeException>();
}