C#: Collections and Generics

1. 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

CSHARP
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);
}
▶ Try it Yourself
TEXT 📖 Display only
42
hello

The generic version determines element types at compile time, requiring no boxing or unboxing, and errors are caught during compilation:

CSHARP
using System;
using System.Collections.Generic;

List````<int>```` numbers = new List````<int>````();
numbers.Add(42);

foreach (int n in numbers)
{
    Console.WriteLine(n);
}
TEXT 📖 Display only
42

2. 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

CSHARP
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)}");
▶ Try it Yourself
TEXT 📖 Display only
apple
blueberry
banana
cherry
date
elderberry
fig
---
Count: 7
Has apple: True
Cherry index: 3
First b-word: blueberry
All b-words: blueberry, banana

(1) Removing Elements

CSHARP
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}");
TEXT 📖 Display only
20, 40, 50
Count: 3

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

CSHARP
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}");
}
▶ Try it Yourself
TEXT 📖 Display only
Dave not found
Count: 2
---Keys---
Alice
Bob
---Values---
90
92
---Pairs---
Alice: 90
Bob: 92

4. 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

CSHARP
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"));
▶ Try it Yourself
TEXT 📖 Display only
7
banana

Generic class example:

CSHARP
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);
TEXT 📖 Display only
Box<Int32>: 42
Box<String>: hello

5. Generic Constraints

Use the where keyword to impose constraints on type parameters, limiting the range of types that can be used.

▶ Example

CSHARP
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}");
▶ Try it Yourself
TEXT 📖 Display only
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

6. Queue<T> Queue

A queue is a first-in-first-out (FIFO) data structure. Use Enqueue to add and Dequeue to remove.

▶ Example

CSHARP
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()}");
}
▶ Try it Yourself
TEXT 📖 Display only
Peek: first
Dequeue: first
Dequeue: second
Dequeue: third

7. Stack<T> Stack

A stack is a last-in-first-out (LIFO) data structure. Use Push to add and Pop to remove.

▶ Example

CSHARP
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()}");
}
▶ Try it Yourself
TEXT 📖 Display only
Peek: 30
Pop: 30
Pop: 20
Pop: 10

8. Collection Initializers

Collection initializers allow you to populate elements directly when creating a collection, with concise and intuitive syntax.

▶ Example

CSHARP
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}");
}
▶ Try it Yourself
TEXT 📖 Display only
1, 2, 3, 4, 5
coffee: $3.5
tea: $2.8
juice: $4

9. 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

CSHARP
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);
}
▶ Try it Yourself
TEXT 📖 Display only
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

Q What is the difference between List<T> and arrays?
A List<T> supports dynamically adding and removing elements, while arrays have a fixed length. List is internally backed by an array; when resizing is needed, it automatically creates a larger array and copies the elements.
Q What happens if you add a duplicate key to a Dictionary?
A Using the Add method with a duplicate key throws ArgumentException; using the indexer assignment dict[key] = value overwrites the existing value.
Q Can where T : class and where T : struct be used together?
A No, the class constraint requires a reference type and the struct constraint requires a value type — they are mutually exclusive.
Q Can Queue and Stack be traversed with foreach?
A Yes, foreach traversal does not remove elements; it only reads them. Dequeue and Pop are what actually remove elements.
Q Why is the generic constraint where T : new() needed?
A It guarantees that you can use new T() to create an instance inside a method. Without this constraint, the compiler does not allow using the new operator on T.

📖 Summary

📝 Exercises

  1. Create a List<double>, add 5 scores, use FindAll to filter scores above 80 and output them
  2. Create a Dictionary<string, string> storing 3 capital city-to-province mappings, implement looking up a province by city name (using TryGetValue)
  3. 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
  4. Use Queue<string> to simulate a print queue: enqueue 3 document names, then dequeue and print them in order
  5. Write an IEnumerable<int> iterator method using yield return to generate the first 10 terms of the Fibonacci sequence
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏