Introduction
Welcome to Module 6 of our Complete C# Course: From Beginner to Advanced! After mastering advanced OOP concepts in Module 5, it’s time to tackle collections and data structures to manage and manipulate data efficiently. In this module, we’ll cover arrays (1D, 2D, jagged), strings and StringBuilder, collections (List, Dictionary, HashSet, Queue, Stack, LinkedList), LINQ (query and method syntax), delegates, events, and lambda expressions. We’ll apply these concepts in a practical order processing system for a retail store, inspired by real-world applications like e-commerce or inventory management. With detailed examples, best practices, and pros/cons, you’ll learn to handle complex datasets like a pro. Let’s dive in!
1. Arrays (1D, 2D, Jagged)
Arrays store fixed-size sequences of elements of the same type.
1D Array
Single-dimensional array for simple lists.
2D Array
Matrix-like structure for tabular data.
Jagged Array
Array of arrays, allowing variable-length rows.
Example: Store Inventory
using System;
namespace OrderSystem
{
class Program
{
static void Main(string[] args)
{
// 1D Array: Product prices
double[] prices = { 999.99, 49.99, 299.99 };
Console.WriteLine($"First product price: {prices[0]:C}");
// 2D Array: Store sales (rows: stores, columns: days)
int[,] sales = { { 10, 20 }, { 15, 25 } };
Console.WriteLine($"Store 1, Day 1 sales: {sales[0, 0]}");
// Jagged Array: Variable daily sales per store
int[][] jaggedSales = new int[2][];
jaggedSales[0] = new int[] { 10, 20, 30 };
jaggedSales[1] = new int[] { 15 };
Console.WriteLine($"Store 1, Day 3 sales: {jaggedSales[0][2]}");
}
}
}
Real-World Use: Storing product prices, sales data, or seating charts.
Pros:
Fast access with O(1) indexing.
Memory-efficient for fixed-size data.
Cons:
Fixed size limits flexibility.
Jagged arrays are complex to initialize.
Best Practices:
Use 1D arrays for simple lists, 2D for matrices, jagged for irregular data.
Prefer List<T> for dynamic sizing.
Alternatives:
List<T> for resizable collections.
Multidimensional List<List<T>> for jagged arrays.
2. Strings and StringBuilder
Strings are immutable sequences of characters, while StringBuilder is mutable for efficient string manipulation.
Example: Order Receipt
using System;
using System.Text;
namespace OrderSystem
{
class Program
{
static void Main(string[] args)
{
// String: Immutable
string order = "Order: Laptop";
order += ", $999.99";
Console.WriteLine(order); // Order: Laptop, $999.99
// StringBuilder: Mutable
StringBuilder receipt = new StringBuilder("Receipt:\n");
receipt.Append("Laptop: $999.99\n");
receipt.Append("Phone: $499.99\n");
Console.WriteLine(receipt.ToString());
}
}
}
Real-World Use: Generating receipts, logs, or user messages.
Pros:
Strings: Simple, safe, and widely used.
StringBuilder: Efficient for frequent modifications.
Cons:
Strings: Immutable, causing memory overhead for concatenation.
StringBuilder: More verbose for simple tasks.
Best Practices:
Use strings for small, static text.
Use StringBuilder for loops or large concatenations.
Use string interpolation ($"...") for readability.
Alternatives:
String.Join for concatenating collections.
Formattable strings for complex formatting.
3. Collections (List, Dictionary, HashSet, Queue, Stack, LinkedList)
Collections provide dynamic, type-safe data structures.
List
Resizable array for ordered elements.
Dictionary<TKey, TValue>
Key-value pairs for fast lookups.
HashSet
Unique elements with fast membership checks.
Queue
First-in, first-out (FIFO) structure.
Stack
Last-in, first-out (LIFO) structure.
LinkedList
Doubly-linked list for flexible insertions/deletions.
Example: Order Management
using System;
using System.Collections.Generic;
namespace OrderSystem
{
class Program
{
static void Main(string[] args)
{
// List: Ordered products
List<string> products = new List<string> { "Laptop", "Phone" };
products.Add("Tablet");
Console.WriteLine($"Products: {string.Join(", ", products)}");
// Dictionary: Product prices
Dictionary<string, double> prices = new Dictionary<string, double>
{
{ "Laptop", 999.99 },
{ "Phone", 499.99 }
};
Console.WriteLine($"Laptop price: {prices["Laptop"]:C}");
// HashSet: Unique categories
HashSet<string> categories = new HashSet<string> { "Electronics", "Electronics" };
Console.WriteLine($"Categories: {categories.Count}"); // 1
// Queue: Order processing
Queue<string> orders = new Queue<string>();
orders.Enqueue("Order 1");
orders.Enqueue("Order 2");
Console.WriteLine($"Processing: {orders.Dequeue()}");
// Stack: Undo operations
Stack<string> actions = new Stack<string>();
actions.Push("Add Laptop");
actions.Push("Add Phone");
Console.WriteLine($"Undo: {actions.Pop()}");
// LinkedList: Navigation history
LinkedList<string> history = new LinkedList<string>();
history.AddLast("Home");
history.AddLast("Products");
Console.WriteLine($"Last page: {history.Last.Value}");
}
}
}
Real-World Use: Managing orders, categories, or user actions in e-commerce.
Pros:
List<T>: Flexible resizing and indexing.
Dictionary<TKey, TValue>: Fast O(1) lookups.
HashSet<T>: Ensures uniqueness.
Queue<T>: Ideal for FIFO processing.
Stack<T>: Perfect for LIFO scenarios like undo.
LinkedList<T>: Efficient for insertions/deletions.
Cons:
List<T>: Slower for searching than Dictionary.
Dictionary: No order preservation (use SortedDictionary if needed).
HashSet: No indexing or duplicates.
Queue/Stack: Limited access patterns.
LinkedList: Higher memory overhead.
Best Practices:
Choose List<T> for general-purpose lists, Dictionary for key-based lookups, HashSet for uniqueness.
Use Queue for processing tasks, Stack for reversible actions.
Use LinkedList only when frequent insertions/deletions are needed.
Alternatives:
Arrays for fixed-size data.
SortedList<TKey, TValue> for sorted key-value pairs.
4. LINQ Basics (Query Syntax & Method Syntax)
LINQ (Language Integrated Query) provides a declarative way to query collections using query or method syntax.
Example: Filter Orders
using System;
using System.Collections.Generic;
using System.Linq;
namespace OrderSystem
{
class Order
{
public string Id { get; set; }
public double Amount { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Order> orders = new List<Order>
{
new Order { Id = "O1", Amount = 999.99 },
new Order { Id = "O2", Amount = 49.99 },
new Order { Id = "O3", Amount = 499.99 }
};
// Query Syntax
var expensiveOrders = from order in orders
where order.Amount > 100
select order.Id;
Console.WriteLine("Expensive orders: " + string.Join(", ", expensiveOrders));
// Method Syntax
var highValue = orders.Where(o => o.Amount > 100)
.Select(o => o.Id);
Console.WriteLine("High-value orders: " + string.Join(", ", highValue));
}
}
}
Real-World Use: Filtering products, aggregating sales, or generating reports.
Pros:
Declarative and readable.
Works with any IEnumerable<T>.
Query and method syntax offer flexibility.
Cons:
Performance overhead for complex queries.
Learning curve for query syntax.
Best Practices:
Use query syntax for readability, method syntax for chaining.
Avoid overusing LINQ for simple operations.
Use ToList() or ToArray() to materialize results when needed.
Alternatives:
Loops for simple filtering.
SQL for database queries.
5. Working with Delegates and Events
Delegates define method signatures, while events enable publisher-subscriber patterns.
Example: Order Notifications
using System;
namespace OrderSystem
{
delegate void OrderProcessedHandler(string orderId);
class OrderProcessor
{
public event OrderProcessedHandler OrderProcessed;
public void ProcessOrder(string orderId)
{
Console.WriteLine($"Processing {orderId}...");
OrderProcessed?.Invoke(orderId);
}
}
class Program
{
static void Main(string[] args)
{
OrderProcessor processor = new OrderProcessor();
processor.OrderProcessed += id => Console.WriteLine($"{id} completed!");
processor.ProcessOrder("O1"); // Processing O1... O1 completed!
}
}
}
Real-World Use: Notifying users of order status changes or logging events.
Pros:
Delegates enable flexible method references.
Events provide safe, encapsulated notifications.
Cons:
Events can lead to memory leaks if subscribers aren’t removed.
Delegates add complexity for simple callbacks.
Best Practices:
Use event keyword to restrict invocation to the declaring class.
Use ?.Invoke() for null-safe event handling.
Unsubscribe from events to prevent leaks.
Alternatives:
Lambda expressions for inline delegates.
Observer pattern for complex pub-sub systems.
6. Lambda Expressions
Lambda expressions provide concise syntax for anonymous methods, often used with delegates or LINQ.
Example: Filter Orders with Lambda
using System;
using System.Collections.Generic;
using System.Linq;
namespace OrderSystem
{
class Order
{
public string Id { get; set; }
public double Amount { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Order> orders = new List<Order>
{
new Order { Id = "O1", Amount = 999.99 },
new Order { Id = "O2", Amount = 49.99 }
};
// Lambda with LINQ
var highValue = orders.Where(o => o.Amount > 100);
foreach (var order in highValue)
{
Console.WriteLine($"{order.Id}: {order.Amount:C}");
}
// Lambda with delegate
Action<string> log = message => Console.WriteLine($"Log: {message}");
log("Order processed");
}
}
}
Real-World Use: Filtering data, defining callbacks, or simplifying event handlers.
Pros:
Concise and expressive.
Seamless integration with LINQ and delegates.
Cons:
Can reduce readability if overused.
Debugging complex lambdas can be challenging.
Best Practices:
Use for short, clear operations.
Avoid nesting multiple lambdas.
Alternatives:
Named methods for complex logic.
Anonymous methods for older C# versions.
Interactive Example: Order Processing System
Let’s build a console-based order processing system to apply collections, LINQ, delegates, and lambda expressions.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
// File: Models/Order.cs
namespace OrderSystem.Models
{
public record Order(string Id, double Amount, string Customer);
}
// File: Services/OrderProcessor.cs
namespace OrderSystem.Services
{
delegate void OrderStatusHandler(string orderId, string status);
class OrderProcessor
{
private List<Models.Order> orders = new List<Models.Order>();
private Queue<Models.Order> processingQueue = new Queue<Models.Order>();
public event OrderStatusHandler OrderStatusChanged;
public void AddOrder(Models.Order order)
{
orders.Add(order);
processingQueue.Enqueue(order);
OrderStatusChanged?.Invoke(order.Id, "Added");
}
public void ProcessNextOrder()
{
if (processingQueue.Count == 0)
{
Console.WriteLine("No orders to process.");
return;
}
var order = processingQueue.Dequeue();
OrderStatusChanged?.Invoke(order.Id, "Processed");
}
public string GetOrderSummary()
{
StringBuilder summary = new StringBuilder("Order Summary:\n");
var highValueOrders = orders.Where(o => o.Amount > 100)
.OrderBy(o => o.Amount);
foreach (var order in highValueOrders)
{
summary.AppendLine($"{order.Id}: {order.Amount:C} ({order.Customer})");
}
return summary.ToString();
}
}
}
// File: Program.cs
namespace OrderSystem
{
class Program
{
static void Main(string[] args)
{
Services.OrderProcessor processor = new Services.OrderProcessor();
processor.OrderStatusChanged += (id, status) =>
Console.WriteLine($"Order {id}: {status}");
while (true)
{
Console.WriteLine("\nOrder Processing System");
Console.WriteLine("1. Add Order");
Console.WriteLine("2. Process Next Order");
Console.WriteLine("3. View High-Value Orders");
Console.WriteLine("4. Exit");
Console.Write("Choose an option: ");
string choice = Console.ReadLine();
if (choice == "4") break;
switch (choice)
{
case "1":
Console.Write("Enter order ID: ");
string id = Console.ReadLine();
Console.Write("Enter amount: ");
if (!double.TryParse(Console.ReadLine(), out double amount) || amount < 0)
{
Console.WriteLine("Invalid amount!");
continue;
}
Console.Write("Enter customer name: ");
string customer = Console.ReadLine();
processor.AddOrder(new Models.Order(id, amount, customer));
break;
case "2":
processor.ProcessNextOrder();
break;
case "3":
Console.WriteLine(processor.GetOrderSummary());
break;
default:
Console.WriteLine("Invalid option!");
break;
}
}
}
}
}
How It Works:
Arrays: Not used here, but could store fixed-size order batches.
Strings/StringBuilder: GetOrderSummary uses StringBuilder for efficient concatenation.
Collections: List for orders, Queue for processing, HashSet could track unique customers.
LINQ: Filters and sorts high-value orders.
Delegates/Events: OrderStatusHandler notifies status changes.
Lambda Expressions: Used in event handlers and LINQ queries.
Why It’s Useful: Mimics order processing in e-commerce or retail systems.
Setup: Create a new Console App in Visual Studio or run dotnet new console. Organize files as shown.
Best Standards for Module 6
Arrays: Use for fixed-size data; prefer List<T> for dynamic sizing.
Strings/StringBuilder: Use StringBuilder for heavy concatenation; strings for static text.
Collections: Choose based on use case (List for lists, Dictionary for lookups, etc.).
LINQ: Use query syntax for readability, method syntax for chaining; avoid overuse for simple tasks.
Delegates/Events: Use events for pub-sub patterns; ensure proper unsubscription.
Lambda Expressions: Keep short and clear; use named methods for complex logic.
Conclusion
You’ve just mastered collections and data structures in C#! By learning arrays, strings, StringBuilder, collections, LINQ, delegates, events, and lambda expressions, you’re equipped to handle complex datasets and build reactive applications. The order processing system showcases how these concepts power real-world solutions.
What’s Next? In Module 7, we’ll explore file I/O, serialization, and async programming for advanced data handling and performance. Keep practicing, and try adding order prioritization or customer filtering to the order system!
Interactive Challenge: Enhance the order system to support order cancellation or export summaries to a file. Share your solution with #C#Hero!
No comments:
Post a Comment
Thanks for your valuable comment...........
Md. Mominul Islam