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.
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:
The results, expressed in a simple Gaussian curve, look like this:
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:
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:
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.
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 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:
3.139.97.53