There was no lecture or tutorial this week.
Our topics this week are local functions, delegates, lambda expressions, and extension methods. You can read about them in Essential C# 7.0:
Chapter 6 "Classes": Extension Methods
Chapter 13 "Delegates and Lambda Expressions"
They are also covered in Programming C# 8.0:
Chapter 3 "Types": Members / Methods / Extension methods
Chapter 9 "Delegates, Lambdas, and Events": Delegate Types, Anonymous Functions
Here are a few notes briefly summarizing these topics.
A local function is a function that is defined inside a method. (Sometimes local functions are called "nested methods", but strictly speaking they are functions, not methods, since they are not invoked on an object.) For example:
static void xyz(string[] args) { WriteLine("hello"); double arg(int n) { return double.Parse(args[n]); } double d = arg(0); double e = arg(1); WriteLine(d + e); }
A local function may
access parameters and local variables in its containing method. For
example, the local function
arg
above accesses the args
parameter.
A delegate is a value that represents a function or method. It is similar to to a function object in Python, or a function pointer in languages such as C.
The delegate
keyword declares a new delegate
type. For example:
delegate bool IntCondition(int i);
With this declaration, an IntCondition
is a type of
delegate that takes an integer argument and returns a boolean. We can
now declare a variable of type IntCondition
, and use it
to refer to a method
of corresponding type:
static bool isOdd(int i) { return (i % 2 == 1); } static void Main() { IntCondition c = isOdd; …
We can invoke the delegate using function call syntax:
WriteLine(c(4)); // writes False
In the example above, the delegate c refers to a static method odd
.
A delegate may also refer to an instance method, in which case it
actually references a particular object on which the method will be
invoked. For example:
class Interval { public int low, high; public Interval(int low, int high) { this.low = low; this.high = high; } public bool contains(int i) { return (low <= i && i <= high); } } static void Main() { IntCondition c = new Interval(1, 5).contains; IntCondition d = new Interval(3, 7).contains; WriteLine(c(2)); // writes True WriteLine(d(2)); // writes False }
Here is a method that counts how many elements in an array of integers satisfy an arbitrary condition:
static int count(int[] a, IntCondition cond) { int n = 0; foreach (int i in a) if (cond(i)) ++n; return n; }
We can invoke this method as follows:
static bool isEven(int i) { return (i % 2 == 0); } int[] a = { 3, 4, 5, 6, 7 }; WriteLine(count(a, isEven)); // writes 2
Delegates may be generic:
delegate bool Condition<T>(T t); // maps type T to bool
Here is the count
method from above, rewritten to work
on an array of any type T. Notice that the
method itself must also be generic (indicated by the "<T>"
after the method name).
static int count<T>(T[] a, Condition<T> cond) { int n = 0; foreach (T val in a) if (cond(val)) ++n; return n; }
A lambda expression is an anonymous function that can appear inside another expression.
For example, here is a delegate type for a function from integers to integers:
delegate int IntFun(int i);
And here is a method that applies a given function to every element of an array of integers:
static int[] map(int[] a, IntFun f) { int[] b = new int[a.Length]; for (int i = 0 ; i < a.Length ; ++i) b[i] = f(a[i]); return b; }
We can define a named method and pass it to map
:
static int plus2(int i) { return i + 2; } static int[] add2(int[] a) { return map(a, plus2); }
Alternatively, we can invoke map
using a lambda
expression:
static int[] add2(int[] a) { return map(a, i => i + 2); }
Here, i => i + 2
is a lambda expression. It is an
anonymous function that takes an integer parameter i and returns the
value i + 2.
Like a local function,
a lambda expression may refer to parameters or local variables in its
containing method. For example, suppose that we want to write a
method that adds a given value k to each element in an array. We
could write a local function
and pass it to map
:
static int[] add_k(int[] a, int k) { int f(int i) { return i + k; } return map(a, f); }
Or we can use a lambda expression that adds k directly:
static int[] add_k(int[] a, int k) { return map(a, i => i + k); }
The lambda expressions in the examples above are expression
lambdas, writen using a compact syntax similar to the expression
syntax for methods. Alternatively, a lambda expression can be written
as a statement lambda, which
can include one or more statements and can use the return
statement to return a value. For
example, we can rewrite the last example above like this:
static int[] add_k(int[] a, int k) { return map(a, i => { return i + k; } ); }
In the examples above we've seen that a method can take a delegate (i.e. a function) as an argument. A method can also return a delegate constructed using a lambda expression.
Here is a simple example:
delegate bool IntCondition(int i); static IntCondition divisibleBy(int k) { return i => (i % k == 0); }
Now we can invoke this method and use the delegate that it returns:
IntCondition div3 = divisibleBy(3); WriteLine(div3(6)); // writes 'True' WriteLine(div3(7)); // writes 'False'
In this example, note that the delegate returned by divisibleBy
can refer to the parameter k even after the method divisibleBy
has returned! To put it differently, the lambda expression i
=> (i % k ==
0
)
has captured the parameter k. Local variables may also be
captured by a lambda expression.
Using lambda expressions we can write functions that transform other functions. This is a powerful technique that you will explore further in more advanced courses (e.g. Non-Procedural Programming). Here are a couple of examples. First, here is a function that composes two functions f and g, returning the function (f ∘ g), which is defined as (f ∘ g)(x) = f(g(x)):
static IntFun compose(IntFun f, IntFun g) { return i => f(g(i)); }
We can call compose
as follows:
IntFun f = compose(i => i * 2, i => i + 1); // now f(x) = 2 * (x + 1) WriteLine(f(4)); // writes 10
Second, here's a function that computes the nth power of a function f, defined as
fn(x) = f(f(...(f(x))) [ f appears n times in the preceding expression ]
static IntFun power(IntFun f, int n) { return i => { for (int j = 0 ; j < n ; ++j) i = f(i); return i; }; }
Addition to a power is multiplication:
IntFun f = power(i => i + 10, 4); WriteLine(f(2)); // writes 42
You can add extension methods to an existing class. An extension method can be used syntactically as if belonged to a class, even though it is written outside the class.
For example, suppose that we are using a C# library that provides this type:
class Vector { public double dx, dy; public Vector(double dx, double dy) { this.dx = dx; this.dy = dy; } }
We wish that the author of the class had provided a length
method that calculates the length
of a Vector
.
Since we cannot modify the class, we can write an extension method:
static class Util { public static double length(this Vector v) => Sqrt(v.dx * v.dx + v.dy * v.dy); }
The keyword this
before the
argument "Vector v
" indicates that this is an
extension method for the Vector
class.
Now we can call the method as if it had been defined inside the
Vector
class itself!
Vector v = new Vector(3.0, 4.0); WriteLine(v.length()); // writes 5.0
Note that an extension method must
be static
.
The containing class can have any name, but must itself
also be declared as static
.