Summary

Arrays (see csharp-variables-and-types) have a fixed size set at creation. The moment you need a collection that can grow or shrink, such as a list of spawned enemies, a player inventory or a registry of active effects, you need a different tool. C# provides two workhorse collections in System.Collections.Generic:

  • List<T>: an ordered, resizable list of items of type T. Use it like an array that can grow.
  • Dictionary<TKey, TValue>: maps unique keys to values for fast lookup. Use it when you need to find something by a label or ID rather than by position.

Both are generic: the types between <> tell the compiler what kind of thing the collection holds, enabling type safety without casts.


Key ideas

What List<T> means in beginner terms

List<T> means “a list of one specific type of thing”. The T is a placeholder for that type.

  • List<int> means a list of integers
  • List<string> means a list of strings
  • List<GameObject> means a list of Unity GameObject references

If you declare List<int>, every value you add must be an int. That is exactly the point of generics: the compiler stops you from accidentally mixing the wrong kind of data into the same collection. Miles argues that this type safety is one of the main advantages of modern generic collections over older weakly typed approaches (Miles, C# Yellow Book, see source-csharp-yellow-book).

List<T>

Creating a list:

List<int>          scores   = new List<int>();
List<string>       names    = new List<string>();
List<GameObject>   enemies  = new List<GameObject>();

No size required. The list grows automatically as items are added.

Core operations:

List<string> inventory = new List<string>();
 
// Add
inventory.Add("Sword");
inventory.Add("Shield");
inventory.Add("Potion");
 
// Count
int itemCount = inventory.Count;   // 3
 
// Access by index
string first = inventory[0];       // "Sword"
 
// Check membership
bool hasPotion = inventory.Contains("Potion");   // true
 
// Remove by value
inventory.Remove("Potion");        // removes first occurrence
 
// Remove by index
inventory.RemoveAt(0);             // removes "Sword"
 
// Clear all
inventory.Clear();

Iterating:

foreach (string item in inventory)
{
    Debug.Log(item);
}
 
// Or with index
for (int i = 0; i < inventory.Count; i++)
{
    Debug.Log($"[{i}] {inventory[i]}");
}

Accessing the actual object in a list

If you have a list of objects, list[i] gives you the actual object reference stored at that position.

List<GameObject> activeEnemies = new List<GameObject>();
 
for (int i = 0; i < activeEnemies.Count; i++)
{
    GameObject enemy = activeEnemies[i];
    Debug.Log(enemy.name);
}

That enemy variable is not a copy of the name or index. It is the actual GameObject reference from the list, so you can call methods on it, read components from it, or destroy it.

for (int i = 0; i < activeEnemies.Count; i++)
{
    GameObject enemy = activeEnemies[i];
    EnemyHealth health = enemy.GetComponent<EnemyHealth>();
 
    if (health != null && health.CurrentHealth <= 0)
    {
        Destroy(enemy);
    }
}

Do not modify a list while iterating it with foreach. This throws an InvalidOperationException. Collect items to remove in a separate list first, then remove them after.

List<Enemy> toRemove = new List<Enemy>();
 
foreach (Enemy e in activeEnemies)
{
    if (!e.IsAlive) toRemove.Add(e);
}
 
foreach (Enemy e in toRemove)
    activeEnemies.Remove(e);

Dictionary<TKey, TValue>

A dictionary stores key-value pairs. Keys must be unique. Lookup by key is very fast regardless of how many entries the dictionary contains.

Creating a dictionary:

Dictionary<string, int>        highScores  = new Dictionary<string, int>();
Dictionary<string, GameObject> spawnedNPCs = new Dictionary<string, GameObject>();
Dictionary<int, Quest>         questLog    = new Dictionary<int, Quest>();

Core operations:

Dictionary<string, int> highScores = new Dictionary<string, int>();
 
// Add
highScores.Add("Alice", 1500);
highScores.Add("Bob",   1200);
 
// Look up by key
int aliceScore = highScores["Alice"];   // 1500
 
// Safe lookup — use ContainsKey to avoid KeyNotFoundException
if (highScores.ContainsKey("Charlie"))
{
    int score = highScores["Charlie"];
}
 
// Update an existing key
highScores["Alice"] = 2000;
 
// Remove
highScores.Remove("Bob");
 
// Count
int playerCount = highScores.Count;
 
// Iterate
foreach (KeyValuePair<string, int> entry in highScores)
{
    Debug.Log($"{entry.Key}: {entry.Value}");
}

TryGetValue, preferred for safe lookup:

if (highScores.TryGetValue("Alice", out int score))
{
    Debug.Log($"Alice scored {score}");
}
// If key absent, no exception — just returns false

In practice (Unity)

Tracking spawned enemies:

public class SpawnManager : MonoBehaviour
{
    private List<GameObject> activeEnemies = new List<GameObject>();
 
    public void SpawnEnemy(GameObject prefab, Vector3 position)
    {
        GameObject enemy = Instantiate(prefab, position, Quaternion.identity);
        activeEnemies.Add(enemy);
    }
 
    public void DespawnAll()
    {
        foreach (GameObject e in activeEnemies)
            Destroy(e);
        activeEnemies.Clear();
    }
 
    public int EnemyCount => activeEnemies.Count;
}

Simple item registry with Dictionary:

public class ItemDatabase : MonoBehaviour
{
    private Dictionary<string, ItemData> items = new Dictionary<string, ItemData>();
 
    private void Awake()
    {
        items.Add("sword",  new ItemData("Sword",  damage: 10));
        items.Add("shield", new ItemData("Shield", defence: 5));
    }
 
    public ItemData GetItem(string id)
    {
        if (items.TryGetValue(id, out ItemData data))
            return data;
 
        Debug.LogWarning($"Item '{id}' not found.");
        return null;
    }
}

Score table:

Dictionary<string, int> scores = new Dictionary<string, int>();
 
public void AddScore(string playerName, int points)
{
    if (scores.ContainsKey(playerName))
        scores[playerName] += points;
    else
        scores.Add(playerName, points);
}

List vs Array vs Dictionary

ArrayList<T>Dictionary<K,V>
SizeFixedDynamicDynamic
AccessBy indexBy indexBy key
LookupO(n) searchO(n) searchO(1) hash
Type safeYesYesYes
When to useFixed-size data known at compile timeVariable-length ordered itemsKey-based lookup

Array vs List in beginner terms

  • Use an array when you know the number of items will stay fixed.
  • Use a List when you expect to add or remove items while the game is running.
  • Use a Dictionary when you need to find something by a name, ID, or other key rather than by position.

Evidence

From Miles (2019, §5.1.2): “The List class gives you everything that an arraylist gives you, with the advantage that it is also typesafe… there is just no way that you can take an Account reference and place it in a list of integers.”

From Miles (2019, §5.1.4, Programmer’s Point): “Every time you need to hold a bunch of things, make a List. If you want to be able to find things on the basis of a key, use a Dictionary. Don’t have any concerns about performance. The programmers who wrote these things are very clever folks.”


Gotchas

  • Accessing a Dictionary by key (dict["key"]) throws KeyNotFoundException if the key is absent. Always check with ContainsKey or TryGetValue first.
  • A Dictionary does not allow duplicate keys. Adding an existing key throws ArgumentException. Check before adding or use dict[key] = value to overwrite.
  • List.Remove(item) removes the first occurrence only. To remove all matching items, use RemoveAll(predicate): activeEnemies.RemoveAll(e => e == null).
  • Lists are not thread-safe. If background threads access the same list, use ConcurrentBag<T> or lock manually. In Unity, avoid modifying lists from coroutines without care.
  • For tiny, fixed collections (e.g., 3 waypoints), an array is simpler and slightly faster. Don’t reach for List when an array is the right tool.

Practice

Build a tiny inventory and score tracker:

List<string> inventory = new List<string>();
Dictionary<string, int> scores = new Dictionary<string, int>();
 
inventory.Add("key");
inventory.Add("coin");
inventory.Add("coin");
 
scores["Player"] = 0;

Tasks:

  1. Add "potion" to the inventory.
  2. Print every inventory item with its index.
  3. Remove one "coin" from the inventory.
  4. Add 50 points to "Player" safely.
  5. Check whether "Boss" exists in the score dictionary before reading it.

Then answer this design question: should a shop inventory use a List<string> or a Dictionary<string, int> if each item has a price?

Self-test

  1. What does T mean in List<T>?
  2. Why does List<int> reject a string value?
  3. Why is TryGetValue safer than scores["Boss"]?
  4. Why should you avoid removing items from a list inside a foreach loop?
  5. Which collection is better for looking up an item price by item ID?

Answers

  1. T stands for the type of item stored in the list, such as int, string or GameObject.
  2. List<int> only accepts integers. The compiler rejects a string because it would break the list’s type safety.
  3. TryGetValue returns false if the key is missing. scores["Boss"] throws an exception if "Boss" is not present.
  4. Removing items during foreach changes the collection while the loop is using it, which causes an InvalidOperationException.
  5. A Dictionary<string, int> is better because it maps each item ID to a price.