C# Collections

Collections in C# are data structures that store groups of objects. They provide ways to manage, organize, and manipulate data efficiently. Understanding the different types of collections, their use cases, and best practices is essential for building robust and maintainable applications.

Table of Contents

  1. Introduction to Collections
  2. - What are Collections?
    - Benefits of Using Collections
  3. Categories of Collections
  4. - Non-Generic Collections
    - Generic Collections
    - Specialized Collections
    - Concurrent Collections
  5. Non-Generic Collections (System.Collections)
  6. - ArrayList
    - Hashtable
    - Stack
    - Queue
  7. Generic Collections (System.Collections.Generic)
  8. - List<T>
    - Dictionary<TKey, TValue>
    - LinkedList<T>
    - Queue<T>
    - Stack<T>
    - HashSet<T>
    - SortedList<TKey, TValue>
    - SortedDictionary<TKey, TValue>
  9. Specialized Collections (System.Collections.Specialized)
  10. - NameValueCollection
    - StringCollection
  11. Concurrent Collections (System.Collections.Concurrent)
  12. - ConcurrentDictionary<TKey, TValue>
    - ConcurrentQueue<T>
    - ConcurrentStack<T>
  13. LINQ and Collections
  14. - Querying Collections with LINQ
    - Manipulating Collections with LINQ
  15. Comparison of Collections
  16. - When to Use Which Collection
    - Performance Considerations
  17. Best Practices
  18. Common Mistakes with Collections
  19. Real-World Example
  20. Summary

1. Introduction to Collections

What are Collections?

Collections are data structures that group multiple elements into a single unit. They provide methods to add, remove, search, and iterate over elements efficiently.

Benefits of Using Collections

- Efficiency: Optimized for specific operations like searching, sorting, and indexing.
- Flexibility: Support dynamic resizing and various data types.
- Reusability: Predefined collections can be reused across different applications.
- Type Safety: Especially with generic collections, ensuring only specific types are stored.

2. Categories of Collections

C# collections are categorized based on their capabilities and usage scenarios:
- Non-Generic Collections: Store objects of any type (`object`), lacking type safety.
- Generic Collections: Store objects of a specified type, ensuring type safety and performance.
- Specialized Collections: Provide specialized functionality beyond standard collections.
- Concurrent Collections: Designed for thread-safe operations in multi-threaded environments.

3. Non-Generic Collections (System.Collections)

Non-generic collections store items as `object`, which requires boxing/unboxing for value types and lacks type safety.

3.1 ArrayList

A dynamically sized array that can hold items of any type.

Example: Using ArrayList
using System;
using System.Collections;

class Program
{
    static void Main()
    {
        ArrayList arrayList = new ArrayList();
        arrayList.Add(1);
        arrayList.Add("Two");
        arrayList.Add(3.0);

        Console.WriteLine("ArrayList Contents:");
        foreach(var item in arrayList)
        {
            Console.WriteLine(item);
        }

        Console.WriteLine($"Total Items: {arrayList.Count}"); // Output: 3
    }
}

Sample Output:
ArrayList Contents:
1
Two
3
Total Items: 3


Explanation:
- Flexibility: Can store elements of any type.
- Downside: Lack of type safety can lead to runtime errors.

3.2 Hashtable

A collection of key-value pairs, optimized for fast lookups.

Example: Using Hashtable
using System;
using System.Collections;

class Program
{
    static void Main()
    {
        Hashtable hashtable = new Hashtable();
        hashtable.Add(101, "Alice");
        hashtable.Add(102, "Bob");
        hashtable.Add(103, "Charlie");

        Console.WriteLine("Hashtable Contents:");
        foreach(DictionaryEntry entry in hashtable)
        {
            Console.WriteLine($"ID: {entry.Key}, Name: {entry.Value}");
        }

        // Accessing by key
        if(hashtable.ContainsKey(102))
        {
            Console.WriteLine($"Name with ID 102: {hashtable[102]}"); // Output: Bob
        }
    }
}

Sample Output:
Hashtable Contents:
ID: 101, Name: Alice
ID: 102, Name: Bob
ID: 103, Name: Charlie
Name with ID 102: Bob

Explanation:
- Key-Value Storage: Efficient for lookup operations.
- Type Safety: Not enforced, leading to potential runtime issues.

3.3 Stack

Represents a last-in, first-out (LIFO) collection.

Example: Using Stack
using System;
using System.Collections;

class Program
{
    static void Main()
    {
        Stack stack = new Stack();
        stack.Push("First");
        stack.Push("Second");
        stack.Push("Third");

        Console.WriteLine("Stack Contents:");
        while(stack.Count > 0)
        {
            Console.WriteLine(stack.Pop());
        }
    }
}

Sample Output:
Stack Contents:
Third
Second
First

Explanation:
- LIFO Behavior: Last item pushed is the first to be popped.
- Use Cases: Undo mechanisms, backtracking algorithms.

3.4 Queue

Represents a first-in, first-out (FIFO) collection.

Example: Using Queue
using System;
using System.Collections;

class Program
{
    static void Main()
    {
        Queue queue = new Queue();
        queue.Enqueue("First");
        queue.Enqueue("Second");
        queue.Enqueue("Third");

        Console.WriteLine("Queue Contents:");
        while(queue.Count > 0)
        {
            Console.WriteLine(queue.Dequeue());
        }
    }
}

Sample Output:
Queue Contents:
First
Second
Third

Explanation:
- FIFO Behavior: First item enqueued is the first to be dequeued.
- Use Cases: Task scheduling, order processing.

4. Generic Collections (System.Collections.Generic)

Generic collections provide type safety and performance benefits by allowing you to specify the type of objects stored.

4.1 List<T>

A dynamic array that can grow and shrink in size, strongly typed.

Example: Using List<T>
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> fruits = new List<string>();
        fruits.Add("Apple");
        fruits.Add("Banana");
        fruits.Add("Cherry");

        Console.WriteLine("Fruits List:");
        foreach(var fruit in fruits)
        {
            Console.WriteLine(fruit);
        }

        Console.WriteLine($"Total Fruits: {fruits.Count}"); // Output: 3
    }
}

Sample Output:
Fruits List:
Apple
Banana
Cherry
Total Fruits: 3

Explanation:
- Type Safety: Only `string` types can be added.
- Performance: Avoids boxing/unboxing for value types.

4.2 Dictionary<TKey, TValue>

A collection of key-value pairs, optimized for fast lookups based on keys.

Example: Using Dictionary<TKey, TValue>
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Dictionary<int, string> studentGrades = new Dictionary<int, string>();
        studentGrades.Add(101, "A");
        studentGrades.Add(102, "B+");
        studentGrades.Add(103, "A-");

        Console.WriteLine("Student Grades:");
        foreach(var kvp in studentGrades)
        {
            Console.WriteLine($"Student ID: {kvp.Key}, Grade: {kvp.Value}");
        }

        // Accessing by key
        if(studentGrades.TryGetValue(102, out string grade))
        {
            Console.WriteLine($"Student 102's Grade: {grade}"); // Output: B+
        }
    }
}

Sample Output:
Student Grades:
Student ID: 101, Grade: A
Student ID: 102, Grade: B+
Student ID: 103, Grade: A-
Student 102's Grade: B+

Explanation:
- Type Safety: Keys and values are strongly typed (`int` and `string` respectively).
- Efficient Lookups: Provides fast retrieval based on keys.

4.3 LinkedList<T>

A doubly linked list that allows efficient insertions and deletions.

Example: Using LinkedList<T>
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        LinkedList<string> linkedList = new LinkedList<string>();
        linkedList.AddLast("First");
        linkedList.AddLast("Second");
        linkedList.AddLast("Third");

        Console.WriteLine("LinkedList Contents:");
        foreach(var item in linkedList)
        {
            Console.WriteLine(item);
        }

        // Adding before a specific node
        LinkedListNode<string> secondNode = linkedList.Find("Second");
        linkedList.AddBefore(secondNode, "Inserted");

        Console.WriteLine("\nAfter Insertion:");
        foreach(var item in linkedList)
        {
            Console.WriteLine(item);
        }
    }
}

Sample Output:
LinkedList Contents:
First
Second
Third
After Insertion:
First
Inserted
Second
Third

Explanation:
- Efficient Operations: Fast insertions and deletions at any position.
- Use Cases: Implementing queues, stacks, or other dynamic data structures.

4.4 Queue<T> and Stack<T>

Generic versions of non-generic collections, providing type safety and better performance.

Example: Using Queue<T>
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Queue<string> tasks = new Queue<string>();
        tasks.Enqueue("Task1");
        tasks.Enqueue("Task2");
        tasks.Enqueue("Task3");

        Console.WriteLine("Tasks Queue:");
        while(tasks.Count > 0)
        {
            string task = tasks.Dequeue();
            Console.WriteLine($"Processing {task}");
        }
    }
}

Sample Output:
Tasks Queue:
Processing Task1
Processing Task2
Processing Task3

Explanation:
- Type Safety: Only `string` types can be enqueued.
- FIFO Behavior: Ensures first-in, first-out processing.

Example: Using Stack<T>
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        Stack<string> history = new Stack<string>();
        history.Push("Page1");
        history.Push("Page2");
        history.Push("Page3");

        Console.WriteLine("Browsing History:");
        while(history.Count > 0)
        {
            string page = history.Pop();
            Console.WriteLine($"Visited {page}");
        }
    }
}

Sample Output:
Browsing History:
Visited Page3
Visited Page2
Visited Page1

Explanation:
- Type Safety: Only `string` types can be pushed.
- LIFO Behavior: Ensures last-in, first-out processing.

4.5 HashSet<T>

A collection that contains no duplicate elements and provides high-performance set operations.

Example: Using HashSet<T>
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        HashSet<string> uniqueNames = new HashSet<string>();
        uniqueNames.Add("Alice");
        uniqueNames.Add("Bob");
        uniqueNames.Add("Alice"); // Duplicate, won't be added

        Console.WriteLine("Unique Names:");
        foreach(var name in uniqueNames)
        {
            Console.WriteLine(name);
        }

        Console.WriteLine($"Total Unique Names: {uniqueNames.Count}"); // Output: 2
    }
}

Sample Output:
Unique Names:
Alice
Bob
Total Unique Names: 2

Explanation:
- No Duplicates: Ensures all elements are unique.
- Performance: Optimized for set operations like union, intersection.

4.6 SortedList<TKey, TValue> and SortedDictionary<TKey, TValue>

Collections that maintain their elements in sorted order based on the key.

Example: Using SortedDictionary<TKey, TValue>
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        SortedDictionary<int, string> sortedDict = new SortedDictionary<int, string>();
        sortedDict.Add(3, "Charlie");
        sortedDict.Add(1, "Alice");
        sortedDict.Add(2, "Bob");

        Console.WriteLine("SortedDictionary Contents:");
        foreach(var kvp in sortedDict)
        {
            Console.WriteLine($"ID: {kvp.Key}, Name: {kvp.Value}");
        }
    }
}

Sample Output:
SortedDictionary Contents:
ID: 1, Name: Alice
ID: 2, Name: Bob
ID: 3, Name: Charlie

Explanation:
- Sorted Order: Automatically sorts entries based on keys.
- Use Cases: When order is important, such as maintaining a sorted list of items.

5. Specialized Collections (System.Collections.Specialized)

Specialized collections provide more specific functionalities beyond standard generic and non-generic collections.

5.1 NameValueCollection

Stores multiple string values under a single key.

Example: Using NameValueCollection
using System;
using System.Collections.Specialized;

class Program
{
    static void Main()
    {
        NameValueCollection queryParameters = new NameValueCollection();
        queryParameters.Add("search", "C# Generics");
        queryParameters.Add("page", "1");
        queryParameters.Add("filter", "recent");
        queryParameters.Add("filter", "popular"); // Multiple values for the same key

        Console.WriteLine("Query Parameters:");
        foreach(string key in queryParameters.AllKeys)
        {
            string[] values = queryParameters.GetValues(key);
            Console.WriteLine($"{key}: {string.Join(", ", values)}");
        }
    }
}

Sample Output:
Query Parameters:
search: C# Generics
page: 1
filter: recent, popular

Explanation:
- Multiple Values per Key: Allows storing multiple values for the same key.
- Use Cases: Managing HTTP query parameters, configuration settings.

5.2 StringCollection

A collection specifically for storing strings with additional methods for string manipulation.

Example: Using StringCollection
using System;
using System.Collections.Specialized;

class Program
{
    static void Main()
    {
        StringCollection stringCol = new StringCollection();
        stringCol.Add("Apple");
        stringCol.Add("Banana");
        stringCol.Add("Cherry");

        Console.WriteLine("StringCollection Contents:");
        foreach(string item in stringCol)
        {
            Console.WriteLine(item);
        }

        // Insert at specific index
        stringCol.Insert(1, "Blueberry");
        Console.WriteLine("\nAfter Insertion:");
        foreach(string item in stringCol)
        {
            Console.WriteLine(item);
        }
    }
}

Sample Output:
StringCollection Contents:
Apple
Banana
Cherry
After Insertion:
Apple
Blueberry
Banana
Cherry

Explanation:
- String-Specific Methods: Provides methods tailored for string manipulation.
- Use Cases: Managing lists of strings with ease.

6. Concurrent Collections (System.Collections.Concurrent)

Concurrent collections are designed for thread-safe operations in multi-threaded environments, ensuring data integrity without significant performance penalties.

6.1 ConcurrentDictionary<TKey, TValue>

A thread-safe dictionary implementation that allows concurrent reads and writes.

Example: Using ConcurrentDictionary<TKey, TValue>
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        ConcurrentDictionary<int, string> concurrentDict = new ConcurrentDictionary<int, string>();

        Parallel.For(0, 1000, i =>
        {
            concurrentDict.TryAdd(i, $"Value{i}");
        });

        Console.WriteLine($"Total Items in ConcurrentDictionary: {concurrentDict.Count}"); // Output: 1000
    }
}

Sample Output:
Total Items in ConcurrentDictionary: 1000

Explanation:
- Thread Safety: Allows multiple threads to add or update items without data corruption.
- Use Cases: Caching, shared resources in multi-threaded applications.

6.2 ConcurrentQueue<T>

A thread-safe first-in, first-out (FIFO) collection.

Example: Using ConcurrentQueue<T>
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        ConcurrentQueue<string> concurrentQueue = new ConcurrentQueue<string>();

        Parallel.For(0, 100, i =>
        {
            concurrentQueue.Enqueue($"Task{i}");
        });

        Console.WriteLine($"Total Items in ConcurrentQueue: {concurrentQueue.Count}"); // Output: 100

        // Dequeue items
        string result;
        while(concurrentQueue.TryDequeue(out result))
        {
            Console.WriteLine($"Dequeued: {result}");
        }
    }
}

Sample Output:
Total Items in ConcurrentQueue: 100
Dequeued: Task0
Dequeued: Task1
...
Dequeued: Task99

Explanation:
- Thread Safety: Multiple threads can enqueue and dequeue without conflicts.
- Use Cases: Task scheduling, producer-consumer scenarios.

6.3 ConcurrentStack<T>

A thread-safe last-in, first-out (LIFO) collection.

Example: Using ConcurrentStack<T>
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        ConcurrentStack<int> concurrentStack = new ConcurrentStack<int>();

        Parallel.For(0, 100, i =>
        {
            concurrentStack.Push(i);
        });

        Console.WriteLine($"Total Items in ConcurrentStack: {concurrentStack.Count}"); // Output: 100

        // Pop items
        int result;
        while(concurrentStack.TryPop(out result))
        {
            Console.WriteLine($"Popped: {result}");
        }
    }
}

Sample Output:
Total Items in ConcurrentStack: 100
Popped: 99
Popped: 98
...
Popped: 0

Explanation:
- Thread Safety: Ensures safe push and pop operations across multiple threads.
- Use Cases: Undo mechanisms, thread-safe LIFO processing.

7. LINQ and Collections

Language Integrated Query (LINQ) provides a powerful way to query and manipulate collections using a declarative syntax.

7.1 Querying Collections with LINQ

Example: Using LINQ to Filter and Select Data
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<Person> people = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "Charlie", Age = 35 },
            new Person { Name = "David", Age = 28 }
        };

        var adults = from person in people
                     where person.Age >= 30
                     select person.Name;

        Console.WriteLine("Adults:");
        foreach(var name in adults)
        {
            Console.WriteLine(name);
        }
    }
}

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Sample Output:
Adults:
Alice
Charlie

Explanation:
- LINQ Query Syntax: Filters and selects data from collections.
- Type Safety and Readability: Enhances code clarity and maintainability.

7.2 Manipulating Collections with LINQ

Example: Using LINQ Methods
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

        // Using LINQ methods
        var squaredNumbers = numbers.Select(n => n * n);
        var evenNumbers = numbers.Where(n => n % 2 == 0);
        var sum = numbers.Sum();

        Console.WriteLine("Squared Numbers:");
        foreach(var num in squaredNumbers)
        {
            Console.WriteLine(num);
        }

        Console.WriteLine("\nEven Numbers:");
        foreach(var num in evenNumbers)
        {
            Console.WriteLine(num);
        }

        Console.WriteLine($"\nSum of Numbers: {sum}"); // Output: 15
    }
}

Sample Output:
Squared Numbers:
1
4
9
16
25
Even Numbers:
2
4
Sum of Numbers: 15

Explanation:
- Select: Projects each element into a new form.
- Where: Filters elements based on a condition.
- Sum: Aggregates the total sum of elements.

8. Comparison of Collections

Choosing the right collection type is crucial for optimizing performance and ensuring code maintainability. Here's a comparison of common collections:

Collection Type Characteristics Use Cases
List<T> Dynamic array, ordered, allows duplicates General-purpose storage, lists of items
Dictionary<TKey, TValue> Key-value pairs, fast lookups, no duplicate keys Lookup tables, caching, associative arrays
LinkedList<T> Doubly linked list, efficient insertions/deletions Implementing queues, stacks, navigation scenarios
HashSet<T> No duplicates, unordered, fast set operations Unique collections, set-based algorithms
SortedList<TKey, TValue> Maintains sorted order by key, allows indexing Ordered dictionaries, maintaining sorted data
Queue<T> FIFO behavior, thread-safe with ConcurrentQueue<T> Task scheduling, buffering data
Stack<T> LIFO behavior, thread-safe with ConcurrentStack<T> Undo operations, backtracking algorithms
NameValueCollection Multiple values per key, string-based HTTP query parameters, configuration settings
StringCollection Stores strings with specialized methods Managing lists of strings
ConcurrentDictionary<TKey, TValue> Thread-safe dictionary operations Multi-threaded applications, shared resources

8.1 When to Use Which Collection

- List<T>: When you need a dynamic, ordered collection with quick access by index.

- Dictionary<TKey, TValue>: When you need fast lookups based on keys.

- LinkedList<T>: When you require frequent insertions and deletions from the middle of the collection.

- HashSet<T>: When you need to ensure all elements are unique and require fast set operations.

- SortedList<TKey, TValue> / SortedDictionary<TKey, TValue>: When maintaining a sorted order is essential.

- Queue<T> / Stack<T>: When implementing FIFO or LIFO behaviors respectively.

- Specialized Collections: When working with specific scenarios like multiple values per key or string-specific operations.

- Concurrent Collections: When dealing with multi-threaded environments needing thread-safe operations.

8.2 Performance Considerations

- List<T>: Provides O(1) access by index but O(n) for insertions/deletions in the middle.

- Dictionary<TKey, TValue>: Offers near O(1) lookups but consumes more memory.

- LinkedList<T>: Efficient for insertions/deletions but slower for access by index.

- HashSet<T>: Provides O(1) complexity for add, remove, and lookup operations.

- Sorted Collections: Typically have O(log n) insertion and retrieval times.

- Concurrent Collections: Designed for high concurrency but may have overhead compared to non-concurrent counterparts.

9. Best Practices

- Choose the Right Collection: Select the collection type that best fits your use case for optimal performance and maintainability.

- Prefer Generic Collections: Use generic collections (`List<T>`, `Dictionary<TKey, TValue>`, etc.) over non-generic ones for type safety and performance.

- Minimize Casting: Avoid unnecessary type casting by leveraging generic collections.

- Use ReadOnly Collections When Appropriate: To prevent modification of collections when not needed.

- Leverage LINQ for Queries: Utilize LINQ for concise and readable data manipulation.

- Be Mindful of Thread Safety: Use concurrent collections in multi-threaded scenarios to avoid data corruption.

- Avoid Overcomplicating Collections: Keep collections simple and focused on their intended purpose.

9.1 Example: Choosing the Right Collection

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // Use List<T> for ordered collection with dynamic sizing
        List<string> names = new List<string> { "Alice", "Bob", "Charlie" };

        // Use Dictionary<TKey, TValue> for key-value associations
        Dictionary<int, string> employeeMap = new Dictionary<int, string>
        {
            { 101, "Alice" },
            { 102, "Bob" },
            { 103, "Charlie" }
        };

        // Use HashSet<T> to store unique items
        HashSet<string> uniqueNames = new HashSet<string> { "Alice", "Bob", "Alice" };

        Console.WriteLine($"Total Unique Names: {uniqueNames.Count}"); // Output: 2
    }
}
Explanation: - List<T>: Ideal for ordered collections with dynamic sizing.
- Dictionary<TKey, TValue>: Best for associating keys with values for quick lookups.
- HashSet<T>: Ensures all elements are unique.

10. Common Mistakes with Collections

Avoiding common pitfalls ensures effective use of collections and prevents bugs or performance issues.

10.1 Using Non-Generic Collections When Generics Are Suitable

Mistake: Using `ArrayList` or `Hashtable` instead of generic collections like `List<T>` or `Dictionary<TKey, TValue>`.

Example:
using System;
using System.Collections;

class Program
{
    static void Main()
    {
        ArrayList list = new ArrayList();
        list.Add(1);
        list.Add("Two"); // Allows adding different types, leading to potential runtime errors

        foreach(var item in list)
        {
            Console.WriteLine(item);
        }
    }
}
Solution: Use generic collections for type safety and performance.

Corrected Example:
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> list = new List<int>();
        list.Add(1);
        // list.Add("Two"); // Compilation Error: Cannot add string to List<int>

        foreach(var item in list)
        {
            Console.WriteLine(item);
        }
    }
}
Explanation:
- Type Safety: Prevents adding incompatible types.
- Performance: Avoids boxing/unboxing associated with non-generic collections.

10.2 Ignoring Collection Capacity and Performance

Mistake: Not initializing collections with an appropriate capacity, leading to frequent resizing and performance degradation.

Example:
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int>();
        for(int i = 0; i < 10000; i++)
        {
            numbers.Add(i);
        }
    }
}
Solution: Initialize collections with an estimated capacity when possible to minimize resizing.

Corrected Example:
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int>(10000);
        for(int i = 0; i < 10000; i++)
        {
            numbers.Add(i);
        }
    }
}
Explanation:
- Pre-Allocation: Allocating sufficient capacity upfront reduces the number of memory reallocations, enhancing performance.

10.3 Misusing Concurrent Collections

Mistake: Using concurrent collections in single-threaded scenarios, introducing unnecessary overhead.

Example:
using System;
using System.Collections.Concurrent;

class Program
{
    static void Main()
    {
        ConcurrentBag<int> bag = new ConcurrentBag<int>();
        bag.Add(1);
        bag.Add(2);
        bag.Add(3);

        foreach(var item in bag)
        {
            Console.WriteLine(item);
        }
    }
}
Solution: Use standard collections (`List<T>`, `HashSet<T>`, etc.) in single-threaded scenarios for better performance.

Corrected Example:
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> list = new List<int> { 1, 2, 3 };

        foreach(var item in list)
        {
            Console.WriteLine(item);
        }
    }
}
Explanation:
- Performance: Concurrent collections have thread-safety mechanisms that add overhead, which is unnecessary in single-threaded contexts.

10.4 Overcomplicating Collection Operations

Mistake: Implementing complex operations manually instead of leveraging built-in collection methods or LINQ.

Example:
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        List<int> evenNumbers = new List<int>();

        foreach(var num in numbers)
        {
            if(num % 2 == 0)
            {
                evenNumbers.Add(num);
            }
        }

        foreach(var num in evenNumbers)
        {
            Console.WriteLine(num);
        }
    }
}
Solution: Use LINQ for more concise and readable code.

Corrected Example:
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        var evenNumbers = numbers.Where(n => n % 2 == 0);

        foreach(var num in evenNumbers)
        {
            Console.WriteLine(num);
        }
    }
}
Explanation:
- Conciseness and Readability: LINQ provides a more streamlined way to perform collection operations.

11. Real-World Example

Example: Managing a Student Database with Generic Collections

This example demonstrates using generic collections to manage a simple student database, including adding, searching, and displaying student information.

Code Example:
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        StudentRepository repository = new StudentRepository();

        // Adding students
        repository.AddStudent(new Student { Id = 1, Name = "Alice", Grade = "A" });
        repository.AddStudent(new Student { Id = 2, Name = "Bob", Grade = "B+" });
        repository.AddStudent(new Student { Id = 3, Name = "Charlie", Grade = "A-" });

        // Display all students
        Console.WriteLine("All Students:");
        repository.DisplayAllStudents();

        // Search for a student by ID
        Console.WriteLine("\nSearching for Student with ID 2:");
        var student = repository.GetStudentById(2);
        if(student != null)
        {
            Console.WriteLine($"Found: {student}");
        }
        else
        {
            Console.WriteLine("Student not found.");
        }

        // Remove a student
        Console.WriteLine("\nRemoving Student with ID 1:");
        repository.RemoveStudent(1);
        repository.DisplayAllStudents();

        // Attempt to remove a non-existent student
        Console.WriteLine("\nAttempting to Remove Student with ID 4:");
        repository.RemoveStudent(4);
    }
}

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Grade { get; set; }

    public override string ToString()
    {
        return $"ID: {Id}, Name: {Name}, Grade: {Grade}";
    }
}

public class StudentRepository
{
    private Dictionary<int, Student> _students = new Dictionary<int, Student>();

    public void AddStudent(Student student)
    {
        if(_students.ContainsKey(student.Id))
        {
            Console.WriteLine($"Student with ID {student.Id} already exists.");
            return;
        }
        _students.Add(student.Id, student);
        Console.WriteLine($"Student {student.Name} added.");
    }

    public Student GetStudentById(int id)
    {
        _students.TryGetValue(id, out Student student);
        return student;
    }

    public void RemoveStudent(int id)
    {
        if(_students.Remove(id))
        {
            Console.WriteLine($"Student with ID {id} removed.");
        }
        else
        {
            Console.WriteLine($"Student with ID {id} does not exist.");
        }
    }

    public void DisplayAllStudents()
    {
        foreach(var student in _students.Values)
        {
            Console.WriteLine(student);
        }
    }
}

Sample Output:
Student Alice added.
Student Bob added.
Student Charlie added.
All Students:
ID: 1, Name: Alice, Grade: A
ID: 2, Name: Bob, Grade: B+
ID: 3, Name: Charlie, Grade: A-
Searching for Student with ID 2:
Found: ID: 2, Name: Bob, Grade: B+
Removing Student with ID 1:
Student with ID 1 removed.
ID: 2, Name: Bob, Grade: B+
ID: 3, Name: Charlie, Grade: A-
Attempting to Remove Student with ID 4:
Student with ID 4 does not exist.

Explanation:
- Dictionary<TKey, TValue>: Efficiently manages students with unique IDs as keys.
- CRUD Operations: Demonstrates adding, retrieving, displaying, and removing students.
- Type Safety and Performance: Ensures fast lookups and type-safe operations.

12. Summary

C# collections are indispensable tools for managing groups of objects efficiently and effectively. They provide a wide range of functionalities tailored to various use cases, from simple lists to complex, thread-safe operations.

Key Takeaways:
- Types of Collections: Understand the differences between non-generic, generic, specialized, and concurrent collections to choose the right tool for your needs.

- Type Safety and Performance: Generic collections offer type safety and performance benefits by eliminating the need for casting and reducing memory overhead.

- Thread Safety: Concurrent collections are essential for multi-threaded applications, ensuring data integrity without sacrificing performance.

- LINQ Integration: LINQ enhances the ability to query and manipulate collections in a readable and concise manner.

- Best Practices: Selecting the appropriate collection type, leveraging generic collections, and adhering to best practices lead to more maintainable and efficient code.

- Avoid Common Mistakes: Steer clear of pitfalls like using non-generic collections unnecessarily or mismanaging collection capacities.

- Real-World Applications: Implementing patterns like repositories, managing unique datasets, and handling concurrent operations showcase the practical use of collections.

By mastering C# collections, you can build scalable, efficient, and robust applications that handle data effectively, catering to a wide range of programming challenges.

Previous: C# Generics | Next: C# List

<
>