Overview
In this chapter, you will learn how delegates are defined and invoked, and you will explore their wide usage across the .NET ecosystem. With this knowledge, you will move on to the inbuilt Action and Func delegates to discover how their usage reduces unnecessary boilerplate code. You will then see how multicast delegates can be harnessed to send messages to multiple parties, and how events can be incorporated into event-driven code. Along the way, you will discover some common pitfalls to avoid and best practices to follow that prevent a great application from turning into an unreliable one.
This chapter will demystify the lambda syntax style and show how it can be used effectively. By the end of the chapter, you will be able to use the lambda syntax comfortably to create code that is succinct, as well as easy to grasp and maintain.
In the previous chapter, you learned some of the key aspects of Object Oriented Programming (OOP). In this chapter, you will build on this by looking at the common patterns used specifically in C# that enable classes to interact.
Have you found yourself working with a code that has to listen to certain signals and act on them, but you cannot be sure until runtime what those actions should be? Maybe you have a block of code that you need to reuse or pass to other methods for them to call when they are ready. Or, you may want to filter a list of objects, but need to base how you would do that on a combination of user preferences. Much of this can be achieved using interfaces, but it is often more efficient to create chunks of code that you can then pass to other classes in a type-safe way. Such blocks are referred to as delegates and form the backbone of many .NET libraries, allowing methods or pieces of code to be passed as parameters.
The natural extension to a delegate is the event, which makes it possible to offer a form of optional behavior in software. For example, you may have a component that broadcasts live news and stock prices, but unless you provide a way to opt into these services, you may limit the usability of such a component.
User Interface (UI) apps often provide notifications of various user actions, for example, keypresses, swiping a screen, or clicking a mouse button; such notifications follow a standard pattern in C#, which will be discussed fully in this chapter. In such scenarios, the UI element detecting such actions is referred to as a publisher, whereas the code that acts upon those messages is called a subscriber. When brought together, they form an event-driven design referred to as the publisher-subscriber, or pub-sub, pattern. You will see how this can be used in all types of C#. Remember that its usage is not just the exclusive domain of UI applications.
Finally, you will learn about lambda statements and lambda expressions, collectively known as lambdas. These have an unusual syntax, which can initially take a while to become comfortable with. Rather than having lots of methods and functions scattered within a class, lambdas allow for smaller blocks of code that are often self-contained and located within close proximity to where they are used in the code, thereby offering an easier way to follow and maintain code. You will learn about lambdas in detail in the latter half of this chapter. First, you will learn about delegates.
The .NET delegate is similar to function pointers found in other languages, such as C++; in other words, it is like a pointer to a method to be invoked at runtime. In essence, it is a placeholder for a block of code, which can be something as simple as a single statement or a full-blown multiline code block, complete with complex branches of execution, that you ask other code to execute at some point in time. The term delegate hints at some form of representative, which is precisely what this placeholder concept relates to.
Delegates allow for minimum coupling between objects, and much less code. There is no need to create classes that are derived from specific classes or interfaces. By using a delegate, you are defining what a compatible method should look like, whether it is in a class or struct, static, or instance-based. The arguments and return type define this calling compatibility.
Furthermore, delegates can be used in a callback fashion, which allows multiple methods to be wired up to a single publication source. They often require much less code and provide more features than found using an interface-based design.
The following example shows how effective delegates can be. Suppose you have a class that searches for users by surname. It would probably look like this:
public User FindBySurname(string name)
{
foreach(var user in _users)
if (user.Surname == name)
return user;
return null;
}
You then need to extend this to include a search of the user's login name:
public User FindByLoginName(string name)
{
foreach(var user in _users)
if (user.LoginName == name)
return user;
return null;
}
Once again, you decide to add yet another search, this time by location:
public User FindByLocation(string name)
{
foreach(var user in _users)
if (user.Location == name)
return user;
return null;
}
You start the searches with code like this:
{
var user1 = FindBySurname("Wright");
var user2 = FindByLoginName("JamesR");
var user3 = FindByLocation("Scotland");
}
Can you see the pattern that is occurring every time? You are repeating the same code that iterates through the list of users, applying a Boolean condition (also known as a predicate) to find the first matching user.
The only thing that is different is that the predicate decides whether a match has been found. This is one of the common cases where delegates are used at a basic level. The predicate can be replaced with a delegate, acting as a placeholder, which is evaluated when required.
Converting this code to a delegate style, you define a delegate named FindUser (this step can be skipped as .NET contains a delegate definition that you can reuse; you will come to this later).
All you need is a single helper method, Find, which is passed a FindUser delegate instance. Find knows how to loop through the users, invoking the delegate passing in the user, which returns true or false for a match:
private delegate bool FindUser(User user);
private User Find(FindUser predicate)
{
foreach (var user in _users)
if (predicate(user))
return user;
return null;
}
public void DoSearch()
{
var user4 = Find(user => user.Surname == "Wright");
var user5 = Find(user => user.LoginName == "JamesR");
var user6 = Find(user => user.Location == "Scotland");
}
As you can see, the code is kept together and is much more concise now. There is no need to cut and paste code that loops through the users, as that is all done in one place. For each type of search, you simply define a delegate once and pass it to Find. To add a new type of search, all you need to do is define it in a single statement line, rather than copying at least eight lines of code that repeat the looping function.
The lambda syntax is a fundamental style used to define method bodies, but its strange syntax can prove to be an obstacle at first. At first glance, lambda expressions can look odd with their => style, but they do offer a cleaner way to specify a target method. The act of defining a lambda is similar to defining a method; you essentially omit the method name and use => to prefix a block of code.
You will now look at another example, using interfaces this time. Consider that you are working on a graphics engine and need to calculate the position of an image onscreen each time the user rotates or zooms in. Note that this example skips any complex math calculations.
Consider that you need to transform a Point class using the ITransform interface with a single method named Move, as shown in the following code snippet:
public class Point
{
public double X { get; set; }
public double Y { get; set; }
}
public interface ITransform
{
Point Move(double height, double width);
}
When the user rotates an object, you need to use RotateTransform, and for a zoom operation, you will use ZoomTransform, as follows. Both are based on the ITransform interface:
public class RotateTransform : ITransform
{
public Point Move(double height, double width)
{
// do stuff
return new Point();
}
}
public class ZoomTransform : ITransform
{
public Point Move(double height, double width)
{
// do stuff
return new Point();
}
}
So, given these two classes, a point can be transformed by creating a new Transform instance, which is passed to a method named Calculate, as shown in the following code. Calculate calls the corresponding Move method, and does some extra unspecified work on point, before returning point to the caller:
public class Transformer
{
public void Transform()
{
var rotatePoint = Calculate(new RotateTransform(), 100, 20);
var zoomPoint = Calculate(new ZoomTransform(), 5, 5);
}
private Point Calculate(ITransform transformer, double height, double width)
{
var point = transformer.Move(height, width);
//do stuff to point
return point;
}
}
This is a standard class and interface-based design, but you can see that you have made a lot of effort to create new classes with just a single numeric value from a Move method. It is a worthwhile idea to have the calculations broken down into an easy-to-follow implementation. After all, it could have led to a future maintenance problem if implemented in a single method with multiple if-then branches.
By re-implementing a delegate-based design, you still have maintainable code, but much less of it to look after. You can have a TransformPoint delegate and a new Calculate function that can be passed a TransformPoint delegate.
You can invoke a delegate by appending brackets around its name and passing in any arguments. This is similar to how you would call a standard class-level function or method. You will cover this invocation in more detail later; for now, consider the following snippet:
private delegate Point TransformPoint(double height, double width);
private Point Calculate(TransformPoint transformer, double height, double width)
{
var point = transformer(height, width);
//do stuff to point
return point;
}
You still need the actual target Rotate and Zoom methods, but you do not have the overhead of creating unnecessary classes to do this. You can add the following code:
private Point Rotate(double height, double width)
{
return new Point();
}
private Point Zoom(double height, double width)
{
return new Point();
}
Now, calling the method delegates is as simple as the following:
public void Transform()
{
var rotatePoint1 = Calculate(Rotate, 100, 20);
var zoomPoint1 = Calculate(Zoom, 5, 5);
}
Notice how using delegates in this way helps eliminate a lot of unnecessary code.
Note
You can find the code used for this example at https://packt.link/AcwZA.
In addition to invoking a single placeholder method, a delegate also contains extra plumbing that allows it to be used in a multicast manner, that is, a way to chain multiple target methods together, each being invoked one after the other. This is often referred to as an invocation list or delegate chain and is initiated by code that acts as a publication source.
A simple example of how this multicast concept applies can be seen in UIs. Imagine you have an application that shows the map of a country. As the user moves their mouse over the map, you may want to perform various actions, such as the following:
To achieve this, you would need some way to detect when the user moves the mouse over the screen. This is often referred to as the publisher. In this example, its sole purpose is to detect mouse movements and publish them to anyone who is listening.
To perform the three required UI actions, you would create a class that has a list of objects to notify when the mouse position changes, allowing each object to perform whatever activity it needs, in isolation from the others. Each of these objects is referred to as a subscriber.
When your publisher detects that the mouse has moved, you follow this pseudo code:
MouseEventArgs args = new MouseEventArgs(100,200)
foreach(subscription in subscriptionList)
{
subscription.OnMouseMoved(args)
}
This assumes that subscriptionList is a list of objects, perhaps based on an interface with the OnMouseMoved method. It is up to you to add code that enables interested parties to subscribe to and unsubscribe from the OnMouseMoved notifications. It would be an unfortunate design if code that has previously subscribed has no way to unsubscribe and gets called repeatedly when there is no longer any need for it to be called.
In the preceding code, there is a fair amount of coupling between the publisher and subscribers, and you are back to using interfaces for a type-safe implementation. What if you then needed to listen for keypresses, both key down and key up? It would soon get quite frustrating having to repeatedly copy such similar code.
Fortunately, the delegate type contains all this as inbuilt behavior. You can use single or multiple target methods interchangeably; all you need to do is invoke a delegate and the delegate will handle the rest for you.
You will take an in-depth look at multicast delegates shortly, but first, you will explore the single-target method scenario.
Delegates are defined in a way that is similar to that of a standard method. The compiler does not care about the code in the body of a target method, only that it can be invoked safely at some point in time.
The delegate keyword is used to define a delegate, using the following format:
public delegate void MessageReceivedHandler(string message, int size);
The following list describes each component of this syntax:
Note
Delegates can be nested within a class or namespace; they can also be defined within the global namespace, although this practice is discouraged. When defining classes in C#, it is common practice to define them within a parent namespace, typically based on a hierarchical convention that starts with the company name, followed by the product name, and finally the feature. This helps to provide a more unique identity to a type.
By defining a delegate without a namespace, there is a high chance that it will clash with another delegate with the same name if it is also defined in a library without the protection of a namespace. This can cause the compiler to become confused as to which delegate you are referring to.
In earlier versions of .NET, it was common practice to define custom delegates. Such code has since been replaced with various inbuilt .NET delegates, which you will look at shortly. For now, you will briefly cover the basics of defining a custom delegate. It is worthwhile know about this if you maintain any legacy C# code.
In the next exercise, you will create a custom delegate, one that is passed a DateTime parameter and returns a Boolean to indicate validity.
Say you have an application that allows users to order products. While filling in the order details, the customer can specify an order date and a delivery date, both of which must be validated before accepting the order. You need a flexible way to validate these dates. For some customers, you may allow weekend delivery dates, while for others, it must be at least seven days away. You may also allow an order to be back-dated for certain customers.
You know that delegates offer a way to vary an implementation at runtime, so that is the best way to proceed. You do not want multiple interfaces, or worse, a complex jumble of if-then statements, to achieve this.
Depending on the customer's profile, you can create a class named Order, which can be passed different date validation rules. These rules can be validated by a Validate method:
Perform the following steps to do so:
sourceChapter03>dotnet new console -o Exercise01
You will see the following output:
The template "Console Application" was created successfully.
Processing post-creation actions...
Running 'dotnet restore' on Exercise01Exercise01.csproj...
Determining projects to restore...
Restored sourceChapter03Exercise01Exercise01.csproj (in 191 ms).
Restore succeeded.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
</Project>
To implement your date validation rules, you will define a delegate that takes a single DateTime argument and returns a Boolean value. You will name it DateValidationHandler:
using System;
namespace Chapter03.Exercise01
{
public delegate bool DateValidationHandler(DateTime dateTime);
}
public class Order
{
private readonly DateValidationHandler _orderDateValidator;
private readonly DateValidationHandler _deliveryDateValidator;
Notice how you have declared two read-only, class-level instances of DateValidationHandler, one to validate the order date and a second to validate the delivery date. This design assumes that the date validation rules are not going to be altered for this Order instance.
public Order(DateValidationHandler orderDateValidator,
DateValidationHandler deliveryDateValidator)
{
_orderDateValidator = orderDateValidator;
_deliveryDateValidator = deliveryDateValidator;
}
In this design, a different class is typically responsible for deciding which delegates to use, based on the selected customer's profile.
public DateTime OrderDate { get; set; }
public DateTime DeliveryDate { get; set; }
public bool IsValid() =>
_orderDateValidator(OrderDate) &&
_deliveryDateValidator(DeliveryDate);
}
If both are valid, then this call will return true. The key here is that Order doesn't need to know about the precise implementation of an individual customer's date validation rules, so you can easily reuse Order elsewhere in a program. To invoke a delegate, you simply wrap any arguments in brackets, in this case passing the correct date property to each delegate instance:
public static class Program
{
private static bool IsWeekendDate(DateTime date)
{
Console.WriteLine("Called IsWeekendDate");
return date.DayOfWeek == DayOfWeek.Saturday ||
date.DayOfWeek == DayOfWeek.Sunday;
}
private static bool IsPastDate(DateTime date)
{
Console.WriteLine("Called IsPastDate");
return date < DateTime.Today;
}
Notice how both have the exact signature that the DateValidationHandler delegate is expecting. Neither is aware of the nature of the date that they are validating, as that is not their concern. They are both marked static as they do not interact with any variables or properties anywhere in this class.
public static void Main()
{
var orderValidator = new DateValidationHandler(IsPastDate);
var deliverValidator = new DateValidationHandler(IsWeekendDate);
var order = new Order(orderValidator, deliverValidator)
{
OrderDate = DateTime.Today.AddDays(-10),
DeliveryDate = new DateTime(2020, 12, 31)
};
There are various ways to create delegates. Here, you have assigned them to variables first to make the code clearer (you will cover different styles later).
Console.WriteLine($"Ordered: {order.OrderDate:dd-MMM-yy}");
Console.WriteLine($"Delivered: {order.DeliveryDate:dd-MMM-yy }");
Console.WriteLine($"IsValid: {order.IsValid()}");
}
}
}
Ordered: 07-May-22
Delivered: 31-Dec-20
Called IsPastDate
Called IsWeekendDate
IsValid: False
This order is not valid as the delivery date is a Thursday, not a weekend as you require:
You have learned how to define a custom delegate and have created two instances that make use of small helper functions to validate dates. This gives you an idea of how flexible delegates can be.
Note
You can find the code used for this exercise at https://packt.link/cmL0s.
When you define a delegate, you are describing its signature, that is, the return type and a list of input parameters. With that said, consider these two delegates:
public delegate string DoStuff(string name, int age);
public delegate string DoMoreStuff(string name, int age);
They both have the same signature but vary by name alone, which is why you can declare an instance of each and have them both point at the same target method when invoked:
public static void Main()
{
DoStuff stuff = new DoStuff(MyMethod);
DoMoreStuff moreStuff = new DoMoreStuff(MyMethod);
Console.WriteLine($"Stuff: {stuff("Louis", 2)}");
Console.WriteLine($"MoreStuff: {moreStuff("Louis", 2)}");
}
private static string MyMethod(string name, int age)
{
return $"{name}@{age}";
}
Running the console app produces the same results in both calls:
Stuff: Louis@2
MoreStuff: Louis@2
Note
You can find the code used for this example at https://packt.link/r6B8n.
It would be great if you could dispense with defining both DoStuff and DoMoreStuff delegates and use a more generalized delegate with precisely the same signature. After all, it does not matter in the preceding snippet if you create a DoStuff or DoMoreStuff delegate, since both make a call to the same target method.
.NET does, in fact, provide various inbuilt delegates that you can make use of directly, saving you the effort of defining such delegates yourself. These are the Action and Func delegates.
There are many possible combinations of Action and Func delegates, each allowing an increasing number of parameters. You can specify anywhere from zero to 16 different parameter types. With so many combinations available, it is extremely unlikely that you will ever need to define your own delegate type.
It is worth noting that Action and Func delegates were added in a later version of .NET and, as such, the use of custom delegates tends to be found in older legacy code. There is no need to create new delegates yourself.
In the following snippet, MyMethod is invoked using the three-argument Func variation; you will cover the odd-looking <string, int, string> syntax shortly:
Func<string, int, string> funcStuff = MyMethod;
Console.WriteLine($"FuncStuff: {funcStuff("Louis", 2)}");
This produces the same return value as the two earlier invocations:
FuncStuff: Louis@2
Before you continue exploring Action and Func delegates, it is useful to explore the Action<string, int, string> syntax a bit further. This syntax allows type parameters to be used to define classes and methods. These are known as generics and act as placeholders for a particular type. In Chapter 4, Data Structures and LINQ, you will cover generics in much greater detail, but it is worth summarizing their usage here with the Action and Func delegates.
The non-generic version of the Action delegate is predefined in .NET as follows:
public delegate void Action()
As you know from your earlier look at delegates, this is a delegate that does not take any arguments and does not have a return type; it is the simplest type of delegate available.
Contrast that with one of the generic Action delegates predefined in .NET:
public delegate void Action<T>(T obj)
You can see this includes a <T> and T parameter section, which means it accepts a single-type argument. Using this, you can declare an Action that is constrained to a string, which takes a single string argument and returns no value, as follows:
Action<string> actionA;
How about an int constrained version? This also has no return type and takes a single int argument:
Action<int> actionB;
Can you see the pattern here? In essence, the type that you specify can be used to declare a type at compile time. What if you wanted two arguments, or three, or four…or 16? Simple. There are Action and Func generic types that can take up to 16 different argument types. You are very unlikely to be writing code that needs more than 16 parameters.
This two-argument Action takes int and string as parameters:
Action<int, string> actionC;
You can spin that around. Here is another two-argument Action, but this takes a string parameter and then an int parameter:
Action<string, int> actionD;
These cover most argument combinations, so you can see that it is very rare to create your own delegate types.
The same rules apply to delegates that return a value; this is where the Func types are used. The generic Func type starts with a single value type parameter:
public delegate T Func<T>()
In the following example, funcE is a delegate that returns a Boolean value and takes no arguments:
Func<bool> funcE;
Can you guess which is the return type from this rather long Func declaration?
Func<bool, int, int, DateTime, string> funcF;
This gives a delegate that returns a string . In other words, the last argument type in a Func defines the return type. Notice that funcF takes four arguments: bool, int, int, and DateTime.
In summary, generics are a great way to define types. They save a lot of duplicate code by allowing type parameters to act as placeholders.
You covered creating custom delegates and briefly how to assign and invoke a delegate in Exercise 3.01. You then looked at using the preferred Action and Func equivalents, but what other options do you have for assigning the method (or methods) that form a delegate? Are there other ways to invoke a delegate?
Delegates can be assigned to a variable in much the same way that you might assign a class instance. You can also pass new instances or static instances around without having to use variables to do so. Once assigned, you can invoke the delegate or pass the reference to other classes so they can invoke it, and this is often done within the Framework API.
You will now look at a Func delegate, which takes a single DateTime argument and returns a bool value to indicate validity. You will use a static class containing two helper methods, which form the actual target:
public static class DateValidators
{
public static bool IsWeekend(DateTime dateTime)
=> dateTime.DayOfWeek == DayOfWeek.Saturday ||
dateTime.DayOfWeek == DayOfWeek.Sunday;
public static bool IsFuture(DateTime dateTime)
=> dateTime.Date > DateTime.Today;
}
Note
You can find the code used for this example at https://packt.link/mwmxh.
Note that the DateValidators class is marked as static. You may have heard the phrase statics are inefficient. In other words, creating an application with many static classes is a weak practice. Static classes are instantiated the first time they are accessed by running code and remain in memory until the application is closed. This makes it difficult to control their lifetime. Defining small utility classes as static is less of an issue, provided they do indeed remain stateless. Stateless means they do not set any local variables. Static classes that set local states are very difficult to unit test; you can never be sure that the variable set is from one test or another test.
In the preceding snippet, IsFuture returns true if the Date property of the DateTime argument is later than the current date. You are using the static DateTime.Today property to retrieve the current system date. IsWeekend is defined using an expression-bodied syntax and will return true if the DateTime argument's day of the week falls on a Saturday or Sunday.
You can assign delegates the same way that you would assign regular variables (remember you do not have to assign a variable to pass to other classes). You will now create two validator variables, futureValidator and weekendValidator. Each constructor is passed the actual target method, the IsFuture or IsWeekend instance, respectively:
var futureValidator = new Func<DateTime, bool>(DateValidators.IsFuture);
var weekendValidator = new Func<DateTime, bool>(DateValidators.IsWeekend);
Note that it is not valid to use the var keyword to assign a delegate without wrapping in the Func prefix:
var futureValidator = DateValidation.IsFuture;
This results in the following compiler error:
Cannot assign method group to an implicitly - typed variable
Taking this knowledge of delegates, proceed to how you can invoke a delegate.
There are several ways to invoke a delegate. For example, consider the following definition:
var futureValidator = new Func<DateTime, bool>(DateValidators.IsFuture);
To invoke futureValidator, you must pass in a DateTime value, and it will return a bool value using any of these styles:
var isFuture1 = futureValidator?.Invoke(new DateTime(2000, 12, 31));
This is the preferred and safest approach; you should always check for a null before calling Invoke. If there is a chance that a delegate does not point to an object in memory, then you must perform a null reference check before accessing methods and properties. A failure to do so will result in NullReferenceException being thrown. This is the runtime's way of warning you that the object is not pointing at anything.
By using the null-coalescing operator, the compiler will add the null check for you. In the code, you explicitly declared futureValidator, so here it cannot be null. But what if you had been passed futureValidator from another method? How can you be sure that the caller had correctly assigned a reference?
Delegates have additional rules that make it possible for them to throw NullReferenceException when invoked. In the preceding example, futureValidator has a single target, but as you will see later, the multicast feature of delegates allows multiple methods to be added and removed from a list of target methods. If all target methods are removed (which can happen), the runtime will throw a NullReferenceException.
This is the same as the previous method, but without the safety of the null check. This is not recommended for the same reason; that is, the delegate can throw a NullReferenceException:
var isFuture1 = futureValidator.Invoke(new DateTime(2000, 12, 31));
This looks more succinct as you simply call the delegate without the Invoke prefix. Again, this is not recommended due to a possible null reference:
var isFuture2 = futureValidator(new DateTime(2050, 1, 20));
Try assigning and safely invoking a delegate through an exercise by bringing them together.
In this exercise, you are going to write a console app showing how a Func delegate can be used to extract numeric values. You will create a Car class that has Distance and JourneyTime properties. You will prompt the user to enter the distance traveled yesterday and today, passing this information to a Comparison class that is told how to extract values and calculate their differences.
Perform the following steps to do so:
sourceChapter03>dotnet new console -o Exercise02
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
</Project>
using System;
using System.Globalization;
namespace Chapter03.Exercise02
{
public record Car
{
public double Distance { get; init; }
public double JourneyTime { get; init; }
}
public class Comparison
{
private readonly Func<Car, double> _valueSelector;
public Comparison(Func<Car, double> valueSelector)
{
_valueSelector = valueSelector;
}
public double Yesterday { get; private set; }
public double Today { get; private set; }
public double Difference { get; private set; }
public void Compare(Car yesterdayCar, Car todayCar)
{
Yesterday = _valueSelector(yesterdayCar);
Today = _valueSelector(todayCar);
Difference = Yesterday - Today;
}
}
public class JourneyComparer
{
public JourneyComparer()
{
Distance = new Comparison(GetCarDistance);
JourneyTime = new Comparison(GetCarJourneyTime);
AverageSpeed = new Comparison(GetCarAverageSpeed);
static double GetCarDistance(Car car) => car.Distance;
static double GetCarJourneyTime(Car car) => car.JourneyTime;
static double GetCarAverageSpeed(Car car) => car.Distance / car.JourneyTime;
}
public Comparison Distance { get; }
public Comparison JourneyTime { get; }
public Comparison AverageSpeed { get; }
public void Compare(Car yesterday, Car today)
{
Distance.Compare(yesterday, today);
JourneyTime.Compare(yesterday, today);
AverageSpeed.Compare(yesterday, today);
}
}
public class Program
{
public static void Main()
{
var random = new Random();
string input;
do
{
Console.Write("Yesterday's distance: ");
input = Console.ReadLine();
double.TryParse(input, NumberStyles.Any, CultureInfo.CurrentCulture, out var distanceYesterday);
var carYesterday = new Car
{
Distance = distanceYesterday,
JourneyTime = random.NextDouble() * 10D
};
Console.Write(" Today's distance: ");
input = Console.ReadLine();
double.TryParse(input, NumberStyles.Any, CultureInfo.CurrentCulture, out var distanceToday);
var carToday = new Car
{
Distance = distanceToday,
JourneyTime = random.NextDouble() * 10D
};
var comparer = new JourneyComparer();
comparer.Compare(carYesterday, carToday);
Console.WriteLine();
Console.WriteLine("Journey Details Distance Time Avg Speed");
Console.WriteLine("-------------------------------------------------");
Console.Write($"Yesterday {comparer.Distance.Yesterday:N0} ");
Console.WriteLine($"{comparer.JourneyTime.Yesterday:N0} {comparer.AverageSpeed.Yesterday:N0}");
Console.Write($"Today {comparer.Distance.Today:N0} "); Console.WriteLine($"{comparer.JourneyTime.Today:N0} {comparer.AverageSpeed.Today:N0}");
Console.WriteLine("=================================================");
Console.Write($"Difference {comparer.Distance.Difference:N0} "); Console.WriteLine($"{comparer.JourneyTime.Difference:N0} {comparer.AverageSpeed.Difference:N0}");
Console.WriteLine("=================================================");
}
while (!string.IsNullOrEmpty(input));
}
}
}
Running the console and entering distances of 1000 and 900 produces the following results:
Yesterday's distance: 1000
Today's distance: 900
Journey Details Distance Time Avg Speed
-------------------------------------------------
Yesterday 1,000 8 132
Today 900 4 242
=================================================
Difference 100 4 -109
The program will run in a loop until you enter a blank value. You will notice a different output as the JourneyTime is set using a random value returned by an instance of Random class.
Note
You can find the code used for this exercise at https://packt.link/EJTtS.
In this exercise, you have seen how a Func<Car, double> delegate is used to create general-purpose code that can be easily reused without the need to create extra interfaces or classes.
Now it is time to look at the second important aspect of deletes and their ability to chain multiple target methods together.
So far, you have invoked delegates that have a single method assigned, typically in the form of a function call. Delegates offer the ability to combine a list of methods that are executed with a single invocation call, using the multicast feature. By using the += operator, any number of additional target methods can be added to the target list. Every time the delegate is invoked, each one of the target methods gets invoked too. But what if you decide you want to remove a target method? That is where the -= operator is used.
In the following code snippet, you have an Action<string> delegate named logger. It starts with a single target method, LogToConsole. If you were to invoke this delegate, passing in a string, then the LogToConsole method will be called once:
Action<string> logger = LogToConsole;
logger("1. Calculating bill");
If you were to watch the call stack, you would observe these calls:
logger("1. Calculating bill")
--> LogToConsole("1. Calculating bill")
To add a new target method, you use the += operator. The following statement adds LogToFile to the logger delegate's invocation list:
logger += LogToFile;
Now, every time you invoke logger, both LogToConsole and LogToFile will be called. Now invoke logger a second time:
logger("2. Saving order");
The call stack looks like this:
logger("2. Saving order")
--> LogToConsole("2. Saving order")
--> LogToFile("2. Saving order")
Again, suppose you use += to add a third target method called LogToDataBase as follows:
logger += LogToDataBase
Now invoke it once again:
logger("3. Closing order");
The call stack looks like this:
logger("3. Closing order")
--> LogToConsole("3. Closing order")
--> LogToFile("3. Closing order")
--> LogToDataBase("3. Closing order")
However, consider that you may no longer want to include LogToFile in the target method list. In such a case, simply use the -= operator to remove it, as follows:
logger -= LogToFile
You can again invoke the delegate as follows:
logger("4. Closing customer");
And now, the call stack looks like this:
logger("4. Closing customer")
--> LogToConsole("4. Closing customer")
--> LogToDataBase("4. Closing customer")
As can be seen, this code resulted in just two method calls, LogToConsole and LogToDataBase.
By using delegates in this way, you can decide which target methods get called based on certain criteria at runtime. This allows you to pass this configured delegate into other methods, to be invoked as and when needed.
You have seen that Console.WriteLine can be used to write messages to the console window. To create a method that logs to a file (as LogToFile does in the preceding example), you need to use the File class from the System.IO namespace. File has many static methods that can be used to read and write files. You will not go into full details about File here, but it is worth mentioning the File.AppendAllText method, which can be used to create or replace a text file containing a string value, File.Exists, which is used to check for the existence of a file, and File.Delete, to delete a file.
Now it is time to practice what you have learned through an exercise.
In this exercise, you will use a multicast delegate to create a cash machine that logs details when a user enters their PIN and asks to see their balance. For this, you will create a CashMachine class that invokes a configured logging delegate, which you can use as a controller class to decide whether messages are sent to the file or to the console.
You will use an Action<string> delegate as you do not need any values to return. Using +=, you can control which target methods get called when your delegate is invoked by CashMachine.
Perform the following steps to do so:
sourceChapter03>dotnet new console -o Exercise03
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
</Project>
using System;
using System.IO;
namespace Chapter03.Exercise03
{
public class CashMachine
{
private readonly Action<string> _logger;
public CashMachine(Action<string> logger)
{
_logger = logger;
}
The CashMachine constructor is passed the Action<string> delegate, which you can assign to a readonly class variable called _logger.
private void Log(string message)
=> _logger?.Invoke(message);
public void VerifyPin(string pin)
=> Log($"VerifyPin called: PIN={pin}");
public void ShowBalance()
=> Log("ShowBalance called: Balance=999");
}
public static class Program
{
private const string OutputFile = "activity.txt";
public static void Main()
{
if (File.Exists(OutputFile))
{
File.Delete(OutputFile);
}
Action<string> logger = LogToConsole;
logger += LogToFile;
var cashMachine = new CashMachine(logger);
Console.Write("Enter your PIN:");
var pin = Console.ReadLine();
if (string.IsNullOrEmpty(pin))
{
Console.WriteLine("No PIN entered");
return;
}
cashMachine.VerifyPin(pin);
Console.WriteLine();
In case you enter a blank value, then it is checked and a warning is displayed. This will then close the program using a return statement.
Console.Write("Press Enter to show balance");
Console.ReadLine();
cashMachine.ShowBalance();
Console.Write("Press Enter to quit");
Console.ReadLine();
static void LogToConsole(string message)
=> Console.WriteLine(message);
static void LogToFile(string message)
=> File.AppendAllText(OutputFile, message);
}
}
}
Enter your PIN:12345
VerifyPin called: PIN=12345
Press Enter to show balance
ShowBalance called: Balance=999
VerifyPin called: PIN=12345ShowBalance called: Balance=999
Note
You can find the code used for this exercise at https://packt.link/h9vic.
It is important to remember that delegates are immutable, so each time you use the += or -= operators, you create a new delegate instance. This means that if you alter a delegate after you have passed it to a target class, you will not see any changes to the methods called from inside that target class.
You can see this in action in the following example:
MulticastDelegatesAddRemoveExample.cs
using System;
namespace Chapter03Examples
{
class MulticastDelegatesAddRemoveExample
{
public static void Main()
{
Action<string> logger = LogToConsole;
Console.WriteLine($"Logger1 #={logger.GetHashCode()}");
logger += LogToConsole;
Console.WriteLine($"Logger2 #={logger.GetHashCode()}");
logger += LogToConsole;
Console.WriteLine($"Logger3 #={logger.GetHashCode()}");
You can find the complete code here: https://packt.link/vqZMF.
All objects in C# have a GetHashCode() function that returns a unique ID. Running the code produces this output:
Logger1 #=46104728
Logger2 #=1567560752
Logger3 #=236001992
You can see that the hashcode is changing after each += call. This shows that the object reference is changing each time.
Now look at another example using an Action<string> delegate. Here, you will use the += operator to add target methods and then use -= to remove the target methods:
MulticastDelegatesExample.cs
using System;
namespace Chapter03Examples
{
class MulticastDelegatesExample
{
public static void Main()
{
Action<string> logger = LogToConsole;
logger += LogToConsole;
logger("Console x 2");
logger -= LogToConsole;
logger("Console x 1");
logger -= LogToConsole;
You can find the complete code here: https://packt.link/Xe0Ct.
You start with one target method, LogToConsole, and then add the same target method a second time. Invoking the logger delegate using logger("Console x 2") results in LogToConsole being called twice.
You then use -= to remove LogToConsole twice such that had two targets and now you do not have any at all. Running the code produces the following output:
Console x 2
Console x 2
Console x 1
However, rather than logger("logger is now null") running correctly, you end up with an unhandled exception being thrown like so:
System.NullReferenceException
HResult=0x80004003
Message=Object reference not set to an instance of an object.
Source=Examples
StackTrace:
at Chapter03Examples.MulticastDelegatesExample.Main() in Chapter03MulticastDelegatesExample.cs:line 16
By removing the last target method, the -= operator returned a null reference, which you then assigned to the logger. As you can see, it is important to always check that a delegate is not null before trying to invoke it.
So far, you have used Action<string> delegates within multicast scenarios. When invoked, a string value is passed to any target method. As the target methods do not return a value, you use Action delegates.
You have seen that Func delegates are used when a return value is required from an invoked delegate. It is also perfectly legal for the C# complier to use Func delegates in multicast delegates.
Consider the following example where you have a Func<string, string> delegate. This delegate supports functions that are passed a string and return a formatted string is returned. This could be used when you need to format an email address by removing the @ sign and dot symbols:
using System;
namespace Chapter03Examples
{
class FuncExample
{
public static void Main()
{
You start by assigning the RemoveDots string function to emailFormatter and invoke it using the Address constant:
Func<string, string> emailFormatter = RemoveDots;
const string Address = "[email protected]";
var first = emailFormatter(Address);
Console.WriteLine($"First={first}");
Then you add a second target, RemoveAtSign, and invoke emailFormatter a second time:
emailFormatter += RemoveAtSign;
var second = emailFormatter(Address);
Console.WriteLine($"Second={second}");
Console.ReadLine();
static string RemoveAtSign(string address)
=> address.Replace("@", "");
static string RemoveDots(string address)
=> address.Replace(".", "");
}
}
}
Running the code produces this output:
First=admin@googlecom
Second=admingoogle.com
The first invocation returns the admin@googlecom string. The dot symbol has been removed, but the next invocation, with RemoveAtSign added to the target list, returns a value with only the @ symbol removed.
Note
You can find the code used for this example at https://packt.link/fshse.
Both Func1 and Func2 are invoked, but only the value from Func2 is returned to both ResultA and ResultB variables, even though the correct arguments are passed in. When a Func<> delegate is used with multicast in this manner, all of the target Func instances are called, but the return value will be that of the last Func<> in the chain. Func<> is better suited in a single method scenario, although the compiler will still allow you to use it as a multicast delegate without any compilation error or warning.
When a delegate is invoked, all methods in the invocation list are called. In the case of single-name delegates, this will be one target method. What happens in the case of multicast delegates if one of those targets throws an exception?
Consider the following code. When the logger delegate is invoked, by passing in try log this, you may expect the methods to be called in the order that they were added: LogToConsole, LogToError, and finally LogToDebug:
MulticastWithErrorsExample.cs
using System;
using System.Diagnostics;
namespace Chapter03Examples
{
class MulticastWithErrorsExample
{
public static void Main()
{
Action<string> logger = LogToConsole;
logger += LogToError;
logger += LogToDebug;
try
{
logger("try log this");
You can find the complete code here: https://packt.link/Ti3Nh.
If any target method throws an exception, such as the one you see in LogToError, then the remaining targets are not called.
Running the code results in the following output:
Console: try log this
Caught oops!
All done
You will see this output because the LogToDebug method wasn't called at all. Consider a UI with multiple targets listening to a mouse button click. The first method fires when a button is pressed and disables the button to prevent double-clicks, the second method changes the button's image to indicate success, and the third method enables the button.
If the second method fails, then the third method will not get called, and the button could remain in a disabled state with an incorrect image assigned, thereby confusing the user.
To ensure that all target methods are run regardless, you can enumerate through the invocation list and invoke each method manually. Take a look at the .NET MulticastDelegate type. You will find that there is a function, GetInvocationList, that returns an array of the delegate objects. This array contains the target methods that have been added:
public abstract class MulticastDelegate : Delegate {
public sealed override Delegate[] GetInvocationList();
}
You can then loop through those target methods and execute each one inside a try/catch block. Now practice what you learned through this exercise.
Throughout this chapter, you have been using Action<string> delegates to perform various logging operations. In this exercise, you have a list of target methods for a logging delegate and you want to ensure that "all" target methods are invoked even if earlier ones fail. You may have a scenario where logging to a database or filesystem fails occasionally, maybe due to network issues. In such a situation, you will want other logging operations to at least have a chance to perform their logging activity.
Perform the following steps to do so:
sourceChapter03>dotnet new console -o Exercise04
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
</Project>
using System;
using System.IO;
namespace Chapter03.Exercise04
{
public static class Program
{
private const string OutputFile = "Exercise04.txt";
public static void Main()
{
if (File.Exists(OutputFile))
{
File.Delete(OutputFile);
}
Action<string> logger = LogToConsole;
InvokeAll(logger, "First call");
logger += LogToConsole;
logger += LogToDatabase;
logger += LogToFile;
InvokeAll(logger, "Second call");
Console.ReadLine();
static void LogToConsole(string message)
=> Console.WriteLine($"LogToConsole: {message}");
static void LogToDatabase(string message)
=> throw new ApplicationException("bad thing happened!");
static void LogToFile(string message)
=> File.AppendAllText(OutputFile, message);
static void InvokeAll(Action<string> logger, string arg)
{
if (logger == null)
return;
It is passed an Action<string> delegate that matches the logger delegate type, along with an arg string to use when invoking each target method. Before that though, it is important to check that logger is not already null and there is nothing you can do with a null delegate.
var delegateList = logger.GetInvocationList();
Console.WriteLine($"Found {delegateList.Length} items in {logger}");
foreach (var del in delegateList)
{
try
{
var action = del as Action<string>;
GetInvocationList returns each item as the base delegate type regardless of their actual type.
if (del is Action<string> action)
{
Console.WriteLine($"Invoking '{action.Method.Name}' with '{arg}'");
action(arg);
}
else
{
Console.WriteLine("Skipped null");
}
You have added some extra details to show what is about to be invoked by using the delegate's Method.Name property.
}
catch (Exception e)
{
Console.WriteLine($"Error: {e.Message}");
}
}
}
}
}
}
Found 1 items in System.Action`1[System.String]
Invoking '<Main>g__LogToConsole|1_0' with 'First call'
LogToConsole: First call
Found 4 items in System.Action`1[System.String]
Invoking '<Main>g__LogToConsole|1_0' with 'Second call'
LogToConsole: Second call
Invoking '<Main>g__LogToConsole|1_0' with 'Second call'
LogToConsole: Second call
Invoking '<Main>g__LogToDatabase|1_1' with 'Second call'
Error: bad thing happened!
Invoking '<Main>g__LogToFile|1_2' with 'Second call'
You will see that it catches the error thrown by LogToDatabase and still allows LogToFile to be called.
Note
You can find the code used for this exercise at https://packt.link/Dp5H4.
It is now important to expand upon the multicast concept using events.
In the previous sections, you have created delegates and invoked them directly in the same method or passed them to another method for it to invoke when needed. By using delegates in this way, you have a simple way for code to be notified when something of interest happens. So far, this has not been a major problem, but you may have noticed that there appears to be no way to prevent an object that has access to a delegate from invoking it directly.
Consider the following scenario: you have created an application that allows other programs to register for notifications when a new email arrives by adding their target method to a delegate that you have provided. What if a program, either by mistake or for malicious reasons, decides to invoke your delegate itself? This could quite easily overwhelm all the target methods in your invocation list. Such listener programs should never be allowed to invoke a delegate in this way—after all, they are meant to be passive listeners.
You could add extra methods that allow listeners to add or remove their target methods from the invocation list and shield the delegate from direct access, but what if you have hundreds of such delegates available in an application? That is a great deal of code to write.
The event keyword instructs the C# complier to add extra code to ensure that a delegate can only be invoked by the class or struct that it is declared in. External code can add or remove target methods but is prevented from invoking the delegate. Attempting to do so results in a compiler error.
This pattern is commonly known as the pub-sub pattern. The object raising an event is called the event sender or publisher; the object(s) receiving the event are called event handlers or subscribers.
The event keyword is used to define an event and its associated delegates. Its definition looks similar to the way delegates are defined, but unlike delegates, you cannot use the global namespace to define events:
public event EventHandler MouseDoubleClicked
Events have four elements:
Events are typically associated with the inbuilt .NET delegates, EventHandler, or its generic EventHandler<> version. It is rare to create custom delegates for events, but you may find this in older legacy code created prior to the Action and generic Action<T> delegates.
The EventHandler delegate was available in early versions of .NET. It has the following signature, taking a sender object and an EventArgs parameter:
public delegate void EventHandler(object sender, EventArgs e);
The more recent generic-based EventHandler<T> delegate looks similar; it also takes a sender object and a parameter defined by the type T:
public delegate void EventHandler<T>(object sender, T e);
The sender parameter is defined as object, allowing any type of object to be sent to subscribers for them to identify the sender of the event. This can be useful in a situation where you have a centralized method that needs to work on various types of objects rather than specific instances.
For example, in a UI app, you may have one subscriber that listens for an OK button being clicked, and a second subscriber that listens for a Cancel button being clicked–each of these could be handled by two separate methods. In the case of multiple checkboxes used to toggle options on or off, you could use a single target method that simply needs to be told that a checkbox is the sender, and to toggle the setting accordingly. This allows you to reuse the same checkbox handler rather than creating a method for every checkbox on a screen.
It is not mandatory to include details of the sender when invoking an EventHandler delegate. Often, you may not want to divulge the inner workings of your code to the outside; in this case, it is common practice to pass a null reference to the delegate.
The second argument in both delegates can be used to provide extra contextual information about the event (for example, was it the left or right mouse button that was pressed?). Traditionally, this extra information was wrapped up using a class derived from EventArgs, but that convention has been relaxed in newer .NET versions.
There are two standard .NET delegates you should for your event definition?
Interestingly, no matter what scope you give to your event (public, for example), the C# compiler will internally create a private member with that name. This is the key concept with events: only the class that defines the event may invoke it. Consumers are free to add or remove their interest, but they cannot invoke it themselves.
When an event is defined, the publisher class in which it is defined can simply invoke it as and when needed, in the same way that you invoke delegates. In the earlier examples, a point was made of always checking that the delegate is not null before invoking. The same approach should be taken with events, as you have little control over how or when a subscriber may add or remove their target methods.
When a publisher class is initially created, all events have an initial value of null. This will change to not null when any subscriber adds a target method. Conversely, as soon as a subscriber removes a target method, the event will revert to null if there are no methods left in the invocation list and all this is handled by the runtime. This is the standard behavior you saw earlier with delegates.
You can prevent an event from ever becoming null by adding an empty delegate to the end of the event definition:
public event EventHandler<MouseEventArgs> MouseDoubleClicked = delegate {};
Rather than having the default null value, you are adding your own default delegate instance—one that does nothing. Hence the blank between the {} symbols.
There is a common pattern often followed when using events within a publisher class, particularly in classes that may be subclassed further. You will now see this with the help of a simple example:
using System;
namespace Chapter03Examples
{
public class MouseClickedEventArgs
{
public MouseClickedEventArgs(int clicks)
{
Clicks = clicks;
}
public int Clicks { get; }
}
Observe the MouseClickPublisher class, This has a MouseClicked event defined using the generic EventHandler<> delegate.
public class MouseClickPublisher
{
public event EventHandler<MouseClickedEventArgs> MouseClicked = delegate { };
protected virtual void OnMouseClicked( MouseClickedEventArgs e)
{
var evt = MouseClicked;
evt?.Invoke(this, e);
}
private void TrackMouseClicks()
{
OnMouseClicked(new MouseClickedEventArgs(2));
}
}
public class MouseSingleClickPublisher : MouseClickPublisher
{
protected override void OnMouseClicked(MouseClickedEventArgs e)
{
if (e.Clicks == 1)
{
OnMouseClicked(e);
}
}
}
}
This MouseSingleClickPublisher overrides the OnMouseClicked method and only calls the base OnMouseClicked if a single click was detected. By implementing this type of pattern, you allow different types of publishers to control whether events are fired to subscribers in a customized manner.
Note
You can find the code used for this example at https://packt.link/J1EiB.
You can now practice what you learned through the following exercise.
In this exercise, you will create an alarm clock as an example of a publisher. The alarm clock will simulate a tick every minute and publish a Ticked event. You will also add a WakeUp event that is published when the current time matches an alarm time. In .NET, DateTime is used to represent a point in time, so you will use that for the current time and alarm time properties. You will use DateTime.Subtract to get the difference between the current time and the alarm time and publish the WakeUp event when it is due.
Perform the following steps to do so:
dotnet new console -o Exercise05
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
</Project>
using System;
namespace Chapter03.Exercise05
{
public class AlarmClock
{
You will offer two events for subscribers to listen to—WakeUp, based on the non-generic EventHandler delegate (since you will not pass any extra information in this event), and Ticked, which uses the generic EventHandler delegate with a DateTime parameter type.
public event EventHandler WakeUp = delegate {};
public event EventHandler<DateTime> Ticked = delegate {};
protected void OnWakeUp()
{
WakeUp.Invoke(this, EventArgs.Empty);
}
public DateTime AlarmTime { get; set; }
public DateTime ClockTime { get; set; }
public void Start()
{
// Run for 24 hours
const int MinutesInADay = 60 * 24;
for (var i = 0; i < MinutesInADay; i++)
{
ClockTime = ClockTime.AddMinutes(1);
Ticked.Invoke(this, ClockTime);
ClockTime.Subtract is used to calculate the difference between the click and alarm times.
var timeRemaining = ClockTime .Subtract(AlarmTime) .TotalMinutes;
if (IsTimeToWakeUp(timeRemaining))
{
OnWakeUp();
break;
}
}
static bool IsTimeToWakeUp(double timeRemaining)
=> timeRemaining is (>= -1.0 and <= 1.0);
}
}
public static class Program
{
public static void Main()
{
var clock = new AlarmClock();
clock.Ticked += ClockTicked;
clock.WakeUp += ClockWakeUp;
clock.ClockTime = DateTime.Now;
clock.AlarmTime = DateTime.Now.AddMinutes(120);
Console.WriteLine($"ClockTime={clock.ClockTime:t}");
Console.WriteLine($"AlarmTime={clock.AlarmTime:t}");
clock.Start();
Console.WriteLine("Press ENTER");
Console.ReadLine();
static void ClockWakeUp(object sender, EventArgs e)
{
Console.WriteLine();
Console.WriteLine("Wake up");
}
ClockWakeUp is passed sender and EventArgs arguments. You don't use either of these, but they are required for the EventHandler delegate. When this subscriber's method is called, you write "Wake up" to the console.
static void ClockTicked(object sender, DateTime e)
=> Console.Write($"{e:t}...");
}
}
}
ClockTime=14:59
AlarmTime=16:59
15:00...15:01...15:02...15:03...15:04...15:05...15:06...15:07...15:08...15:09...15:10...15:11...15:12...15:13...15:14...15:15...15:16...15:17...15:18...15:19...15:20...15:21...15:22...15:23...15:24...15:25...15:26...15:27...15:28...15:29...15:30...15:31...15:32...15:33...15:34...15:35...15:36...15:37...15:38...15:39...15:40...15:41...15:42...15:43...15:44...15:45...15:46...15:47...15:48...15:49...15:50...15:51...15:52...15:53...15:54...15:55...15:56...15:57...15:58...15:59...16:00...16:01...16:02...16:03...16:04...16:05...16:06...16:07...16:08...16:09...16:10...16:11...16:12...16:13...16:14...16:15...16:16...16:17...16:18...16:19...16:20...16:21...16:22...16:23...16:24...16:25...16:26...16:27...16:28...16:29...16:30...16:31...16:32...16:33...16:34...16:35...16:36...16:37...16:38...16:39...16:40...16:41...16:42...16:43...16:44...16:45...16:46...16:47...16:48...16:49...16:50...16:51...16:52...16:53...16:54...16:55...16:56...16:57...16:58...16:59...
Wake up
Press ENTER
In this example you see that the alarm clock simulates a tick every minute and publishes a Ticked event.
Note
You can find the code used for this exercise at https://packt.link/GPkYQ.
Now it is time to grasp the difference between events and delegates.
On the face of it, events and delegates look remarkably similar:
The key considerations are as follows:
Before you wrap up your look at events, it pays to be careful when using events, particularly those that are statically defined.
Whenever you add a subscriber's target method to a publisher's event, the publisher class will store a reference to your target method. When you have finished using a subscriber instance and it remains attached to a static publisher, it is possible that the memory used by your subscriber will not be cleared up.
These are often referred to as orphaned, phantom, or ghost events. To prevent this, always try to pair up each += call with a corresponding -= operator.
Note
Reactive Extensions (Rx) (https://github.com/dotnet/reactive) is a great library for leveraging and taming event-based and asynchronous programming using LINQ-style operators. Rx provides a way to time-shift, for example, buffering a very chatty event into manageable streams with just a few lines of code. What's more, Rx streams are very easy to unit test, allowing you to effectively take control of time.
Now read about the interesting topic of lambda expressions.
Throughout the previous sections, you have mainly used class-level methods as targets for your delegates and events, such as the ClockTicked and ClockWakeUp methods, that were also used in Exercise 3.05:
clock.Ticked += ClockTicked;
clock.WakeUp += ClockWakeUp;
static void ClockTicked(object sender, DateTime e)
=> Console.Write($"{e:t}...");
static void ClockWakeUp(object sender, EventArgs e)
{
Console.WriteLine("Wake up");
}
The ClockWakeUp and ClockTicked methods are easy to follow and step through. However, by converting them into lambda expression syntax, you can have a more succinct syntax and closer proximity to where they are in code.
Now convert the Ticked and WakeUp events to use two different lambda expressions:
clock.Ticked += (sender, e) =>
{
Console.Write($"{e:t}...");
};
clock.WakeUp += (sender, e) =>
{
Console.WriteLine();
Console.WriteLine("Wake up");
};
You have used the same += operator, but instead of method names, you see (sender, e) => and identical blocks of code, as seen in ClockTicked and ClockWakeUp.
When defining a lambda expression, you can pass any parameters within parentheses, (), followed by => (this is often read as goes to), and then by your expression/statement block:
(parameters) => expression-or-block
The code block can be as complex as you need and can return a value if it is a Func-based delegate.
The compiler can normally infer each of the parameter types, so you do not even need to specify their types. Moreover, you can omit the parentheses if there is only one argument and the compiler can infer its type.
Wherever a delegate (remember that Action, Action<T>, and Func<T> are inbuilt examples of a delegate) needs to be used as an argument, rather than creating a class or local method or function, you should consider using a lambda expression. The main reason is that this often results in less code, and that code is placed closer to the location where it is used.
Now consider another example on Lambda. Given a list of movies, you can use the List<string> class to store these string-based names, as shown in the following snippet:
using System;
using System.Collections.Generic;
namespace Chapter03Examples
{
class LambdaExample
{
public static void Main()
{
var names = new List<string>
{
"The A-Team",
"Blade Runner",
"There's Something About Mary",
"Batman Begins",
"The Crow"
};
You can use the List.Sort method to sort the names alphabetically (the final output will be shown at the end of this example):
names.Sort();
Console.WriteLine("Sorted names:");
foreach (var name in names)
{
Console.WriteLine(name);
}
Console.WriteLine();
If you need more control over how this sort works, the List class has another Sort method that accepts a delegate of this form: delegate int Comparison<T>(T x, T y). This delegate is passed two arguments of the same type (x and y) and returns an int value. The int value can be used to define the sort order of items in the list without you having to worry about the internal workings of the Sort method.
As an alternative, you can sort the names to exclude "The" from the beginning of movie titles. This is often used as an alternative way to list names. You can achieve this by passing a lambda expression, using the ( ) syntax to wrap two strings, x, y, that will be passed by Sort() when it invokes your lambda.
If x or y starts with your noise word, "The", then you use the string.Substring function to skip the first four characters. String.Compare is then used to return a numeric value that compares the resulting string values, as follows:
const string Noise = "The ";
names.Sort( (x, y) =>
{
if (x.StartsWith(Noise))
{
x = x.Substring(Noise.Length);
}
if (y.StartsWith(Noise))
{
y = x.Substring(Noise.Length);
}
return string.Compare(x , y);
});
You can then write out the sorted results to the console:
Console.WriteLine($"Sorted excluding leading '{Noise}':");
foreach (var name in names)
{
Console.WriteLine(name);
}
Console.ReadLine();
}
}
}
Running the example code produces the following output:
Sorted names:
Batman Begins
Blade Runner
The A-Team
The Crow
There's Something About Mary
Sorted excluding leading 'The ':
The A-Team
Batman Begins
Blade Runner
The Crow
There's Something About Mary
You can see that the second set of names is sorted with "The" is ignored.
Note
You can find the code used for this example at http://packt.link/B3NmQ.
To see these lambda statements put into practice, try your hand at the following exercise.
In this exercise, you are going to create a utility class that splits the words in a sentence and returns that sentence with the words in reverse order.
Perform the following steps to do so:
sourceChapter03>dotnet new console -o Exercise06
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
</Project>
using System;
using System.Linq;
namespace Chapter03.Exercise06
{
public static class WordUtilities
{
public static string ReverseWords(string sentence)
{
Func<string, string> swapWords =
phrase =>
{
const char Delimit = ' ';
var words = phrase
.Split(Delimit)
.Reverse();
return string.Join(Delimit, words);
};
String.Reverse reverses the order of strings in the array, before finally joining the reversed words string array in a single string using string.Join.
return swapWords(sentence);
}
}
public static class Program
{
public static void Main()
{
do
{
Console.Write("Enter a sentence:");
var input = Console.ReadLine();
if (string.IsNullOrEmpty(input))
{
break;
}
var result = WordUtilities.ReverseWords(input);
Console.WriteLine($"Reversed: {result}")
Running the console app produces results output similar to this:
Enter a sentence:welcome to c#
Reversed: c# to welcome
Enter a sentence:visual studio by microsoft
Reversed: microsoft by studio visual
Note
You can find the code used for this exercise at https://packt.link/z12sR.
You will conclude this look at lambdas with some of the less obvious issues that you might not expect to see when running and debugging.
Lambda expressions can capture any of the variables or parameters within the method where they are defined. The word capture is used to describe the way that a lambda expression captures or reaches up into the parent method to access any variables or parameters.
To grasp this better, consider the following example. Here you will create a Func<int, string> called joiner that joins words together using the Enumerable.Repeat method. The word variable (known as an Outer Variables) is captured inside the body of the joiner expression:
var word = "hello";
Func<int, string> joiner =
input =>
{
return string.Join(",", Enumerable.Repeat(word, input));
};
Console.WriteLine($"Outer Variables: {joiner(2)}");
Running the preceding example produces the following output:
Outer Variables: hello,hello
You invoked the joiner delegate by passing 2 as an argument. At that moment in time, the outer word variable has a value of "hello", which is repeated twice.
This confirms that captured variables, from the parent method, were evaluated only when Func was invoked. Now change the value of word from hello to goodbye and invoke joiner once again, passing 3 as the argument:
word = "goodbye";
Console.WriteLine($"Outer Variables Part2: {joiner(3)}");
Running this example produces the following output:
Outer Variables Part2: goodbye,goodbye,goodbye
It is worth remembering that it does not matter where in the code you defined joiner. You could have changed the value of word to any number of strings before or after declaring joiner.
Taking captures one step further, if you define a variable with the same name inside a lambda, it will be scoped locally to the expression. This time, you have a locally defined variable, word, which will have no effect on the outer variable with the same name:
Func<int, string> joinerLocal =
input =>
{
var word = "local";
return string.Join(",", Enumerable.Repeat(word, input));
};
Console.WriteLine($"JoinerLocal: {joinerLocal(2)}");
Console.WriteLine($"JoinerLocal: word={word}");
The preceding example results in the following output. Notice how the outer variable, word, remains unchanged from goodbye:
JoinerLocal: local,local
JoinerLocal: word=goodbye
Finally, you will look at the concept of closures that is a subtle part of the C# language and often leads to unexpected results.
In the following example, you have a variable, actions, that contains a List of Action delegates. You use a basic for loop to add five separate Action instances to the list. The lambda expression for each Action simply writes that value of i from the for loop to the console. Finally, the code simply runs through each Action in the actions list and invokes each one:
var actions = new List<Action>();
for (var i = 0; i < 5; i++)
{
actions.Add( () => Console.WriteLine($"MyAction: i={i}")) ;
}
foreach (var action in actions)
{
action();
}
Running the example produces the following output:
MyAction: i=5
MyAction: i=5
MyAction: i=5
MyAction: i=5
MyAction: i=5
The reason why MyAction: i did not start from 0 is that the value of i, when accessed from inside a Action delegate, is only evaluated once the Action is invoked. By the time each delegate is invoked, the outer loop has already repeated five times over.
Note
You can find the code used for this example at https://packt.link/vfOPx.
This is similar to the capture concept you observed, where the outer variables, i in this case, are only evaluated when invoked. You used i in the for loop to add each Action to the list, but by the time you invoked each action, i had its final value of 5.
This can often lead to unexpected behavior, especially if you assume that an incrementing value for i is being used inside each action's loop variable. To ensure that the incrementing value of i is used inside each lambda expression, you need to introduce a new local variable inside the for loop, one that takes a copy of the iterator variable.
In the following code snippet, you have added the closurei variable. It looks very subtle, but you now have a more locally scoped variable, which you access from inside the lambda expression, rather than the iterator, i:
var actionsSafe = new List<Action>();
for (var i = 0; i < 5; i++)
{
var closurei = i;
actionsSafe.Add(() => Console.WriteLine($"MyAction: closurei={closurei}"));
}
foreach (var action in actionsSafe)
{
action();
}
Running the example produces the following output. You can see that the incrementing value is used when each Action is invoked, rather than the value of 5 that you saw earlier:
MyAction: closurei=0
MyAction: closurei=1
MyAction: closurei=2
MyAction: closurei=3
MyAction: closurei=4
You have covered the key aspects of delegates and events in event-driven applications. You extended this by using the succinct coding style offered by lambdas, to be notified when events of interest occur.
You will now bring these ideas together into an activity in which you will use some of the inbuilt .NET classes with their own events. You will need to adapt these events to your own format and publish so they can be subscribed to by a console app.
Now it is time to practice all you have learned through the following activity.
You plan to investigate patterns in US storm events. To do this, you need to download storm event datasets from online sources for later analysis. The National Oceanic and Atmospheric Administration is one such source of data and can be accessed from https://www1.ncdc.noaa.gov/pub/data/swdi/stormevents/csvfiles.
You are tasked with creating a .NET Core console app that allows a web address to be entered, the contents of which are downloaded to a local disk. To be as user-friendly as possible, the application needs to use events that signal when an invalid address is entered, the progress of a download, and when it completes.
Ideally, you should try to hide the internal implementation that you use to download files, preferring to adapt any events that you use to ones that your caller can subscribe to. This form of adaption is often used to make code more maintainable by hiding internal details from callers.
For this purpose, the WebClient class in C# can be used for download requests. As with many parts of .NET, this class returns objects that implement the IDisposable interface. This is a standard interface and it indicates that the object you are using should be wrapped in a using statement to ensure that any resources or memory are cleaned away for you when you have finished using the object. using takes this format:
using (IDisposable) { statement_block }
Finally, the WebClient.DownloadFileAsync method downloads files in the background. Ideally, you should use a mechanism that allows one part of your code to wait for a signal to be set once the download has been completed. System.Threading.ManualResetEventSlim is a class that has Set and Wait methods that can help with this type of signaling.
For this activity, you will need to perform the following steps:
Enter a URL:
https://www1.ncdc.noaa.gov/pub/data/swdi/stormevents/csvfiles/StormEvents_details-ftp_v1.0_d1950_c20170120.csv.gz
Downloading https://www1.ncdc.noaa.gov/pub/data/swdi/stormevents/csvfiles/StormEvents_details-ftp_v1.0_d1950_c20170120.csv.gz...
Downloading...73% complete (7,758 bytes)
Downloading...77% complete (8,192 bytes)
Downloading...100% complete (10,597 bytes)
Downloaded to C:TempStormEvents_details-ftp_v1.0_d1950_c20170120.csv.gz
Enter a URL:
https://www1.ncdc.noaa.gov/pub/data/swdi/stormevents/csvfiles/StormEvents_details-ftp_v1.0_d1954_c20160223.csv.gz
Downloading https://www1.ncdc.noaa.gov/pub/data/swdi/stormevents/csvfiles/StormEvents_details-ftp_v1.0_d1954_c20160223.csv.gz...
Downloading...29% complete (7,758 bytes)
Downloading...31% complete (8,192 bytes)
Downloading...54% complete (14,238 bytes)
Downloading...62% complete (16,384 bytes)
Downloading...84% complete (22,238 bytes)
Downloading...93% complete (24,576 bytes)
Downloading...100% complete (26,220 bytes)
Downloaded to C:TempStormEvents_details-ftp_v1.0_d1954_c20160223.csv.gz
By completing this activity, you have seen how to subscribe to events from an existing .NET event-based publisher class (WebClient), adapting them to your own specification before republishing them in your adapter class (WebClientAdapter), which were ultimately subscribed to by a console app.
Note
The solution to this activity can be found at https://packt.link/qclbF.
In this chapter, you took an in-depth look at delegates. You created custom delegates and saw how they could be replaced with their modern counterparts, the inbuilt Action and Func delegates. By using null reference checks, you discovered the safe way to invoke delegates and how multiple methods can be chained together to form multicast delegates. You extended delegates further to use them with the event keyword to restrict invocation and followed the preferred pattern when defining and invoking events. Finally, you covered the succinct lambda expression style and saw how bugs can be avoided by recognising the use of captures and closures.
In the next chapter, you will look at LINQ and data structures, the fundamental parts of the C# language.