I'm new to C# and would appreciate any feedback you might have on the following doubly-linked list implementation, particularly WRT the following language features:
- Exception handling
- Debug.Assert
- Null state attributes (e.g.
MaybeNullWhen) - The Try pattern with
outparams - Test style with
xunit - Anything else that looks "weird" or unconventional
Thanks very much for your time! <3
Imlementation:
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace DataStructures
{
public class LinkedList<T>
{
public LinkedList(T val)
{
Add(val);
}
public LinkedList()
{
}
public LinkedListNode<T>? HeadNode { get; set; }
public LinkedListNode<T>? TailNode { get; set; }
public int Count { get; private set; }
public T GetHead()
{
return HeadNode is null ? throw new InvalidOperationException(nameof(HeadNode)) : HeadNode.Val;
}
public T GetTail()
{
return TailNode is null ? throw new InvalidOperationException(nameof(TailNode)) : TailNode.Val;
}
public T Add(T val, int? index = null)
{
return index switch
{
null => AddLast(val),
0 => AddFirst(val),
_ => index == Count ? AddLast(val) : AddToMiddle(val, index.Value)
};
}
public void Clear()
{
HeadNode = null;
TailNode = null;
Count = 0;
}
public T GetAt(int index)
{
return GetNodeAt(index).Val;
}
public T RemoveAt(int index)
{
ValidateIndex(index);
return Remove(GetNodeAt(index));
}
public bool TryRemoveFor(Predicate<T> predicate, [ MaybeNullWhen(false) ] out T removed)
{
if (TryFindNode(predicate, out LinkedListNode<T>? node))
{
removed = Remove(node);
return true;
}
removed = default;
return false;
}
public bool TryFind(Predicate<T> predicate, [ MaybeNullWhen(false) ] out T found)
{
if (TryFindNode(predicate, out LinkedListNode<T>? foundNode))
{
found = foundNode.Val;
return true;
}
found = default;
return false;
}
bool TryFindNode(Predicate<T> predicate, [ NotNullWhen(true) ] out LinkedListNode<T>? node)
{
node = HeadNode;
while (node != null)
{
if (predicate(node.Val)) return true;
node = node.Next;
}
return false;
}
LinkedListNode<T> GetNodeAt(int index)
{
ValidateIndex(index);
Debug.Assert(TailNode != null, "The linked list should be non-empty");
if (index == Count - 1)
{
return TailNode;
}
Debug.Assert(HeadNode != null, "The linked list should be non-empty");
LinkedListNode<T> node = HeadNode;
for (var i = 0; i != index; i++)
{
Debug.Assert(node != null, "All nodes within the linked list must be defined");
Debug.Assert(node.Next != null, "This loop shouldn't run for the last entry in the linked list");
node = node.Next;
}
return node;
}
T Remove(LinkedListNode<T> node)
{
if (node.Prev is null)
{
HeadNode = node.Next;
}
else
{
node.Prev.Next = node.Next;
}
if (node.Next is null)
{
TailNode = node.Prev;
}
else
{
node.Next.Prev = node.Prev;
}
Count--;
if (Count <= 1) HeadNode = TailNode = HeadNode ?? TailNode;
return node.Val;
}
T AddFirst(T val)
{
if (HeadNode is null) return AddLast(val);
var newNode = new LinkedListNode<T>(val);
HeadNode.Prev = newNode;
HeadNode = newNode;
Count++;
return newNode.Val;
}
T AddToMiddle(T val, int index)
{
ValidateIndex(index, 1);
var newNode = new LinkedListNode<T>(val);
LinkedListNode<T> toShift = GetNodeAt(index);
LinkedListNode<T>? precedingNode = toShift.Prev;
Debug.Assert(precedingNode != null, "index should not refer to the head");
precedingNode.Next = newNode;
newNode.Prev = precedingNode;
toShift.Prev = newNode;
newNode.Next = toShift;
Count++;
return val;
}
T AddLast(T val)
{
var newNode = new LinkedListNode<T>(val);
if (HeadNode is null)
{
HeadNode = TailNode = newNode;
}
else
{
Debug.Assert(TailNode != null, "Head and tail should both be either null or non-null");
TailNode.Next = newNode;
newNode.Prev = TailNode;
TailNode = newNode;
}
Count++;
return newNode.Val;
}
void ValidateIndex(int index, int lowerBoundInclusive = 0)
{
if (index >= lowerBoundInclusive ||
index < Count)
{
return;
}
throw new ArgumentOutOfRangeException(nameof(index));
}
}
public class LinkedListNode<T>
{
public LinkedListNode(T val, LinkedListNode<T>? prev = null, LinkedListNode<T>? next = null)
{
Val = val;
Prev = prev;
Next = next;
}
public LinkedListNode<T>? Next { get; set; }
public LinkedListNode<T>? Prev { get; set; }
public T Val { get; }
}
}
Tests:
using System;
using DataStructures;
using Xunit;
namespace TestDataStructures
{
public class LinkedListTest
{
const int TestValue = -100;
[ Fact ]
public void InitializeEmptyTest()
{
var ll = new LinkedList<int>();
Assert.Equal(0, ll.Count);
Assert.Null(ll.HeadNode!);
Assert.Null(ll.TailNode!);
Assert.Throws<InvalidOperationException>(() => ll.GetHead());
Assert.Throws<InvalidOperationException>(() => ll.GetTail());
}
[ Fact ]
public void InitializeNonEmptyTest()
{
var ll = new LinkedList<int>(TestValue);
Assert.Equal(1, ll.Count);
Assert.NotNull(ll.HeadNode!);
Assert.NotNull(ll.TailNode!);
Assert.Equal(TestValue, ll.GetHead());
Assert.Equal(TestValue, ll.GetTail());
}
[ Fact ]
public void AddTest()
{
const int numToAdd = 5;
var ll = new LinkedList<int>();
for (var i = 0; i < numToAdd; i++)
{
int added = ll.Add(i);
Assert.Equal(i, added);
}
LinkedListNode<int>? node = ll.HeadNode;
for (var i = 0; i < numToAdd; i++)
{
Assert.Equal(i, node?.Val);
node = node?.Next;
}
Assert.Equal(numToAdd, ll.Count);
}
[ Theory ]
[ InlineData(0, 0) ]
[ InlineData(3, 0) ]
[ InlineData(3, 1) ]
[ InlineData(3, 2) ]
[ InlineData(5, 3) ]
[ InlineData(3, 3) ]
public void AddAtIndexTest(int collectionSize, int? insertIndex = null)
{
var ll = new LinkedList<int>();
for (var i = 0; i < collectionSize; i++)
{
ll.Add(i);
}
int added = ll.Add(TestValue, insertIndex);
Assert.Equal(TestValue, added);
LinkedListNode<int>? node = ll.HeadNode;
for (var i = 0; i != insertIndex; i++)
{
node = node?.Next;
}
Assert.Equal(TestValue, node?.Val);
Assert.Equal(collectionSize + 1, ll.Count);
}
[ Fact ]
public void ClearTest()
{
const int numToAdd = 3;
var ll = new LinkedList<int>();
for (var i = 0; i < numToAdd; i++)
{
int added = ll.Add(i);
Assert.Equal(i, added);
}
ll.Clear();
Assert.Equal(0, ll.Count);
Assert.Null(ll.HeadNode!);
Assert.Null(ll.TailNode!);
Assert.Throws<InvalidOperationException>(() => ll.GetHead());
Assert.Throws<InvalidOperationException>(() => ll.GetTail());
}
[ Theory ]
[ InlineData(0, 3) ]
[ InlineData(1, 3) ]
[ InlineData(2, 3) ]
public void GetAtTest(int getIndex, int collectionSize)
{
var ll = new LinkedList<int>();
for (var i = 0; i < collectionSize; i++)
{
ll.Add(i);
}
int got = ll.GetAt(getIndex);
Assert.Equal(getIndex, got);
}
[ Theory ]
[ InlineData(3, 0, 0) ]
[ InlineData(3, 1, 1) ]
[ InlineData(3, 2, 2) ]
[ InlineData(3, 3) ]
[ InlineData(3, -1) ]
public void TryFindTest(int collectionSize, int toCompare, int? expected = null)
{
bool Predicate(int i)
{
return i == toCompare;
}
var ll = new LinkedList<int>();
for (var i = 0; i < collectionSize; i++)
{
ll.Add(i);
}
bool wasFound = ll.TryFind(Predicate, out int got);
Assert.Equal(expected.HasValue, wasFound);
Assert.Equal(expected ?? default, got);
}
[ Theory ]
[ InlineData(0, 3) ]
[ InlineData(1, 3) ]
[ InlineData(2, 3) ]
[ InlineData(2, 5) ]
public void RemoveAtTest(int removeIndex, int collectionSize)
{
var ll = new LinkedList<int>();
for (var i = 0; i < collectionSize; i++)
{
ll.Add(i);
}
int got = ll.RemoveAt(removeIndex);
Assert.Equal(removeIndex, got);
Assert.Equal(collectionSize - 1, ll.Count);
}
[ Theory ]
[ InlineData(3, 0, 0) ]
[ InlineData(3, 1, 1) ]
[ InlineData(3, 2, 2) ]
[ InlineData(3, 3) ]
[ InlineData(3, -1) ]
public void RemoveForTest(int collectionSize, int toCompare, int? expected = null)
{
bool Predicate(int i)
{
return i == toCompare;
}
var ll = new LinkedList<int>();
for (var i = 0; i < collectionSize; i++)
{
ll.Add(i);
}
bool wasFound = ll.TryRemoveFor(Predicate, out int got);
Assert.Equal(expected.HasValue, wasFound);
Assert.Equal(expected ?? default, got);
Assert.Equal(wasFound ? collectionSize - 1 : collectionSize, ll.Count);
}
}
}
```
nullable-reference-typestag I'll add it to this question. StackOverflow has the feature tagged with that name, so best to keep it the same. \$\endgroup\$