Chapter 20: Introduction to Lists - Moving beyond Fixed Arrays
Theoretical Foundations
Remember back in Chapter 11 when we first learned about arrays? We declared them like this:
This created a container that could hold exactly 5 integers. If you needed to store a 6th score, you hit a wall. The array is "fixed"—it cannot grow. In real-world applications, especially AI applications, we rarely know exactly how much data we will need to store ahead of time.
Imagine you are building an AI chat application. You need to store the conversation history. You might start with an array sized for 10 messages, but what happens when the user has a long conversation with 50 messages? You would have to manually create a new, larger array and copy everything over. This is tedious and error-prone.
This is exactly why C# provides the List<T> collection. It is a dynamic array that automatically handles resizing for you, while still giving you the fast access to items by index that you are used to with arrays.
The Dynamic Box Analogy
Think of a standard array like a fixed-size toolbox with exactly 5 slots. If you have 6 tools, you cannot fit the last one in. You would need to buy a new, bigger toolbox and move all your tools from the old one to the new one.
A List<T> is like a magical toolbox. When you try to put a 6th tool into it, it automatically expands its size to accommodate it. If you take tools out and have empty space, it might shrink to save space (though usually it keeps the space ready for future tools).
Creating and Initializing Lists
To use a List, you must first tell C# where to find the definition. It lives in a namespace called System.Collections.Generic. You add this to the top of your file:
Now, let's create a list. We use the new keyword just like we did with classes in Chapter 16, but we use List<T> syntax. The <T> part is called a "generic type parameter." For now, just replace T with the data type you want to store, like int or string.
using System.Collections.Generic;
// Creating an empty list of integers
List<int> highScores = new List<int>();
// Creating a list with initial values (similar to array initialization)
List<string> playerNames = new List<string> { "Alice", "Bob", "Charlie" };
How AI Uses This
In AI development, you often process data in batches. For example, if you are training a model on text data, you might load a list of sentences from a file. You don't know how many sentences are in the file until you read it. Using List<string> allows you to read line by line and Add() each sentence to the list without worrying about the size.
Adding and Removing Data
The primary advantage of a List is its ability to change size dynamically.
Adding Items
You use the Add() method to append an item to the end of the list.
List<int> sensorReadings = new List<int>();
sensorReadings.Add(24); // List: [24]
sensorReadings.Add(25); // List: [24, 25]
sensorReadings.Add(23); // List: [24, 25, 23]
Inserting Items
Sometimes you need to put an item in a specific position, shifting existing items over. We use the Insert() method. It takes two arguments: the index (position) and the value.
// Current list: [24, 25, 23]
sensorReadings.Insert(1, 99); // Insert 99 at index 1
// List becomes: [24, 99, 25, 23]
Removing Items
There are two main ways to remove items:
Remove(value): Finds the first occurrence of that value and removes it.RemoveAt(index): Removes the item at a specific position.
List<string> tasks = new List<string> { "Train", "Test", "Deploy" };
tasks.Remove("Test"); // List: ["Train", "Deploy"]
tasks.RemoveAt(0); // Removes "Train"
// List: ["Deploy"]
AI Application: Filtering Data
Imagine you have a list of potential image labels predicted by an AI model: ["Cat", "Dog", "Car", "Cat", "Bicycle"]. You might want to remove all non-animal labels. You could loop through the list and Remove() any item that isn't "Cat" or "Dog".
Accessing and Iterating
Just like arrays, List<T> stores items in a specific order. You can access them using the index syntax [ ].
List<string> models = new List<string>();
models.Add("GPT-4");
models.Add("BERT");
models.Add("RoBERTa");
Console.WriteLine(models[1]); // Outputs: BERT
You can also iterate over them using a foreach loop, which we learned in Chapter 12.
Capacity vs. Count
This is a crucial concept for performance. A List has two important properties:
Count: The number of items currently in the list (what you actually see).Capacity: The amount of memory the list has reserved behind the scenes.
When you create a new List, it usually starts with a small capacity (often 0 or 4). As you add items, when the Count reaches the Capacity, the list does something expensive: it doubles the capacity, allocates a new block of memory, and copies all the existing elements to the new location.
The Analogy: Imagine you are writing in a notebook.
- Count is the number of lines you have written.
- Capacity is the number of pages in the notebook.
If you fill up the last line on a page, you can't write there anymore. You have to get a new notebook (double the capacity) and rewrite everything from the old notebook into the new one just to add one more line. This copying takes time.
Why this matters for AI
If you are reading 1,000,000 data points into a list one by one, and the list resizes 20 times (doubling each time), that is a lot of copying. It is more efficient to tell the list roughly how big it needs to be upfront if you know the approximate size.
// We know we are about to load 100,000 rows of data
List<double> neuralWeights = new List<double>(100000);
// Now we can add 100,000 items efficiently without resizing
for (int i = 0; i < 100000; i++)
{
neuralWeights.Add(0.0); // No resizing occurs!
}
Checking Capacity and Optimization
You can check the current state of the list using properties:
List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
Console.WriteLine($"Count: {numbers.Count}"); // Output: 2
Console.WriteLine($"Capacity: {numbers.Capacity}"); // Output: 4 (or similar)
If you have added many items and then removed most of them, the capacity might still be high, wasting memory. You can force the list to shrink to exactly fit the count using TrimExcess().
Visualizing the Structure
Here is a visual representation of how a List<int> grows. Notice how the underlying array (Capacity) jumps in size while the Count increases smoothly.
Summary of Key Differences
| Feature | Array (type[]) |
List (List<type>) |
|---|---|---|
| Size | Fixed at creation | Dynamic (grows/shrinks) |
| Memory | Contiguous block | Resizable array internally |
| Syntax | new int[5] |
new List<int>() |
| Adding | Not possible (unless resizing manually) | .Add(item) |
| Count | Length property |
Count property |
Recap
We have moved past the rigid world of fixed arrays. List<T> provides the flexibility needed for real-world programming where data volumes are unpredictable. By understanding how to add, remove, and manage the capacity of these lists, you are preparing to handle dynamic datasets essential in AI applications.
In the next section, we will look at specific methods available to manipulate these lists in more detail.
Basic Code Example
Here is a simple code example demonstrating the creation and manipulation of a List<string> to manage a dynamic collection of grocery items.
using System;
using System.Collections.Generic; // Required for List<T>
class Program
{
static void Main()
{
// 1. Initialize a new List of strings to hold grocery items.
// Unlike arrays (string[]), we don't need to specify a size here.
List<string> groceries = new List<string>();
// 2. Add items to the list using the Add() method.
groceries.Add("Apples");
groceries.Add("Milk");
groceries.Add("Bread");
// 3. Display the current list of items.
Console.WriteLine("Current Grocery List:");
// Use a foreach loop (Chapter 12) to iterate through the list.
foreach (string item in groceries)
{
Console.WriteLine("- " + item);
}
// 4. Remove an item from the list.
// We remove "Milk" because we found some at home.
groceries.Remove("Milk");
Console.WriteLine("\nUpdated Grocery List (after removing Milk):");
// Iterate again to show the updated list.
foreach (string item in groceries)
{
Console.WriteLine("- " + item);
}
// 5. Access a specific item by index (similar to arrays).
// Note: The index is now 0 because "Milk" was removed.
string firstItem = groceries[0];
Console.WriteLine("\nThe first item on the list is: " + firstItem);
}
}
Explanation
-
Initialization:
List<string> groceries = new List<string>();- We declare a variable named
groceries. Its type isList<string>, which means it is a list designed to holdstringelements. - We use the
newkeyword to create an instance of theListclass in memory. Unlike a fixed array (e.g.,string[3]), we do not define a size here. The list starts empty and grows as we add items.
-
Adding Items:
groceries.Add("Apples");- The
Addmethod appends a new element to the end of the list. - Internally, the list checks if it has enough space (Capacity). If not, it automatically creates a larger array behind the scenes and copies the existing items over. This is the "dynamic" behavior that solves the fixed-size limitation of arrays.
-
Iterating with
foreach:foreach (string item in groceries)- We use the
foreachloop (learned in Chapter 12) to read every item in the collection sequentially. - This loop runs once for every item currently stored in the
grocerieslist, assigning the value to the variableitemfor that specific iteration.
-
Removing Items:
groceries.Remove("Milk");- The
Removemethod searches the list for the specified value ("Milk") and deletes it. - If the item exists, the list shifts subsequent elements to fill the gap, ensuring the list remains contiguous.
-
Indexing:
string firstItem = groceries[0];- Lists support index access using square brackets
[], just like arrays. - Important: After removing "Milk", the list contains "Apples" and "Bread". "Apples" is now at index 0, and "Bread" is at index 1.
Visualizing List Growth
The following diagram illustrates how a List<string> manages memory compared to a fixed array. When the list's internal capacity is exceeded, it allocates a new, larger block of memory.
Common Pitfalls
1. Forgetting the using System.Collections.Generic; Directive
The List<T> class is not part of the core System namespace. If you try to use List<string> without adding using System.Collections.Generic; at the top of your file, the compiler will generate an error stating that the type or namespace name 'List' could not be found.
2. Confusing Remove with Index Removal
A common mistake is trying to remove an item by its index using the Remove method.
- Incorrect:
groceries.Remove(0);(This will cause a compiler error because the list expects a string value, not an integer index). - Correct:
groceries.RemoveAt(0);(This method specifically removes the item at index 0).
3. Index Out of Bounds
Even though lists are dynamic, accessing an index that does not contain data will throw an ArgumentOutOfRangeException.
- If the list has 3 items, valid indices are 0, 1, and 2.
- Accessing
groceries[3]will crash the program. Always check theCountproperty (e.g.,if (groceries.Count > 0)) before accessing an index.
The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon
Code License: All code examples are released under the MIT License. Github repo.
Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.
All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.