Collections and Generics
Generic Concepts and Motivation
Generics allow us to write type-parameterized code, achieving type safety and code reuse. Before generics, ArrayList stored object types; value types were boxed when added and unboxed when retrieved, which hurt performance and lacked compile-time type checking.
Example
using System;
using System.Collections;
ArrayList list = new ArrayList();
list.Add(42);
list.Add("hello");
int value = (int)list[0];
foreach (object item in list)
{
Console.WriteLine(item);
}
42
hello
The generic version determines element types at compile time, requiring no boxing or unboxing, and errors are caught during compilation:
using System;
using System.Collections.Generic;
List<int> numbers = new List<int>();
numbers.Add(42);
foreach (int n in numbers)
{
Console.WriteLine(n);
}
42
List<T> Dynamic List
List<T> is the most commonly used generic collection, supporting a dynamically resizable ordered list with rich methods for adding, removing, searching, and modifying.
Example
using System;
using System.Collections.Generic;
List<string> fruits = new List<string> { "apple", "banana", "cherry" };
fruits.Add("date");
fruits.AddRange(new string[] { "elderberry", "fig" });
fruits.Insert(1, "blueberry");
bool hasApple = fruits.Contains("apple");
int idx = fruits.IndexOf("cherry");
string found = fruits.Find(f => f.StartsWith("b"));
List<string> allB = fruits.FindAll(f => f.StartsWith("b"));
fruits.Sort();
fruits.ForEach(f => Console.WriteLine(f));
Console.WriteLine("---");
Console.WriteLine($"Count: {fruits.Count}");
Console.WriteLine($"Has apple: {hasApple}");
Console.WriteLine($"Cherry index: {idx}");
Console.WriteLine($"First b-word: {found}");
Console.WriteLine($"All b-words: {string.Join(", ", allB)}");
apple
blueberry
banana
cherry
date
elderberry
fig
---
Count: 7
Has apple: True
Cherry index: 3
First b-word: blueberry
All b-words: blueberry, banana
Removing Elements
using System;
using System.Collections.Generic;
List<int> nums = new List<int> { 10, 20, 30, 40, 50 };
nums.Remove(30);
nums.RemoveAt(0);
Console.WriteLine(string.Join(", ", nums));
Console.WriteLine($"Count: {nums.Count}");
20, 40, 50
Count: 3
Dictionary<TKey, TValue> Dictionary
Dictionary<TKey, TValue> stores key-value pairs, enabling fast value lookup by key with near O(1) lookup time.
Example
using System;
using System.Collections.Generic;
Dictionary<string, int> scores = new Dictionary<string, int>
{
{ "Alice", 90 },
{ "Bob", 85 }
};
scores.Add("Charlie", 78);
scores["Bob"] = 92;
bool exists = scores.ContainsKey("Alice");
if (scores.TryGetValue("Dave", out int daveScore))
{
Console.WriteLine($"Dave: {daveScore}");
}
else
{
Console.WriteLine("Dave not found");
}
scores.Remove("Charlie");
Console.WriteLine($"Count: {scores.Count}");
Console.WriteLine("---Keys---");
foreach (string key in scores.Keys)
{
Console.WriteLine(key);
}
Console.WriteLine("---Values---");
foreach (int val in scores.Values)
{
Console.WriteLine(val);
}
Console.WriteLine("---Pairs---");
foreach (KeyValuePair<string, int> kv in scores)
{
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
Dave not found
Count: 2
---Keys---
Alice
Bob
---Values---
90
92
---Pairs---
Alice: 90
Bob: 92
Generic Methods and Generic Classes
Generics are not limited to collections; methods and classes can also use type parameters, making algorithms applicable to multiple types.
Example
using System;
T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}
Console.WriteLine(Max(3, 7));
Console.WriteLine(Max("apple", "banana"));
7
banana
Generic class example:
using System;
class Box<T>
{
private T _value;
public Box(T value)
{
_value = value;
}
public T Value => _value;
public override string ToString() => $"Box<{typeof(T).Name}>: {_value}";
}
Box<int> intBox = new Box<int>(42);
Box<string> strBox = new Box<string>("hello");
Console.WriteLine(intBox);
Console.WriteLine(strBox);
Box<Int32>: 42
Box<String>: hello
Generic Constraints
Use the where keyword to impose constraints on type parameters, limiting the range of types that can be used.
Example
using System;
class Repository<T> where T : class, new()
{
public T Create()
{
T instance = new T();
Console.WriteLine($"Created: {typeof(T).Name}");
return instance;
}
}
class Player
{
public string Name { get; set; } = "Unknown";
}
Repository<Player> repo = new Repository<Player>();
Player p = repo.Create();
Console.WriteLine($"Name: {p.Name}");
Created: Player
Name: Unknown
Common constraints reference:
| Constraint | Description |
|---|---|
where T : class |
T must be a reference type |
where T : struct |
T must be a value type |
where T : new() |
T must have a parameterless public constructor |
where T : IFoo |
T must implement the IFoo interface |
where T : Base |
T must inherit from the Base class |
Queue<T> Queue
A queue is a first-in-first-out (FIFO) data structure. Use Enqueue to add and Dequeue to remove.
Example
using System;
using System.Collections.Generic;
Queue<string> queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");
queue.Enqueue("third");
string front = queue.Peek();
Console.WriteLine($"Peek: {front}");
while (queue.Count > 0)
{
Console.WriteLine($"Dequeue: {queue.Dequeue()}");
}
Peek: first
Dequeue: first
Dequeue: second
Dequeue: third
Stack<T> Stack
A stack is a last-in-first-out (LIFO) data structure. Use Push to add and Pop to remove.
Example
using System;
using System.Collections.Generic;
Stack<int> stack = new Stack<int>();
stack.Push(10);
stack.Push(20);
stack.Push(30);
int top = stack.Peek();
Console.WriteLine($"Peek: {top}");
while (stack.Count > 0)
{
Console.WriteLine($"Pop: {stack.Pop()}");
}
Peek: 30
Pop: 30
Pop: 20
Pop: 10
Collection Initializers
Collection initializers allow you to populate elements directly when creating a collection, with concise and intuitive syntax.
Example
using System;
using System.Collections.Generic;
List<int> nums = new List<int> { 1, 2, 3, 4, 5 };
Dictionary<string, double> prices = new Dictionary<string, double>
{
{ "coffee", 3.5 },
{ "tea", 2.8 },
{ "juice", 4.0 }
};
Console.WriteLine(string.Join(", ", nums));
foreach (var kv in prices)
{
Console.WriteLine($"{kv.Key}: ${kv.Value}");
}
1, 2, 3, 4, 5
coffee: $3.5
tea: $2.8
juice: $4
Introduction to IEnumerable and Iterators
IEnumerable<T> is the foundational interface for all traversable collections, and the foreach loop relies on it. Using yield return, you can easily write custom iterators (details in Lesson 35).
Example
using System;
using System.Collections.Generic;
IEnumerable<int> GetRange(int start, int end)
{
for (int i = start; i <= end; i++)
{
yield return i;
}
}
foreach (int n in GetRange(1, 5))
{
Console.WriteLine(n);
}
1
2
3
4
5
yield return turns a method into an iterator: each call returns one value and pauses, and the next iteration resumes from the pause point. For a complete explanation, see Lesson 35.
❓ FAQ
dict[key] = value overwrites the existing value.where T : class and where T : struct be used together?where T : new() needed?new T() to create an instance inside a method. Without this constraint, the compiler does not allow using the new operator on T.📖 Summary
- Generics provide type safety and code reuse, avoiding boxing and unboxing overhead
List<T>is a dynamic array supporting rich operations like add, remove, search, modify, sort, and findDictionary<TKey, TValue>stores key-value pairs, providing O(1) lookup performance- Generic methods and classes make algorithms and data structures applicable to multiple types
whereconstraints limit the range of type parameters: class, struct, new(), interfaces, base classesQueue<T>is first-in-first-out,Stack<T>is last-in-first-out- Collection initializers simplify collection creation and population
IEnumerable<T>is the foundation for traversal, andyield returncan create iterators
📝 Exercises
- Create a
List<double>, add 5 scores, useFindAllto filter scores above 80 and output them - Create a
Dictionary<string, string>storing 3 capital city-to-province mappings, implement looking up a province by city name (usingTryGetValue) - Write a generic method
T Clamp<T>(T value, T min, T max) where T : IComparable<T>that clamps a value within the [min, max] range - Use
Queue<string>to simulate a print queue: enqueue 3 document names, then dequeue and print them in order - Write an
IEnumerable<int>iterator method usingyield returnto generate the first 10 terms of the Fibonacci sequence



