Every great city needs a resident registry — but imagine the chaos if someone accidentally registered a lamppost as a citizen! In old .NET (pre-generics), collections like ArrayList stored everything as object, meaning you could mix strings, integers, and toasters in the same list. Generics solved this by letting you declare exactly what type a collection holds, catching mistakes at compile time instead of runtime.
Why Generics Matter
Before generics (introduced in C# 2.0), developers used ArrayList from System.Collections. The problem? Everything was boxed/unboxed as object:
// OLD WAY — no type safety!
ArrayList old = new ArrayList();
old.Add("Alice");
old.Add(42); // Compiles fine, but is this intentional?
string name = (string)old[1]; // Runtime crash! InvalidCastException
With generics, the compiler enforces type safety at compile time, eliminating costly boxing/unboxing for value types and preventing invalid casts entirely:
// NEW WAY — type-safe!
List<string> residents = new List<string>();
residents.Add("Alice");
// residents.Add(42); // Compile error! Can't add int to List<string>
List<T> — The Architect's Essential Tool
List<T> lives in System.Collections.Generic and is the most commonly used collection in C#. It's a dynamically-sized array that provides O(1) indexed access and amortized O(1) additions at the end.
Add(T item) — Appends an item to the end of the list
Remove(T item) — Removes the first occurrence; returns true if found
Count — Property returning the number of elements (not a method!)
Contains(T item) — Returns true if the item exists (uses Equals)
Sort() — Sorts in-place using the default comparer (alphabetical for strings)
Find(Predicate<T>) — Returns the first element matching the predicate, or default(T)
Insert(int index, T item) — Inserts at a specific position
RemoveAt(int index) — Removes element at the specified index
ForEach(Action<T>) — Executes an action on each element
List<string> residents = new List<string>();
// Adding residents to the city registry
residents.Add("Alice");
residents.Add("Bob");
residents.Add("Charlie");
Console.WriteLine($"Residents: {residents.Count}"); // 3
// Check membership
Console.WriteLine($"Contains Bob: {residents.Contains("Bob")}"); // True
// Sort alphabetically
residents.Sort();
Console.WriteLine(string.Join(", ", residents)); // Alice, Bob, Charlie
// Find with a predicate
string found = residents.Find(r => r.StartsWith("C"));
Console.WriteLine($"Found: {found}"); // Charlie
List<T> vs ArrayList — A Performance Comparison
For value types like int, List<int> is dramatically faster than ArrayList because it avoids boxing (wrapping a value type in an object on the heap). For reference types like string, the performance difference is smaller but type safety alone makes List<T> the clear winner.
Generic Type Constraints — A Taste
You can constrain what types are allowed with the where keyword. This is a preview — we'll explore this deeper later:
// Only types that implement IComparable<T> can be used
public static T FindMax<T>(List<T> items) where T : IComparable<T>
{
T max = items[0];
foreach (T item in items)
{
if (item.CompareTo(max) > 0)
max = item;
}
return max;
}
Time to build the city's resident registry, Architect! Register residents using List<T>, then query and sort the data.
📋 Instructions
**Build the City's Type-Safe Resident Registry!**
1. Create a `List` called `residents`
2. Add three residents: `"Alice"`, `"Bob"`, `"Charlie"`
3. Print `"Residents: "` followed by the count
4. Add `"Diana"` to the list and print `"Added: Diana"`
5. Print the new count as `"Residents: "` followed by the count
6. Check if `"Bob"` is in the list and print `"Contains Bob: "` followed by the result
7. Sort the list alphabetically
8. Print `"Sorted: "` followed by all residents joined with `", "`
Use `new List<string>()` to create the list. The `Count` property (not method) gives the size. `Sort()` sorts in place. Use `string.Join(", ", residents)` to create a comma-separated string.