Home Random Page


CATEGORIES:

BiologyChemistryConstructionCultureEcologyEconomyElectronicsFinanceGeographyHistoryInformaticsLawMathematicsMechanicsMedicineOtherPedagogyPhilosophyPhysicsPolicyPsychologySociologySportTourism






Nbsp;   Demystifying Delegates

On the surface, delegates seem easy to use: you define them by using C#’s delegate keyword, you construct instances of them by using the familiar new operator, and you invoke the callback by using the familiar method-call syntax (except instead of a method name, you use the variable that refers to the delegate object).

However, what’s really going on is quite a bit more complex than what the earlier examples illus- trate. The compilers and the CLR do a lot of behind-the-scenes processing to hide the complexity. In this section, I’ll focus on how the compiler and the CLR work together to implement delegates. Having this knowledge will improve your understanding of delegates and will teach you how to use them ef- ficiently and effectively. I’ll also touch on some additional features delegates make available.

Let’s start by reexamining this line of code.

 

internal delegate void Feedback(Int32 value);

 

When the compiler sees this line, it actually defines a complete class that looks something like this.

 

internal class Feedback : System.MulticastDelegate {

// Constructor

public Feedback(Object @object, IntPtr method);

 

// Method with same prototype as specified by the source code public virtual void Invoke(Int32 value);

 

// Methods allowing the callback to be called asynchronously public virtual IAsyncResult BeginInvoke(Int32 value,

AsyncCallback callback, Object @object);

public virtual void EndInvoke(IAsyncResult result);

}

 

The class defined by the compiler has four methods: a constructor, Invoke, BeginInvoke, and EndInvoke. In this chapter, I’ll concentrate on the constructor and Invoke methods. The Begin­ Invoke and EndInvoke methods are related to the .NET Framework's Asynchronous Programming Model which is now considered obsolete and has been replaced by tasks that I discuss in Chapter 27, “Compute-Bound Asynchronous Operations.”


In fact, you can verify that the compiler did indeed generate this class automatically by examining the resulting assembly with ILDasm.exe, as shown in Figure 17-1.

 
 

FIGURE 17-1ILDasm.exe showing the metadata produced by the compiler for the delegate.

 

In this example, the compiler has defined a class called Feedback that is derived from the System.MulticastDelegate type defined in the Framework Class Library (FCL). (All delegate types are derived from MulticastDelegate.)

       
   
 
 

 

The class has private visibility because the delegate is declared as internal in the source code. If the source code had indicated public visibility, the Feedback class the compiler generated would also be public. You should be aware that delegate types can be defined within a type (nested within another type) or at global scope. Basically, because delegates are classes, a delegate can be defined anywhere a class can be defined.

Because all delegate types are derived from MulticastDelegate, they inherit Multicast­ Delegate’s fields, properties, and methods. Of all of these members, three non-public fields are probably most significant. Table 17-1 describes these significant fields.




TABLE 17-1MulticastDelegate’s Significant Non-Public Fields

 

Field Type Description
_target System.Object When the delegate object wraps a static method, this field is null. When the delegate objects wraps an instance method, this field refers to the object that should be operated on when the callback method is called. In other words, this field indicates the value that should be passed for the instance method’s implicit this parameter.
_methodPtr System.IntPtr An internal integer the CLR uses to identify the method that is to be called back.
_invocationList System.Object This field is usually null. It can refer to an array of delegates when building a delegate chain (discussed later in this chapter).

 

Notice that all delegates have a constructor that takes two parameters: a reference to an object and an integer that refers to the callback method. However, if you examine the source code, you’ll see that I’m passing in values such as Program.FeedbackToConsole or p.FeedbackToFile. Everything you’ve learned about programming tells you that this code shouldn’t compile!

However, the C# compiler knows that a delegate is being constructed and parses the source code to determine which object and method are being referred to. A reference to the object is passed for the constructor’s object parameter, and a special IntPtr value (obtained from a MethodDef or MemberRef metadata token) that identifies the method is passed for the method parameter. For static methods, null is passed for the object parameter. Inside the constructor, these two arguments are saved in the _target and _methodPtr private fields, respectively. In addition, the constructor sets the _invocationList field to null. I’ll postpone discussing this _invocationList field until the next section, “Using Delegates to Call Back Many Methods (Chaining).”

So each delegate object is really a wrapper around a method and an object to be operated on when the method is called. So if I have two lines of code that look like this:

 

Feedback fbStatic = new Feedback(Program.FeedbackToConsole); Feedback fbInstance = new Feedback(new Program().FeedbackToFile);

 

the fbStatic and fbInstance variables refer to two separate Feedback delegate objects that are initialized, as shown in Figure 17-2.

 
 

 

FIGURE 17-2A variable that refers to a delegate to a static method and a variable that refers to a delegate to an instance method.


Now that you know how delegate objects are constructed and what their internal structure looks like, let’s talk about how the callback method is invoked. For convenience, I’ve repeated the code for the Counter method here.

 

private static void Counter(Int32 from, Int32 to, Feedback fb) { for (Int32 val = from; val <= to; val++) {

// If any callbacks are specified, call them if (fb != null)

fb(val);

}

}

 

Look at the line of code just below the comment. The if statement first checks to see if fb is not null. If fb is not null, on the next line, you see the code that invokes the callback method. The null check is required because fb is really just a variable that can refer to a Feedback delegate object; it could also be null. It might seem as if I’m calling a function named fb and passing it one parameter (val). However, there is no function called fb. Again, because it knows that fb is a variable that refers to a delegate object, the compiler generates code to call the delegate object’s Invoke method. In other words, the compiler sees the following.

 

fb(val);

 

But the compiler generates code as though the source code said the following.

 

fb.Invoke(val);

 

You can verify that the compiler produces code to call the delegate type’s Invoke method by using ILDasm.exe to examine the Intermediate Language (IL) code created for the Counter method. Here is the IL for the Counter method. The instruction at IL_0009 in the figure indicates the call to Feedback’s Invoke method.

 

.method private hidebysig static void Counter(int32 from,

int32 'to',

class Feedback fb) cil managed

{

// Code size 23 (0x17)

.maxstack 2

.locals init (int32 val) IL_0000: ldarg.0 IL_0001: stloc.0

IL_0002: br.s IL_0012

IL_0004: ldarg.2

IL_0005: brfalse.s IL_000e IL_0007: ldarg.2

IL_0008: ldloc.0

IL_0009: callvirt instance void Feedback::Invoke(int32) IL_000e: ldloc.0

IL_000f: ldc.i4.1 IL_0010: add IL_0011: stloc.0 IL_0012: ldloc.0 IL_0013: ldarg.1

IL_0014: ble.s IL_0004 IL_0016: ret

} // end of method Program::Counter


In fact, you could modify the Counter method to call Invoke explicitly, as shown here.

 

private static void Counter(Int32 from, Int32 to, Feedback fb) { for (Int32 val = from; val <= to; val++) {

// If any callbacks are specified, call them if (fb != null)

fb.Invoke(val);

}

}

 

You’ll recall that the compiler defined the Invoke method when it defined the Feedback class.

When Invoke is called, it uses the private _target and _methodPtr fields to call the desired method on the specified object. Note that the signature of the Invoke method matches the signature of the delegate; because the Feedback delegate takes one Int32 parameter and returns void, the Invoke method (as produced by the compiler) takes one Int32 parameter and returns void.

 

 

 
 

Using Delegates to Call Back Many Methods (Chaining)

By themselves, delegates are incredibly useful. But add in their support for chaining, and delegates become even more useful. Chaining is a set or collection of delegate objects, and it provides the ability to invoke, or call, all of the methods represented by the delegates in the set. To understand this, see the ChainDelegateDemo1 method that appears in the code shown at the beginning of this chapter. In this method, after the Console.WriteLine statement, I construct three delegate objects and have variables—fb1, fb2, and fb3—refer to each object, as shown in Figure 17-3.

 
 

 

 

FIGURE 17-3Initial state of the delegate objects referred to by the fb1, fb2, and fb3 variables.

 

The reference variable to a Feedback delegate object, fbChain, is intended to refer to a chain or set of delegate objects that wrap methods that can be called back. Initializing fbChain to null indicates that there currently are no methods to be called back. The Delegate class’s public, static Combine method is used to add a delegate to the chain.

 

fbChain = (Feedback) Delegate.Combine(fbChain, fb1);


When this line of code executes, the Combine method sees that we are trying to combine null and fb1. Internally, Combine will simply return the value in fb1, and the fbChain variable will be set to refer to the same delegate object referred to by the fb1 variable, as shown in Figure 17-4.

 
 

 

 

FIGURE 17-4State of the delegate objects after inserting the first delegate in the chain.

 

To add another delegate to the chain, the Combine method is called again.

 

fbChain = (Feedback) Delegate.Combine(fbChain, fb2);

 

Internally, the Combine method sees that fbChain already refers to a delegate object, so Combine

will construct a new delegate object. This new delegate object initializes its private _target and

_methodPtr fields to values that are not important for this discussion. However, what is important is that the _invocationList field is initialized to refer to an array of delegate objects. The first element of this array (index 0) will be initialized to refer to the delegate that wraps the FeedbackToConsole method (this is the delegate that fbChain currently refers to). The second element of the array (index

1) will be initialized to refer to the delegate that wraps the FeedbackToMsgBox method (this is the delegate that fb2 refers to). Finally, fbChain will be set to refer to the newly created delegate object, shown in Figure 17-5.

To add the third delegate to the chain, the Combine method is called once again.

 

fbChain = (Feedback) Delegate.Combine(fbChain, fb3);

 

Again, Combine sees that fbChain already refers to a delegate object, and this causes a new delegate object to be constructed, as shown in Figure 17-6. As before, this new delegate object initializes the private _target and _methodPtr fields to values unimportant to this discussion, and the _invocationList field is initialized to refer to an array of delegate objects. The first and second elements of this array (indexes 0 and 1) will be initialized to refer to the same delegates the previous delegate object referred to in its array. The third element of the array (index 2) will be initialized to refer to the delegate that wraps the FeedbackToFile method (this is the delegate that fb3 refers to). Finally, fbChain will be set to refer to this newly created delegate object. Note that the previously cre- ated delegate and the array referred to by its _invocationList field are now candidates for garbage collection.


 
 

FIGURE 17-5State of the delegate objects after inserting the second delegate in the chain.

 

 
 

FIGURE 17-6Final state of the delegate objects when the chain is complete.


After all of the code has executed to set up the chain, the fbChain variable is then passed to the

Counter method.

 

Counter(1, 2, fbChain);

 

Inside the Counter method is the code that implicitly calls the Invoke method on the Feedback delegate object as I detailed earlier. When Invoke is called on the delegate referred to by fbChain, the delegate sees that the private _invocationList field is not null, causing it to execute a loop that iterates through all of the elements in the array, calling the method wrapped by each delegate. In this example, FeedbackToConsole will get called first, followed by FeedbackToMsgBox, followed by FeedbackToFile.

Feedback’s Invoke method is essentially implemented something like the following (in pseudo- code).

 

public void Invoke(Int32 value) {

Delegate[] delegateSet = _invocationList as Delegate[]; if (delegateSet != null) {

// This delegate's array indicates the delegates that should be called foreach (Feedback d in delegateSet)

d(value); // Call each delegate

} else {

// This delegate identifies a single method to be called back

// Call the callback method on the specified target object.

_methodPtr.Invoke(_target, value);

// The preceding line is an approximation of the actual code.

// What really happens cannot be expressed in C#.

}

}

 

Note that it is also possible to remove a delegate from a chain by calling Delegate’s public, static

Remove method. This is demonstrated toward the end of the ChainDelegateDemo1 method.

 

fbChain = (Feedback) Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));

 

When Remove is called, it scans the delegate array (from the end toward index 0) maintained in- side the delegate object referred to by the first parameter (fbChain, in my example). Remove is look- ing for a delegate entry whose _target and _methodPtr fields match those in the second argument (the new Feedback delegate, in my example). If a match is found and there is only one item left in the array, that array item is returned. If a match is found and there are multiple items left in the array, a new delegate object is constructed—the _invocationList array created and initialized will refer to all items in the original array except for the item being removed, of course—and a reference to this new delegate object is returned. If you are removing the only element in the chain, Remove returns null. Note that each call to Remove removes just one delegate from the chain; it does not remove all delegates that have matching _target and _methodPtr fields.

So far, I’ve shown examples in which my delegate type, Feedback, is defined as having a void

return value. However, I could have defined my Feedback delegate as follows.

 

public delegate Int32 Feedback(Int32 value);


If I had, its Invoke method would have internally looked like the following (again, in pseudocode).

 

public Int32 Invoke(Int32 value) { Int32 result;

Delegate[] delegateSet = _invocationList as Delegate[]; if (delegateSet != null) {

// This delegate's array indicates the delegates that should be called foreach (Feedback d in delegateSet)

result = d(value); // Call each delegate

} else {

// This delegate identifies a single method to be called back

// Call the callback method on the specified target object. result = _methodPtr.Invoke(_target, value);

// The preceding line is an approximation of the actual code.

// What really happens cannot be expressed in C#.

}

return result;

}

 

As each delegate in the array is called, its return value is saved in the result variable. When the loop is complete, the result variable will contain only the result of the last delegate called (previous return values are discarded); this value is returned to the code that called Invoke.

 

C#’s Support for Delegate Chains

To make things easier for C# developers, the C# compiler automatically provides overloads of the

+= and ­= operators for instances of delegate types. These operators call Delegate.Combine and Delegate.Remove, respectively. Using these operators simplifies the building of delegate chains. The ChainDelegateDemo1 and ChainDelegateDemo2 methods in the source code shown at the begin- ning of this chapter produce absolutely identical IL code. The only difference between the methods

is that the ChainDelegateDemo2 method simplifies the source code by taking advantage of C#’s +=

and -= operators.

 

If you require proof that the resulting IL code is identical for the two methods, you can build the code and look at its IL for both methods by using ILDasm.exe. This will confirm that the C# compiler did in fact replace all += and -= operators with calls to the Delegate type’s public static Combine and Remove methods, respectively.

 

Having More Control over Delegate Chain Invocation

At this point, you understand how to build a chain of delegate objects and how to invoke all of the objects in that chain. All items in the chain are invoked because the delegate type’s Invoke method includes code to iterate through all of the items in the array, invoking each item. This is obviously a very simple algorithm. And although this simple algorithm is good enough for a lot of scenarios, it has many limitations. For example, the return values of the callback methods are all discarded except for the last one. Using this simple algorithm, there’s no way to get the return values for all of the call- back methods called. But this isn’t the only limitation. What happens if one of the invoked delegates throws an exception or blocks for a very long time? Because the algorithm invoked each delegate in


the chain serially, a “problem” with one of the delegate objects stops all of the subsequent delegates

in the chain from being called. Clearly, this algorithm isn’t robust.

 

For those scenarios in which this algorithm is insufficient, the MulticastDelegate class offers an instance method, GetInvocationList, that you can use to call each delegate in a chain explicitly, using any algorithm that meets your needs.

 

public abstract class MulticastDelegate : Delegate {

// Creates a delegate array where each element refers

// to a delegate in the chain.

public sealed override Delegate[] GetInvocationList();

}

 

The GetInvocationList method operates on a MulticastDelegate-derived object and returns an array of Delegate references where each reference points to one of the chain’s delegate objects. Internally, GetInvocationList constructs an array and initializes it with each element referring to a delegate in the chain; a reference to the array is then returned. If the _invocationList field is null, the returned array contains one element that references the only delegate in the chain: the delegate instance itself.

You can easily write an algorithm that explicitly calls each object in the array. The following code demonstrates.

 

using System;

using System.Reflection; using System.Text;

 

// Define a Light component. internal sealed class Light {

// This method returns the light's status. public String SwitchPosition() {

return "The light is off";

}

}

 

// Define a Fan component. internal sealed class Fan {

// This method returns the fan's status. public String Speed() {

throw new InvalidOperationException("The fan broke due to overheating");

}

}

 

// Define a Speaker component. internal sealed class Speaker {

// This method returns the speaker's status. public String Volume() {

return "The volume is loud";

}

}

 

public sealed class Program {


// Definition of delegate that allows querying a component's status. private delegate String GetStatus();

 

public static void Main() {

// Declare an empty delegate chain. GetStatus getStatus = null;

 

// Construct the three components, and add their status methods

// to the delegate chain.

getStatus += new GetStatus(new Light().SwitchPosition); getStatus += new GetStatus(new Fan().Speed);

getStatus += new GetStatus(new Speaker().Volume);

 

// Show consolidated status report reflecting

// the condition of the three components. Console.WriteLine(GetComponentStatusReport(getStatus));

}

 

// Method that queries several components and returns a status report private static String GetComponentStatusReport(GetStatus status) {

 

// If the chain is empty, there is nothing to do. if (status == null) return null;

 

// Use this to build the status report. StringBuilder report = new StringBuilder();

 

// Get an array where each element is a delegate from the chain. Delegate[] arrayOfDelegates = status.GetInvocationList();

 

// Iterate over each delegate in the array.

foreach (GetStatus getStatus in arrayOfDelegates) {

 

try {

// Get a component's status string, and append it to the report. report.AppendFormat("{0}{1}{1}", getStatus(), Environment.NewLine);

}

catch (InvalidOperationException e) {

// Generate an error entry in the report for this component. Object component = getStatus.Target;

report.AppendFormat(

"Failed to get status from {1}{2}{0} Error: {3}{0}{0}", Environment.NewLine,

((component == null) ? "" : component.GetType() + "."), getStatus.GetMethodInfo().Name,

e.Message);

}

}

 

// Return the consolidated report to the caller. return report.ToString();

}

}


When you build and run this code, the following output appears.

 

The light is off

 

Failed to get status from Fan.Speed

Error: The fan broke due to overheating The volume is loud

 


Date: 2016-03-03; view: 585


<== previous page | next page ==>
Nbsp;   Using Delegates to Call Back Static Methods | Enough with the Delegate Definitions Already
doclecture.net - lectures - 2014-2024 year. Copyright infringement or personal data (0.022 sec.)