The content below is retrieved from Jupyter notebooks that you can find in this repository.
Source: Microsoft documentation.
A delegate encapsulates a method.
Actions
System.Action
is the standard library type for void delegates, methods that don’t return a value. By default it’s parameterless, but it can have parameters with generics.
Action action = () => Console.WriteLine("Action done.");
// invoked as method call
action()
Action done.
// user-declared delegate, no parameters
delegate void MyAction();
MyAction myAction = () => Console.WriteLine("My action done.");
myAction()
My action done.
I’m using lambdas here, which are anonymous functions that are defined on the fly, but delegates can accept already defined methods as well:
/// Method that writes "Done." to console output.
public void WriteDone() {
Console.WriteLine("Done.");
}
Action actionDone = WriteDone;
actionDone()
Done.
Action actionWithParam = (int i) => Console.WriteLine(i);
(1,26): error CS1593: Delegate 'Action' does not take 1 arguments
Declaring an Action
with generic types to invoke it with parameters:
// type of lambda parameter `i` is determined from declaration of delegate object
Action<int> actionOnInteger1 = i => Console.WriteLine(i);
// same as
// type of delegate object is determined from definition of lambda
var actionOnInteger2 = (int i) => Console.WriteLine(i);
actionOnInteger2(0)
0
actionOnInteger2.GetType()
// user-declared delegate, one parameter
delegate void MyActionOnInteger(int i);
MyActionOnInteger myActionOnInteger = i => Console.WriteLine(i);
myActionOnInteger(0)
0
Action<int> actionThatReturns = i => { return i * 2; };
(1,40): error CS8030: Anonymous function converted to a void returning delegate cannot return a value
Remember, Action
is declared as a void delegate, it can’t return any value.
Funcs
System.Func
is the standard library type for delegates that encapsulate methods that return a value. It can have parameters with generics too.
// the last generic type is the return type. Parameters are optional
Func<string> func = () => "Function returned.";
var returned = func();
returned
Function returned.
// user-declared delegate that returns a string, same as previous Func
delegate string MyFunc();
MyFunc myFunc = () => "My function returned.";
returned = myFunc();
returned
My function returned.
Func<int, int, int> funcMultiply = (x, y) => x * y;
funcMultiply(2, 3)
delegate int MyFuncMultiply(int x, int y);
MyFuncMultiply myFuncMultiply = (x, y) => x * y;
myFuncMultiply(2, 3)
Multicasting
A delegate can call multiple methods when invoked:
Action actions = () => Console.WriteLine("first method");
actions += action;
actions += actionDone;
actions += () => Console.WriteLine("last method");
actions()
first method
Action done.
Done.
last method
actions.GetInvocationList().Count()
Use cases
- There’s no need to declare new custom delegates, use
Action
andFunc
types. - You can just use
var
and declare the types in the lambda definition. So why being aware of delegate types?:
Delegates as abstractions
Code calling a delegate used as an abstraction doesn’t need to know (and doesn’t care) about how that delegate is implemented.
public void WriteAsUpper(string message) {
Console.WriteLine(message.ToUpper());
}
public void WriteLength(string message) {
Console.WriteLine(message.Length);
}
public string CreateMessage(string title, string text, DateTime date, Action<string> writeCallback) {
var message = $"{title}\n{text}\n{date.ToLongDateString()}";
// I already know the delegate type: it receives a string and doesn't return anything. Ok more than enough info
// But what does it do? I COULD CARE LESS, just call it :)
writeCallback(message);
return message;
}
var redMessage = CreateMessage("Red", "Roses are red", DateTime.Now, WriteLength);
44
redMessage
Red
Roses are red
Saturday, January 15, 2022
var blueMessage = CreateMessage("Blue", "Blue roses are fake", DateTime.Now, WriteAsUpper);
BLUE
BLUE ROSES ARE FAKE
SATURDAY, JANUARY 15, 2022
In the above example, CreateMessage
was defined to create a message and return it. It wasn’t defined thinking about the console. If this method could speak it would tell you:
If you want to show the message in console, create a delegate to do it and pass it to me as the
callback
parameter. Or whatever you want to do with it just create a delegate for that and pass it. I don’t care, that’s the callback’s job.
Implementing custom LINQ operations
If you have used LINQ you know that it uses functions to process collections of data. The purpose of this exercise is to apply delegates in a more practical way than just dummy examples.
Note: I’m not using tags inside docstrings because Visual Studio Code doesn’t autocomplete them in notebooks, but they must be included in real code.
/// Inheriting from List to skip specific implementations from interfaces, irrelevant for our topic,
/// so we can use foreach statements directly and focus on our custom methods and delegates.
class IntList : List<int> {
/// Custom delegate equivalent to Func<int, bool>
public delegate bool ConditionCallback(int integer);
/// Equivalent for Where
public IEnumerable<int> Filter(ConditionCallback condition) {
foreach (var integer in this) {
if (condition(integer)) {
yield return integer;
}
}
}
/// Equivalent for Select<T>
public IEnumerable<TReturn> Map<TReturn>(Func<int, TReturn> mapper) {
foreach (var integer in this) {
yield return mapper(integer);
}
}
/// Equivalent for All
public bool Every(ConditionCallback condition) {
foreach (var integer in this) {
if (!condition(integer)) {
return false;
}
}
return true;
}
/// Equivalent for Any
public bool Some(ConditionCallback condition) {
foreach (var integer in this) {
if (condition(integer)) {
return true;
}
}
return false;
}
}
In the above code, each method invokes its delegate parameter, thus calling the function it encapsulates.
var ints = new IntList { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Using lambdas for throwaway functions:
// get the fifth part of each integer as double
ints.Map(i => i / 5.0)
index | value |
---|---|
0 | 0.2 |
1 | 0.4 |
2 | 0.6 |
3 | 0.8 |
4 | 1 |
5 | 1.2 |
6 | 1.4 |
7 | 1.6 |
8 | 1.8 |
9 | 2 |
// get integers as string percentages
ints.Map(i => $"{i * 10}%")
index | value |
---|---|
0 | 10% |
1 | 20% |
2 | 30% |
3 | 40% |
4 | 50% |
5 | 60% |
6 | 70% |
7 | 80% |
8 | 90% |
9 | 100% |
Implementing methods to reuse as callbacks:
bool IsEven(int integer) {
return integer % 2 == 0;
}
bool IsOdd(int integer) {
// reuse code!
return !IsEven(integer);
}
ints.Filter(IsEven)
index | value |
---|---|
0 | 2 |
1 | 4 |
2 | 6 |
3 | 8 |
4 | 10 |
ints.Filter(IsOdd)
index | value |
---|---|
0 | 1 |
1 | 3 |
2 | 5 |
3 | 7 |
4 | 9 |
// are there some even integers?
ints.Some(IsEven)
var odds = new IntList { 3, 5, 7, 11, 13, 17 };
// is every integer odd?
odds.Every(IsOdd)