Introduction
Here we will cover why we need local functions and how they could improve your application performance. You will learn when you should choose Local Function over Lambda expression (Delegates). You will understand how the IL code looks when we execute our code with Lambda expression and local functions.
Scenario 1
Using Lambda Expression
Consider a scenario where you have a list of items with buying and selling price & you want to calculate the percentage of profit of all the items individually.
So, we have a class which represents order details (Flatten object):
- public class OrderDetails
- {
- public int Id { get; set; }
-
- public string ItemName { get; set; }
-
- public double PurchasePrice { get; set; }
-
- public double SellingPrice { get; set; }
-
- public OrderDetails()
- {
- }
- }
We have a list of items, we will iterate each item and calculate the percentage of profit for each item:
- static void Main(string[] args)
- {
-
- List<OrderDetails> lstOrderDetails = new List<OrderDetails>();
-
- lstOrderDetails.Add(new OrderDetails() { Id = 1, ItemName = "Item 1", PurchasePrice = 100, SellingPrice = 120 });
- lstOrderDetails.Add(new OrderDetails() { Id = 2, ItemName = "Item 2", PurchasePrice = 800, SellingPrice = 1200 });
- lstOrderDetails.Add(new OrderDetails() { Id = 3, ItemName = "Item 3", PurchasePrice = 150, SellingPrice = 150 });
- lstOrderDetails.Add(new OrderDetails() { Id = 4, ItemName = "Item 4", PurchasePrice = 155, SellingPrice = 310 });
- lstOrderDetails.Add(new OrderDetails() { Id = 5, ItemName = "Item 5", PurchasePrice = 500, SellingPrice = 550 });
-
-
- Func<double, double, double> GetPercentageProfit = (purchasePrice, sellPrice) => (((sellPrice - purchasePrice) / purchasePrice) * 100);
-
-
- foreach(var order in lstOrderDetails)
- {
- Console.WriteLine($"Item Name: {order.ItemName}, Profit(%) : {GetPercentageProfit(order.PurchasePrice, order.SellingPrice)} ");
- }
-
- Console.ReadLine();
- }
In #2 above, we have created a Lambda expression which calculates the percentage of profit and we are calling it in the #3 "foreach" loop.
Now let's see how this Lambda Expression looks in the IL(Intermediate Language) code, and what it does behind the scenes.
To check that, we will use ILDASM.exe, which ships with the .Net framework. You can find it here: "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" Or you can attach it in your visual studio tools as an extension (I am not covering that, if anyone want to know then please ask in the comments):
I am running it from my Visual Studio 2019 as I added it as an extension:
Then I am opening the dll file of my project "Local Function.dll" in ILDASM tool:
Double click on your "Main : void(string[])" function, it will open the IL code in a new window, see the marked code below:
- You can see that our lambda expression is converted into class. Why? Because lambda expressions are converted to delegates.
- As it is a class, we should use it by an instance, which we are getting from "new obj". IT means it will be allocated on Heap.
- To execute/invoke this method, The IL code uses "callvirt" (We will cover callvirt (call virtual method) later with Local Function implementation)
Now, we got to know that Lambda is converted into the delegate and then class, and we need an instance of this class to use it further. The lifecycle of these objects will have to be handled by the Garbage collector.
Using Local Function
Here we replaced Lambda expression with "Local Function":
- static void Main(string[] args)
- {
-
- List<OrderDetails> lstOrderDetails = new List<OrderDetails>();
-
- lstOrderDetails.Add(new OrderDetails() { Id = 1, ItemName = "Item 1", PurchasePrice = 100, SellingPrice = 120 });
- lstOrderDetails.Add(new OrderDetails() { Id = 2, ItemName = "Item 2", PurchasePrice = 800, SellingPrice = 1200 });
- lstOrderDetails.Add(new OrderDetails() { Id = 3, ItemName = "Item 3", PurchasePrice = 150, SellingPrice = 150 });
- lstOrderDetails.Add(new OrderDetails() { Id = 4, ItemName = "Item 4", PurchasePrice = 155, SellingPrice = 310 });
- lstOrderDetails.Add(new OrderDetails() { Id = 5, ItemName = "Item 5", PurchasePrice = 500, SellingPrice = 550 });
-
-
- foreach (var order in lstOrderDetails)
- {
- Console.WriteLine($"Item Name: {order.ItemName}, Profit(%) : {GetPercentageProfit(order.PurchasePrice, order.SellingPrice)} ");
- }
-
-
- double GetPercentageProfit(double purchasePrice, double sellPrice)
- {
- return (((sellPrice - purchasePrice) / purchasePrice) * 100);
- }
-
- Console.ReadLine();
- }
So, at #3 above, we have implemented the Local Function, which is embedded inside the Main Function. Let's run the ILDASM.exe again to check how this local function has converted into IL,
See there is no new class, no new object. Its just a simple method/function call. There is no new object needed to call this method. It would save some memory. The other important difference between the Lambda expression and this Local Function is how the framework is calling this method in IL. Here it is called as "call", which is faster than "callvirt". Why? Because it is stored on Stack not on Heap (Lambda expression instance is on Heap). (Both call and callvirt are IL instructions)
I know as a developer you don't need to focus on how the compiler/IL is calling the method, but I strongly feel that for writing very performant applications, you should know the internal details of Framework.
So the difference between call and callvert is, the call IL instruction. Always try to call this method whether the instance variable which is going to call this method is null or not. It does not check the existence of this caller instance. callvirt always checks the reference before calling the actual method under the hood. So, we can say callvirt cannot call the static class method, it can only call instance methods.
Scenario 2
Consider a scenario where you want to use iterators in your logic (using "yield" keyword in the "foreach" loop). We are considering the same example of order details, where we need to iterate to all the items and print its selling price. We will add a check on the items list that it should not be null, It should have at least one element.
- static void Main(string[] args)
- {
-
- List<OrderDetails> lstOrderDetails = new List<OrderDetails>();
- lstOrderDetails.Add(new OrderDetails() { Id = 1, ItemName = "Item 1", PurchasePrice = 100, SellingPrice = 120 });
- lstOrderDetails.Add(new OrderDetails() { Id = 2, ItemName = "Item 2", PurchasePrice = 800, SellingPrice = 1200 });
- lstOrderDetails.Add(new OrderDetails() { Id = 3, ItemName = "Item 3", PurchasePrice = 150, SellingPrice = 150 });
- lstOrderDetails.Add(new OrderDetails() { Id = 4, ItemName = "Item 4", PurchasePrice = 155, SellingPrice = 310 });
- lstOrderDetails.Add(new OrderDetails() { Id = 5, ItemName = "Item 5", PurchasePrice = 500, SellingPrice = 550 });
-
-
- var result = GetItemSellingPice(lstOrderDetails);
-
-
- foreach(string s in result)
- {
- Console.WriteLine(s.ToString());
- }
-
- Console.ReadLine();
- }
-
-
-
-
-
-
- public static IEnumerable<string> GetItemSellingPice(List<OrderDetails> lstOrderDetails)
- {
-
- if (lstOrderDetails == null) throw new ArgumentNullException();
-
- foreach (var order in lstOrderDetails)
- {
- yield return ($"Item Name:{order.ItemName}, Selling Price:{order.SellingPrice}");
- }
- }
So, in the above code, we have a list of 5 items, we are passing it to the method "GetItemSellingPice" to get the item's price. You can see that we are applying validation that this list should not be null and we are using yield return in for loop. When we run the application we get the following output,
All looks good here, right?
Now consider a scenario where your order details list is null. So, when we call the method "GetItemSellingPice" it should return the ArgumentNullException as List is null. But this is not the case, when you use iterators, It does not execute right away. It starts executing when you start processing its"result". So in the code below we are making the lstOrderDetails as null:
- static void Main(string[] args)
- {
-
- List<OrderDetails> lstOrderDetails = null;
-
-
- var result = GetItemSellingPice(lstOrderDetails);
-
-
-
-
-
- foreach (string s in result)
- {
- Console.WriteLine(s.ToString());
- }
-
- Console.ReadLine();
- }
-
-
-
-
-
-
- public static IEnumerable<string> GetItemSellingPice(List<OrderDetails> lstOrderDetails)
- {
-
- if (lstOrderDetails == null) throw new ArgumentNullException();
- //#5. Loop through each item
- foreach (var order in lstOrderDetails)
- {
- yield return ($"Item Name:{order.ItemName}, Selling Price:{order.SellingPrice}");
- }
- }
The above code will not throw exception at #2. It will thrown on #4, where we start working on the resulting property "result". So, What could be the solution to that?
Local Functions is the solution. Here is the code:
- static void Main(string[] args)
- {
-
- List<OrderDetails> lstOrderDetails = null;
-
-
- var result = GetItemSellingPice(lstOrderDetails);
-
-
- foreach(string s in result)
- {
- Console.WriteLine(s.ToString());
- }
-
- Console.ReadLine();
- }
-
-
-
-
-
-
- public static IEnumerable<string> GetItemSellingPice(List<OrderDetails> lstOrderDetails)
- {
-
- if (lstOrderDetails == null) throw new ArgumentNullException();
-
-
- return GetItemPrice();
-
-
- IEnumerable<string> GetItemPrice()
- {
- foreach (var order in lstOrderDetails)
- {
- yield return ($"Item Name:{order.ItemName}, Selling Price:{order.SellingPrice}");
- }
- }
- }
You can see that we moved the foreach loop block into Local Function. So, when we execute it, we will get the exception at #3, if the list is null. This is the eager validation for iterators.
Conclusion
Local Function is a very powerful feature. We learned about the performance as compared to Lambda expression, and we got to know about validations in iterators. A Local function could be recursive, while Lambda expression is not meant to be recursive, so it could solve many of your problems.