Skip to main content

C# Generic Collections

Introduction

Generic collections are an essential part of C# programming that allow you to create strongly-typed collections of objects. Introduced in C# 2.0 with the .NET Framework 2.0, generic collections provide type safety, better performance, and cleaner code compared to non-generic collections.

In this tutorial, we'll explore the various generic collections available in C#, how they work, when to use them, and their benefits over non-generic collections.

What are Generic Collections?

Generic collections are a set of classes in the System.Collections.Generic namespace that provide type-safe data structures. Unlike the older non-generic collections in the System.Collections namespace, generic collections allow you to specify the exact type of elements they can contain.

The main benefits of using generic collections include:

  • Type safety: Compiler checks ensure you can only add appropriate types
  • Elimination of boxing/unboxing: Better performance with value types
  • Code clarity: Less casting and cleaner code
  • Reusability: Same collection class can be used with different types

Common Generic Collections

Let's explore the most commonly used generic collections in C#:

1. List<T>

List<T> is one of the most commonly used generic collections. It represents a strongly typed list of objects that can be accessed by index.

csharp
// Creating a list of integers
List<int> numbers = new List<int>();

// Adding items
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);

// Adding multiple items at once
numbers.AddRange(new int[] { 4, 5, 6 });

// Accessing items by index
Console.WriteLine($"First number: {numbers[0]}");

// Iterating through the list
Console.WriteLine("All numbers:");
foreach (int number in numbers)
{
Console.WriteLine(number);
}

// Finding elements
bool containsThree = numbers.Contains(3); // true
int indexOfFive = numbers.IndexOf(5); // 4

// Removing elements
numbers.Remove(3);
numbers.RemoveAt(1); // Remove element at index 1

// Count of elements
Console.WriteLine($"Count: {numbers.Count}");

Output:

First number: 1
All numbers:
1
2
3
4
5
6
Count: 4

2. Dictionary<TKey, TValue>

Dictionary<TKey, TValue> represents a collection of key-value pairs where each key is unique.

csharp
// Creating a dictionary with string keys and int values
Dictionary<string, int> ages = new Dictionary<string, int>();

// Adding key-value pairs
ages.Add("John", 25);
ages.Add("Mary", 30);
ages.Add("Bob", 22);

// Alternative way to add or set values
ages["Alice"] = 28;

// Accessing values
Console.WriteLine($"John's age: {ages["John"]}");

// Checking if a key exists
if (ages.ContainsKey("Bob"))
{
Console.WriteLine($"Bob's age: {ages["Bob"]}");
}

// Safely getting a value
if (ages.TryGetValue("Charlie", out int charlieAge))
{
Console.WriteLine($"Charlie's age: {charlieAge}");
}
else
{
Console.WriteLine("Charlie's age is unknown");
}

// Iterating through a dictionary
Console.WriteLine("\nAll ages:");
foreach (KeyValuePair<string, int> pair in ages)
{
Console.WriteLine($"{pair.Key}: {pair.Value}");
}

// Accessing just keys or values
Console.WriteLine("\nAll names:");
foreach (string name in ages.Keys)
{
Console.WriteLine(name);
}

Output:

John's age: 25
Bob's age: 22
Charlie's age is unknown

All ages:
John: 25
Mary: 30
Bob: 22
Alice: 28

All names:
John
Mary
Bob
Alice

3. HashSet<T>

HashSet<T> represents a set of unique values. It's useful when you need to ensure you don't have duplicates.

csharp
// Creating a HashSet of strings
HashSet<string> fruits = new HashSet<string>();

// Adding items
fruits.Add("Apple");
fruits.Add("Banana");
fruits.Add("Orange");

// Adding duplicate (will be ignored)
bool added = fruits.Add("Apple");
Console.WriteLine($"Was 'Apple' added again? {added}");

// Check if item exists
bool containsBanana = fruits.Contains("Banana"); // true

// Iterating through the set
Console.WriteLine("\nAll fruits:");
foreach (string fruit in fruits)
{
Console.WriteLine(fruit);
}

// Set operations
HashSet<string> moreFruits = new HashSet<string> { "Apple", "Grape", "Kiwi" };

// Union - combines both sets
fruits.UnionWith(moreFruits);

Console.WriteLine("\nAfter union:");
foreach (string fruit in fruits)
{
Console.WriteLine(fruit);
}

// Create new sets for demonstration
HashSet<string> set1 = new HashSet<string> { "A", "B", "C" };
HashSet<string> set2 = new HashSet<string> { "B", "C", "D" };

// Intersection - items that exist in both sets
set1.IntersectWith(set2);

Console.WriteLine("\nIntersection result:");
foreach (string item in set1)
{
Console.WriteLine(item);
}

Output:

Was 'Apple' added again? False

All fruits:
Apple
Banana
Orange

After union:
Apple
Banana
Orange
Grape
Kiwi

Intersection result:
B
C

4. Queue<T>

Queue<T> represents a first-in, first-out (FIFO) collection of objects.

csharp
// Creating a queue of string messages
Queue<string> messageQueue = new Queue<string>();

// Adding items (enqueue)
messageQueue.Enqueue("Message 1");
messageQueue.Enqueue("Message 2");
messageQueue.Enqueue("Message 3");

// Checking what's next without removing (peek)
string nextMessage = messageQueue.Peek();
Console.WriteLine($"Next message to process: {nextMessage}");

// Processing the queue (dequeue)
Console.WriteLine("\nProcessing messages:");
while (messageQueue.Count > 0)
{
string currentMessage = messageQueue.Dequeue();
Console.WriteLine($"Processing: {currentMessage}");
}

// Queue is now empty
Console.WriteLine($"\nMessages remaining: {messageQueue.Count}");

Output:

Next message to process: Message 1

Processing messages:
Processing: Message 1
Processing: Message 2
Processing: Message 3

Messages remaining: 0

5. Stack<T>

Stack<T> represents a last-in, first-out (LIFO) collection of objects.

csharp
// Creating a stack of integers
Stack<int> numberStack = new Stack<int>();

// Adding items (push)
numberStack.Push(10);
numberStack.Push(20);
numberStack.Push(30);

// Checking top item without removing (peek)
int topNumber = numberStack.Peek();
Console.WriteLine($"Top number: {topNumber}");

// Removing and processing items (pop)
Console.WriteLine("\nPopping numbers:");
while (numberStack.Count > 0)
{
int current = numberStack.Pop();
Console.WriteLine($"Popped: {current}");
}

Output:

Top number: 30

Popping numbers:
Popped: 30
Popped: 20
Popped: 10

Real-World Applications

Let's explore some practical scenarios where generic collections are commonly used:

Example 1: Managing a Product Inventory

csharp
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int StockQuantity { get; set; }

public override string ToString()
{
return $"{Id}: {Name} - ${Price} ({StockQuantity} in stock)";
}
}

// Usage example
public void ManageInventory()
{
// Create product inventory using Dictionary with product ID as key
Dictionary<int, Product> inventory = new Dictionary<int, Product>();

// Add products
inventory.Add(1, new Product { Id = 1, Name = "Laptop", Price = 1200.00m, StockQuantity = 10 });
inventory.Add(2, new Product { Id = 2, Name = "Phone", Price = 800.00m, StockQuantity = 15 });
inventory.Add(3, new Product { Id = 3, Name = "Headphones", Price = 150.00m, StockQuantity = 25 });

// Find a product by ID
if (inventory.TryGetValue(2, out Product phone))
{
Console.WriteLine($"Found product: {phone}");

// Update stock
phone.StockQuantity -= 1;
Console.WriteLine($"Updated stock: {phone}");
}

// List all products with less than 15 items in stock
Console.WriteLine("\nLow stock items:");
List<Product> lowStockItems = inventory.Values
.Where(p => p.StockQuantity < 15)
.ToList();

foreach (Product product in lowStockItems)
{
Console.WriteLine(product);
}

// Calculate total inventory value
decimal totalValue = inventory.Values.Sum(p => p.Price * p.StockQuantity);
Console.WriteLine($"\nTotal inventory value: ${totalValue}");
}

Example 2: Task Processing Queue

csharp
public class TaskProcessor
{
private readonly Queue<Action> _taskQueue = new Queue<Action>();

public void EnqueueTask(Action task)
{
if (task != null)
{
_taskQueue.Enqueue(task);
Console.WriteLine($"Task added. Queue length: {_taskQueue.Count}");
}
}

public void ProcessTasks()
{
Console.WriteLine($"Starting to process {_taskQueue.Count} tasks...");

while (_taskQueue.Count > 0)
{
Action currentTask = _taskQueue.Dequeue();
Console.WriteLine($"Executing task. Remaining: {_taskQueue.Count}");

try
{
currentTask.Invoke();
}
catch (Exception ex)
{
Console.WriteLine($"Error executing task: {ex.Message}");
}
}

Console.WriteLine("All tasks processed.");
}
}

// Usage example
public void DemonstrateTaskQueue()
{
TaskProcessor processor = new TaskProcessor();

// Add some tasks
processor.EnqueueTask(() => Console.WriteLine("Task 1: Sending email"));
processor.EnqueueTask(() => Console.WriteLine("Task 2: Processing payment"));
processor.EnqueueTask(() => Console.WriteLine("Task 3: Updating database"));

// Process all tasks
processor.ProcessTasks();
}

Choosing the Right Collection

The right generic collection depends on your specific needs:

Collection TypeWhen to Use
List<T>When you need an ordered collection that can change size and access elements by index
Dictionary<TKey, TValue>When you need fast lookups by unique keys
HashSet<T>When you need to ensure uniqueness and perform set operations
Queue<T>When you need first-in-first-out (FIFO) behavior
Stack<T>When you need last-in-first-out (LIFO) behavior
SortedList<TKey, TValue>When you need a sorted dictionary with minimal memory usage
LinkedList<T>When you need fast insertions and deletions anywhere in the list

Performance Considerations

Generic collections offer significant performance advantages over non-generic ones:

  1. Avoid boxing/unboxing: With value types, generic collections avoid the expensive boxing/unboxing operations.
  2. Type-specific operations: Operations optimized for the specific type.
  3. Memory efficiency: Generic collections typically use memory more efficiently.

Here's a simple benchmark example:

csharp
using System.Diagnostics;

// Benchmark generic vs. non-generic collections
void RunBenchmark()
{
const int itemCount = 10000000;

// Generic List<int>
Stopwatch genericTimer = Stopwatch.StartNew();
List<int> genericList = new List<int>();
for (int i = 0; i < itemCount; i++)
{
genericList.Add(i);
}
int genericSum = 0;
foreach (int i in genericList)
{
genericSum += i;
}
genericTimer.Stop();

// Non-generic ArrayList
Stopwatch nonGenericTimer = Stopwatch.StartNew();
System.Collections.ArrayList nonGenericList = new System.Collections.ArrayList();
for (int i = 0; i < itemCount; i++)
{
nonGenericList.Add(i);
}
int nonGenericSum = 0;
foreach (object o in nonGenericList)
{
nonGenericSum += (int)o;
}
nonGenericTimer.Stop();

Console.WriteLine($"Generic List<int> time: {genericTimer.ElapsedMilliseconds}ms");
Console.WriteLine($"Non-generic ArrayList time: {nonGenericTimer.ElapsedMilliseconds}ms");
}

Summary

Generic collections in C# provide type-safe, efficient, and flexible ways to store and manipulate data. They offer significant improvements over non-generic collections by providing compile-time type checking, eliminating boxing/unboxing operations, and making code more readable and maintainable.

Key points to remember:

  • List<T> for dynamic arrays
  • Dictionary<TKey, TValue> for key-value pairs
  • HashSet<T> for unique values and set operations
  • Queue<T> for first-in, first-out operations
  • Stack<T> for last-in, first-out operations

When working with collections in modern C# applications, you should almost always choose generic collections over their non-generic counterparts.

Exercises

  1. Create a Dictionary<string, List<string>> to represent a course catalog where the key is the course name and the value is a list of enrolled student names.

  2. Implement a simple task scheduler using Queue<T> that processes tasks in the order they were added.

  3. Create a HashSet<T> to find and remove duplicate entries from a list of items.

  4. Implement a simple browser history feature using Stack<string> to track visited URLs.

  5. Create a custom generic class that uses a List<T> internally but provides additional methods specific to your application's needs.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)