We have been racing through the C#7 features in my latest series of posts; here is a list of what I have covered and what will be covered:
- Binary literals
- Numeric literal digit separators
out
variablesthrow
expressions- Expression-bodied constructors
- Expression-bodied finalizers
- Expression-bodied property accessors
- Pattern matching
- Local functions
- Generalized async return types
ref
locals and returns- Tuples
In today's post, we will look at local functions. Those who are familiar with JavaScript will be familiar with local functions; the ability to define a method inside of another method. We have had a similar ability in C# since anonymous methods were introduced albeit in a slightly less flexible form1. Up until C#7, methods defined within another method were assigned to variables and were limited in use and content. For example, one could not use yield
inside anonymous methods.
Local functions allow us to declare a method within the scope of another method; not as an assignment to a run-time variable as with anonymous methods and lambda expressions, but as a compile-time symbol to be referenced locally by its parent function. With lambda expressions, the compiler does some heavy lifting for us, creating an anonymous type to hold our method and its closures; also, to call them, a delegate must be instantiated and then invoked, which incurs additional memory overhead to standard method calls: none of this is necessary for local functions as they are regular methods. Not only that, but because these are regular methods, the full range of method syntax is available to us, including yield
.
The official documentation provides a good overview of the differences between anonymous methods/lambda expressions and local functions, which ends with this paragraph:
While local functions may seem redundant to lambda expressions, they actually serve different purposes and have different uses. Local functions are more efficient for the case when you want to write a function that is called only from the context of another method.
The last sentence of that infers one of the most useful things about local methods; streaming enumerable sequences with fail early support. The yield
syntax introduced in C#3 has been incredibly useful, simplifying the work necessary for defining an enumerator from writing an entire class to just writing a method. However, due to the way enumeration works, we often have to split our enumerator methods into two so that things like argument validation occur immediately, rather than when the first item in the sequence is requested, like this:
public static IEnumerable<int> Numbers(int count) { if (count <= 0) throw new ArgumentException(); return NumbersImpl(count); } private static IEnumerable<int> NumbersImpl(int count) { for (int i = 0; i < count; i++) { yield return i; } }
The NumbersImpl
method is only ever used by the public-facing Numbers
method, but we cannot make that any clearer. However, with C#7 and local functions, we can now embed that method declaration into the Numbers
method and make our code explicit.
public static IEnumerable<int> Numbers(int count) { if (count <= 0) throw new ArgumentException(); return NumbersImpl(); IEnumerable<int> NumbersImpl() { for (int i = 0; i < count; i++) { yield return i; } } }
There are a couple of things to note here. First, we can declare the local function after it has been called; unlike anonymous methods and lambda expressions, local functions are just like any other method; they are part of the program declaration rather than its execution. Second, and somewhat surprisingly2, we can use closures.
Closures in Local Functions
With lambda expressions and anonymous methods, closures are hoisted into member variables of an anonymous type, but this is not how local functions work; local functions do not necessarily get their own anonymous type3. So how do closures work in local functions? Well, the compiler performs a little code-rewriting for us to effectively hoist the closures into method arguments so that we don't have to repeat ourselves.
In Conclusion
Local functions provide a valuable alternative to anonymous methods and lambda expressions, and one-time use member functions. Not only do they make a clear distinction between run-time and compile-time, making the intent clear, but also by co-locating a single-use method with the code that calls it, we can make our code more readable. Of course, as with many other features of high-level languages like C#, this could be abused and make code horribly unreadable, so keep each other honest and be sure to call out incorrect or dubious usage when you see it.
You can read more about local functions in the official documentation.
What do you think? Will you use this new feature of C#? Does it make the language better? Please leave your views in the comments. Next week, we'll take a look at some of the changes C#7 makes to returning values from our methods.