C# List

The `List<T>` class in C# is a part of the `System.Collections.Generic` namespace and represents a strongly typed list of objects that can be accessed by index. It provides methods to search, sort, and manipulate lists efficiently. Understanding `List<T>` is fundamental for effective data management in C# applications.

Table of Contents

  1. Introduction to List<T>
  2. - What is List<T>?
    - Benefits of Using List<T>
  3. Declaring and Initializing List<T>
  4. - Declaration Syntax
    - Initialization Methods
    - Collection Initializers
  5. Adding and Removing Elements
  6. - Adding Elements
    - Removing Elements
    - Clearing the List
  7. Accessing Elements
  8. - By Index
    - Using LINQ
    - Range Operations
  9. Iterating Through List<T>
  10. - Using foreach Loop
    - Using for Loop
    - Using LINQ Queries
  11. Searching and Sorting
  12. - Searching Elements
    - Sorting the List
    - Custom Sorting with Comparer
  13. Capacity and Performance
  14. - Understanding Capacity vs. Count
    - Managing Capacity
    - Performance Considerations
  15. Common Methods and Properties
  16. - Key Methods
    - Important Properties
    - Useful Events
  17. Best Practices
  18. Common Mistakes with List<T>
  19. Advanced Topics
  20. - List<T> with Custom Objects
    - Using LINQ with List<T>
    - Thread Safety
  21. Real-World Example
  22. Summary

1. Introduction to List<T>

What is List<T>?

`List<T>` is a generic collection provided by C# that represents a list of objects that can be accessed by index. It allows dynamic resizing, type safety, and a plethora of methods for managing data.

Syntax:
using System.Collections.Generic;

List<T> list = new List<T>();

Benefits of Using List<T>

- Type Safety: Ensures that only objects of type `T` are stored, preventing runtime errors.
- Dynamic Resizing: Automatically resizes as elements are added or removed.
- Performance: Optimized for frequent additions and removals.
- Rich API: Provides numerous methods for searching, sorting, and manipulating data.
- Integration with LINQ: Seamlessly works with LINQ for advanced querying.

2. Declaring and Initializing List<T>

2.1 Declaration Syntax

Basic Declaration:
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int>();
    }
}
Explanation:
- Namespace: Ensure `System.Collections.Generic` is included.
- Type Parameter: Replace `int` with any desired type.

2.2 Initialization Methods

Default Initialization:
List<string> fruits = new List<string>();

Initialization with Capacity:
Specifying an initial capacity can optimize performance by reducing the number of resizing operations.

List<string> fruits = new List<string>(100);

Explanation:
- Capacity: The number passed defines the initial size. The list grows as needed beyond this capacity.

2.3 Collection Initializers

Allows initializing a list with elements at the time of creation.
List<string> fruits = new List<string> { "Apple", "Banana", "Cherry" };

Explanation:
- Readability: Enhances code clarity by initializing with predefined elements.

3. Adding and Removing Elements

3.1 Adding Elements

Using Add Method:
List<string> fruits = new List<string>();
fruits.Add("Apple");
fruits.Add("Banana");

Using AddRange Method:
Adds multiple elements from another collection.
List<string> moreFruits = new List<string> { "Cherry", "Date" };
fruits.AddRange(moreFruits);

Using Insert Method:
Inserts an element at a specific index.
fruits.Insert(1, "Blueberry"); // Inserts "Blueberry" at index 1

Explanation:
- Add: Appends to the end.
- AddRange: Appends multiple items.
- Insert: Places an item at a specified position, shifting subsequent items.

3.2 Removing Elements

Using Remove Method:
Removes the first occurrence of a specific object.
fruits.Remove("Banana"); // Removes "Banana"

Using RemoveAt Method:
Removes the element at the specified index.
fruits.RemoveAt(0); // Removes the first element

Using RemoveAll Method:
Removes all elements that match a predicate.
fruits.RemoveAll(f => f.StartsWith("B")); // Removes all fruits starting with 'B'

Explanation:
- Remove: Deletes the first matching item.
- RemoveAt: Deletes by index.
- RemoveAll: Deletes based on a condition.

3.3 Clearing the List

Using Clear Method:
Removes all elements from the list.
fruits.Clear();
Explanation: - Clear: Empties the list but retains the capacity.

4. Accessing Elements

4.1 By Index

Accessing elements directly using their index.
List<string> fruits = new List<string> { "Apple", "Banana", "Cherry" };
string firstFruit = fruits[0]; // "Apple"
string secondFruit = fruits[1]; // "Banana"

// Updating an element
fruits[2] = "Date"; // Replaces "Cherry" with "Date"

Explanation:
- Zero-Based Indexing: The first element is at index `0`.
- Bound Checking: Accessing an invalid index throws an `ArgumentOutOfRangeException`.

4.2 Using LINQ

Leveraging LINQ for advanced querying.

Example: Finding Elements:
using System.Linq;

var banana = fruits.FirstOrDefault(f => f == "Banana");

Explanation:
- FirstOrDefault: Retrieves the first matching element or the default value if none found.

4.3 Range Operations

Accessing a range of elements using `GetRange`.
List<string> selectedFruits = fruits.GetRange(0, 2); // Gets first two elements

Explanation:
- GetRange: Extracts a subset from the list, starting at a specified index and spanning a specified number of elements.

5. Iterating Through List<T>

5.1 Using foreach Loop

The most common way to iterate through a list.
foreach(string fruit in fruits)
{
    Console.WriteLine(fruit);
}

Explanation:
- Read-Only: Iterates through each element without modifying the list.

5.2 Using for Loop

Allows access by index, useful for modifying elements.
for(int i = 0; i < fruits.Count; i++)
{
    Console.WriteLine($"Fruit at index {i}: {fruits[i]}");
}

Explanation:
- Index-Based: Provides flexibility to manipulate elements during iteration.

5.3 Using LINQ Queries

Utilizes LINQ for more declarative iteration. Example: Selecting Specific Elements
var longNamedFruits = fruits.Where(f => f.Length > 5);

foreach(var fruit in longNamedFruits)
{
    Console.WriteLine(fruit);
}

Explanation:
- Where: Filters elements based on a condition.
- Deferred Execution: The query is evaluated when iterated over.

6. Searching and Sorting

6.1 Searching Elements

Using Contains Method:
Checks if an element exists in the list.
bool hasApple = fruits.Contains("Apple"); // true or false

Using IndexOf Method:
Finds the index of the first occurrence.
int index = fruits.IndexOf("Banana"); // Returns the index or -1 if not found

Using Find Method:
Finds the first element matching a predicate.
string foundFruit = fruits.Find(f => f.StartsWith("C")); // Finds first fruit starting with 'C'

Explanation:
- Contains: Simple existence check.
- IndexOf: Retrieves the position of an element.
- Find: Searches based on a condition.

6.2 Sorting the List

Using Sort Method:
Sorts the list in ascending order using the default comparer.
fruits.Sort();

Custom Sorting with Comparison Delegate:
Defines custom sorting logic.
fruits.Sort((a, b) => b.CompareTo(a)); // Sorts in descending order

Using Sort with IComparer<T>:
Implements a custom comparer.
public class LengthComparer : IComparer<string>
{
    public int Compare(string x, string y)
    {
        return x.Length.CompareTo(y.Length);
    }
}

// Usage
fruits.Sort(new LengthComparer());

Explanation:
- Default Sort: Alphabetical for strings, numerical for numbers.
- Custom Sort: Allows defining any sorting logic, such as descending order or based on object properties.

6.3 Sorting with LINQ

Creates a new sorted sequence without modifying the original list.
var sortedFruits = fruits.OrderBy(f => f);

foreach(var fruit in sortedFruits)
{
    Console.WriteLine(fruit);
}

Explanation:
- OrderBy: Returns a new ordered sequence.
- Immutable: Does not alter the original list.

7. Capacity and Performance

7.1 Understanding Capacity vs. Count

- Count: The number of elements actually in the list.
- Capacity: The number of elements the list can store before resizing is needed.

Example:
List<int> numbers = new List<int>();
Console.WriteLine($"Initial Capacity: {numbers.Capacity}"); // Typically 0

numbers.Add(1);
Console.WriteLine($"Capacity after adding one element: {numbers.Capacity}"); // Often 4

numbers.Add(2);
numbers.Add(3);
numbers.Add(4);
numbers.Add(5);
Console.WriteLine($"Capacity after adding five elements: {numbers.Capacity}"); // Often 8

Explanation:
- Dynamic Resizing: The capacity increases automatically as elements are added beyond the current capacity.
- Doubling Strategy: Typically, the capacity doubles to optimize performance and minimize reallocations.

7.2 Managing Capacity

Using EnsureCapacity Method:
Ensures that the list can hold a specified number of elements without resizing.
numbers.EnsureCapacity(1000);

Using TrimExcess Method:
Sets the capacity to the actual number of elements, reducing memory overhead.
numbers.TrimExcess();

Explanation:
- EnsureCapacity: Prevents frequent resizing by allocating sufficient space upfront.
- TrimExcess: Frees unused memory after bulk operations.

7.3 Performance Considerations

- Pre-Allocating Capacity: If the number of elements is known in advance, initializing with the appropriate capacity can enhance performance.
- Avoiding Excessive Capacity: Over-allocating capacity can lead to unnecessary memory usage.
- Batch Operations: Using methods like `AddRange` for bulk additions is more efficient than multiple `Add` calls.

8. Common Methods and Properties

8.1 Key Methods

- Add(T item): Adds an item to the end of the list.

- AddRange(IEnumerable<T> collection): Adds multiple items from a collection.

- Insert(int index, T item): Inserts an item at the specified index.

- Remove(T item): Removes the first occurrence of a specific object.

- RemoveAt(int index): Removes the element at the specified index.

- RemoveAll(Predicate<T> match): Removes all elements that match the conditions defined by the specified predicate.

- Clear(): Removes all elements from the list.

- Contains(T item): Determines whether the list contains a specific value.

- IndexOf(T item): Searches for the specified object and returns the zero-based index of the first occurrence.

- Find(Predicate<T> match): Searches for an element that matches the conditions defined by the specified predicate.

- FindAll(Predicate<T> match): Retrieves all elements that match the conditions defined by the specified predicate.

- Sort(): Sorts the elements in the entire list using the default comparer.

- Sort(Comparison<T> comparison): Sorts the elements using the specified comparison.

- ToArray(): Copies the elements to a new array.

- ConvertAll<TOutput>(Converter<T, TOutput> converter): Converts each element to another type.

8.2 Important Properties

- Count: Gets the number of elements contained in the list.

- Capacity: Gets or sets the total number of elements the internal data structure can hold without resizing.

- Item[int index]: Gets or sets the element at the specified index.

8.3 Useful Events

`List<T>` does not provide built-in events. However, developers can implement custom events or use observable collections like `ObservableCollection<T>` for event-driven scenarios.

9. Best Practices

9.1 Prefer Generic Over Non-Generic Collections

Always use `List<T>` over non-generic collections like `ArrayList` for type safety and performance benefits.

9.2 Initialize with Appropriate Capacity

If the number of elements is known in advance, initialize the list with that capacity to minimize resizing operations.

List<int> numbers = new List<int>(1000);

9.3 Use ReadOnly Collections When Necessary

To prevent modification of the list, expose it as a read-only collection.

ReadOnlyCollection<int> readOnlyNumbers = numbers.AsReadOnly();

9.4 Utilize LINQ for Complex Queries

Leverage LINQ to perform complex data manipulations in a readable and concise manner.

var highScores = scores.Where(s => s > 90).OrderByDescending(s => s);

9.5 Minimize Capacity Overhead

Avoid over-allocating capacity to prevent unnecessary memory usage. Use `TrimExcess` after bulk operations if needed.

numbers.TrimExcess();

9.6 Use Appropriate Iteration Methods

Choose the right iteration method (`foreach`, `for`, or LINQ) based on the use case for optimal readability and performance.

9.7 Handle Exceptions Gracefully

When accessing elements by index, ensure that the index is within bounds to prevent `ArgumentOutOfRangeException`.

if(index >= 0 && index < list.Count)
{
    var item = list[index];
}
else
{
    // Handle invalid index
}

10. Common Mistakes with List<T>

10.1 Not Specifying the Type Parameter

Attempting to use `List` without specifying the type parameter leads to the non-generic `List` class, which does not exist. Always specify `List<T>` with the appropriate type.

Incorrect:
List list = new List(); // Compilation Error

Correct:
List<int> list = new List<int>();

10.2 Exceeding List Capacity Without Proper Initialization

Adding a large number of elements without pre-defining capacity can lead to multiple resizing operations, affecting performance.

Example:
List<int> numbers = new List<int>();
for(int i = 0; i < 10000; i++)
{
    numbers.Add(i);
}

Solution: Initialize with an estimated capacity.
List<int> numbers = new List<int>(10000);

10.3 Modifying the List During Iteration

Altering the list (adding or removing elements) while iterating using `foreach` can cause runtime exceptions.

Example:
foreach(var item in list)
{
    if(item == target)
    {
        list.Remove(item); // InvalidOperationException
    }
}

Solution: Use a `for` loop or iterate over a copy of the list.
for(int i = list.Count - 1; i >= 0; i--)
{
    if(list[i] == target)
    {
        list.RemoveAt(i);
    }
}

10.4 Ignoring Null Values in Reference Types

When working with reference types, not handling potential `null` values can lead to `NullReferenceException`.

Example:
List<string> names = new List<string> { "Alice", null, "Bob" };

foreach(var name in names)
{
    Console.WriteLine(name.ToUpper()); // Throws exception for null
}
Solution: Check for `null` before accessing members.
foreach(var name in names)
{
    if(name != null)
    {
        Console.WriteLine(name.ToUpper());
    }
    else
    {
        Console.WriteLine("Null name encountered.");
    }
}

10.5 Misusing Sort Methods

Assuming that `Sort()` returns a sorted list instead of sorting in place can lead to confusion.

Example:
List<int> numbers = new List<int> { 3, 1, 2 };
var sortedNumbers = numbers.Sort(); // Compilation Error: Sort() returns void

Solution: Call `Sort()` without expecting a return value.
numbers.Sort();

11. Advanced Topics

11.1 List<T> with Custom Objects

Managing lists of custom objects requires overriding methods like `ToString`, `Equals`, and implementing interfaces like `IComparable<T>` for sorting.

Example: Managing a List of Custom Objects
using System;
using System.Collections.Generic;

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 }
        };

        // Display all people
        foreach(var person in people)
        {
            Console.WriteLine(person);
        }

        // Sort by Age
        people.Sort();
        Console.WriteLine("\nAfter Sorting by Age:");
        foreach(var person in people)
        {
            Console.WriteLine(person);
        }
    }
}

public class Person : IComparable<Person>
{
    public string Name { get; set; }
    public int Age { get; set; }

    public int CompareTo(Person other)
    {
        if(other == null) return 1;
        return this.Age.CompareTo(other.Age);
    }

    public override string ToString()
    {
        return $"{Name}, Age: {Age}";
    }
}

Sample Output:
Alice, Age: 30
Bob, Age: 25
Charlie, Age: 35
After Sorting by Age:
Bob, Age: 25
Alice, Age: 30
Charlie, Age: 35

Explanation:
- IComparable<T> Implementation: Allows sorting the list based on the `Age` property.
- ToString Override: Provides a readable string representation of the `Person` object.

11.2 Using LINQ with List<T>

Leveraging LINQ for powerful and concise data queries.

Example: Filtering and Selecting Data
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).Select(n => n * 10);

        Console.WriteLine("Even Numbers Multiplied by 10:");
        foreach(var num in evenNumbers)
        {
            Console.WriteLine(num);
        }
    }
}

Sample Output:
Even Numbers Multiplied by 10:
20
40

Explanation:
- Where: Filters elements based on a condition.
- Select: Projects each element into a new form.

11.3 Thread Safety

`List<T>` is not thread-safe. To safely use a list across multiple threads, implement synchronization mechanisms like locks or use thread-safe collections.

Example: Using Lock for Thread Safety
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

class Program
{
    static List<int> numbers = new List<int>();
    static object lockObj = new object();

    static void Main()
    {
        Parallel.For(0, 1000, i =>
        {
            lock(lockObj)
            {
                numbers.Add(i);
            }
        });

        Console.WriteLine($"Total Numbers Added: {numbers.Count}"); // Output: 1000
    }
}
Explanation:
- Locking Mechanism: Ensures that only one thread can modify the list at a time, preventing data corruption.

12. Real-World Example

Example: Task Management System Using List<T>

This example demonstrates a simple task management system where tasks can be added, completed, and listed. It utilizes `List<T>` to manage the collection of tasks.

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

class Program
{
    static void Main()
    {
        TaskManager taskManager = new TaskManager();

        // Adding tasks
        taskManager.AddTask(new TaskItem { Id = 1, Description = "Complete project report", IsCompleted = false });
        taskManager.AddTask(new TaskItem { Id = 2, Description = "Review pull requests", IsCompleted = false });
        taskManager.AddTask(new TaskItem { Id = 3, Description = "Plan team meeting", IsCompleted = false });

        // Display all tasks
        Console.WriteLine("All Tasks:");
        taskManager.DisplayAllTasks();

        // Complete a task
        taskManager.CompleteTask(2);

        // Display completed tasks
        Console.WriteLine("\nCompleted Tasks:");
        taskManager.DisplayCompletedTasks();

        // Display pending tasks
        Console.WriteLine("\nPending Tasks:");
        taskManager.DisplayPendingTasks();

        // Remove a task
        taskManager.RemoveTask(1);

        // Display all tasks after removal
        Console.WriteLine("\nAll Tasks After Removal:");
        taskManager.DisplayAllTasks();
    }
}

public class TaskItem
{
    public int Id { get; set; }
    public string Description { get; set; }
    public bool IsCompleted { get; set; }

    public override string ToString()
    {
        return $"ID: {Id}, Description: {Description}, Completed: {IsCompleted}";
    }
}

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

    public void AddTask(TaskItem task)
    {
        if(tasks.Any(t => t.Id == task.Id))
        {
            Console.WriteLine($"Task with ID {task.Id} already exists.");
            return;
        }
        tasks.Add(task);
        Console.WriteLine($"Added Task: {task.Description}");
    }

    public void CompleteTask(int id)
    {
        TaskItem task = tasks.FirstOrDefault(t => t.Id == id);
        if(task != null)
        {
            task.IsCompleted = true;
            Console.WriteLine($"Completed Task: {task.Description}");
        }
        else
        {
            Console.WriteLine($"Task with ID {id} not found.");
        }
    }

    public void RemoveTask(int id)
    {
        TaskItem task = tasks.FirstOrDefault(t => t.Id == id);
        if(task != null)
        {
            tasks.Remove(task);
            Console.WriteLine($"Removed Task: {task.Description}");
        }
        else
        {
            Console.WriteLine($"Task with ID {id} not found.");
        }
    }

    public void DisplayAllTasks()
    {
        foreach(var task in tasks)
        {
            Console.WriteLine(task);
        }
    }

    public void DisplayCompletedTasks()
    {
        var completedTasks = tasks.Where(t => t.IsCompleted);
        foreach(var task in completedTasks)
        {
            Console.WriteLine(task);
        }
    }

    public void DisplayPendingTasks()
    {
        var pendingTasks = tasks.Where(t => !t.IsCompleted);
        foreach(var task in pendingTasks)
        {
            Console.WriteLine(task);
        }
    }
}

Sample Output:
Added Task: Complete project report
Added Task: Review pull requests
Added Task: Plan team meeting
All Tasks:
ID: 1, Description: Complete project report, Completed: False
ID: 2, Description: Review pull requests, Completed: False
ID: 3, Description: Plan team meeting, Completed: False
Completed Task: Review pull requests
Completed Tasks:
ID: 2, Description: Review pull requests, Completed: True
Pending Tasks:
ID: 1, Description: Complete project report, Completed: False
ID: 3, Description: Plan team meeting, Completed: False
Removed Task: Complete project report
All Tasks After Removal:
ID: 2, Description: Review pull requests, Completed: True
ID: 3, Description: Plan team meeting, Completed: False


Explanation:
- TaskItem Class: Represents a task with properties like `Id`, `Description`, and `IsCompleted`.
- TaskManager Class: Manages a list of tasks using `List<TaskItem>`. Provides methods to add, complete, remove, and display tasks.
- Main Method: Demonstrates adding tasks, completing a task, displaying completed and pending tasks, and removing a task.

13. Summary

The `List<T>` class in C# is a versatile and powerful collection that provides dynamic resizing, type safety, and a rich set of methods for managing data. Whether you're dealing with simple lists of primitives or complex lists of custom objects, `List<T>` offers the functionality needed to handle various scenarios efficiently.

Key Takeaways:
- Type Safety: Ensures that only specified types are stored, preventing runtime errors.

- Dynamic Resizing: Automatically adjusts capacity as elements are added or removed.

- Rich API: Provides a wide range of methods for adding, removing, searching, and sorting elements.

- Performance: Optimized for performance with considerations for capacity management and avoiding unnecessary operations.

- Integration with LINQ: Seamlessly works with LINQ for advanced querying and data manipulation.

- Best Practices: Include initializing with appropriate capacity, using read-only collections when necessary, and handling exceptions gracefully.

- Common Mistakes: Avoid modifying the list during iteration, not specifying type parameters, and mismanaging capacity and performance.

- Advanced Usage: Manage lists of custom objects, utilize LINQ for complex operations, and ensure thread safety in multi-threaded environments.

- Real-World Applications: Implementing task managers, repositories, and other data-driven systems showcase the practical utility of `List<T>`.

By mastering `List<T>`, you enhance your ability to manage collections of data effectively, leading to more robust, maintainable, and efficient C# applications.

Previous: C# Collections | Next: C# Dictionary

<
>