Introduction

A C# collection is a group of multiple objects. The .NET Framework provides collection classes in both System.Collections and System.Collections.Generic namespaces. As discussed in previous articles, System.Collection classes are not type-safe. Since .NET Framework 2.0, we can use generic collections that provide type safety, better performance, and improved code maintainability.

Additional Collection Namespaces

  • System.Collections.Specialized: For specific collection types
  • System.Collections.Concurrent: For thread-safe collections

List<T> - Dynamic Arrays

List<T> is a generic class for creating dynamic lists. It implements multiple interfaces:

Interface Description
Interface IList<T> Description Index-based access, InsertAt(), RemoveAt() methods
Interface ICollection<T> Description Count property, Add(), Remove(), Clear(), CopyTo() methods
Interface IEnumerable<T> Description Required for foreach statements, defines GetEnumerator()

Creating a List

// Default constructor (initial capacity = 4)
List<int> numbers = new List<int>();

// With specified capacity
List<int> numbers = new List<int>(10);

// With collection initializer
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };

// From existing collection
List<int> copy = new List<int>(numbers);

Adding and Accessing Elements

public class Student : IComparable<Student>
{
    public int RollNo { get; set; }
    public string Name { get; set; }
    public int Marks { get; set; }

    public Student(string name, int marks = 0, int rollNo = 0)
    {
        Name = name;
        Marks = marks;
        RollNo = rollNo;
    }

    public int CompareTo(Student other)
    {
        return this.Name.CompareTo(other.Name);
    }
}

// Create and populate list
List<Student> students = new List<Student>()
{
    new Student("Jay", 80, 1),
    new Student("Ajay", 70, 2),
    new Student("Sanjay", 60, 3)
};

// Access elements
foreach (Student student in students)
{
    Console.WriteLine($"{student.Name} - {student.RollNo} - {student.Marks}");
}

// Insert at specific position
students.Insert(1, new Student("Vijay", 75, 4));

Capacity Management

Performance Tip: List capacity doubles when exceeded (4 → 8 → 16 → 32). If you know the size beforehand, specify capacity to avoid reallocations.
List<int> list = new List<int>();
list.Capacity = 100; // Set capacity explicitly

// After operations, remove unused capacity
list.TrimExcess(); // Only if count > 90% of capacity

ForEach Method with Action<T>

List<Student> students = new List<Student>() { /* ... */ };

// Using ForEach with Console.WriteLine
students.ForEach(Console.WriteLine);

// Using lambda expression
students.ForEach(s => Console.WriteLine($"{s.Name}: {s.Marks}"));

Removing Elements

// Remove by object (uses IEquatable<T> or Equals())
if (students.Remove(student))
{
    Console.WriteLine("Student removed");
}

// Remove by index (more efficient)
students.RemoveAt(0);

// Remove range
students.RemoveRange(0, 2); // Remove 2 elements starting at index 0

Searching Elements

// IndexOf - returns index or -1
int index = students.IndexOf(student);

// FindIndex with predicate
int index = students.FindIndex(s => s.RollNo == 2);

// Find - returns first matching element
Student student = students.Find(s => s.Name == "Jay");

// FindAll - returns all matching elements
List<Student> jays = students.FindAll(s => s.Name == "Jay");

// Exists - returns bool
bool exists = students.Exists(s => s.Marks > 70);

Sorting

// Default sort (uses IComparable<T>)
students.Sort();

// Custom comparer with IComparer<T>
public class StudentComparer : IComparer<Student>
{
    private enum SortType { Name, RollNo, Marks }
    private SortType type;

    public StudentComparer(SortType type)
    {
        this.type = type;
    }

    public int Compare(Student x, Student y)
    {
        switch (type)
        {
            case SortType.Marks:
                return x.Marks.CompareTo(y.Marks);
            case SortType.RollNo:
                return x.RollNo.CompareTo(y.RollNo);
            default:
                return x.Name.CompareTo(y.Name);
        }
    }
}

// Usage
students.Sort(new StudentComparer(StudentComparer.SortType.Marks));

// Lambda expression sort
students.Sort((s1, s2) => s1.Marks.CompareTo(s2.Marks));

Queue<T> - FIFO Collection

Queue implements First-In-First-Out (FIFO) processing. Elements added first are accessed first.

Method/Property Description
Method/Property Enqueue(T) Description Adds element to the end
Method/Property Dequeue() Description Removes and returns element from start
Method/Property Peek() Description Returns element from start without removing
Method/Property Count Description Number of items in queue
Method/Property TrimExcess() Description Reduces capacity to match count

Queue Example: Music Player

public class Music
{
    public string SongName { get; set; }
    public string Song { get; set; }

    public Music(string songName, string song)
    {
        SongName = songName;
        Song = song;
    }
}

public class MusicLibrary
{
    private readonly Queue<Music> queue = new Queue<Music>();

    public void AddSong(Music music)
    {
        lock (this)
        {
            queue.Enqueue(music);
        }
    }

    public Music GetSong()
    {
        Music music = null;
        lock (this)
        {
            if (queue.Count > 0)
                music = queue.Dequeue();
        }
        return music;
    }

    public bool HasSongs => queue.Count > 0;
}

// Usage
MusicLibrary library = new MusicLibrary();
library.AddSong(new Music("Song1", "song1.mp3"));
library.AddSong(new Music("Song2", "song2.mp3"));

while (library.HasSongs)
{
    Music music = library.GetSong();
    Console.WriteLine($"Playing: {music.SongName}");
}

Stack<T> - LIFO Collection

Stack implements Last-In-First-Out (LIFO) processing. The most recently added element is accessed first.

Method/Property Description
Method/Property Push(T) Description Adds element on top
Method/Property Pop() Removes and returns top element Description
Method/Property Peek() Returns top element without removing Description
Method/Property Contains(T) Description Checks if element exists
Method/Property Count Description Number of items in stack

Stack Example

Stack<Music> stack = new Stack<Music>();

stack.Push(new Music("Song1", "song1.mp3"));
stack.Push(new Music("Song2", "song2.mp3"));
stack.Push(new Music("Song3", "song3.mp3"));

// Access elements (doesn't remove them)
foreach (Music music in stack)
{
    Console.WriteLine($"{music.SongName} - {music.Song}");
}

// Pop removes elements
while (stack.Count > 0)
{
    Music music = stack.Pop();
    Console.WriteLine($"Popped: {music.SongName}");
}

LinkedList<T> - Doubly-Linked List

LinkedList<T> is a doubly-linked list where each element holds references to both next and previous elements. Great for insertions/deletions but slower for searching.

Use When:
  • Frequent insertions/deletions at any position
  • No need for index-based access
  • Sequential access is acceptable

LinkedList Methods

Method Description
Method AddFirst(T) Description Add at the beginning
Method AddLast(T) Description Add at the end
Method AddAfter(node, T) Description Add after specific node
Method AddBefore(node, T) Description Add before specific node
Method Remove(T) Description Remove specific value
Method RemoveFirst() Description Remove first element
Method RemoveLast() Description Remove last element
Method Find(T) Description Find first occurrence
Method FindLast(T) Description Find last occurrence

LinkedList Example

LinkedList<string> list = new LinkedList<string>();

// Add elements
list.AddLast("First");
list.AddLast("Second");
list.AddFirst("Zero");

// Navigate using nodes
LinkedListNode<string> node = list.First;
while (node != null)
{
    Console.WriteLine(node.Value);
    node = node.Next;
}

// Insert relative to existing node
LinkedListNode<string> secondNode = list.Find("Second");
if (secondNode != null)
{
    list.AddAfter(secondNode, "After Second");
    list.AddBefore(secondNode, "Before Second");
}

SortedList<TKey, TValue>

SortedList maintains elements sorted by key. It combines features of both lists and dictionaries.

Creating and Using SortedList

SortedList<string, Music> sortedList = new SortedList<string, Music>();

// Add elements
sortedList.Add("3Key", new Music("Song3", "song3.mp3"));
sortedList.Add("1Key", new Music("Song1", "song1.mp3"));
sortedList.Add("2Key", new Music("Song2", "song2.mp3"));

// Or use indexer
sortedList["4Key"] = new Music("Song4", "song4.mp3");

// Access elements (automatically sorted by key)
foreach (KeyValuePair<string, Music> item in sortedList)
{
    Console.WriteLine($"{item.Key}: {item.Value.SongName}");
}

// Safe access with ContainsKey
if (sortedList.ContainsKey("1Key"))
{
    Music music = sortedList["1Key"];
    Console.WriteLine($"Found: {music.SongName}");
}

// Or use TryGetValue
Music value;
if (sortedList.TryGetValue("2Key", out value))
{
    Console.WriteLine($"Found: {value.SongName}");
}

Important Note

SortedList<TKey, TValue> stores only one value per key. If you need multiple values per key, use Lookup<TKey, TElement> class (covered in advanced topics).

Performance Comparison

Collection Add Insert Remove Search Best Use Case
Collection List<T> Add O(1)* Insert O(n) Remove O(n) Search O(n) Best Use Case General purpose, index access
Collection LinkedList<T> Add O(1) Insert O(1)** Remove O(1)** Search O(n) Best Use Case Frequent insertions/deletions
Collection Queue<T> Add O(1) Insert N/A Remove O(1) Search N/A Best Use Case FIFO processing
Collection Stack<T> Add O(1) Insert N/A Remove O(1) Search N/A Best Use Case LIFO processing
Collection SortedList<K,V> Add O(n) Insert O(n) Remove O(n) Search O(log n) Best Use Case Sorted key-value pairs

* Amortized O(1) - O(n) when capacity exceeded
** When node reference is known

Best Practices

Collection Selection Guidelines:
  • List<T>: Default choice for most scenarios
  • LinkedList<T>: Use when frequently inserting/removing in the middle
  • Queue<T>: Use for task processing, breadth-first search
  • Stack<T>: Use for undo operations, depth-first search
  • SortedList<K,V>: Use when you need sorted key-value pairs
  • Dictionary<K,V>: Use for fast lookups (covered in Part 2)

Common Patterns

Converting Between Collections

// List to Array
List<int> list = new List<int> { 1, 2, 3 };
int[] array = list.ToArray();

// Array to List
int[] numbers = { 1, 2, 3, 4, 5 };
List<int> list2 = new List<int>(numbers);

// List to Queue
Queue<int> queue = new Queue<int>(list);

// List to Stack
Stack<int> stack = new Stack<int>(list);

Read-Only Collections

List<int> list = new List<int> { 1, 2, 3 };

// Create read-only wrapper
System.Collections.ObjectModel.ReadOnlyCollection<int> readOnly = list.AsReadOnly();

// Modifications not allowed
// readOnly.Add(4); // Compile error!

Conclusion

Generic collection classes are essential tools in C# development. They provide type safety, better performance compared to non-generic collections, and a rich set of features for managing data. Understanding when to use each collection type is crucial for writing efficient applications.

Arrays are fixed in size, but generic collections provide dynamic sizing. List<T> is the general-purpose choice, while specialized collections like Queue, Stack, and LinkedList serve specific use cases. Always consider your access patterns, performance requirements, and data organization needs when choosing a collection type.