I wrote an implementation of the Linked list data structure. I mostly did this because it's the simplest collection data structure (at least the simplest to implement), and I wanted to practice implementing IEnumerable<T>
and IEnumerator<T>
. I also wanted to play around with generics.
This was entirely a practice exercise to come to better grips with the language (it's also why I avoided using the yield return
syntax for my enumerator; I wanted to implement an enumerator the "hard" way without any of the syntactic sugar).
As a learning exercise, it was fruitful.
That said, my project has three namespaces:
- SimpleTypes (
Node<TData>
is defined here) - CollectionTypes.Enumerators (
LinkedListEnumerator
is defined here) - CollectionTypes (
LinkedList<TData>
is defined here)
Node.cs
namespace SimpleTypes
{
public class Node<TData>
{
public TData Data { get; set; }
public Node<TData> Next { get; set; }
public Node(TData data)
{
Data = data;
}
public override string ToString()
{
return Data.ToString();
}
}
}
LinkedListEnumerator.cs
using System.Collections.Generic;
using System.Collections;
using SimpleTypes;
namespace CollectionTypes.Enumerators
{
internal class LinkedListEnumerator<TData>: IEnumerator<TData>
{
private Node<TData> _current;
private readonly Node<TData> _start;
private bool _isDisposed;
public TData Current { get => _current.Data; set { _current.Data = value; } }
// Implementing `IEnumerator<T>` requires an implementation of `IEnumerator`
object IEnumerator.Current { get => this.Current; } // Explicit interface specifications don't have any access specifiers
public LinkedListEnumerator(Node<TData> head)
{
_current = _start = new Node<TData>(default(TData));
_start.Next = _current.Next = head;
_isDisposed = false;
}
public bool MoveNext()
{
bool keepGoing = true;
if (_current.Next is null)
keepGoing = false;
_current = _current.Next;
return keepGoing;
}
public void Reset()
{
_current = _start;
}
public void Dispose()
{
// Dispose of unmanaged resources
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (_isDisposed)
return;
if (disposing)
{
// Dispose of managed resources
}
_current = null;
_isDisposed = true;
}
~LinkedListEnumerator()
{
Dispose(false);
}
}
}
LinkedList.cs
using System.Collections;
using System.Collections.Generic;
using SimpleTypes;
using CollectionTypes.Enumerators;
namespace CollectionTypes
{
public class LinkedList<TData>: IEnumerable<TData>
{
public Node<TData> Head { get; set; }
public Node<TData> Tail { get; set; }
public LinkedList()
{
Head = Tail = null;
}
#region List API
public void AddHead(TData data)
{
Node<TData> newHead = new Node<TData>(data);
if (Head is null)
Tail = newHead;
else
newHead.Next = Head;
Head = newHead;
}
public void AddTail(TData data)
{
Node<TData> newTail = new Node<TData>(data);
if (Tail is null)
Head = newTail;
else
Tail.Next = newTail;
Tail = newTail;
}
#endregion
#region Interface Implementations
public IEnumerator<TData> GetEnumerator()
{
return new LinkedListEnumerator<TData>(Head);
}
// Implementing `IEnumerable<T>` requires an implementation of `IEnumerable`
IEnumerator IEnumerable.GetEnumerator() // Explicit interface specifications don't have any access specifiers
{
return this.GetEnumerator();
}
#endregion
public override string ToString()
{
string result = "";
foreach (TData item in this)
{
result += item.ToString() + ", ";
}
return result.TrimEnd(new char[]{' ', ','});
}
}
}
For feedback, I'm most interested in:
- Style, idioms and best practices
- My implementation of the data type and how closely it matches the linked list API
- Performance concerns
ToString
method is very inefficient. Use eitherstring.Join
orStringBuilder
instead of string concatenation. \$\endgroup\$string.Join()
worked for anyIEnumerable
. \$\endgroup\$