System.Delegate and the delegate keyword

Previous

This article covers the classes in .NET that support delegates, and how those map to the delegate keyword.

Define delegate types

Let's start with the 'delegate' keyword, because that's primarily what you will use as you work with delegates. The code that the compiler generates when you use the delegate keyword will map to method calls that invoke members of the Delegate and MulticastDelegate classes.

You define a delegate type using syntax that is similar to defining a method signature. You just add the delegate keyword to the definition.

Let's continue to use the List.Sort() method as our example. The first step is to create a type for the comparison delegate:

// From the .NET Core library

// Define the delegate type:
public delegate int Comparison<in T>(T left, T right);

The compiler generates a class, derived from System.Delegate that matches the signature used (in this case, a method that returns an integer, and has two arguments). The type of that delegate is Comparison. The Comparison delegate type is a generic type. For details on generics see here.

Notice that the syntax may appear as though it is declaring a variable, but it is actually declaring a type. You can define delegate types inside classes, directly inside namespaces, or even in the global namespace.

Note

Declaring delegate types (or other types) directly in the global namespace is not recommended.

The compiler also generates add and remove handlers for this new type so that clients of this class can add and remove methods from an instance's invocation list. The compiler will enforce that the signature of the method being added or removed matches the signature used when declaring the method.

Declare instances of delegates

After defining the delegate, you can create an instance of that type. Like all variables in C#, you cannot declare delegate instances directly in a namespace, or in the global namespace.

// inside a class definition:

// Declare an instance of that type:
public Comparison<T> comparator;

The type of the variable is Comparison<T>, the delegate type defined earlier. The name of the variable is comparator.

That code snippet above declared a member variable inside a class. You can also declare delegate variables that are local variables, or arguments to methods.

Invoke delegates

You invoke the methods that are in the invocation list of a delegate by calling that delegate. Inside the Sort() method, the code will call the comparison method to determine which order to place objects:

int result = comparator(left, right);

In the line above, the code invokes the method attached to the delegate. You treat the variable as a method name, and invoke it using normal method call syntax.

That line of code makes an unsafe assumption: There's no guarantee that a target has been added to the delegate. If no targets have been attached, the line above would cause a NullReferenceException to be thrown. The idioms used to address this problem are more complicated than a simple null-check, and are covered later in this series.

Assign, add, and remove invocation targets

That's how a delegate type is defined, and how delegate instances are declared and invoked.

Developers that want to use the List.Sort() method need to define a method whose signature matches the delegate type definition, and assign it to the delegate used by the sort method. This assignment adds the method to the invocation list of that delegate object.

Suppose you wanted to sort a list of strings by their length. Your comparison function might be the following:

private static int CompareLength(string left, string right) =>
    left.Length.CompareTo(right.Length);

The method is declared as a private method. That's fine. You may not want this method to be part of your public interface. It can still be used as the comparison method when attached to a delegate. The calling code will have this method attached to the target list of the delegate object, and can access it through that delegate.

You create that relationship by passing that method to the List.Sort() method:

phrases.Sort(CompareLength);

Notice that the method name is used, without parentheses. Using the method as an argument tells the compiler to convert the method reference into a reference that can be used as a delegate invocation target, and attach that method as an invocation target.

You could also have been explicit by declaring a variable of type Comparison<string> and doing an assignment:

Comparison<string> comparer = CompareLength;
phrases.Sort(comparer);

In uses where the method being used as a delegate target is a small method, it's common to use lambda expression syntax to perform the assignment:

Comparison<string> comparer = (left, right) => left.Length.CompareTo(right.Length);
phrases.Sort(comparer);

Using lambda expressions for delegate targets is covered more in a later section.

The Sort() example typically attaches a single target method to the delegate. However, delegate objects do support invocation lists that have multiple target methods attached to a delegate object.

Delegate and MulticastDelegate classes

The language support described above provides the features and support you'll typically need to work with delegates. These features are built on two classes in the .NET Core framework: Delegate and MulticastDelegate.

The System.Delegate class and its single direct subclass, System.MulticastDelegate, provide the framework support for creating delegates, registering methods as delegate targets, and invoking all methods that are registered as a delegate target.

Interestingly, the System.Delegate and System.MulticastDelegate classes are not themselves delegate types. They do provide the basis for all specific delegate types. That same language design process mandated that you cannot declare a class that derives from Delegate or MulticastDelegate. The C# language rules prohibit it.

Instead, the C# compiler creates instances of a class derived from MulticastDelegate when you use the C# language keyword to declare delegate types.

This design has its roots in the first release of C# and .NET. One goal for the design team was to ensure that the language enforced type safety when using delegates. That meant ensuring that delegates were invoked with the right type and number of arguments. And, that any return type was correctly indicated at compile time. Delegates were part of the 1.0 .NET release, which was before generics.

The best way to enforce this type safety was for the compiler to create the concrete delegate classes that represented the method signature being used.

Even though you cannot create derived classes directly, you will use the methods defined on these classes. Let's go through the most common methods that you will use when you work with delegates.

The first, most important fact to remember is that every delegate you work with is derived from MulticastDelegate. A multicast delegate means that more than one method target can be invoked when invoking through a delegate. The original design considered making a distinction between delegates where only one target method could be attached and invoked, and delegates where multiple target methods could be attached and invoked. That distinction proved to be less useful in practice than originally thought. The two different classes were already created, and have been in the framework since its initial public release.

The methods that you will use the most with delegates are Invoke() and BeginInvoke() / EndInvoke(). Invoke() will invoke all the methods that have been attached to a particular delegate instance. As you saw above, you typically invoke delegates using the method call syntax on the delegate variable. As you'll see later in this series, there are patterns that work directly with these methods.

Now that you've seen the language syntax and the classes that support delegates, let's examine how strongly typed delegates are used, created, and invoked.

Next