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:
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
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.
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.
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.
- Frequent insertions/deletions at any position
- No need for index-based access
- Sequential access is acceptable
LinkedList Methods
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
* Amortized O(1) - O(n) when capacity exceeded
** When node reference is known
Best Practices
- 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.