4.7 - Collections and Generics
Collections are objects that group multiple elements into a single unit. C# provides a rich set of collection types through the .NET Framework Class Library (FCL), particularly in the System.Collections.Generic
namespace. This chapter explores these collection types and how to use them effectively.
π§© Visual Learning: What Are Collections?β
Think of collections like different types of containers for storing multiple items:
βββββββββββββββββββββ βββββββββββββββββββββ βββββββββββββββββββββ
β LIST β β DICTIONARY β β QUEUE β
β β β β β β
β βββββ¬ββββ¬ββββ¬βββββ β βββββ βββββ β β βββββ¬ββββ¬ββββ¬βββββ
β β 0 β 1 β 2 β 3 ββ β β"A"ββββββΊβ 1 β β β β 1 β 2 β 3 β 4 ββ
β βββββ΄ββββ΄ββββ΄βββββ β βββββ βββββ β β βββββ΄ββββ΄ββββ΄βββββ
β β β βββββ βββββ β β β² βΌ β
β β’ Ordered items β β β"B"ββββββΊβ 2 β β β Add here Take β
β β’ Access by index β β βββββ βββββ β β hereβ
β β’ Easy to resize β β β β β
β β β β’ Key-value pairs β β β’ First in, β
βββββββββββββββββββββ βββββββββββββββββββββ β first out β
βββββββββββββββββββββ
π‘ Concept Breakdown: Why Use Collections?β
Collections solve several common programming problems:
- Storing variable amounts of data - Unlike arrays, most collections can grow or shrink as needed
- Organizing data in specific ways - Different collections organize data differently (by key, in order, etc.)
- Performing common operations efficiently - Collections have built-in methods for searching, sorting, etc.
- Type safety with generics - Generic collections ensure you only store one type of data
π° Beginner's Corner: Arrays vs. Collectionsβ
If you're new to programming, you might wonder why we need collections when we already have arrays:
Feature | Arrays | Collections |
---|---|---|
Size | Fixed size when created | Most can grow and shrink dynamically |
Flexibility | Limited built-in functionality | Rich set of methods for manipulation |
Type Safety | Can only hold one type | Generic collections ensure type safety |
Performance | Very fast for simple operations | Optimized for specific scenarios |
Ease of Use | More manual coding required | Built-in methods for common tasks |
When to use arrays:
- When you know the exact size needed in advance
- When maximum performance is critical
- For simple, straightforward data storage
When to use collections:
- When size might change
- When you need specialized behavior (dictionaries, queues, etc.)
- When you want built-in functionality for common operations
4.7.1 - Lists and Arraysβ
Lists and arrays are the most commonly used collection types in C#, providing ordered collections of elements.
4.7.1.1 - Arraysβ
Arrays are fixed-size collections of elements of the same type:
Example:
using System;
class Program
{
static void Main()
{
// Declare and initialize an array
int[] numbers = new int[5]; // Creates an array of 5 integers, all initialized to 0
// Assign values to array elements
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
numbers[3] = 40;
numbers[4] = 50;
// Access array elements
Console.WriteLine($"First element: {numbers[0]}");
Console.WriteLine($"Third element: {numbers[2]}");
// Array initialization with values
string[] fruits = new string[] { "Apple", "Banana", "Cherry", "Date", "Elderberry" };
// Shorthand initialization
char[] vowels = { 'a', 'e', 'i', 'o', 'u' };
// Get array length
Console.WriteLine($"Number of fruits: {fruits.Length}");
// Iterate through an array with for loop
Console.WriteLine("\nFruits (using for loop):");
for (int i = 0; i < fruits.Length; i++)
{
Console.WriteLine($" {i + 1}. {fruits[i]}");
}
// Iterate through an array with foreach loop
Console.WriteLine("\nVowels (using foreach loop):");
foreach (char vowel in vowels)
{
Console.WriteLine($" {vowel}");
}
// Multidimensional arrays
int[,] matrix = new int[3, 3]; // 3x3 matrix
// Assign values to the matrix
matrix[0, 0] = 1; matrix[0, 1] = 2; matrix[0, 2] = 3;
matrix[1, 0] = 4; matrix[1, 1] = 5; matrix[1, 2] = 6;
matrix[2, 0] = 7; matrix[2, 1] = 8; matrix[2, 2] = 9;
// Display the matrix
Console.WriteLine("\nMatrix:");
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
Console.Write($"{matrix[i, j]} ");
}
Console.WriteLine();
}
// Jagged arrays (arrays of arrays)
int[][] jaggedArray = new int[3][];
jaggedArray[0] = new int[] { 1, 2, 3 };
jaggedArray[1] = new int[] { 4, 5 };
jaggedArray[2] = new int[] { 6, 7, 8, 9 };
// Display the jagged array
Console.WriteLine("\nJagged Array:");
for (int i = 0; i < jaggedArray.Length; i++)
{
Console.Write($"Row {i}: ");
for (int j = 0; j < jaggedArray[i].Length; j++)
{
Console.Write($"{jaggedArray[i][j]} ");
}
Console.WriteLine();
}
}
}
4.7.1.2 - Array Methodsβ
The Array
class provides several methods for working with arrays:
Example:
using System;
class Program
{
static void Main()
{
// Create an array
int[] numbers = { 5, 2, 8, 1, 9, 3, 7, 4, 6 };
// Display the original array
Console.WriteLine("Original array:");
DisplayArray(numbers);
// Sort the array
Array.Sort(numbers);
Console.WriteLine("\nSorted array:");
DisplayArray(numbers);
// Reverse the array
Array.Reverse(numbers);
Console.WriteLine("\nReversed array:");
DisplayArray(numbers);
// Clear part of the array
Array.Clear(numbers, 0, 3); // Clear the first 3 elements
Console.WriteLine("\nArray after clearing first 3 elements:");
DisplayArray(numbers);
// Create a new array and copy elements
int[] newArray = new int[5];
Array.Copy(numbers, 4, newArray, 0, 5);
Console.WriteLine("\nNew array after copying:");
DisplayArray(newArray);
// Find an element
int index = Array.IndexOf(newArray, 6);
Console.WriteLine($"\nIndex of 6 in the new array: {index}");
// Resize an array
Array.Resize(ref newArray, 10);
Console.WriteLine("\nResized array:");
DisplayArray(newArray);
// Fill an array with a value
Array.Fill(newArray, 99, 5, 5); // Fill the last 5 elements with 99
Console.WriteLine("\nArray after filling last 5 elements with 99:");
DisplayArray(newArray);
// Binary search (requires a sorted array)
int[] sortedArray = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int position = Array.BinarySearch(sortedArray, 5);
Console.WriteLine($"\nPosition of 5 in the sorted array: {position}");
}
static void DisplayArray(int[] array)
{
foreach (int item in array)
{
Console.Write($"{item} ");
}
Console.WriteLine();
}
}
4.7.1.3 - List<T>β
The List<T>
class provides a dynamic array that can grow or shrink as needed:
π° Beginner's Corner: Understanding Listsβ
Think of a List<T>
like an expandable array - it's like a magic container that grows as you add more items:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β List<string> fruits β
βββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ€
β 0 β 1 β 2 β 3 β β β β β
βββββββΌββββββΌββββββΌββββββΌββββββΌββββββΌββββββΌββββββ€
βAppleβBananaβCherryβDate β β β β β
βββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ΄ββββββ
β²
β
Current capacity
(automatically expands as needed)
Key advantages of List<T>
over arrays:
- Dynamic sizing - Lists grow automatically as you add items
- Built-in methods - Lists have many helpful methods like
Add()
,Remove()
,Contains()
, etc. - More flexible - Easier to insert, delete, and manipulate items
When to use List<T>
:
- When you don't know exactly how many items you'll need
- When you need to frequently add or remove items
- When you want convenient built-in methods for common operations
π‘ Concept Breakdown: List<T>
vs. Arrayβ
Operation | With Array | With List<T> |
---|---|---|
Creating | string[] arr = new string[3]; | List<string> list = new List<string>(); |
Adding | arr[0] = "Apple"; (need to know index) | list.Add("Apple"); (adds to end) |
Inserting | Need to shift elements manually | list.Insert(1, "Banana"); |
Removing | Need to shift elements manually | list.Remove("Apple"); or list.RemoveAt(0); |
Finding | Need to write a loop | list.Contains("Apple") or list.IndexOf("Apple") |
Resizing | Array.Resize(ref arr, newSize); | Happens automatically |
Example:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Create a List
List<string> fruits = new List<string>();
// Add elements
fruits.Add("Apple");
fruits.Add("Banana");
fruits.Add("Cherry");
// Display the list
Console.WriteLine("Fruits list:");
foreach (string fruit in fruits)
{
Console.WriteLine($" {fruit}");
}
// Get the count
Console.WriteLine($"\nNumber of fruits: {fruits.Count}");
// Access elements by index
Console.WriteLine($"First fruit: {fruits[0]}");
Console.WriteLine($"Last fruit: {fruits[fruits.Count - 1]}");
// Insert an element at a specific position
fruits.Insert(1, "Blueberry");
Console.WriteLine("\nAfter inserting 'Blueberry' at index 1:");
foreach (string fruit in fruits)
{
Console.WriteLine($" {fruit}");
}
// Remove an element
fruits.Remove("Banana");
Console.WriteLine("\nAfter removing 'Banana':");
foreach (string fruit in fruits)
{
Console.WriteLine($" {fruit}");
}
// Remove an element at a specific index
fruits.RemoveAt(0);
Console.WriteLine("\nAfter removing the element at index 0:");
foreach (string fruit in fruits)
{
Console.WriteLine($" {fruit}");
}
// Check if an element exists
bool containsCherry = fruits.Contains("Cherry");
Console.WriteLine($"\nDoes the list contain 'Cherry'? {containsCherry}");
// Find the index of an element
int indexOfCherry = fruits.IndexOf("Cherry");
Console.WriteLine($"Index of 'Cherry': {indexOfCherry}");
// Add a range of elements
string[] moreFruits = { "Date", "Elderberry", "Fig" };
fruits.AddRange(moreFruits);
Console.WriteLine("\nAfter adding more fruits:");
foreach (string fruit in fruits)
{
Console.WriteLine($" {fruit}");
}
// Sort the list
fruits.Sort();
Console.WriteLine("\nSorted fruits:");
foreach (string fruit in fruits)
{
Console.WriteLine($" {fruit}");
}
// Clear the list
fruits.Clear();
Console.WriteLine($"\nAfter clearing the list, count: {fruits.Count}");
}
}
4.7.1.4 - List<T> vs. Arrayβ
Comparison of List<T>
and arrays:
Example:
using System;
using System.Collections.Generic;
using System.Diagnostics;
class Program
{
static void Main()
{
// Array advantages:
// 1. Slightly better performance for fixed-size collections
// 2. Less memory overhead
// 3. Can be multidimensional
// List<T> advantages:
// 1. Dynamic size
// 2. More built-in methods
// 3. Easier to add and remove elements
// Performance comparison
const int size = 1000000;
// Create an array and a list
int[] array = new int[size];
List<int> list = new List<int>(size);
// Fill the array and list
for (int i = 0; i < size; i++)
{
array[i] = i;
list.Add(i);
}
// Measure access time
Stopwatch sw = new Stopwatch();
// Array access
sw.Start();
for (int i = 0; i < size; i++)
{
int value = array[i];
}
sw.Stop();
Console.WriteLine($"Array access time: {sw.ElapsedMilliseconds} ms");
// List access
sw.Restart();
for (int i = 0; i < size; i++)
{
int value = list[i];
}
sw.Stop();
Console.WriteLine($"List access time: {sw.ElapsedMilliseconds} ms");
// Measure insertion time
int[] smallArray = new int[10];
List<int> smallList = new List<int>();
// Array insertion (requires creating a new array)
sw.Restart();
for (int i = 0; i < 10; i++)
{
int[] newArray = new int[smallArray.Length + 1];
Array.Copy(smallArray, newArray, smallArray.Length);
newArray[newArray.Length - 1] = i;
smallArray = newArray;
}
sw.Stop();
Console.WriteLine($"Array insertion time: {sw.ElapsedMilliseconds} ms");
// List insertion
sw.Restart();
for (int i = 0; i < 10; i++)
{
smallList.Add(i);
}
sw.Stop();
Console.WriteLine($"List insertion time: {sw.ElapsedMilliseconds} ms");
}
}
4.7.2 - Dictionaries and Hash Tablesβ
Dictionaries and hash tables store key-value pairs, allowing for efficient lookup by key.
4.7.2.1 - Dictionary<TKey, TValue>β
The Dictionary<TKey, TValue>
class provides a collection of key-value pairs:
π° Beginner's Corner: Understanding Dictionariesβ
Think of a Dictionary<TKey, TValue>
like a real dictionary or a phone book:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Dictionary<string, int> ages β
βββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ€
β KEY β VALUE β
β (string) β (int) β
βββββββββββββββΌββββββββββββββββββββββββββββββββββββ€
β "Alice" β 30 β
βββββββββββββββΌββββββββββββββββββββββββββββββββββββ€
β "Bob" β 25 β
βββββββββββββββΌββββββββββββββββββββββββββββββββββββ€
β "Charlie" β 35 β
βββββββββββββββΌββββββββββββββββββββββββββββββββββββ€
β "David" β 28 β
βββββββββββββββ΄ββββββββββββββββββββββββββββββββββββ
Key features of Dictionary<TKey, TValue>
:
- Fast lookups - Finding a value by key is very fast (nearly instant)
- Unique keys - Each key can only appear once in the dictionary
- Any type for keys and values - Keys and values can be any type (with some restrictions for keys)
- Unordered - Items are not stored in any particular order
π‘ Concept Breakdown: When to Use Dictionariesβ
Dictionaries are perfect when you need to:
- Look up values by a unique identifier - Like finding a person's age by their name
- Count occurrences - Like counting how many times each word appears in a text
- Group or categorize data - Like organizing products by their category
- Create a mapping - Like mapping country codes to country names
Real-world examples:
- A phone book (name β phone number)
- A product catalog (product ID β product details)
- A language translator (word β translation)
- A configuration system (setting name β setting value)
Example:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Create a Dictionary
Dictionary<string, int> ages = new Dictionary<string, int>();
// Add key-value pairs
ages.Add("Alice", 30);
ages.Add("Bob", 25);
ages.Add("Charlie", 35);
// Alternative way to add or update a key-value pair
ages["David"] = 28;
// Display the dictionary
Console.WriteLine("Ages dictionary:");
foreach (KeyValuePair<string, int> pair in ages)
{
Console.WriteLine($" {pair.Key}: {pair.Value}");
}
// Get the count
Console.WriteLine($"\nNumber of entries: {ages.Count}");
// Access a value by key
Console.WriteLine($"Alice's age: {ages["Alice"]}");
// Update a value
ages["Bob"] = 26;
Console.WriteLine($"Bob's updated age: {ages["Bob"]}");
// Check if a key exists
bool containsEve = ages.ContainsKey("Eve");
Console.WriteLine($"\nDoes the dictionary contain 'Eve'? {containsEve}");
// Safe way to get a value
if (ages.TryGetValue("Charlie", out int charlieAge))
{
Console.WriteLine($"Charlie's age: {charlieAge}");
}
else
{
Console.WriteLine("Charlie not found in the dictionary");
}
// Remove a key-value pair
ages.Remove("David");
Console.WriteLine("\nAfter removing 'David':");
foreach (var pair in ages)
{
Console.WriteLine($" {pair.Key}: {pair.Value}");
}
// Get all keys
Console.WriteLine("\nAll keys:");
foreach (string key in ages.Keys)
{
Console.WriteLine($" {key}");
}
// Get all values
Console.WriteLine("\nAll values:");
foreach (int value in ages.Values)
{
Console.WriteLine($" {value}");
}
// Clear the dictionary
ages.Clear();
Console.WriteLine($"\nAfter clearing the dictionary, count: {ages.Count}");
}
}
4.7.2.2 - SortedDictionary<TKey, TValue>
β
The SortedDictionary<TKey, TValue>
class is similar to Dictionary<TKey, TValue>
, but keeps the keys sorted:
Example:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Create a SortedDictionary
SortedDictionary<string, int> scores = new SortedDictionary<string, int>();
// Add key-value pairs (in random order)
scores.Add("Charlie", 85);
scores.Add("Alice", 95);
scores.Add("Eve", 75);
scores.Add("David", 80);
scores.Add("Bob", 90);
// Display the sorted dictionary
Console.WriteLine("Scores (sorted by name):");
foreach (var pair in scores)
{
Console.WriteLine($" {pair.Key}: {pair.Value}");
}
// The rest of the operations are similar to Dictionary<TKey, TValue>
Console.WriteLine($"\nNumber of entries: {scores.Count}");
Console.WriteLine($"Alice's score: {scores["Alice"]}");
// Update a value
scores["Bob"] = 92;
Console.WriteLine($"Bob's updated score: {scores["Bob"]}");
// Remove a key-value pair
scores.Remove("Eve");
Console.WriteLine("\nAfter removing 'Eve':");
foreach (var pair in scores)
{
Console.WriteLine($" {pair.Key}: {pair.Value}");
}
}
}
4.7.2.3 - Hashtableβ
The Hashtable
class is a non-generic collection of key-value pairs:
Example:
using System;
using System.Collections;
class Program
{
static void Main()
{
// Create a Hashtable
Hashtable table = new Hashtable();
// Add key-value pairs
table.Add("Alice", 30);
table.Add("Bob", 25);
table.Add("Charlie", 35);
table.Add(1, "One"); // Keys and values can be of any type
table.Add(true, 42);
// Alternative way to add or update a key-value pair
table["David"] = 28;
// Display the hashtable
Console.WriteLine("Hashtable entries:");
foreach (DictionaryEntry entry in table)
{
Console.WriteLine($" {entry.Key}: {entry.Value}");
}
// Get the count
Console.WriteLine($"\nNumber of entries: {table.Count}");
// Access a value by key
Console.WriteLine($"Alice's age: {table["Alice"]}");
Console.WriteLine($"Value for key 1: {table[1]}");
Console.WriteLine($"Value for key true: {table[true]}");
// Update a value
table["Bob"] = 26;
Console.WriteLine($"Bob's updated age: {table["Bob"]}");
// Check if a key exists
bool containsEve = table.ContainsKey("Eve");
Console.WriteLine($"\nDoes the hashtable contain 'Eve'? {containsEve}");
// Remove a key-value pair
table.Remove("David");
Console.WriteLine("\nAfter removing 'David':");
foreach (DictionaryEntry entry in table)
{
Console.WriteLine($" {entry.Key}: {entry.Value}");
}
// Clear the hashtable
table.Clear();
Console.WriteLine($"\nAfter clearing the hashtable, count: {table.Count}");
}
}
4.7.2.4 - Dictionary<TKey, TValue> vs. Hashtableβ
Comparison of Dictionary<TKey, TValue>
and Hashtable
:
Example:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
class Program
{
static void Main()
{
// Dictionary<TKey, TValue> advantages:
// 1. Type safety
// 2. Better performance
// 3. More features
// Hashtable advantages:
// 1. Can store keys and values of different types
// 2. Backward compatibility with older code
// Performance comparison
const int size = 1000000;
// Create a Dictionary and a Hashtable
Dictionary<int, string> dictionary = new Dictionary<int, string>();
Hashtable hashtable = new Hashtable();
// Fill the Dictionary and Hashtable
for (int i = 0; i < size; i++)
{
dictionary.Add(i, i.ToString());
hashtable.Add(i, i.ToString());
}
// Measure lookup time
Stopwatch sw = new Stopwatch();
// Dictionary lookup
sw.Start();
for (int i = 0; i < size; i++)
{
string value = dictionary[i];
}
sw.Stop();
Console.WriteLine($"Dictionary lookup time: {sw.ElapsedMilliseconds} ms");
// Hashtable lookup
sw.Restart();
for (int i = 0; i < size; i++)
{
string value = (string)hashtable[i];
}
sw.Stop();
Console.WriteLine($"Hashtable lookup time: {sw.ElapsedMilliseconds} ms");
// Type safety example
Dictionary<string, int> typeSafeDictionary = new Dictionary<string, int>();
typeSafeDictionary["One"] = 1;
// This would cause a compile-time error:
// typeSafeDictionary["Two"] = "2";
Hashtable nonTypeSafeHashtable = new Hashtable();
nonTypeSafeHashtable["One"] = 1;
nonTypeSafeHashtable["Two"] = "2"; // This is allowed but can cause runtime errors
// This would cause a runtime error:
// int value = (int)nonTypeSafeHashtable["Two"];
}
}
4.7.3 - Queues and Stacksβ
Queues and stacks are collections that follow specific patterns for adding and removing elements.
4.7.3.1 - Queue<T>β
The Queue<T>
class represents a first-in, first-out (FIFO) collection:
Example:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Create a Queue
Queue<string> queue = new Queue<string>();
// Enqueue elements
queue.Enqueue("First");
queue.Enqueue("Second");
queue.Enqueue("Third");
// Display the queue
Console.WriteLine("Queue elements:");
foreach (string item in queue)
{
Console.WriteLine($" {item}");
}
// Get the count
Console.WriteLine($"\nNumber of elements: {queue.Count}");
// Peek at the first element (without removing it)
Console.WriteLine($"First element (peek): {queue.Peek()}");
// Dequeue elements
Console.WriteLine("\nDequeuing elements:");
while (queue.Count > 0)
{
string item = queue.Dequeue();
Console.WriteLine($" Dequeued: {item}");
}
// Check if the queue is empty
Console.WriteLine($"\nQueue is empty: {queue.Count == 0}");
// Queue with initial elements
string[] items = { "Apple", "Banana", "Cherry" };
Queue<string> fruitQueue = new Queue<string>(items);
Console.WriteLine("\nFruit queue:");
foreach (string fruit in fruitQueue)
{
Console.WriteLine($" {fruit}");
}
// Clear the queue
fruitQueue.Clear();
Console.WriteLine($"\nAfter clearing the queue, count: {fruitQueue.Count}");
}
}
4.7.3.2 - Stack<T>β
The Stack<T>
class represents a last-in, first-out (LIFO) collection:
Example:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Create a Stack
Stack<string> stack = new Stack<string>();
// Push elements
stack.Push("First");
stack.Push("Second");
stack.Push("Third");
// Display the stack
Console.WriteLine("Stack elements (top to bottom):");
foreach (string item in stack)
{
Console.WriteLine($" {item}");
}
// Get the count
Console.WriteLine($"\nNumber of elements: {stack.Count}");
// Peek at the top element (without removing it)
Console.WriteLine($"Top element (peek): {stack.Peek()}");
// Pop elements
Console.WriteLine("\nPopping elements:");
while (stack.Count > 0)
{
string item = stack.Pop();
Console.WriteLine($" Popped: {item}");
}
// Check if the stack is empty
Console.WriteLine($"\nStack is empty: {stack.Count == 0}");
// Stack with initial elements
string[] items = { "Apple", "Banana", "Cherry" };
Stack<string> fruitStack = new Stack<string>(items);
Console.WriteLine("\nFruit stack (top to bottom):");
foreach (string fruit in fruitStack)
{
Console.WriteLine($" {fruit}");
}
// Clear the stack
fruitStack.Clear();
Console.WriteLine($"\nAfter clearing the stack, count: {fruitStack.Count}");
}
}
4.7.3.3 - Practical Applicationsβ
Practical applications of queues and stacks:
Example:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Queue example: Print queue
Console.WriteLine("Print Queue Example:");
PrintQueue printQueue = new PrintQueue();
printQueue.AddDocument("Document 1", 5);
printQueue.AddDocument("Document 2", 10);
printQueue.AddDocument("Document 3", 3);
printQueue.ProcessQueue();
// Stack example: Undo/Redo functionality
Console.WriteLine("\nUndo/Redo Example:");
TextEditor editor = new TextEditor();
editor.AddText("Hello");
editor.AddText(" World");
editor.AddText("!");
editor.DisplayText();
editor.Undo();
editor.DisplayText();
editor.Undo();
editor.DisplayText();
editor.Redo();
editor.DisplayText();
editor.Redo();
editor.DisplayText();
}
}
// Print queue example
class PrintQueue
{
private Queue<PrintJob> queue = new Queue<PrintJob>();
public void AddDocument(string name, int pages)
{
PrintJob job = new PrintJob(name, pages);
queue.Enqueue(job);
Console.WriteLine($"Added to print queue: {job.Name} ({job.Pages} pages)");
}
public void ProcessQueue()
{
Console.WriteLine("\nProcessing print queue:");
while (queue.Count > 0)
{
PrintJob job = queue.Dequeue();
Console.WriteLine($"Printing: {job.Name} ({job.Pages} pages)");
// Simulate printing
System.Threading.Thread.Sleep(100);
Console.WriteLine($"Finished printing: {job.Name}");
}
Console.WriteLine("Print queue is empty");
}
private class PrintJob
{
public string Name { get; }
public int Pages { get; }
public PrintJob(string name, int pages)
{
Name = name;
Pages = pages;
}
}
}
// Undo/Redo example
class TextEditor
{
private string text = "";
private Stack<string> undoStack = new Stack<string>();
private Stack<string> redoStack = new Stack<string>();
public void AddText(string newText)
{
undoStack.Push(text);
text += newText;
redoStack.Clear(); // Clear redo stack when new text is added
Console.WriteLine($"Added text: '{newText}'");
}
public void Undo()
{
if (undoStack.Count > 0)
{
redoStack.Push(text);
text = undoStack.Pop();
Console.WriteLine("Undo performed");
}
else
{
Console.WriteLine("Nothing to undo");
}
}
public void Redo()
{
if (redoStack.Count > 0)
{
undoStack.Push(text);
text = redoStack.Pop();
Console.WriteLine("Redo performed");
}
else
{
Console.WriteLine("Nothing to redo");
}
}
public void DisplayText()
{
Console.WriteLine($"Current text: '{text}'");
}
}
4.7.4 - Setsβ
Sets are collections that contain no duplicate elements.
4.7.4.1 - HashSet<T>β
The HashSet<T>
class provides a high-performance set implementation:
Example:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Create a HashSet
HashSet<string> fruits = new HashSet<string>();
// Add elements
fruits.Add("Apple");
fruits.Add("Banana");
fruits.Add("Cherry");
// Trying to add a duplicate (will be ignored)
bool added = fruits.Add("Apple");
Console.WriteLine($"Was 'Apple' added? {added}");
// Display the set
Console.WriteLine("\nFruits set:");
foreach (string fruit in fruits)
{
Console.WriteLine($" {fruit}");
}
// Get the count
Console.WriteLine($"\nNumber of elements: {fruits.Count}");
// Check if an element exists
bool containsBanana = fruits.Contains("Banana");
Console.WriteLine($"Does the set contain 'Banana'? {containsBanana}");
// Remove an element
bool removed = fruits.Remove("Cherry");
Console.WriteLine($"Was 'Cherry' removed? {removed}");
Console.WriteLine("\nAfter removing 'Cherry':");
foreach (string fruit in fruits)
{
Console.WriteLine($" {fruit}");
}
// Create another set
HashSet<string> moreFruits = new HashSet<string> { "Banana", "Date", "Elderberry" };
// Set operations
// Union (elements in either set)
fruits.UnionWith(moreFruits);
Console.WriteLine("\nAfter union with moreFruits:");
foreach (string fruit in fruits)
{
Console.WriteLine($" {fruit}");
}
// Create new sets for intersection and difference
HashSet<string> set1 = new HashSet<string> { "Apple", "Banana", "Cherry" };
HashSet<string> set2 = new HashSet<string> { "Banana", "Cherry", "Date" };
// Intersection (elements in both sets)
set1.IntersectWith(set2);
Console.WriteLine("\nIntersection of set1 and set2:");
foreach (string item in set1)
{
Console.WriteLine($" {item}");
}
// Create new sets for difference
HashSet<string> set3 = new HashSet<string> { "Apple", "Banana", "Cherry" };
HashSet<string> set4 = new HashSet<string> { "Banana", "Cherry", "Date" };
// Difference (elements in set3 but not in set4)
set3.ExceptWith(set4);
Console.WriteLine("\nDifference of set3 and set4:");
foreach (string item in set3)
{
Console.WriteLine($" {item}");
}
// Create new sets for symmetric difference
HashSet<string> set5 = new HashSet<string> { "Apple", "Banana", "Cherry" };
HashSet<string> set6 = new HashSet<string> { "Banana", "Cherry", "Date" };
// Symmetric difference (elements in either set but not in both)
set5.SymmetricExceptWith(set6);
Console.WriteLine("\nSymmetric difference of set5 and set6:");
foreach (string item in set5)
{
Console.WriteLine($" {item}");
}
// Check if a set is a subset of another
HashSet<string> set7 = new HashSet<string> { "Apple", "Banana", "Cherry" };
HashSet<string> set8 = new HashSet<string> { "Apple", "Banana" };
bool isSubset = set8.IsSubsetOf(set7);
bool isProperSubset = set8.IsProperSubsetOf(set7);
Console.WriteLine($"\nIs set8 a subset of set7? {isSubset}");
Console.WriteLine($"Is set8 a proper subset of set7? {isProperSubset}");
// Check if a set is a superset of another
bool isSuperset = set7.IsSupersetOf(set8);
bool isProperSuperset = set7.IsProperSupersetOf(set8);
Console.WriteLine($"Is set7 a superset of set8? {isSuperset}");
Console.WriteLine($"Is set7 a proper superset of set8? {isProperSuperset}");
}
}
4.7.4.2 - SortedSet<T>β
The SortedSet<T>
class is similar to HashSet<T>
, but keeps the elements sorted:
Example:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Create a SortedSet
SortedSet<string> fruits = new SortedSet<string>();
// Add elements (in random order)
fruits.Add("Cherry");
fruits.Add("Apple");
fruits.Add("Banana");
// Display the sorted set
Console.WriteLine("Fruits (sorted):");
foreach (string fruit in fruits)
{
Console.WriteLine($" {fruit}");
}
// The rest of the operations are similar to HashSet<T>
Console.WriteLine($"\nNumber of elements: {fruits.Count}");
bool containsBanana = fruits.Contains("Banana");
Console.WriteLine($"Does the set contain 'Banana'? {containsBanana}");
// Remove an element
bool removed = fruits.Remove("Cherry");
Console.WriteLine($"Was 'Cherry' removed? {removed}");
Console.WriteLine("\nAfter removing 'Cherry':");
foreach (string fruit in fruits)
{
Console.WriteLine($" {fruit}");
}
// SortedSet-specific operations
// Get the minimum and maximum elements
if (fruits.Count > 0)
{
string min = fruits.Min;
string max = fruits.Max;
Console.WriteLine($"\nMinimum element: {min}");
Console.WriteLine($"Maximum element: {max}");
}
// Get a subset of the set
SortedSet<int> numbers = new SortedSet<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Get a view of the set between 3 and 7 (inclusive)
SortedSet<int> subset = numbers.GetViewBetween(3, 7);
Console.WriteLine("\nSubset (3 to 7):");
foreach (int number in subset)
{
Console.WriteLine($" {number}");
}
}
}
4.7.5 - Concurrent Collectionsβ
Concurrent collections are designed for scenarios where multiple threads need to access a collection simultaneously.
For more information about multithreading and asynchronous programming, see Section 7.3 - Concurrency and Asynchronous Programming in the Advanced C# Topics chapter.
4.7.5.1 - ConcurrentDictionary<TKey, TValue>β
The ConcurrentDictionary<TKey, TValue>
class provides a thread-safe dictionary:
Example:
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// Create a ConcurrentDictionary
ConcurrentDictionary<int, string> dictionary = new ConcurrentDictionary<int, string>();
// Add items
dictionary.TryAdd(1, "One");
dictionary.TryAdd(2, "Two");
dictionary.TryAdd(3, "Three");
// Display the dictionary
Console.WriteLine("Dictionary contents:");
foreach (var pair in dictionary)
{
Console.WriteLine($" {pair.Key}: {pair.Value}");
}
// Update an item
string oldValue = dictionary.AddOrUpdate(2, "TWO", (key, existingValue) => "TWO");
Console.WriteLine($"\nUpdated value for key 2: {dictionary[2]}");
// Get or add an item
string value = dictionary.GetOrAdd(4, "Four");
Console.WriteLine($"Value for key 4: {value}");
// Try to remove an item
if (dictionary.TryRemove(1, out string removedValue))
{
Console.WriteLine($"\nRemoved key 1 with value: {removedValue}");
}
// Parallel operations
Console.WriteLine("\nPerforming parallel operations...");
Parallel.For(5, 10, i =>
{
dictionary.TryAdd(i, $"Item {i}");
Console.WriteLine($"Added key {i}");
});
Console.WriteLine("\nFinal dictionary contents:");
foreach (var pair in dictionary)
{
Console.WriteLine($" {pair.Key}: {pair.Value}");
}
}
}
4.7.5.2 - ConcurrentQueue<T>β
The ConcurrentQueue<T>
class provides a thread-safe queue:
Example:
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// Create a ConcurrentQueue
ConcurrentQueue<int> queue = new ConcurrentQueue<int>();
// Enqueue items
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
// Display the queue
Console.WriteLine("Queue contents:");
foreach (int item in queue)
{
Console.WriteLine($" {item}");
}
// Try to dequeue an item
if (queue.TryDequeue(out int dequeuedItem))
{
Console.WriteLine($"\nDequeued item: {dequeuedItem}");
}
// Try to peek at the next item
if (queue.TryPeek(out int peekedItem))
{
Console.WriteLine($"Peeked item: {peekedItem}");
}
// Parallel operations
Console.WriteLine("\nPerforming parallel operations...");
Parallel.For(4, 10, i =>
{
queue.Enqueue(i);
Console.WriteLine($"Enqueued item: {i}");
});
Console.WriteLine("\nFinal queue contents:");
foreach (int item in queue)
{
Console.WriteLine($" {item}");
}
}
}
4.7.5.3 - ConcurrentStack<T>β
The ConcurrentStack<T>
class provides a thread-safe stack:
Example:
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// Create a ConcurrentStack
ConcurrentStack<int> stack = new ConcurrentStack<int>();
// Push items
stack.Push(1);
stack.Push(2);
stack.Push(3);
// Display the stack
Console.WriteLine("Stack contents (top to bottom):");
foreach (int item in stack)
{
Console.WriteLine($" {item}");
}
// Try to pop an item
if (stack.TryPop(out int poppedItem))
{
Console.WriteLine($"\nPopped item: {poppedItem}");
}
// Try to peek at the top item
if (stack.TryPeek(out int peekedItem))
{
Console.WriteLine($"Peeked item: {peekedItem}");
}
// Push multiple items
stack.PushRange(new int[] { 4, 5, 6 });
Console.WriteLine("\nAfter pushing 4, 5, 6:");
foreach (int item in stack)
{
Console.WriteLine($" {item}");
}
// Pop multiple items
int[] poppedItems = new int[2];
int actuallyPopped = stack.TryPopRange(poppedItems);
Console.WriteLine($"\nPopped {actuallyPopped} items:");
for (int i = 0; i < actuallyPopped; i++)
{
Console.WriteLine($" {poppedItems[i]}");
}
// Parallel operations
Console.WriteLine("\nPerforming parallel operations...");
Parallel.For(7, 13, i =>
{
stack.Push(i);
Console.WriteLine($"Pushed item: {i}");
});
Console.WriteLine("\nFinal stack contents (top to bottom):");
foreach (int item in stack)
{
Console.WriteLine($" {item}");
}
}
}
4.7.5.4 - ConcurrentBag<T>β
The ConcurrentBag<T>
class provides a thread-safe unordered collection:
Example:
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// Create a ConcurrentBag
ConcurrentBag<int> bag = new ConcurrentBag<int>();
// Add items
bag.Add(1);
bag.Add(2);
bag.Add(3);
// Display the bag
Console.WriteLine("Bag contents (order may vary):");
foreach (int item in bag)
{
Console.WriteLine($" {item}");
}
// Try to take an item
if (bag.TryTake(out int takenItem))
{
Console.WriteLine($"\nTaken item: {takenItem}");
}
// Try to peek at an item
if (bag.TryPeek(out int peekedItem))
{
Console.WriteLine($"Peeked item: {peekedItem}");
}
// Parallel operations
Console.WriteLine("\nPerforming parallel operations...");
Parallel.For(4, 10, i =>
{
bag.Add(i);
Console.WriteLine($"Added item: {i}");
});
Console.WriteLine("\nFinal bag contents (order may vary):");
foreach (int item in bag)
{
Console.WriteLine($" {item}");
}
}
}
4.7.6 - Custom Collectionsβ
You can create custom collections by implementing collection interfaces or by extending existing collection classes.
4.7.6.1 - Implementing IEnumerable<T>β
The IEnumerable<T>
interface is the foundation of all collections in C#:
Example:
using System;
using System.Collections;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Create a custom collection
FibonacciSequence fibonacci = new FibonacciSequence(10);
// Use the custom collection in a foreach loop
Console.WriteLine("Fibonacci sequence:");
foreach (int number in fibonacci)
{
Console.WriteLine($" {number}");
}
// Create a custom collection with a custom enumerator
PrimeNumbers primes = new PrimeNumbers(10);
Console.WriteLine("\nPrime numbers:");
foreach (int prime in primes)
{
Console.WriteLine($" {prime}");
}
}
}
// Custom collection using yield return
class FibonacciSequence : IEnumerable<int>
{
private readonly int count;
public FibonacciSequence(int count)
{
this.count = count;
}
public IEnumerator<int> GetEnumerator()
{
int a = 0;
int b = 1;
yield return a;
if (count > 1)
yield return b;
for (int i = 2; i < count; i++)
{
int c = a + b;
yield return c;
a = b;
b = c;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
// Custom collection with a custom enumerator
class PrimeNumbers : IEnumerable<int>
{
private readonly int count;
public PrimeNumbers(int count)
{
this.count = count;
}
public IEnumerator<int> GetEnumerator()
{
return new PrimeNumberEnumerator(count);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private class PrimeNumberEnumerator : IEnumerator<int>
{
private readonly int count;
private int position;
private int current;
public PrimeNumberEnumerator(int count)
{
this.count = count;
Reset();
}
public int Current => current;
object IEnumerator.Current => Current;
public void Dispose()
{
// No resources to dispose
}
public bool MoveNext()
{
if (position >= count)
return false;
if (position == 0)
{
current = 2;
position++;
return true;
}
// Find the next prime number
int candidate = current + 1;
while (!IsPrime(candidate))
{
candidate++;
}
current = candidate;
position++;
return true;
}
public void Reset()
{
position = 0;
current = 0;
}
private bool IsPrime(int number)
{
if (number < 2)
return false;
for (int i = 2; i <= Math.Sqrt(number); i++)
{
if (number % i == 0)
return false;
}
return true;
}
}
}
4.7.6.2 - Extending Existing Collectionsβ
You can create custom collections by extending existing collection classes:
Example:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Create a custom list
SortedList<int> sortedList = new SortedList<int>();
// Add items in random order
sortedList.Add(5);
sortedList.Add(2);
sortedList.Add(8);
sortedList.Add(1);
sortedList.Add(9);
// Display the sorted list
Console.WriteLine("Sorted list:");
foreach (int item in sortedList)
{
Console.WriteLine($" {item}");
}
// Create a custom dictionary
CaseInsensitiveDictionary<string> dictionary = new CaseInsensitiveDictionary<string>();
// Add items
dictionary.Add("Apple", "A fruit");
dictionary.Add("Banana", "A yellow fruit");
// Access items with different case
Console.WriteLine("\nDictionary values:");
Console.WriteLine($" apple: {dictionary["apple"]}");
Console.WriteLine($" BANANA: {dictionary["BANANA"]}");
// Check if a key exists
bool containsOrange = dictionary.ContainsKey("orange");
Console.WriteLine($"Contains 'orange': {containsOrange}");
}
}
// Custom list that keeps items sorted
class SortedList<T> : List<T> where T : IComparable<T>
{
public new void Add(T item)
{
// Find the index where the item should be inserted
int index = 0;
while (index < Count && item.CompareTo(this[index]) > 0)
{
index++;
}
// Insert the item at the appropriate position
base.Insert(index, item);
}
// Override other methods as needed to maintain the sorted order
public new void AddRange(IEnumerable<T> collection)
{
foreach (T item in collection)
{
Add(item);
}
}
public new void Insert(int index, T item)
{
// Ignore the index and insert in sorted order
Add(item);
}
public new void InsertRange(int index, IEnumerable<T> collection)
{
// Ignore the index and insert in sorted order
AddRange(collection);
}
}
// Custom dictionary with case-insensitive keys
class CaseInsensitiveDictionary<TValue> : Dictionary<string, TValue>
{
public CaseInsensitiveDictionary()
: base(StringComparer.OrdinalIgnoreCase)
{
}
}
4.7.7 - LINQ with Collectionsβ
LINQ (Language Integrated Query) provides a powerful way to query and manipulate collections.
4.7.7.1 - Basic LINQ Queriesβ
Basic LINQ queries with collections:
Example:
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// Create a list of numbers
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Query syntax: Get even numbers
var evenNumbers = from num in numbers
where num % 2 == 0
select num;
Console.WriteLine("Even numbers (query syntax):");
foreach (int num in evenNumbers)
{
Console.WriteLine($" {num}");
}
// Method syntax: Get odd numbers
var oddNumbers = numbers.Where(num => num % 2 != 0);
Console.WriteLine("\nOdd numbers (method syntax):");
foreach (int num in oddNumbers)
{
Console.WriteLine($" {num}");
}
// Ordering
var descendingNumbers = numbers.OrderByDescending(num => num);
Console.WriteLine("\nNumbers in descending order:");
foreach (int num in descendingNumbers)
{
Console.WriteLine($" {num}");
}
// Projection
var squaredNumbers = numbers.Select(num => num * num);
Console.WriteLine("\nSquared numbers:");
foreach (int num in squaredNumbers)
{
Console.WriteLine($" {num}");
}
// Aggregation
int sum = numbers.Sum();
double average = numbers.Average();
int min = numbers.Min();
int max = numbers.Max();
Console.WriteLine($"\nSum: {sum}");
Console.WriteLine($"Average: {average}");
Console.WriteLine($"Minimum: {min}");
Console.WriteLine($"Maximum: {max}");
// Quantifiers
bool allLessThan100 = numbers.All(num => num < 100);
bool anyGreaterThan5 = numbers.Any(num => num > 5);
Console.WriteLine($"\nAll numbers less than 100? {allLessThan100}");
Console.WriteLine($"Any number greater than 5? {anyGreaterThan5}");
}
}
4.7.7.2 - LINQ with Complex Objectsβ
LINQ queries with complex objects:
Example:
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// Create a list of products
List<Product> products = new List<Product>
{
new Product { Id = 1, Name = "Laptop", Category = "Electronics", Price = 1200.00m },
new Product { Id = 2, Name = "Smartphone", Category = "Electronics", Price = 800.00m },
new Product { Id = 3, Name = "Headphones", Category = "Electronics", Price = 150.00m },
new Product { Id = 4, Name = "T-shirt", Category = "Clothing", Price = 25.00m },
new Product { Id = 5, Name = "Jeans", Category = "Clothing", Price = 50.00m },
new Product { Id = 6, Name = "Book", Category = "Books", Price = 15.00m },
new Product { Id = 7, Name = "Tablet", Category = "Electronics", Price = 300.00m }
};
// Filter by category
var electronicsProducts = products.Where(p => p.Category == "Electronics");
Console.WriteLine("Electronics products:");
foreach (var product in electronicsProducts)
{
Console.WriteLine($" {product.Name} - ${product.Price}");
}
// Order by price
var orderedByPrice = products.OrderBy(p => p.Price);
Console.WriteLine("\nProducts ordered by price:");
foreach (var product in orderedByPrice)
{
Console.WriteLine($" {product.Name} - ${product.Price}");
}
// Group by category
var groupedByCategory = products.GroupBy(p => p.Category);
Console.WriteLine("\nProducts grouped by category:");
foreach (var group in groupedByCategory)
{
Console.WriteLine($"Category: {group.Key}");
foreach (var product in group)
{
Console.WriteLine($" {product.Name} - ${product.Price}");
}
}
// Projection to anonymous type
var productInfos = products.Select(p => new { p.Name, p.Price, Tax = p.Price * 0.1m });
Console.WriteLine("\nProduct information with tax:");
foreach (var info in productInfos)
{
Console.WriteLine($" {info.Name} - Price: ${info.Price}, Tax: ${info.Tax}");
}
// Aggregation by category
var categoryStats = products
.GroupBy(p => p.Category)
.Select(g => new
{
Category = g.Key,
Count = g.Count(),
AveragePrice = g.Average(p => p.Price),
TotalPrice = g.Sum(p => p.Price)
});
Console.WriteLine("\nCategory statistics:");
foreach (var stat in categoryStats)
{
Console.WriteLine($"Category: {stat.Category}");
Console.WriteLine($" Count: {stat.Count}");
Console.WriteLine($" Average Price: ${stat.AveragePrice:F2}");
Console.WriteLine($" Total Price: ${stat.TotalPrice:F2}");
}
// Join with another collection
List<Supplier> suppliers = new List<Supplier>
{
new Supplier { Id = 1, Name = "ABC Electronics", Categories = new[] { "Electronics" } },
new Supplier { Id = 2, Name = "Fashion World", Categories = new[] { "Clothing" } },
new Supplier { Id = 3, Name = "Book Haven", Categories = new[] { "Books" } }
};
var productSuppliers = products
.Join(
suppliers,
product => product.Category,
supplier => supplier.Categories.First(),
(product, supplier) => new
{
ProductName = product.Name,
ProductPrice = product.Price,
SupplierName = supplier.Name
}
);
Console.WriteLine("\nProducts with suppliers:");
foreach (var item in productSuppliers)
{
Console.WriteLine($" {item.ProductName} (${item.ProductPrice}) - Supplier: {item.SupplierName}");
}
}
}
class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
}
class Supplier
{
public int Id { get; set; }
public string Name { get; set; }
public string[] Categories { get; set; }
}
4.7.7.3 - Deferred Executionβ
LINQ queries use deferred execution, which means they are not executed until the results are actually needed:
Example:
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// Create a list of numbers
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// Define a query
var evenNumbers = numbers.Where(n =>
{
Console.WriteLine($"Checking if {n} is even");
return n % 2 == 0;
});
Console.WriteLine("Query defined but not executed yet");
// Add more numbers to the list
numbers.Add(6);
numbers.Add(7);
Console.WriteLine("\nExecuting the query:");
foreach (int num in evenNumbers)
{
Console.WriteLine($"Even number: {num}");
}
// Execute the query again
Console.WriteLine("\nExecuting the query again:");
foreach (int num in evenNumbers)
{
Console.WriteLine($"Even number: {num}");
}
// Force immediate execution with ToList()
Console.WriteLine("\nForcing immediate execution with ToList():");
var evenNumbersList = numbers.Where(n =>
{
Console.WriteLine($"Checking if {n} is even");
return n % 2 == 0;
}).ToList();
// Add more numbers to the original list
numbers.Add(8);
numbers.Add(9);
Console.WriteLine("\nExecuting the materialized query:");
foreach (int num in evenNumbersList)
{
Console.WriteLine($"Even number: {num}");
}
}
}
Collections and generics are fundamental to C# programming, providing efficient ways to store, organize, and manipulate data. By understanding the various collection types and their capabilities, you can choose the right collection for your specific needs and use it effectively in your applications.