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 2Delegates 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.