3. Delegates, Events, and Lambdas

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.

Introduction

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.

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:

public void DoSearch()

{

  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:

  • Changing the mouse pointer to a different shape while over a building.
  • Showing a tooltip that calculates the real-world longitude and latitude coordinates.
  • Showing a message in a status bar that calculates the population of the area where the mouse is hovering.

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.

Defining a Custom Delegate

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:

  • Scope: An access modifier, such as public, private, or protected, to define the scope of the delegate. If you do not include a modifier, the compiler will default to marking it as private, but it is always better to be explicit in showing the intent of your code.
  • The delegate keyword.
  • Return type: If there is no return type, void is used.
  • Delegate name: This can be anything that you like, but the name must be unique within the namespace. Many naming conventions (including Microsoft's) suggest adding Handler or EventHandler to your delegate's name.
  • Arguments, if required.

    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.

Exercise 3.01: Defining and Invoking Custom Delegates

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:

  1. Create a new folder called Chapter03.
  2. Change to the Chapter03 folder and create a new console app, called Exercise01, using the CLI dotnet command, as follows:

    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.

  1. Open Chapter03Exercise01.csproj and replace the contents with these settings:

    <Project Sdk="Microsoft.NET.Sdk">

      <PropertyGroup>

        <OutputType>Exe</OutputType>

        <TargetFramework>net6.0</TargetFramework>

    </PropertyGroup>

    </Project>

  2. Open Exercise01Program.cs and clear the contents.
  3. The preference for using namespaces to prevent a clash with objects from other libraries was mentioned earlier, so to keep things isolated, use Chapter03.Exercise01 as the namespace.

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);

}

  1. Next, you will create a class named Order, which contains details of the order and can be passed to two date validation delegates:

       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.

  1. Now for the constructor, you pass the two delegates:

           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.

  1. You need to add the two date properties that are to be validated. These dates may be set using a UI that listens to keypresses and applies user edits directly to this class:

            public DateTime OrderDate { get; set; }

            public DateTime DeliveryDate { get; set; }

  2. Now add an IsValid method that passes OrderDate to the orderDateValidator delegate and DeliveryDate to the deliveryDateValidator delegate:

            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:

  1. To create a console app to test this, add a static class called Program:

        public static class Program

        {

  2. You want to create two functions that validate whether the date passed to them is valid. These functions will form the basis of your delegate target methods:

            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.

  1. Now for the Main entry point. Here, you create two DateValidationHandler delegate instances, passing IsPastDate to one and IsWeekendDate to the second. These are the target methods that will get called when each of the delegates is invoked:

            public static void Main()

            {

               var orderValidator = new DateValidationHandler(IsPastDate);

               var deliverValidator = new DateValidationHandler(IsWeekendDate);

  2. Now you can create an Order instance, passing in the delegates and setting the order and delivery dates:

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

  1. Now it's just a case of displaying the dates in the console and calling IsValid, which, in turn, will invoke each of your delegate methods once. Notice that a custom date format is used to make the dates more readable:

              Console.WriteLine($"Ordered: {order.OrderDate:dd-MMM-yy}");

              Console.WriteLine($"Delivered: {order.DeliveryDate:dd-MMM-yy }");

              Console.WriteLine($"IsValid: {order.IsValid()}");

            }

        }

    }

  2. Running the console app produces output like this:

    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.

The Inbuilt Action and Func Delegates

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.

Assigning Delegates

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.

Invoking 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:

  • Invoke with the null-coalescing operator:

    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.

  • Direct Invoke

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));

  • Without the Invoke prefix

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.

Exercise 3.02: Assigning and Invoking Delegates

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:

  1. Change to the Chapter03 folder and create a new console app, called Exercise02, using the CLI dotnet command:

    sourceChapter03>dotnet new console -o Exercise02

  2. Open Chapter03Exercise02.csproj and replace the entire file with these settings:

    <Project Sdk="Microsoft.NET.Sdk">

      <PropertyGroup>

        <OutputType>Exe</OutputType>

        <TargetFramework>net6.0</TargetFramework>

      </PropertyGroup>

    </Project>

  3. Open Exercise02Program.cs and clear the contents.
  4. Start by adding a record called Car. Include the System.Globalization namespace for string parsing. Use the Chapter03.Exercise02 namespace to keep code separate from the other exercises.
  5. Add two properties, Distance and JourneyTime. They will have init-only properties, so you will use the init keyword:

    using System;

    using System.Globalization;

    namespace Chapter03.Exercise02

    {

        public record Car

        {

            public double Distance { get; init; }

            public double JourneyTime { get; init; }

        }

  6. Next, create a class named Comparison that is passed a Func delegate to work with. The Comparison class will use the delegate to extract either the Distance or JourneyTime properties and calculate the difference for two Car instances. By using the flexibility of delegates, Comparison will not know whether it is extracting Distance or JourneyTime, just that it is using a double to calculate the differences. This shows that you can reuse this class should you need to calculate other Car properties in the future:

        public class Comparison

        {

            private readonly Func<Car, double> _valueSelector;

            public Comparison(Func<Car, double> valueSelector)

            {

                _valueSelector = valueSelector;

            }

  7. Add three properties that form the results of the calculation, as follows:

            public double Yesterday { get; private set; }

            public double Today { get; private set; }

            public double Difference { get; private set; }

  8. Now for the calculation, pass two Car instances, one for the car journey yesterday, yesterdayCar, and one for today, todayCar:

            public void Compare(Car yesterdayCar, Car todayCar)

            {

  9. To calculate a value for Yesterday, invoke the valueSelector Func delegate, passing in the yesterdayCar instance. Again, remember that the Comparison class is unaware whether it is extracting Distance or JourneyTime; it just needs to know that when the delegate is invoked with a Car argument, it will get a double number back:

                Yesterday = _valueSelector(yesterdayCar);

  10. Do the same to extract the value for Today by using the same Func delegate, but passing in the todayCar instance instead:

                Today = _valueSelector(todayCar);

  11. Now it is just a case of calculating the difference between the two extracted numbers; you don't need to use the Func delegate to do that:

                Difference = Yesterday - Today;

            }

         }

  12. So, you have a class that knows how to invoke a Func delegate to extract a certain Car property when it is told how to. Now, you need a class to wrap up the Comparison instances. For this, add a class called JourneyComparer:

        public class JourneyComparer

        {

            public JourneyComparer()

            {

  13. For the car journey, you need to calculate the difference between the Yesterday and Today Distance properties. To do so, create a Comparison class that is told how to extract a value from a Car instance. You may as well use the same name for this Comparison class as you will extract a car's Distance. Remember that the Comparison constructor needs a Func delegate that is passed a Car instance and returns a double value. You will add GetCarDistance() shortly; this will eventually be invoked by passing Car instances for yesterday's and today's journeys:

              Distance = new Comparison(GetCarDistance);

  14. Repeat the process as described in the preceding steps for a JourneyTime Comparison; this one should be told to use GetCarJourneyTime() as follows:

              JourneyTime = new Comparison(GetCarJourneyTime);

  15. Finally, add another Comparison property called AverageSpeed as follows. You will see shortly that GetCarAverageSpeed() is yet another function:

               AverageSpeed = new Comparison(GetCarAverageSpeed);

  16. Now for the GetCarDistance and GetCarJourneyTime local functions, they are passed a Car instance and return either Distance or JourneyTime accordingly:

               static double GetCarDistance(Car car) => car.Distance;

               static double GetCarJourneyTime(Car car) => car.JourneyTime;

  17. GetCarAverageSpeed, as the name suggests, returns the average speed. Here, you have shown that the Func delegate just needs a compatible function; it doesn't matter what it returns as long as it is double. The Comparison class does not need to know that it is returning a calculated value such as this when it invokes the Func delegate:

              static double GetCarAverageSpeed(Car car)             => car.Distance / car.JourneyTime;

           }

  18. The three Comparison properties should be defined like this:

            public Comparison Distance { get; }

            public Comparison JourneyTime { get; }

            public Comparison AverageSpeed { get; }

  19. Now for the main Compare method. This will be passed two Car instances, one for yesterday and one for today, and it simply calls Compare on the three Comparison items passing in the two Car instances:

            public void Compare(Car yesterday, Car today)

            {

                Distance.Compare(yesterday, today);

                JourneyTime.Compare(yesterday, today);

                AverageSpeed.Compare(yesterday, today);

            }

        }

  20. You need a console app to enter the miles traveled per day, so add a class called Program with a static Main entry point:

        public class Program

        {

            public static void Main()

            {

  21. You can randomly assign journey times to save some input, so add a new Random instance and the start of a do-while loop, as follows:

                var random = new Random();

                string input;

                do

                {

  22. Read for yesterday's distance, as follows:

                    Console.Write("Yesterday's distance: ");

                    input = Console.ReadLine();

                    double.TryParse(input, NumberStyles.Any,                    CultureInfo.CurrentCulture, out var distanceYesterday);

  23. You can use the distance to create yesterday's Car with a random JourneyTime, as follows:

                    var carYesterday = new Car

                    {

                        Distance = distanceYesterday,

                        JourneyTime = random.NextDouble() * 10D

                    };

  24. Do the same for today's distance:

                    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

                    };

  25. Now that you have two Car instances populated with values for yesterday and today, you can create the JourneyComparer instance and call Compare. This will then call Compare on your three Comparison instances:

                    var comparer = new JourneyComparer();

                    comparer.Compare(carYesterday, carToday);

  26. Now, write the results to the console:

                    Console.WriteLine();

                    Console.WriteLine("Journey Details   Distance Time Avg Speed");

                    Console.WriteLine("-------------------------------------------------");

  27. Write out yesterday's results:

                    Console.Write($"Yesterday         {comparer.Distance.Yesterday:N0}    ");

                    Console.WriteLine($"{comparer.JourneyTime.Yesterday:N0} {comparer.AverageSpeed.Yesterday:N0}");

  28. Write out today's results:

                    Console.Write($"Today             {comparer.Distance.Today:N0}     ");                 Console.WriteLine($"{comparer.JourneyTime.Today:N0} {comparer.AverageSpeed.Today:N0}");

  29. Finally, write the summary values using the Difference properties:

                    Console.WriteLine("=================================================");

                    Console.Write($"Difference             {comparer.Distance.Difference:N0}     ");                Console.WriteLine($"{comparer.JourneyTime.Difference:N0} {comparer.AverageSpeed.Difference:N0}");

                   Console.WriteLine("=================================================");

  30. Finish off the do-while loop, exiting if the user enters an empty string:

                }

                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.

Multicast Delegates

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.

Exercise 3.03: Invoking a Multicast Delegate

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:

  1. Change to the Chapter03 folder and create a new console app, called Exercise03, using the CLI dotnet command:

    sourceChapter03>dotnet new console -o Exercise03

  2. Open Chapter03Exercise03.csproj and replace the entire file with these settings:

    <Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>

        <OutputType>Exe</OutputType>

        <TargetFramework>net6.0</TargetFramework>

    </PropertyGroup>

    </Project>

  3. Open Exercise03Program.cs and clear the contents.
  4. Add a new class called CashMachine.
  5. Use the Chapter03.Exercise03 namespace:

    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.

  1. Add a Log helper function that checks whether the _logger delegate is null before invoking:

            private void Log(string message)

                => _logger?.Invoke(message);

  2. When the VerifyPin and ShowBalance methods are called, a message should be logged with some details. Create these methods as follows:

            public void VerifyPin(string pin)

                => Log($"VerifyPin called: PIN={pin}");

            public void ShowBalance()

                => Log("ShowBalance called: Balance=999");

        }

  3. Now, add a console app that configures a logger delegate that you can pass into a CashMachine object. Note that this is a common form of usage: a class that is responsible for deciding how messages are logged by other classes. Use a constant, OutputFile, for the filename to be used for file logging, as follows:

        public static class Program

        {

            private const string OutputFile = "activity.txt";

            public static void Main()

            {

  4. Each time the program runs, it should start with a clean text file for logging, so use File.Delete to delete the output file:

                if (File.Exists(OutputFile))

                {

                    File.Delete(OutputFile);

                }

  5. Create a delegate instance, logger, that starts with a single target method, LogToConsole:

                Action<string> logger = LogToConsole;

  6. Using the += operator, add LogToFile as a second target method to also be called whenever the delegate is invoked by CashMachine:

                logger += LogToFile;

  7. You will implement the two target logging methods shortly; for now, create a cashMachine instance and get ready to call its methods, as follows:

                var cashMachine = new CashMachine(logger);

  8. Prompt for a pin and pass it to the VerifyPin method:

                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.

  1. Wait for the Enter key to be pressed before calling the ShowBalance method:

                Console.Write("Press Enter to show balance");

                Console.ReadLine();

                cashMachine.ShowBalance();

                Console.Write("Press Enter to quit");

                Console.ReadLine();

  2. Now for the logging methods. They must be compatible with your Action<string> delegate. One writes a message to the console and the other appends it to the text file. Add these two static methods as follows:

                static void LogToConsole(string message)

                    => Console.WriteLine(message);

                static void LogToFile(string message)

                    => File.AppendAllText(OutputFile, message);

            }

         }

    }

  3. Running the console app, you see that VerifyPin and ShowBalance calls are written to the console:

    Enter your PIN:12345

    VerifyPin called: PIN=12345

    Press Enter to show balance

    ShowBalance called: Balance=999

  4. For each logger delegate invocation, the LogToFile method will also be called, so when opening activity.txt, you should see the following line:

    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()}");

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

Multicasting with a Func Delegate

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.

What Happens When Things Go Wrong?

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");

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.

Exercise 3.04: Ensuring All Target Methods Are Invoked in a Multicast Delegate

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:

  1. Change to the Chapter03 folder and create a new console app, called Exercise04, using the CLI dotnet command:

    sourceChapter03>dotnet new console -o Exercise04

  2. Open Chapter03Exercise04.csproj and replace the entire file with these settings:

    <Project Sdk="Microsoft.NET.Sdk">

      <PropertyGroup>

        <OutputType>Exe</OutputType>

        <TargetFramework>net6.0</TargetFramework>

      </PropertyGroup>

    </Project>

  3. Open Exercise04Program.cs and clear the contents.
  4. Now add a static Program class for your console app, including System and, additionally, System.IO as you want to create a file:

    using System;

    using System.IO;

    namespace Chapter03.Exercise04

    {

        public static class Program

        {

  5. Use a const to name the logging file. This file is created when the program executes:

            private const string OutputFile = "Exercise04.txt";

  6. Now you must define the app's Main entry point. Here you delete the output file if it already exists. It is best to start with an empty file here, as otherwise, the log file will keep growing every time you run the app:

            public static void Main()

            {

                if (File.Exists(OutputFile))

                {

                    File.Delete(OutputFile);

                }

  7. You will start with logger having just one target method, LogToConsole, which you will add shortly:

                Action<string> logger = LogToConsole;

  8. You use the InvokeAll method to invoke the delegate, passing in "First call" as an argument. This will not fail as logger has a single valid method and you will add InvokeAll shortly, too:

                InvokeAll(logger, "First call");

  9. The aim of this exercise is to have a multicast delegate, so add some additional target methods:

                logger += LogToConsole;

                logger += LogToDatabase;

                logger += LogToFile;

  10. Try a second call using InvokeAll as follows:

                InvokeAll(logger, "Second call");

                Console.ReadLine();

  11. Now for the target methods that were added to the delegate. Add the following code for this:

                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);

            

  12. You can now implement the InvokeAll method:

                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.

  1. Use the delegate's GetInvocationList() method to get a list of all the target methods:

                    var delegateList = logger.GetInvocationList();

                    Console.WriteLine($"Found {delegateList.Length} items in {logger}");

  2. Now, loop through each item in the list as follows:

                    foreach (var del in delegateList)

                    {

  3. After wrapping each loop element in a try/catch, cast del into an Action<string>:

                       try

                       {

                         var action = del as Action<string>;

GetInvocationList returns each item as the base delegate type regardless of their actual type.

  1. If it is the correct type and not null, then it is safe to try invoking:

                          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.

  1. Finish with a catch block that logs the error message if an error was caught:

                      }

                      catch (Exception e)

                      {

                          Console.WriteLine($"Error: {e.Message}");

                      }

                    }

                }

            }

        }

    }

  2. Running the code, creates a file called Exercise04.txt with the following results:

    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.

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.

Defining an Event

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:

  • Scope: An access modifier, such as public, private, or protected, to define the scope.
  • The event keyword.
  • Delegate type: The associated delegate, EventHandler in this example.
  • Event name: This can be anything you like, MouseDoubleClicked, for example. However, the name must be unique within the namespace.

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?

  • EventHandler: This can be used when there is no extra information to describe the event. For example, a checkbox click event may not need any extra information, it was simply clicked. In this case, it is perfectly valid to pass null or EventArgs.Empty as the second parameter. This delegate can often be found in legacy apps that use a class derived from EventArgs to describe the event further. Was it a double-click of the mouse that triggered this event? In this case, a Clicks property may have been added to an EventArgs derived class to provide such extra details.
  • EventHandler<T>: Since the inclusion of generics in C#, this has become the more frequently used delegate for events, simply because using generics requires fewer classes to be created.

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:

  1. Define a class, MouseClickedEventArgs, that contains additional information about the event, in this case, the number of mouse clicks that were detected:

    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.

  1. Now add the delegate { }; block to prevent MouseClicked from being null initially:

        public class MouseClickPublisher

        {

         public event EventHandler<MouseClickedEventArgs> MouseClicked = delegate { };

  2. Add an OnMouseClicked virtual method that gives any further subclassed MouseClickPublisher classes a chance to suppress or change the event notification, as follows:

            protected virtual void OnMouseClicked( MouseClickedEventArgs e)

            {

                var evt = MouseClicked;

                evt?.Invoke(this, e);

            }

  3. Now you need a method that tracks the mouse clicks. In this example, you will not actually show how mouse clicks are detected, but you will call OnMouseClicked, passing in 2 to indicate a double-click.
  4. Notice how you have not invoked the MouseClicked event directly; you always go via the OnMouseClicked intermediary method. This provides a way for other implementations of MouseClickPublisher to override the event notification if they need to:

            private void TrackMouseClicks()

            {

                OnMouseClicked(new MouseClickedEventArgs(2));

            }

        }

  5. Now add a new type of publisher that is based on MouseClickPublisher:

        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.

Exercise 3.05: Publishing and Subscribing to Events

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:

  1. Change to the Chapter03 folder and create a new console app, called Exercise05, using the CLI dotnet command:

    dotnet new console -o Exercise05

  2. Open Chapter03Exercise05.csproj and replace the entire file with these settings:

    <Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>

        <OutputType>Exe</OutputType>

        <TargetFramework>net6.0</TargetFramework>

      </PropertyGroup>

    </Project>

  3. Open Exercise05Program.cs and clear the contents.
  4. Add a new class called AlarmClock. Here you need to use a DateTime class, so include the System namespace:

    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.

  1. You will use this to pass along the current time to display in the console. Notice that both have the initial delegate {}; safety mechanism:

            public event EventHandler WakeUp = delegate {};

            public event EventHandler<DateTime> Ticked = delegate {};

  2. Include an OnWakeUp override as an example, but do not do the same with Ticked; this is to show the different invocation approaches:

            protected void OnWakeUp()

            {

                WakeUp.Invoke(this, EventArgs.Empty);

            }

  3. Now add two DateTime properties, the alarm and clock times, as follows:

            public DateTime AlarmTime { get; set; }

            public DateTime ClockTime { get; set; }

  4. A Start method is used to start the clock. You simulate a clock ticking once every minute for 24 hours using a simple loop as follows:

            public void Start()

            {

                // Run for 24 hours

                const int MinutesInADay = 60 * 24;

  5. For each simulated minute, increment the clock using DateTime.AddMinute and publish the Ticked event, passing in this (the AlarmClock sender instance) and the clock time:

                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.

  1. You pass the timeRemaining value to the local function, IsTimeToWakeUp, calling the OnWakeUp method and break out of the loop if it is time to wake up:

                  var timeRemaining = ClockTime                 .Subtract(AlarmTime)                .TotalMinutes;

                   if (IsTimeToWakeUp(timeRemaining))

                    {

                        OnWakeUp();

                        break;

                    }

                }

  2. Use the IsTimeToWakeUp, a relational pattern, to see whether there is less than one minute remaining. Add the following code for this:

                static bool IsTimeToWakeUp(double timeRemaining)

                    => timeRemaining is (>= -1.0 and <= 1.0);

            }

        }   

  3. Now add a console app that subscribes to the alarm clock and its two events by starting from the static void Main entry point:

             public static class Program

        {

            public static void Main()

            {

  4. Create the AlarmClock instance and use the += operator to subscribe to the Ticked event and the WakeUp events. You will define ClockTicked and ClockWakeUp shortly. For now, just add the following code:

                var clock = new AlarmClock();

                clock.Ticked += ClockTicked;

                clock.WakeUp += ClockWakeUp;

  5. Set the clock's current time, use DateTime.AddMinutes to add 120 minutes to the alarm time, and then start the clock, as follows:

                clock.ClockTime = DateTime.Now;

                clock.AlarmTime = DateTime.Now.AddMinutes(120);

                Console.WriteLine($"ClockTime={clock.ClockTime:t}");

                Console.WriteLine($"AlarmTime={clock.AlarmTime:t}");

                clock.Start();

  6. Finish off Main by prompting for the Enter key to be pressed:

                Console.WriteLine("Press ENTER");

                Console.ReadLine();

            

  7. Now you can add the event subscriber local methods:

                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.

  1. ClockTicked is passed the DateTime argument as required by the EventHandler<DateTime> delegate. Here, you pass the current time, so you write that to the console using :t to show the time in a short format:

                 static void ClockTicked(object sender, DateTime e)

                    => Console.Write($"{e:t}...");

            }

        }

    }

  2. Running the app produces this output:

    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.

Events or Delegates?

On the face of it, events and delegates look remarkably similar:

  • Events are an extended form of delegates.
  • Both offer late-bound semantics, so rather than calling methods that are known precisely at compile-time, you can defer a list of target methods when known at runtime.
  • Both are called using Invoke() or, more simply, the () suffix shortcut, ideally with a null check before doing so.

The key considerations are as follows:

  • Optionality: Events offer an optional approach; callers can decide to opt into events or not. If your component can complete its task without needing any subscriber methods, then it is preferable to use an event-based approach.
  • Return types: Do you need to handle return types? Delegates associated with events are always void.
  • Lifetime: Event subscribers typically have a shorter lifetime than their publishers, leaving the publisher to continue detecting new messages even if there are no active subscribers.

Static Events Can Cause Memory Leaks

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.

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:

var clock = new AlarmClock();

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();

    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.

Exercise 3.06: Using a Statement Lambda to Reverse Words in a Sentence

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:

  1. Change to the Chapter03 folder and create a new console app, called Exercise06, using the CLI dotnet command:

    sourceChapter03>dotnet new console -o Exercise06

  2. Open Chapter03Exercise06.csproj and replace the entire file with these settings:

    <Project Sdk="Microsoft.NET.Sdk">

      <PropertyGroup>

        <OutputType>Exe</OutputType>

        <TargetFramework>net6.0</TargetFramework>

      </PropertyGroup>

    </Project>

  3. Open Exercise02Program.cs and clear the contents.
  4. Add a new class named WordUtilities with a string function called ReverseWords. You need to include the System.Linq namespace to help with the string operations:

    using System;

    using System.Linq;

    namespace Chapter03.Exercise06

    {

        public static class WordUtilities

        {

            public static string ReverseWords(string sentence)

            {

  5. Define a Func<string, string> delegate called swapWords that takes a string input and returns a string value:

              Func<string, string> swapWords =

  6. You will accept a string input argument named phrase:

                phrase =>

  7. Now for the lambda statement body. Use the string.Split function to split the phrase string into an array of strings using a space as the splitting character:

                      {

                        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.

  1. You have defined the required Func, so invoke it by passing the sentence parameter and returning that as the result:

                return swapWords(sentence);

             }

        }

  2. Now for a console app that prompts for a sentence to be entered, which is passed to WordUtilities.ReverseWords, with the result being written to the console:

        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.

Captures and Closures

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.

Activity 3.01: Creating a Web File Downloader

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:

  1. Add a progress changed EventArgs class (an example name could be DownloadProgressChangedEventArgs) that can be used when publishing progress events. This should have ProgressPercentage and BytesReceived properties.
  2. The WebClient class from System.Net should be used to download a requested web file. You should create an adapter class (a suggested name is WebClientAdapter) that hides your internal usage of WebClient from your callers.
  3. Your adapter class should provide three events—DownloadCompleted, DownloadProgressChanged, and InvalidUrlRequested—that a caller can subscribe to.
  4. The adapter class will need a DownloadFile method that calls the WebClient class's DownloadFileAsync method to start the download request. This requires converting a string-based web address into a Uniform Resource Identifier (URI) class. The Uri.TryCreate() method can create an absolute address from the string entered via the console. If the call to Uri.TryCreate fails, you should publish the InvalidUrlRequested event to indicate this failure.
  5. WebClient has two events—DownloadFileCompleted and DownloadProgressChanged. You should subscribe to these two events and republish them using your own similar events.
  6. Create a console app that uses an instance of WebClientAdapter (as created in Step 2) and subscribe to the three events.
  7. By subscribing to the DownloadCompleted event, you should indicate success in the console.
  8. By subscribing to DownloadProgressChanged, you should report progress messages to the console showing the ProgressPercentage and BytesReceived values.
  9. By subscribing to the InvalidUrlRequested event, you should show a warning on the console using a different console background color.
  10. Use a do loop that allows the user to repeatedly enter a web address. This address and a temporary destination file path can be passed to WebClientAdapter.DownloadFile() until the user enters a blank address to quit.
  11. Once you run the console app with various download requests, you should see an output similar to the following:

    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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.85.175