Skip to main content

.NET Collections

Introduction

Collections in .NET provide a way to work with groups of related objects. Unlike arrays that have a fixed size, collections are dynamic and can grow or shrink as needed. The .NET Framework offers a comprehensive suite of collection classes that are designed to serve various purposes, from simple lists to complex key-value pair dictionaries.

In this tutorial, we'll explore the most common collection types in .NET, understand when to use each one, and see practical examples that demonstrate their real-world applications.

Collections vs Arrays

Before diving into collections, let's understand why we need them when we already have arrays.

csharp
// Array - fixed size
string[] names = new string[3];
names[0] = "Alice";
names[1] = "Bob";
names[2] = "Charlie";
// names[3] = "David"; // This would cause an IndexOutOfRangeException

Arrays have a fixed size determined at creation time, making them less flexible when the exact number of elements is unknown. Collections solve this limitation by providing dynamic sizing.

Types of Collections

.NET offers several collection types, each designed for specific scenarios:

1. List<T>

The List<T> is one of the most commonly used collections. It's similar to an array but can dynamically resize itself.

csharp
// Creating a List
List<string> names = new List<string>();

// Adding elements
names.Add("Alice");
names.Add("Bob");
names.Add("Charlie");
names.Add("David"); // No issues with adding more elements!

// Accessing elements
Console.WriteLine(names[1]); // Output: Bob

// Removing elements
names.Remove("Bob");
Console.WriteLine(names[1]); // Output: Charlie now (Bob was removed)

// Getting count
Console.WriteLine($"The list contains {names.Count} names."); // Output: The list contains 3 names.

The List<T> provides methods like Add(), Remove(), Contains(), and properties like Count, making it versatile for many scenarios.

2. Dictionary<TKey, TValue>

When you need to store key-value pairs, Dictionary<TKey, TValue> is the go-to collection.

csharp
// Creating a Dictionary
Dictionary<string, int> ages = new Dictionary<string, int>();

// Adding key-value pairs
ages.Add("Alice", 25);
ages.Add("Bob", 30);
ages["Charlie"] = 35; // Another way to add or update

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

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

// Iterating through a Dictionary
foreach(var pair in ages)
{
Console.WriteLine($"{pair.Key} is {pair.Value} years old.");
}
// Output:
// Alice is 25 years old.
// Bob is 30 years old.
// Charlie is 35 years old.

Dictionaries are perfect when you need to look up values by a specific key, and they offer fast lookups due to their hash-based implementation.

3. Queue<T>

The Queue<T> collection follows the First-In-First-Out (FIFO) principle, where the first element added is the first one to be removed.

csharp
// Creating a Queue
Queue<string> printQueue = new Queue<string>();

// Adding elements (Enqueue)
printQueue.Enqueue("Document1.pdf");
printQueue.Enqueue("Resume.docx");
printQueue.Enqueue("Image.jpg");

// Removing elements (Dequeue)
string nextToPrint = printQueue.Dequeue();
Console.WriteLine($"Printing: {nextToPrint}"); // Output: Printing: Document1.pdf

// Peeking (viewing the next element without removing)
Console.WriteLine($"Next in queue: {printQueue.Peek()}"); // Output: Next in queue: Resume.docx

// Count
Console.WriteLine($"Items in queue: {printQueue.Count}"); // Output: Items in queue: 2

Queues are ideal for scenarios like print spooling, message processing, or any other situation where order matters and you want to process items in the order they were received.

4. Stack<T>

The Stack<T> collection follows the Last-In-First-Out (LIFO) principle, where the last element added is the first one to be removed.

csharp
// Creating a Stack
Stack<string> browserHistory = new Stack<string>();

// Adding elements (Push)
browserHistory.Push("www.google.com");
browserHistory.Push("www.microsoft.com");
browserHistory.Push("www.stackoverflow.com");

// Removing elements (Pop)
string currentPage = browserHistory.Pop();
Console.WriteLine($"Current page: {currentPage}"); // Output: Current page: www.stackoverflow.com

// Peeking
Console.WriteLine($"Previous page: {browserHistory.Peek()}"); // Output: Previous page: www.microsoft.com

// Count
Console.WriteLine($"Pages in history: {browserHistory.Count}"); // Output: Pages in history: 2

Stacks are useful for scenarios like undo operations, browser history navigation, or expression evaluation.

5. HashSet<T>

The HashSet<T> is a collection of unique elements, ensuring that no duplicates are added.

csharp
// Creating a HashSet
HashSet<int> uniqueNumbers = new HashSet<int>();

// Adding elements
uniqueNumbers.Add(1);
uniqueNumbers.Add(2);
uniqueNumbers.Add(3);
uniqueNumbers.Add(2); // This won't be added as 2 already exists

// Checking if an element exists
bool exists = uniqueNumbers.Contains(2);
Console.WriteLine($"Does 2 exist? {exists}"); // Output: Does 2 exist? True

// Count
Console.WriteLine($"Unique numbers: {uniqueNumbers.Count}"); // Output: Unique numbers: 3

// Set operations
HashSet<int> moreNumbers = new HashSet<int> { 2, 3, 4, 5 };
uniqueNumbers.UnionWith(moreNumbers); // Combines both sets
Console.WriteLine($"After union, count: {uniqueNumbers.Count}"); // Output: After union, count: 5

HashSets are perfect for scenarios where you need to ensure uniqueness or perform set operations like unions, intersections, or differences.

Practical Examples

Example 1: Task Manager

Let's create a simple task manager using a List<T>:

csharp
class Task
{
public string Title { get; set; }
public bool IsCompleted { get; set; }
}

class TaskManager
{
private List<Task> tasks = new List<Task>();

public void AddTask(string title)
{
tasks.Add(new Task { Title = title, IsCompleted = false });
Console.WriteLine($"Task '{title}' added.");
}

public void CompleteTask(string title)
{
Task task = tasks.Find(t => t.Title == title);
if (task != null)
{
task.IsCompleted = true;
Console.WriteLine($"Task '{title}' marked as completed.");
}
else
{
Console.WriteLine($"Task '{title}' not found.");
}
}

public void ShowAllTasks()
{
Console.WriteLine("\nAll Tasks:");
foreach (var task in tasks)
{
string status = task.IsCompleted ? "[X]" : "[ ]";
Console.WriteLine($"{status} {task.Title}");
}
}
}

// Usage:
TaskManager manager = new TaskManager();
manager.AddTask("Buy groceries");
manager.AddTask("Pay bills");
manager.CompleteTask("Buy groceries");
manager.ShowAllTasks();

// Output:
// Task 'Buy groceries' added.
// Task 'Pay bills' added.
// Task 'Buy groceries' marked as completed.
//
// All Tasks:
// [X] Buy groceries
// [ ] Pay bills

Example 2: Browser History

Let's simulate a browser's back and forward buttons using Stack<T>:

csharp
class SimpleBrowser
{
private Stack<string> backHistory = new Stack<string>();
private Stack<string> forwardHistory = new Stack<string>();
private string currentPage;

public void Navigate(string url)
{
if (currentPage != null)
{
backHistory.Push(currentPage);
forwardHistory.Clear(); // Clear forward history on new navigation
}
currentPage = url;
Console.WriteLine($"Navigated to: {url}");
}

public void Back()
{
if (backHistory.Count > 0)
{
forwardHistory.Push(currentPage);
currentPage = backHistory.Pop();
Console.WriteLine($"Went back to: {currentPage}");
}
else
{
Console.WriteLine("No more back history.");
}
}

public void Forward()
{
if (forwardHistory.Count > 0)
{
backHistory.Push(currentPage);
currentPage = forwardHistory.Pop();
Console.WriteLine($"Went forward to: {currentPage}");
}
else
{
Console.WriteLine("No more forward history.");
}
}

public void ShowCurrentPage()
{
Console.WriteLine($"Current page: {currentPage}");
}
}

// Usage:
SimpleBrowser browser = new SimpleBrowser();
browser.Navigate("www.google.com");
browser.Navigate("www.microsoft.com");
browser.Navigate("www.stackoverflow.com");
browser.Back(); // Back to microsoft.com
browser.Back(); // Back to google.com
browser.Forward(); // Forward to microsoft.com
browser.Navigate("www.github.com"); // This clears forward history
browser.Forward(); // No forward history

// Output:
// Navigated to: www.google.com
// Navigated to: www.microsoft.com
// Navigated to: www.stackoverflow.com
// Went back to: www.microsoft.com
// Went back to: www.google.com
// Went forward to: www.microsoft.com
// Navigated to: www.github.com
// No more forward history.

Best Practices

  1. Choose the Right Collection:

    • Use List<T> for simple dynamic lists
    • Use Dictionary<TKey, TValue> for key-value lookups
    • Use Queue<T> for FIFO operations
    • Use Stack<T> for LIFO operations
    • Use HashSet<T> for unique collections
  2. Consider Performance:

    • Be mindful of the performance characteristics of each collection
    • Large collections might impact memory usage
    • Operations like searching or inserting have different complexities in different collections
  3. Use Generic Collections:

    • Prefer generic collections (like List<T>) over non-generic ones (like ArrayList)
    • Generic collections provide type safety and better performance
  4. Initial Capacity:

    • If you know the approximate size of your collection, initialize it with that capacity to reduce memory reallocations

Summary

.NET Collections provide powerful tools for working with groups of data in your applications. By understanding the unique features and use cases of each collection type, you can write more efficient and cleaner code.

We've explored:

  • Lists for dynamic, indexed collections
  • Dictionaries for key-value pairs
  • Queues for first-in-first-out operations
  • Stacks for last-in-first-out operations
  • HashSets for unique collections

Each collection has its strengths and ideal use cases, so choose the one that best fits your specific needs.

Exercises

  1. Create a simple phone book application using a Dictionary<string, string> to store names and phone numbers.
  2. Implement a document printing system using a Queue<string> where users can add documents to print and the system processes them in order.
  3. Write a program that reads a set of words from input and uses a HashSet<string> to display only the unique words.
  4. Create a simple calculator that uses a Stack<double> to evaluate postfix expressions (e.g., "5 3 + 2 *" should evaluate to 16).

Additional Resources



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