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 typeT. 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 integersList<string>means a list of stringsList<GameObject>means a list of UnityGameObjectreferences
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 falseIn 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
| Array | List<T> | Dictionary<K,V> | |
|---|---|---|---|
| Size | Fixed | Dynamic | Dynamic |
| Access | By index | By index | By key |
| Lookup | O(n) search | O(n) search | O(1) hash |
| Type safe | Yes | Yes | Yes |
| When to use | Fixed-size data known at compile time | Variable-length ordered items | Key-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
Dictionaryby key (dict["key"]) throwsKeyNotFoundExceptionif the key is absent. Always check withContainsKeyorTryGetValuefirst. - A
Dictionarydoes not allow duplicate keys. Adding an existing key throwsArgumentException. Check before adding or usedict[key] = valueto overwrite. List.Remove(item)removes the first occurrence only. To remove all matching items, useRemoveAll(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:
- Add
"potion"to the inventory. - Print every inventory item with its index.
- Remove one
"coin"from the inventory. - Add
50points to"Player"safely. - 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
- What does
Tmean inList<T>? - Why does
List<int>reject a string value? - Why is
TryGetValuesafer thanscores["Boss"]? - Why should you avoid removing items from a list inside a
foreachloop? - Which collection is better for looking up an item price by item ID?
Answers
Tstands for the type of item stored in the list, such asint,stringorGameObject.List<int>only accepts integers. The compiler rejects a string because it would break the list’s type safety.TryGetValuereturnsfalseif the key is missing.scores["Boss"]throws an exception if"Boss"is not present.- Removing items during
foreachchanges the collection while the loop is using it, which causes anInvalidOperationException. - A
Dictionary<string, int>is better because it maps each item ID to a price.
Related
- csharp-variables-and-types: arrays, the fixed-size alternative
- csharp-oop-fundamentals: generics context, classes and type parameters
- unity-prefabs-scripting: List
for tracking instantiated prefabs - unity-gamemanager-pattern: registry patterns using collections