Deep C# - Delegates
Written by Mike James   
Wednesday, 06 January 2016
Article Index
Deep C# - Delegates
Signatures and Methods

 

 

Delegate patterns

Why would you use a delegate rather than just calling a method?

The answer, explained in the introduction,  is simply that a delegate can be passed as a parameter to another method, so determining what method is called at run time.

More widely a delegate can be used anywhere an object can which means you can have references to a delegate and data structures of delegates such as arrays. 

In functional programming terms it converts a method or a function into a "first class object" i.e. one that can be used like any other object.

There are two well-known patterns that demand the use of a delegate.

The first is the “callback” or notification method which is supplied to an object for it to call with intermediate or final results of its working. Of course in this instance the object is usually run on a separate thread and the callback/notification method provides some asynchronous communication between the caller and the called thread.

The second well-known pattern is event handling. A delegate can be set up within and object so that clients can provide a method to be called when an event occurs.

If you think carefully you will see that there is little difference between the callback and the event pattern.

In both cases a delegate is called when some condition occurs – a buffer is full, the user has clicked a button, an error condition has been detected etc. However while events are based on delegates they add some additional structure – an add and remove accessor similar to a property.

A third, slightly less common, use is in creating a new thread of execution.

coverCsharp

Signatures and methods

At this point it is worth making clear that the delegate’s signature always determines how the method that the delegate wraps is called. That is, you always have to invoke the delegate with the parameter types specified and the delegate always returns the type specified.

However, it is possible to create instances of the delegate that encapsulate methods that don’t have exactly the specified signature.

So to summarise:

  • A delegate type’s signature specifies how the delegate is invoked – i.e. the parameters and return type are always given by the delegate’s signature.

  • A delegate type’s signature specifies what sort of methods an instance of the type can encapsulate.

Notice that the encapsulated method can be a static or an instance method. That is the method can belong to an instance of the class or it can be a static "class method". 

The delegate's Target property stores the instance and the Method property stores the method that that the delegate encapsulates. This is how a delegate "knows" what method to call. 

If the delegate encapsulates a static method then Target is null i.e. there is no instance. To encapsulate a method on a specific object you simply have to qualify the method name with the object’s name e.g. this.hello or MyObject.hello.

Covariance and contravarance

In the simplest case a method’s signature and return type have to match the signature of the delegate type that encapsulates it.

However, there is more flexibility in how a method signature can match a delegate type signature. In the documentation this is called covariance and contravariance -  just to make is sound more sophisticated. See: Covariance And Contravariance - A Simple Guide

Put simply covariance allows the method to return a sub-class or derived type of the return type defined in the delegate.

Assume for the moment that MyType1 is the base class and MyType2 is the derived class, that is:

public class MyType1 {};
public class MyType2 : MyType1 {};

If you now define the delegate type:

delegate MyType1 HelloType(MyType2 param);

then clearly a method returning MyType1 matches the signature but, by covariance so does any method returning a type that inherits from MyType1, e.g. MyType2. That is you can use HelloType with a function like

MyType2 myFunc1(myType2 param);
HelloType myDelegate=new HelloType(myFunc1);

So the delegate type can encapsulate a method that returns a MyType2. But following the rule that the delegate signature determines its invocation, the return type is always treated as MyType1. If the method does return a derived type, MyType2 say, you have to use a cast to work with the result as a MyType2 object. For example:

MyType2 myResult=(MyType2) myDelegate(myType2);

Contravariance allows the method to have parameters that are base types of the types specified in the delegate that encapsulates it. That is, if you define the delegate type as before:

delegate MyType1 HelloType(MyType2 param);

then a method that that has a parameter that is a base class for MyType2, e.g. MyType1, matches the signature. For example:

MyType2 myFunc2(myType1 param);
HelloType myDelegate=new HelloType(myFunc2);

is perfectly acceptable. 

Once again you can only invoke the delegate by passing a MyType2 object

MyType1 myResult= myDelegate(myType2);

but this all works because the method that the delegate invokes can treat this as a more primitive type, e.g. as a MyType1 object.

Notice that covariance and contravariance go in opposite directions - as they should to honour the original use and meaning of the terms.

An output type can be more that it is supposed to be because the client receiving it and making use of it treats it as a lesser type i.e. it can be a derived type.

An input type can be less than it is supposed to be i.e. if a method treats a derived type as the base type then everything is still ok - a parameter can be a base type.

Generic delegates

To confuse matters even more, or should that be to confer further elegance and power, you can also create generic delegate types.

Put simply you can use a generic type anywhere within a delegate definition. For example:

delegate int HelloType<T>(T param);

creates a delegate type with a generic parameter.

To use the delegate you have to provide the type information.

For example:

HelloType<int> HelloInst=new HelloType<int>(hello);

Where Hello is a method which returns an int and has a single int parameter. You can also use the shorter:

HelloType<int> HelloInst = hello;

Before you start to invent clever ways of creating generic delegates for every purpose, I should warn you that in the main generic delegates should only be used within generic classes.

In this context they provide a way to create delegate instances that “fit in” with the functioning of the entire delegate class.

For example, you might create a delegate as a notification or event method which can be customised to work with the same data types as the rest of the generic class.

Anonymous methods and Lambda expressions

As of .NET 2.0 you can save some typing and names by using  anonymous, as opposed to named, methods.

However as of .NET 3.5 anonymous methods are overshadowed by lambda expressions.

In both cases you can simply treat the new facilities as easier ways of defining a delegate.

Anonymous methods are covered in depth in another chapter as are Lambda Expressions.

Also of interest is the way an anonymous method or Lambda expression enforces closure, which is the subject of another chapter.

 

 

Futher Reading

Covariance And Contravariance - A Simple Guide

 

Deep C#

 Buy Now From Amazon

DeepCsharp360

 Chapter List

  1. Why C#?

    I Strong Typing & Type Safety
  2. Strong Typing
       Extract 
    Why Strong Typing
  3. Value & Reference
  4.    Extract Value And Reference
  5. Structs & Classes
       Extract
    Structs & Classes 
  6. Inheritance
      
    Extract
    Inheritance
  7. Interfaces & Multiple Inheritance
      
    Extract Interface
  8. Controlling Inheritance
    II Casting & Generics
  9. Casting - The Escape From Strong Typing
      
    Extract Casting I ***NEW!
  10. Generics
  11. Advanced Generics
  12. Anonymous & Dynamic
    Typing
    III Functions
  13. Delegates
  14. Multicast Delegates
  15. Anonymous Methods, Lambdas & Closures
    IV Async
  16. Threading, Tasks & Locking
  17. The Invoke Pattern
  18. Async Await
  19. The Parallel For
    V Data - LINQ, XML & Regular Expressions
  20. The LINQ Principle
  21. XML
  22. LINQ To XML
  23. Regular Expressions
    VI Unsafe & Interop
  24. Interop
  25. COM
  26. Custom Attributes
  27. Bit Manipulation
  28. Advanced Structs
  29. Pointers 

Extra Material

 <ASIN:1871962714>

 <ASIN:B09FTLPTP9>

raspberry pi books

 

Comments




or email your comment to: comments@i-programmer.info

 

<ASIN:0321658701>
<ASIN:1451531168>

<ASIN:0321637003>

<ASIN:0596800959>

<ASIN:047043452X>

<ASIN:0123745144>



Last Updated ( Thursday, 07 January 2016 )