Evolution of Anonymous Functions in C#
The capability to pass code as data has evolved significantly through the history of C#, transforming from verbose delegate instantiations to concise lambda expressions.
C# 1.0: Named Delegates
In the initial version, passing methods required defining explicit delegate types and separate named methods. There was no syntax for inline code definitions.
public delegate int BinaryOperation(int x, int y);
public static int Multiply(int a, int b)
{
return a * b;
}
// Usage
BinaryOperation op = new BinaryOperation(Multiply);
int result = op(5, 10);
C# 2.0: Anonymous Methods
C# 2.0 introduced anonymous methods, allowing developers to define code blocks inline without creating a separate named method. This reduced boilerplate for event handlers and callbacks.
// Inline delegate definition
BinaryOperation op = delegate(int a, int b)
{
return a + b;
};
C# 3.0: Lambda Expressions
The release of C# 3.0 brought lambda expressions, a more succinct syntax that became the foundation for LINQ. The lambda operator (=>) clearly separates input parameters from the execution logic.
// Expression lambda
BinaryOperation add = (a, b) => a + b;
// Statement lambda
BinaryOperation complexOp = (a, b) =>
{
int temp = a * 2;
return temp + b;
};
Modern Enhancements
Later versions extended this functionality with features like async lambda expressions (async () => await ...), expression-bodied members, and improved type inference, making functional programming patterns more accessible.
Syntax and Usage Patterns
A lambda expression is fundamentally an anonymous function used to create a delegate or an expression tree. The general syntax follows this pattern:
(input-parameters) => expression
Parameter Types and Inference
When the target delegate type is known (e.g., a specific Func or Action), the compiler can infer the parameter types, allowing you to omit them entirely.
- Explicit Types:
(int x, string s) => s + x - Inferred Types:
(x, s) => s + x - Single Parameter: Parentheses can be omitted for single parameters:
x => x * 2
Expression vs. Statement Lambdas
An expression lambda consists of a single expression and returns its result. A statement lambda contains a block of code enclosed in braces {}, useful for multi-line logic.
// Expression lambda (return type inferred)
Func<int, int> square = x => x * x;
// Statement lambda (requires return keyword)
Func<int, string> stringify = x =>
{
Console.WriteLine($"Processing {x}");
return x.ToString();
};
Predefined Delegates: Action and Func
Rather than defining custom delegate types for every scenario, the .NET Framework provides generic delegates: Action for methods without a return value and Func for methods that return a value.
The Action Delegate
Action encapsulates a method that accepts parameters but returns void. There are overloads ranging from zero to sixteen parameters.
// Action with no parameters
Action logStart = () => Console.WriteLine("Process started.");
// Action with two parameters
Action<string, int> logDetails = (name, count) =>
{
Console.WriteLine($"Item: {name}, Quantity: {count}");
};
logStart();
logDetails("Widget", 20);
The Func Delegate
Func represents a method that returns a value. The last type parameter in the definition always specifies the return type.
// Func with no input parameters, returning a boolean
Func<bool> isConnected = () => true;
// Func taking two integers and returning an integer
Func<int, int, int> calculateDifference = (a, b) => a - b;
int diff = calculateDifference(10, 4); // Returns 6
Practical Code Examples
The following examples demonstrate the progression from verbose syntax to modern concise lambda usage, including the use of anonymous types.
Delegate Evolution
public delegate void NotificationHandler(string message);
public void ShowExamples()
{
// 1. Standard Delegate instantiation (C# 1.0 style)
NotificationHandler handler1 = new NotificationHandler(DisplayMessage);
handler1("Standard Method Call");
// 2. Anonymous Method (C# 2.0 style)
NotificationHandler handler2 = delegate(string msg)
{
Console.WriteLine($"Anonymous: {msg}");
};
handler2("Inline Method");
// 3. Lambda Expression (C# 3.0+ style)
NotificationHandler handler3 = msg => Console.WriteLine($"Lambda: {msg}");
handler3("Concise Syntax");
}
void DisplayMessage(string text)
{
Console.WriteLine($"Method: {text}");
}
Using System Delegates (Action & Func)
public void ExecuteSystemDelegates()
{
// Action: Perform a task with no return value
Action<int> printSquare = val =>
{
Console.WriteLine($"The square of {val} is {val * val}");
};
printSquare(7);
// Func: Calculate a value
Func<double, double, double> calculateArea = (width, height) => width * height;
double area = calculateArea(5.5, 3.0);
Console.WriteLine($"Calculated Area: {area}");
}
Anonymous Types and Var
public void DemonstrateAnonymousTypes()
{
// Creating an anonymous type using 'var'
var userProfile = new
{
Username = "dev_user",
Role = "Administrator",
LastLogin = DateTime.Now
};
Console.WriteLine($"User: {userProfile.Username}, Role: {userProfile.Role}");
// Note: Properties of anonymous types are read-only.
// userProfile.Username = "new_name"; // Compiler Error
// Using dynamic to bypass compile-time checking
dynamic data = new
{
Id = 101,
Description = "Sample Data"
};
Console.WriteLine($"Dynamic ID: {data.Id}");
// Accessing non-existent members throws a runtime exception
// data.NonExistentProperty;
}