The true reason for delegates

It so happens that besides these architectural considerations that we've mentioned, there was another reason that was key to the design: ensuring that a .NET program would never produce a BSOD (Blue Screen of Death).

So, the team tackled the problem scientifically and made a statistical analysis of their causes (more than 70,000 of these screens were used in the analysis). It turned out that around 90% of the causes for this problem were due to drivers, and the only thing they could do was get serious with manufacturers, asking them to pass the Hardware Compatibility List (HCL) and little else.

Note

The current HCL page for Windows can be found at https://sysdev.microsoft.com/en-us/hardware/lpl/.

So, they had a remaining 10% problem due to their own software, but the big surprise was that instead of finding five or 10 core causes for these failures, the problem focused mainly on just two reasons:

  • Pointer to functions that get lost, which I represent in the graphic by p* -> f(x)
  • Casting problems (trying to convert types passed to a function; failing could drive to unpredictable results)

The results, expressed in a simple Gaussian curve, look like this:

The true reason for delegates

So, covering these two issues, more than 95% (or more) of the problems, were solved. The first goal was achieved: focusing on the problem and reducing it to the maximum.

At this point, he had to find a solution that could possibly resolve both issues. This is where the genius of this Danish man came in. He thought back to the origins of the two problems and realized that both cases were related to method calls. Given a twist and a return to rethink the foundations of General Information Theory in order to identify the specific problem within the theoretical model (the first pages of any book on the subject), we would find something like what is shown in this figure:

The true reason for delegates

But, wait! ...this is also the core architecture of the event system! So, there is a correspondence between the two schemas in the four elements implied:

  • Issuer: It is the method that makes the call
  • Receiver: Another class (or the same) responding in another method
  • Channel: It is the environment, replaced by a managed environment in .NET
  • Message: The information sent to the receiver

Now, the second problem is solved: the model of the target is identified as a case of the general pattern of information theory as well as its parts: the channel and the information expected to be received.

What was missing? What has always been done in computer science to solve problems of direct calls? That would be calling in an intermediary. Or if you prefer otherwise, applying the fifth principle of the SOLID design: Dependency Inversion.

Note

We'll talk in more detail about dependency inversion when we cover Design patterns in Chapter 10, Design Patterns, but for now, suffice to say what the principle states (in short): modules should not depend on low-level modules or on details but on abstractions.

This is where the factor responsible for this solution comes in: the delegate. Calls are never made directly but always through the delegate, which is administered by CLR and will not attempt to call something that is not available (it's managed, remember). The function pointer problem is solved via the channel (and the elimination of function pointers, of course).

If you take a look at the official (Wikipedia's) article explaining this principle (https://en.wikipedia.org/wiki/Dependency_inversion_principle), you'll discover that the recommended solution of the pattern is to change from scenario 1 (at the left-hand side of the figure) to scenario 2 (at the right-hand side), in which it is proposed that the method that is called (in object B) inherits (implements) an interface to make sure that the call is realized with no risks:

The true reason for delegates

The solution to the second cause, as Hejlsberg said, seemed trivial, once turned above. He just had to make the delegate's signature equal to the receiving method (remember, the same types of parameters and return value), and bid goodbye to the problems of casting, since CLR is strongly typed and the compiler (and even the IDE) will mark any violation of this principle, indicating that it won't compile.

This architecture avoids the problems of BSOD originated by these causes. Can we look at this structure in code? Sure. Actually, I'm pretty sure you have seen it often, only not from this point of view maybe.

Let's go back a second to the previous case with our Reflected window. And let's identify the protagonists. The emitter is clearly the bntLaunch button member, and the receiver is the previous code:

void btnLaunch_Click(object sender, RoutedEventArgs e)

So, when we see the click's event handler method's definition, we also see two of the members of scenario 2: the sender (the emitter) and the information passed in (an instance of the RoutedEventArgs class).

Remember, a delegate in charge of the call should have the same signature as that in this method. Just right-click on the name of the method, Search all references, and you'll find out where the connection between the method and the delegate is established (the usual syntax):

this.btnLaunch.Click += new System.Windows.RoutedEventHandler(this.btnLaunch_Click);

So, the click member of btnLaunch is connected to the btnLaunch_Click method by means of a new instance of a delegate of type RoutedEventHandler. Once again, right-click on RoutedEventHandler and select Go to definition in order to take a look at the delegate's signature:

public delegate void RoutedEventHandler(object sender, RoutedEventArgs e);

Voilà, the signature is exactly the same as the receiver. No more casting problems, and if the CLR does its work, no calls will be made unless the receiver method is not accessible. This is because only a kernel-level component can cause a BSOD, never a user mode component.

So, a delegate is a very special class that can be declared outside or inside any other class and has the ability to target any method as long as their signatures are compatible. The += syntax also tell us something important: they are multicast. That is, they can target more than one method in a single call.

Let's place this in a scenario where we need to evaluate which numbers are divisible by another sequence of numbers. To put it simply, let's start with two methods, checking the divisibility by 2 and 3, respectively:

static List<int> numberList;
static List<int> divisibleNumbers = new List<int>();
private void CheckMod2(int x)
{
  if (x % 2 == 0) divisibleNumbers.Add(x);
}
private void CheckMod3(int x)
{
  if (x % 3 == 0) divisibleNumbers.Add(x);
}

Now, we want to evaluate the list and fill a Listbox including those numbers that comply with the rules:

delegate void DivisibleBy(int number);
private void ClassicDelegateMethod()
{
  DivisibleBy ed = new DivisibleBy(CheckMod2);
  // Invocation of several methods (Multicasting)
  ed += CheckMod3;
  // Every call to ed generates a multicast sequence
  foreach (int x in numberList) { ed(x); }
}

We declare a delegate, DivisibleBy, which receives a number and performs an action (later, we'll find that this is renamed to Action). So, the same delegate can call both methods in a sequence (note that this can make the sequence pretty long).

The delegate is invoked using another button that will call the following code when clicked:

// Generic delegate way
numberList = Enumerable.Range(1, 100).ToList();
ClassicDelegateMethod();
PrintResults("Numbers divisible by 2 and 3");

Here, we don't include the implementation of PrintResults, which you can imagine and which is also included in the Chapter02_02 demo. The following result is expected:

The true reason for delegates
..................Content has been hidden....................

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