NET Centric ComputingUnit 1213 min read
Advanced C: Delegates, Events, Generics & Indexers
Unit 12 of NET Centric Computing covers delegates (type-safe function pointers), events (publisher-subscriber pattern), generics (compile-time type safety), and indexers (array-like access). Learn their syntax, real-world use cases (e.g., WhatsApp message callbacks, Ncell billing systems), and how they integrate with A
TAKEAWAYS
- Delegates enable type-safe function references (like callbacks) and are the backbone of events in C#.
- Events implement the observer pattern (e.g., WhatsApp notifications) using
+/–operators for subscription. - Generics provide compile-time type safety (e.g.,
List<T>) and avoid boxing/unboxing overhead. - Indexers allow custom array-like access (e.g.,
Dictionary<K,V>) viathis[]syntax. - Lambda expressions (
=>) simplify delegate creation (e.g., LINQ queries in NEPSE stock data). - Partial classes split code across files (e.g., ASP.NET Core controllers), while sealed classes prevent inheritance.
1. Delegates: The Bridge Between Methods and Callbacks
Delegates are type-safe function pointers that store references to methods. They enable:
- Event handling (e.g., button clicks in Windows Forms).
- Callback mechanisms (e.g., async operations in
Task). - Passing methods as arguments (e.g., LINQ’s
Wherepredicate).
How Delegates Work
A delegate defines a method signature (return type + parameters). You can:
- Declare a delegate type.
- Instantiate it with a method.
- Invoke it like a function.
// Step 1: Declare a delegate
public delegate int MathOperation(int a, int b);
// Step 2: Define methods matching the signature
int Add(int x, int y) => x + y;
int Multiply(int x, int y) => x * y;
// Step 3: Instantiate and invoke
MathOperation op = Add;
Console.WriteLine(op(5, 3)); // Output: 8
op = Multiply;
Console.WriteLine(op(5, 3)); // Output: 15
Real-World Example: WhatsApp Message Callbacks
WhatsApp uses delegates internally to route incoming messages to different handlers (e.g., notifications, media processing). When you receive a message:
- The system subscribes a delegate to the
OnMessageReceivedevent. - The delegate invokes the appropriate handler (e.g.,
ShowNotification()orSaveToCloud()).
flowchart TD
A["WhatsApp Server"] -->|"Triggers"| B["OnMessageReceived Event"]
B --> C["Delegate Instance"]
C --> D["ShowNotification()"]
C --> E["SaveToCloud()"]
C --> F["PlaySound()"]Built-in Delegates
C# provides pre-defined delegates for common scenarios:
| Delegate | Signature | Use Case |
|---|---|---|
Action |
void Action<T1, T2>(T1, T2) |
Fire-and-forget operations |
Func<T> |
TResult Func<T1, T2>(T1, T2) |
Methods returning a value |
Predicate<T> |
bool Predicate<T>(T) |
Conditions (e.g., LINQ Where) |
Example: Using Action for Logging
Action<string> logger = message => Console.WriteLine($"LOG: {message}");
logger("User logged in"); // Output: LOG: User logged in
2. Events: The Observer Pattern in Action
Events extend delegates to enable publisher-subscriber communication. Key features:
- Encapsulation: Publishers expose events but control invocation.
- Thread safety: Events are thread-safe by default.
- Weak references: Prevent memory leaks (via
WeakEventManager).
Syntax
public class TemperatureSensor
{
// Step 1: Declare an event using a delegate
public event EventHandler<TemperatureEventArgs> TemperatureChanged;
// Step 2: Raise the event (invokes all subscribers)
protected virtual void OnTemperatureChanged(TemperatureEventArgs e)
{
TemperatureChanged?.Invoke(this, e);
}
}
// Custom event args (must inherit EventArgs)
public class TemperatureEventArgs : EventArgs
{
public double Celsius { get; }
public TemperatureEventArgs(double temp) => Celsius = temp;
}
Real-World Example: Ncell Billing Alerts
Ncell’s billing system uses events to notify users when their balance is low:
- The
UserAccountclass publishes aBalanceLowevent. - Subscribers (e.g.,
SMSService,EmailService) react when the event fires.
sequenceDiagram
UserAccount->>SMSService: Subscribe(BalanceLow)
UserAccount->>EmailService: Subscribe(BalanceLow)
UserAccount->>UserAccount: CheckBalance() (balance < 100)
UserAccount->>SMSService: BalanceLow(95)
UserAccount->>EmailService: BalanceLow(95)
SMSService->>User: Send SMS: "Low balance!"
EmailService->>User: Send Email: "Top-up now!"Event Best Practices
- Use
protected virtualfor raising events (allows derived classes to override). - Null-check with
?.Invoketo avoidNullReferenceException. - Avoid exposing delegates directly (use
add/removesyntax for events).
Example: Custom Exception Event
public class InvalidSubjectException : Exception
{
public event EventHandler<SubjectEventArgs> SubjectInvalid;
public InvalidSubjectException(string subject)
{
if (subject != "C#")
SubjectInvalid?.Invoke(this, new SubjectEventArgs(subject));
}
}
public class SubjectEventArgs : EventArgs
{
public string InvalidSubject { get; }
public SubjectEventArgs(string subject) => InvalidSubject = subject;
}
3. Generics: Type-Safe Reusable Code
Generics allow compile-time type checking for classes, methods, and collections. Benefits:
- Performance: Avoids boxing/unboxing (e.g.,
List<int>vs.ArrayList). - Type safety: Compile-time errors for invalid types.
- Code reuse: Single implementation for multiple types (e.g.,
Dictionary<K,V>).
Generic Classes and Methods
// Generic class
public class Box<T>
{
public T Content { get; set; }
}
// Generic method
public T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
Real-World Example: Daraz Order Processing
Daraz uses generics to process orders of any type (books, electronics, etc.) with a single OrderProcessor<T> class:
public class OrderProcessor<T> where T : IOrder
{
public void ProcessOrder(T order)
{
Console.WriteLine($"Processing {typeof(T).Name}: {order.Id}");
// Common logic for all order types
}
}
// Example usage:
var bookOrder = new BookOrder { Id = 101 };
var electronicsOrder = new ElectronicsOrder { Id = 201 };
OrderProcessor<IOrder> processor = new OrderProcessor<IOrder>();
processor.ProcessOrder(bookOrder); // Output: Processing BookOrder: 101
processor.ProcessOrder(electronicsOrder); // Output: Processing ElectronicsOrder: 201
Constraints on Generic Types
| Constraint | Example | Purpose |
|---|---|---|
where T : struct |
Stack<T> |
Value types only (no null) |
where T : class |
List<T> |
Reference types only (allows null) |
where T : new() |
T item = new T(); |
Type must have a parameterless constructor |
where T : IComparable<T> |
Max<T>(a, b) |
Type must implement IComparable<T> |
Example: Generic Exception Handler
public class CustomException<T> : Exception where T : Exception
{
public T InnerException { get; }
public CustomException(T ex) => InnerException = ex;
}
// Usage:
try { /* Risky code */ }
catch (SqlException ex) { throw new CustomException<SqlException>(ex); }
4. Indexers: Custom Array Access
Indexers allow array-like access to objects using this[] syntax. Use cases:
- Collections:
List<T>,Dictionary<K,V>. - Custom data structures: E.g., a
Matrixclass with 2D access.
Syntax
public class MyList<T>
{
private T[] _items = new T[100];
private int _count;
// Indexer declaration
public T this[int index]
{
get { return _items[index]; }
set { _items[index] = value; }
}
public int Count => _count;
}
Real-World Example: Kathmandu Traffic Routes
Imagine a TrafficSystem class that uses indexers to access routes by ID:
public class TrafficSystem
{
private string[] _routes = new string[1000];
public string this[int routeId]
{
get => _routes[routeId];
set => _routes[routeId] = value;
}
}
// Usage:
var traffic = new TrafficSystem();
traffic[42] = "Ring Road to Thamel"; // Set route
Console.WriteLine(traffic[42]); // Output: Ring Road to Thamel
Indexers vs. Properties
| Feature | Indexer | Property |
|---|---|---|
| Syntax | this[args] |
PropertyName |
| Purpose | Array-like access | Named access |
| Parameters | Can have multiple parameters | None |
| Example | list[0] = "Hello" |
person.Name = "Alice" |
Example: Read-Only Indexer
public class ReadOnlyCollection<T>
{
private T[] _data;
public ReadOnlyCollection(T[] data) => _data = data;
public T this[int index] => _data[index]; // Only getter
}
5. Lambda Expressions: Concise Delegates
Lambda expressions (=>) provide a shorter syntax for delegates, especially useful with LINQ.
Syntax
// Lambda with no parameters
Action greet = () => Console.WriteLine("Hello!");
greet();
// Lambda with parameters
Func<int, int, int> add = (x, y) => x + y;
Console.WriteLine(add(5, 3)); // Output: 8
Real-World Example: NEPSE Stock Filter
NEPSE’s website filters stocks using lambda expressions to select high-value shares:
var stocks = new List<Stock>
{
new Stock { Name = "NMB", Price = 300 },
new Stock { Name = "NTC", Price = 150 },
new Stock { Name = "Ncell", Price = 450 }
};
// Filter stocks priced > 300 using lambda
var expensiveStocks = stocks.Where(s => s.Price > 300);
foreach (var stock in expensiveStocks)
Console.WriteLine(stock.Name); // Output: Ncell
Lambda vs. Anonymous Methods
| Feature | Lambda (=>) |
Anonymous Method (delegate) |
|---|---|---|
| Syntax | (args) => expression |
delegate { ... } |
| Readability | More concise | Verbose |
| Use Case | LINQ, short callbacks | Complex logic |
Example: Lambda for Event Handler
button.Click += (sender, e) => MessageBox.Show("Clicked!");
6. Partial Classes and Sealed Classes
Partial Classes: Split Code Across Files
Used in ASP.NET Core to separate:
- Designers (e.g.,
.Designer.csfor WinForms). - Logic (e.g.,
Controller.csfor MVC).
// File 1: HomeController.Designer.cs
public partial class HomeController { /* Auto-generated */ }
// File 2: HomeController.cs
public partial class HomeController : Controller
{
public IActionResult Index() => View();
}
Sealed Classes: Prevent Inheritance
- Performance: Slightly faster (no virtual method overhead).
- Security: Prevent derived classes (e.g.,
stringis sealed).
public sealed class ImmutablePoint
{
public int X { get; }
public int Y { get; }
public ImmutablePoint(int x, int y) => (X, Y) = (x, y);
}
Exam Tip
Delegates/Events:
- Always declare delegates before use.
- Use
EventHandler<T>for custom events with data. - Common pitfall: Forgetting
?.Invoke(causesNullReferenceException).
Generics:
- Constraints (
where T :) are critical for type safety. - Performance: Prefer
List<T>overArrayList(no boxing).
- Constraints (
Indexers:
- Signature: Must match
this[args](e.g.,this[int index]). - Comparison: Indexers are to arrays as properties are to objects.
- Signature: Must match
Lambda Expressions:
- LINQ:
Where,Select,OrderByuse lambdas. - Shortcut:
x => x.ToString()is equivalent todelegate { return x.ToString(); }.
- LINQ:
Partial/Sealed:
- Partial: Used in ASP.NET Core (e.g.,
Controllerclasses). - Sealed: Prevents inheritance (e.g.,
string,System.ValueType).
- Partial: Used in ASP.NET Core (e.g.,
In the real world
- WhatsApp (Meta): Uses delegates to route incoming messages to different handlers (e.g.,
OnMessageReceivedevent triggers notifications, media processing, or cloud backups). - Ncell Billing System: Implements the observer pattern via events (
BalanceLow) to notify users via SMS/email when their balance drops below NPR 100. - Daraz Order Processing: Leverages generics (
OrderProcessor<T>) to handle orders for books, electronics, or groceries with a single codebase, avoiding type-specific logic duplication.
Based on the TU BSc CSIT syllabus for NET Centric Computing (CSC367), unit 12.
Discussion
Loading…