Paramclasses Academy - Blog

Tutorials Details

←
Post 21 of 30
→

What is C# Collections ? Explained: List, Dictionary, HashSet and Queue

What is C# Collections ? Explained: List, Dictionary, HashSet and Queue

October 26, 2026

Introduction

In C# programming, applications often need to store and manage multiple values. For example, a student management system may store student names, an e-commerce application may maintain product details, and a banking application may keep track of transactions.

You can use arrays to store multiple values, but arrays have a fixed length. When an application needs to add, remove, search, or organize data dynamically, C# collections provide more flexible solutions.

The .NET collection libraries include several types designed for different requirements. In this tutorial, you will learn about four commonly used collections: List<T>, Dictionary<TKey, TValue>, HashSet<T>, and Queue<T>.

What You Will Learn

  • What collections are in C#

  • The difference between arrays and collections

  • How to create and use a List<T>

  • How to store key-value pairs using Dictionary<TKey, TValue>

  • How to remove duplicate values using HashSet<T>

  • How to process items in order using Queue<T>

  • How to choose the right collection for a particular task

  • Practical examples, common mistakes, and best practices

1. What Are Collections in C#?

A collection is an object that stores and manages a group of related elements.

Instead of declaring separate variables for every value, you can store multiple items in one collection and process them using loops and collection methods.

For example, without a collection:

string student1 = "Aman";
string student2 = "Priya";
string student3 = "Rahul";

With a collection:

List<string> students = new List<string>
{
    "Aman",
    "Priya",
    "Rahul"
};

The second approach makes it easier to add students, remove entries, search for names, and iterate through the data.

Most commonly used generic collections are available in the System.Collections.Generic namespace.

using System.Collections.Generic;

Generic collections use type parameters to specify the types of elements they can store. For example, List<int> stores integers, while List<string> stores strings.

2. Arrays vs. Collections

Arrays and collections both store multiple values, but their capabilities differ.

Feature Array List<T>
Size Fixed after creation Can grow or shrink
Adding items Requires a new array or another strategy Use Add()
Removing items Usually requires creating or shifting array contents Use Remove() or RemoveAt()
Index-based access Supported Supported
Duplicate values Allowed Allowed
Best use Fixed-size data Dynamically changing data

For example, an array is suitable for storing the seven days of the week, while a list is useful for storing a changing number of registered users.

Other collections, such as dictionaries, sets, and queues, provide different ways to organize and access information.

3. List<T> in C#

A List<T> is a dynamically sized collection that stores elements of the same specified type. It preserves insertion order and allows duplicate values.

It is useful when you need to maintain an ordered group of items and add or remove elements as the application runs.

Creating a List

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> students = new List<string>
        {
            "Aman",
            "Priya",
            "Rahul"
        };

        foreach (string student in students)
        {
            Console.WriteLine(student);
        }
    }
}

Output:

Aman
Priya
Rahul

The List<string> declaration indicates that the list stores strings. The foreach loop visits each element in order.

Common List Methods

Method or Property Purpose
Add(item) Adds an item to the end
AddRange(items) Adds multiple items
Remove(item) Removes the first matching item
RemoveAt(index) Removes an item at a specified index
Contains(item) Checks whether an item exists
IndexOf(item) Returns the first matching index, or -1
Count Returns the number of elements
Clear() Removes all elements
Sort() Sorts elements using the default comparer

Example: Adding and Removing Items

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> products = new List<string>();

        products.Add("Laptop");
        products.Add("Keyboard");
        products.Add("Mouse");

        Console.WriteLine($"Total products: {products.Count}");

        products.Remove("Keyboard");

        Console.WriteLine("Updated product list:");

        foreach (string product in products)
        {
            Console.WriteLine(product);
        }
    }
}

Output:

Total products: 3
Updated product list:
Laptop
Mouse

This example demonstrates how a list can change dynamically without requiring you to create a new array manually.

Accessing List Elements

Lists use zero-based indexes, just like arrays.

List<int> numbers = new List<int> { 10, 20, 30 };

Console.WriteLine(numbers[0]); // 10
Console.WriteLine(numbers[1]); // 20
Console.WriteLine(numbers[2]); // 30

Attempting to access an invalid index throws an ArgumentOutOfRangeException. Check the index or use a safe access strategy when the index may be invalid.

When Should You Use List<T>?

Use a list when:

  • You need to maintain an ordered sequence.

  • Items may be added or removed.

  • Duplicate values are allowed.

  • You need convenient index-based access.

Examples include shopping cart items, student names, product lists, and task lists.

4. Dictionary<TKey, TValue> in C#

A Dictionary<TKey, TValue> stores data as key-value pairs. Each key must be unique within the dictionary, while values may be duplicated.

A dictionary is useful when you want to retrieve a value using a meaningful key instead of searching a list manually.

For example, a student management system might associate a student ID with a student name.

Creating a Dictionary

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Dictionary<int, string> students =
            new Dictionary<int, string>
            {
                { 101, "Aman" },
                { 102, "Priya" },
                { 103, "Rahul" }
            };

        Console.WriteLine(students[101]);
        Console.WriteLine(students[102]);
    }
}

Output:

Aman
Priya

Here, the integer is the key, and the string is the value.

Adding and Updating Dictionary Entries

Dictionary<int, string> employees = new Dictionary<int, string>();

employees.Add(1001, "Neha");
employees.Add(1002, "Arjun");

employees[1003] = "Kavita"; // Adds a new key

employees[1002] = "Rohan";  // Updates an existing key

The Add() method throws an exception if the key already exists. Using the indexer to assign a value creates an entry if the key is new or replaces the value if the key already exists.

Safely Retrieving Values with TryGetValue()

Using dictionary[key] throws a KeyNotFoundException if the key does not exist. When a key may be missing, use TryGetValue().

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Dictionary<int, string> students = new Dictionary<int, string>
        {
            { 101, "Aman" },
            { 102, "Priya" }
        };

        if (students.TryGetValue(103, out string? studentName))
        {
            Console.WriteLine(studentName);
        }
        else
        {
            Console.WriteLine("Student not found.");
        }
    }
}

Output:

Student not found.

This approach is useful when searching for users, products, employees, or other records by ID.

Iterating Through a Dictionary

Dictionary<string, int> scores = new Dictionary<string, int>
{
    { "Aman", 85 },
    { "Priya", 92 },
    { "Rahul", 78 }
};

foreach (KeyValuePair<string, int> entry in scores)
{
    Console.WriteLine($"{entry.Key}: {entry.Value}");
}

Output:

Aman: 85
Priya: 92
Rahul: 78

A dictionary is designed for key-based lookup. Do not rely on it when your application requires a particular sorting order; explicitly sort the entries when that order matters.

When Should You Use Dictionary<TKey, TValue>?

Use a dictionary when:

  • Every entry needs a unique key.

  • You need to retrieve values efficiently by key.

  • You need to check whether a particular key exists.

  • You want to associate IDs with records or names with values.

Common examples include product IDs and product details, employee IDs and employee names, and configuration settings.

5. HashSet<T> in C#

A HashSet<T> stores a collection of unique elements. It does not allow duplicate elements according to its equality comparer.

It is especially useful when you want to eliminate duplicates or quickly check whether a value belongs to a set.

Unlike a list, a hash set does not provide index-based access, and you should not rely on a specific iteration order.

Creating a HashSet

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        HashSet<int> numbers = new HashSet<int>
        {
            10,
            20,
            30,
            20,
            10
        };

        foreach (int number in numbers)
        {
            Console.WriteLine(number);
        }
    }
}

The set contains only the unique values 10, 20, and 30. Their displayed order should not be treated as guaranteed.

Adding Items to a HashSet

The Add() method returns true if an element is added and false if an equivalent element is already present.

HashSet<string> languages = new HashSet<string>();

Console.WriteLine(languages.Add("C#"));   // True
Console.WriteLine(languages.Add("Java")); // True
Console.WriteLine(languages.Add("C#"));   // False

The duplicate "C#" entry is not added again.

Checking Whether an Item Exists

HashSet<string> registeredUsers = new HashSet<string>
{
    "Aman",
    "Priya",
    "Rahul"
};

if (registeredUsers.Contains("Priya"))
{
    Console.WriteLine("User is registered.");
}
else
{
    Console.WriteLine("User is not registered.");
}

Output:

User is registered.

For typical hash sets, membership checks are very efficient on average.

Set Operations

A HashSet<T> supports useful mathematical set operations.

HashSet<int> setA = new HashSet<int> { 1, 2, 3 };
HashSet<int> setB = new HashSet<int> { 3, 4, 5 };

setA.UnionWith(setB);

After UnionWith(), setA contains the unique values from both sets: 1, 2, 3, 4, and 5.

Other useful operations include:

  • IntersectWith() — retains elements common to both sets.

  • ExceptWith() — removes elements present in another collection.

  • IsSubsetOf() — checks whether all elements belong to another set.

  • Overlaps() — checks whether the sets have at least one element in common.

When Should You Use HashSet<T>?

Use a hash set when:

  • Duplicate values should not be stored.

  • You need to check membership efficiently.

  • You want to compare groups of unique values.

  • You do not need index-based access or a guaranteed iteration order.

Examples include unique email addresses, registered user IDs, unique tags, and removing duplicates from imported data.

6. Queue<T> in C#

A Queue<T> is a collection that follows the FIFO (First In, First Out) principle. The first item added is the first item removed.

Imagine a line at a ticket counter: the person who arrives first is served first. A queue models this type of processing.

Creating a Queue

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Queue<string> customers = new Queue<string>();

        customers.Enqueue("Aman");
        customers.Enqueue("Priya");
        customers.Enqueue("Rahul");

        while (customers.Count > 0)
        {
            string customer = customers.Dequeue();
            Console.WriteLine($"Serving: {customer}");
        }
    }
}

Output:

Serving: Aman
Serving: Priya
Serving: Rahul

The Enqueue() method adds an item to the back of the queue. The Dequeue() method removes and returns the item at the front.

Important Queue Methods

Method or Property Purpose
Enqueue(item) Adds an item to the back
Dequeue() Removes and returns the front item
Peek() Returns the front item without removing it
Count Returns the number of queued items
Contains(item) Checks whether an item exists
Clear() Removes all items

Calling Dequeue() or Peek() on an empty queue throws an InvalidOperationException. Check Count first or use a safe operation pattern.

Using Peek()

Queue<int> tasks = new Queue<int>();

tasks.Enqueue(101);
tasks.Enqueue(102);
tasks.Enqueue(103);

Console.WriteLine(tasks.Peek()); // 101
Console.WriteLine(tasks.Count);  // 3

Peek() inspects the next item without removing it, so the queue still contains all three tasks.

When Should You Use Queue<T>?

Use a queue when:

  • Items should be processed in arrival order.

  • You need to schedule work.

  • You are managing requests or messages.

  • You need to model a waiting line.

Examples include print jobs, background processing, customer service systems, and breadth-first search algorithms.

7. List vs. Dictionary vs. HashSet vs. Queue

The best collection depends on how your application needs to store and access data.

Feature List<T> Dictionary<TKey, TValue> HashSet<T> Queue<T>
Stores Ordered elements Key-value pairs Unique elements Items in FIFO order
Duplicates Allowed Keys must be unique Not stored as duplicates Allowed
Index-based access Yes No No No
Main access pattern Index or iteration Lookup by key Membership check Front item
Typical use Product list Product ID lookup Unique email addresses Task processing

Quick Selection Guide

  • Choose List<T> for an ordered collection that can grow or shrink.

  • Choose Dictionary<TKey, TValue> for looking up values by unique keys.

  • Choose HashSet<T> for uniqueness and membership checks.

  • Choose Queue<T> for first-in, first-out processing.

Other collections are available too. For example, Stack<T> follows last-in, first-out (LIFO) order, while LinkedList<T> supports linked-node operations.

8. Practical Example: Managing a Product Inventory

The following example combines a list and a dictionary to manage product information.

The list maintains the inventory's display order, while the dictionary enables lookup by product ID.

using System;
using System.Collections.Generic;

class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
}

class Program
{
    static void Main()
    {
        List<Product> products = new List<Product>
        {
            new Product { Id = 101, Name = "Laptop", Price = 55000m },
            new Product { Id = 102, Name = "Keyboard", Price = 1500m },
            new Product { Id = 103, Name = "Mouse", Price = 700m }
        };

        Dictionary<int, Product> productsById = new Dictionary<int, Product>();

        foreach (Product product in products)
        {
            productsById.Add(product.Id, product);
        }

        Console.WriteLine("Available Products:");

        foreach (Product product in products)
        {
            Console.WriteLine(
                $"{product.Id} - {product.Name} - {product.Price:C}");
        }

        Console.WriteLine("\nSearch for product ID 102:");

        if (productsById.TryGetValue(102, out Product? foundProduct))
        {
            Console.WriteLine(
                $"Found: {foundProduct.Name}, Price: {foundProduct.Price:C}");
        }
        else
        {
            Console.WriteLine("Product not found.");
        }
    }
}

Example output:

Available Products:
101 - Laptop - ₹55,000.00
102 - Keyboard - ₹1,500.00
103 - Mouse - ₹700.00

Search for product ID 102:
Found: Keyboard, Price: ₹1,500.00

The exact currency formatting depends on the environment's culture settings.

This example demonstrates two complementary structures: a list for ordered display and a dictionary for lookup by ID. In a larger application, you would also need to keep both structures synchronized when inventory entries are added, updated, or removed.

9. Performance Considerations

Different collections are optimized for different operations. Their performance depends on collection size, equality and hashing implementations, and the specific operation.

Operation Typical Performance
List<T> index access O(1)
List<T> search with Contains() O(n)
List<T> append with spare capacity Amortized O(1)
Dictionary<TKey, TValue> key lookup Average O(1)
HashSet<T> membership check Average O(1)
Queue<T> enqueue/dequeue Amortized O(1)

Here, O(1) means constant-time complexity, while O(n) means the work may grow proportionally with the number of elements.

These are typical complexity expectations, not guarantees for every input or implementation. Hash collisions, custom equality logic, resizing, and other factors can affect performance.

For most beginner applications, selecting a collection that matches the required behavior is more important than attempting premature optimization.

10. Best Practices for Working with Collections

  1. Choose the right collection. Use a list for ordered items, a dictionary for key-based lookup, a hash set for uniqueness, and a queue for FIFO processing.

  2. Use generic collections. Prefer List<T> and related generic types over legacy non-generic collections when appropriate.

  3. Check for missing dictionary keys. Use TryGetValue() when a key may not exist.

  4. Avoid modifying a collection during foreach. If you need to remove items, use a suitable loop, create a filtered collection, or use another safe approach.

  5. Do not rely on a hash set's iteration order. Sort the values explicitly if a specific display order is required.

  6. Validate input before adding data. Avoid inserting invalid or inconsistent records.

  7. Use meaningful types. For example, Dictionary<int, Product> is clearer and safer than storing unrelated values in a loosely typed collection.

  8. Consider memory and scale. Choose a suitable collection for the volume of data and the operations your application performs.

11. Common Mistakes Beginners Should Avoid

  • Accessing a list using an invalid index.

  • Adding duplicate keys to a dictionary with Add().

  • Assuming a dictionary always provides the desired display order.

  • Expecting a HashSet<T> to preserve insertion order.

  • Calling Dequeue() or Peek() when a queue is empty.

  • Using a list for repeated membership checks on a large dataset without considering alternatives.

  • Changing a collection inside a foreach loop.

  • Forgetting to update related collections when the same data is stored in more than one place.

Understanding these mistakes helps you write safer and more maintainable C# applications.

12. Practice Exercises

Try the following exercises to reinforce what you have learned.

Exercise 1: Student List

Create a List<string> containing five student names. Add another name, remove one name, and display the final list.

Exercise 2: Employee Dictionary

Create a dictionary that stores employee IDs and employee names. Search for an employee using TryGetValue() and display a message if the employee is not found.

Exercise 3: Remove Duplicates

Create a list containing duplicate integers. Use a HashSet<int> to produce a collection of unique values.

Exercise 4: Task Queue

Create a queue of five tasks. Add the tasks using Enqueue() and process them using Dequeue() until the queue is empty.

Exercise 5: Product Inventory

Create a list of product objects and a dictionary indexed by product ID. Display the inventory and retrieve one product using its ID.

13. Knowledge Check

1. Which collection allows duplicate elements and supports index-based access?

A. HashSet<T>
B. Dictionary<TKey, TValue>
C. List<T>
D. Queue<T>

2. Which collection stores key-value pairs?

A. List<T>
B. Dictionary<TKey, TValue>
C. HashSet<T>
D. Queue<T>

3. Which collection is designed to store unique elements?

A. HashSet<T>
B. List<T>
C. Queue<T>
D. Array

4. What does FIFO mean for a queue?

A. First In, First Out
B. First In, Final Out
C. Fast Input, Fast Output
D. Final In, First Out

5. Which dictionary method safely attempts to retrieve a value without requiring the key to exist?

A. FindValue()
B. GetValueAlways()
C. TryGetValue()
D. ReadKey()

Answers

  1. C — List<T>

  2. B — Dictionary<TKey, TValue>

  3. A — HashSet<T>

  4. A — First In, First Out

  5. C — TryGetValue()

14. Frequently Asked Questions

What are collections in C#?

Collections are data structures used to store and manage groups of elements. They provide operations for adding, removing, searching, and processing data.

What is the difference between an array and a list?

An array has a fixed length after creation, while a List<T> can grow or shrink dynamically and provides convenient methods for modifying its contents.

What is the difference between a list and a dictionary?

A list stores elements in an indexed sequence. A dictionary stores key-value pairs and retrieves values using unique keys.

Why should I use a HashSet<T>?

Use a HashSet<T> when you need unique elements or efficient membership checks and do not need index-based access.

What is FIFO in C#?

FIFO means First In, First Out. A Queue<T> follows this principle by removing elements in the order they were added.

Can a dictionary contain duplicate values?

Yes. Dictionary keys must be unique, but multiple keys can refer to equal values or even the same object.

Which C# collection is fastest?

There is no single fastest collection for every operation. A dictionary is generally suitable for key-based lookup, a hash set for membership checks, a list for indexed access, and a queue for FIFO operations.

What namespace contains List<T> and Dictionary<TKey, TValue>?

Both are available in System.Collections.Generic. In modern .NET projects, implicit global usings may already make the namespace available.

Conclusion

C# collections provide flexible ways to store, retrieve, and process data. A List<T> is useful for ordered groups of elements, a Dictionary<TKey, TValue> supports key-based lookup, a HashSet<T> maintains unique elements, and a Queue<T> processes items in FIFO order.

Understanding how these structures differ will help you select the right data structure for your application and write clearer, more maintainable code.

←
Post 21 of 30
→