A great feature of C# 7.0 are local functions. Local functions have the syntax of methods and can be used within the scope of methods, properties, constructors…
With articles showing this feature, I often see questions: “What is it good for?” “Why is this needed?” To understand the usefulness of local functions, some good examples are needed. I try to show these with this article.
Local Functions with the yield Statement
Let’s start with a simplified filter method Where
. This implementation checks for
parameters resulting in an ArgumentNullException
in case null is passed:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate) | |
{ | |
if (source == null) throw new ArgumentNullException(nameof(source)); | |
if (predicate == null) throw new ArgumentNullException(nameof(predicate)); | |
foreach (T item in source) | |
{ | |
if (predicate(item)) | |
{ | |
yield return item; | |
} | |
} | |
} |
Invoking this method, the ArgumentNullException is not thrown when the query statement is defined, but – because of the delayed execution of yield
, with the foreach
iteration in line 4:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
string[] names = { "James", "Niki", "John", "Gerhard", "Jack" }; | |
var q = names.Where(null); | |
foreach (var n in q) // callstack position for exception | |
{ | |
Console.WriteLine(n); | |
} |
For having error information when it is needed, the Where
method can be splitted into two methods. The Where method just checks the parameters without any yield
statement included in the implementation, and invokes the WhereImpl
method where the yield
is done.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate) | |
{ | |
if (source == null) throw new ArgumentNullException(nameof(source)); | |
if (predicate == null) throw new ArgumentNullException(nameof(predicate)); | |
return WhereImpl(source, predicate); | |
} | |
private static IEnumerable<T> WhereImpl<T>(IEnumerable<T> source, Func<T, bool> predicate) | |
{ | |
foreach (T item in source) | |
{ | |
if (predicate(item)) | |
{ | |
yield return item; | |
} | |
} | |
} |
With this in place, the ArgumentNullException happens in line 2 where the error is more helpful.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
string[] names = { "James", "Niki", "John", "Gerhard", "Jack" }; | |
var q = names.Where(null); | |
foreach (var n in q) // callstack position for exception | |
{ | |
Console.WriteLine(n); | |
} |
Now, the Where
method has nothing than a parameter check and an invocation of the real implementation. Here, local functions are of great help. The implementation is simpler compared to the private method – the local function Iterator
can access variables from the outer scope, and thus here parameters are not needed:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate) | |
{ | |
if (source == null) throw new ArgumentNullException(nameof(source)); | |
if (predicate == null) throw new ArgumentNullException(nameof(predicate)); | |
return Iterator(); | |
IEnumerable<T> Iterator() | |
{ | |
foreach (T item in source) | |
{ | |
if (predicate(item)) | |
{ | |
yield return item; | |
} | |
} | |
} | |
} |
Recursive Functions
Another scenario for local functions are recursive calls.
In the following implementation of the QuickSort
method, Sort
is a local function that is called recursively until the collection is sorted.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void QuickSort<T>(T[] elements) where T : IComparable<T> | |
{ | |
void Sort(int start, int end) | |
{ | |
int i = start, j = end; | |
var pivot = elements[(start + end) / 2]; | |
while (i <= j) | |
{ | |
while (elements[i].CompareTo(pivot) < 0) i++; | |
while (elements[j].CompareTo(pivot) > 0) j—; | |
if (i <= j) | |
{ | |
T tmp = elements[i]; | |
elements[i] = elements[j]; | |
elements[j] = tmp; | |
i++; | |
j—; | |
} | |
} | |
if (start < j) Sort(start, j); | |
if (i < end) Sort(i, end); | |
} | |
Sort(0, elements.Length – 1); | |
} |
Using recursive calls with C# you need to be careful:
With C# you need to be careful with recursive calls. Contrary to functional programming languages like F#, the C# compiler does not tail call optimization where recursive method calls are converted to iterations to not consume call stack. With C# you can easily result in a
StackOverflowException
.
When does it end with the default stack configuration doing recursive calls with C#?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void WhenDoesItEnd() | |
{ | |
Console.WriteLine(nameof(WhenDoesItEnd)); | |
void InnerLoop(int ix) | |
{ | |
Console.WriteLine(ix++); | |
InnerLoop(ix); | |
} | |
InnerLoop(1); | |
} |
This simple sample that doesn’t need a lot of stack memory with every iteration ends after 24020 iterations with a StackOverflowException
, so be careful doing recursive calls with C#.
Local Functions instead of Lambda Expressions
Recently I came across an older sample where a Lambda expression is used in the AsynchronousPattern
method:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
static void AsynchronousPattern() | |
{ | |
WebRequest request = WebRequest.Create(url); | |
IAsyncResult result = request.BeginGetResponse(ar => | |
{ | |
using (WebResponse response = request.EndGetResponse(ar)) | |
{ | |
Stream stream = response.GetResponseStream(); | |
var reader = new StreamReader(stream); | |
string content = reader.ReadToEnd(); | |
Console.WriteLine(content.Substring(0, 100)); | |
Console.WriteLine(); | |
} | |
}, null); | |
} |
This one can be replaced by a local function as well:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private static void AsynchronousPattern() | |
{ | |
WebRequest request = WebRequest.Create(url); | |
IAsyncResult result = request.BeginGetResponse(ReadResponse, null); | |
void ReadResponse(IAsyncResult ar) | |
{ | |
using (WebResponse response = request.EndGetResponse(ar)) | |
{ | |
Stream stream = response.GetResponseStream(); | |
StreamReader reader = new StreamReader(stream); | |
string content = reader.ReadToEnd(); | |
Console.WriteLine(content.Substring(0, 100)); | |
Console.WriteLine(); | |
} | |
} | |
} |
This is just a matter of taste, but doesn’t look the syntax with the local function easier?
What are your thoughts on local functions with C# 7?
Have fun programming and learning,
Christian
Some more C# 7 articles:
C# 7 – What’s New
C# 7.0 Pattern Matching
C# 7.0 Out Vars and Ref Returns
C# 7.0 Expression Bodied Members
Tuples with C# 7.0
Binary Literals and Digit Separators
More information on C# 7 features in my new book Professional C# 7 and .NET Core 2.0 with source code updates at GitHub.
Please see my post about C# 7 features (https://codingforsmarties.wordpress.com/2017/04/05/c-7-features-i-dont-like) and its follow-up specifically regarding local functions (https://codingforsmarties.wordpress.com/2017/04/27/local-functions-at-it-again). Your yield example is interesting, but it’s a lot of work just to throw an exception at declaration instead of invocation, especially since this is contrary to how the base Linq methods work.
LikeLike
The base LINQ methods work in the same way. You probably had errors passing null because of different overloads. Just try this out:
string[] data = { “one”, “two” };
Func predicate = null;
var q = data.Where(predicate);
foreach (var item in q)
{
Console.WriteLine(item);
}
The exception happens with the initialization of q, not in the foreach.
Cheers,
Christian
LikeLike
I’m quite familiar with how .Net works. My point in my post is that I don’t see much use in this feature. Examples that I’ve seen can be equally implemented using private (not local) methods or lambdas.
Also, your example changes the expected behavior of a standard Linq function by throwing at a different point in execution. While I agree with your reason for doing this, the deviation from a common standard is enough for me to question the design. Developers that come after me and encounter this code may not expect it to behave the way it does, which could make it more difficult to debug.
LikeLiked by 1 person
Of course, the local function can also be implemented as a private method or using lambdas. Sometimes it’s just a matter of taste.
However, lambdas have additional overhead compared to local functions, and private methods can be called from other methods of the class as well. The scope is more restricted with local functions, and they can also access variables in the scope of the outer method (similar to lambdas but different to other private methods).
I see your point in naming my method “Where”. Would you be fine if I would have named this method “Filter”? I just named it “Where” to demonstrate a simplified implementation of the Where method from the framework. However, the framework itself has multiple Where methods implemented (not just overloads, but implementations in different classes).
LikeLike
Actually, for iterators you can’t use lambdas. The yield return keyword(s) is not supported in lambdas.
LikeLiked by 1 person
Yes, you can’t use lambdas with yield return. One more reason for local functions 🙂
LikeLike