Functions in C#
Learn how to define and call functions in C# - parameters, default and named arguments, scope, recursion, and higher-order functions with lambdas and LINQ, all runnable with Docker
Functions are how you wrap a piece of logic in a name so you can call it again and again instead of repeating yourself. In C# the line between a “function” and a “method” is mostly about where it lives: a function declared inside a class is a method, while modern top-level programs let you write local functions that read like standalone functions. Both share the same shape — a return type, a name, a parameter list, and a body.
As a multi-paradigm language, C# treats functions from several angles at once. On the object-oriented side you have instance and static methods bound to types. On the functional side, functions are values: the Func<> and Action<> delegate types let you store a function in a variable, pass it as an argument, and return it from another function. This is what powers lambda expressions and the LINQ query methods that are everywhere in idiomatic C#.
Because C# is statically and strongly typed, every parameter and return value has a type the compiler checks before your program runs. That means a mismatched argument, a forgotten return, or the wrong return type is caught at compile time rather than blowing up at runtime. In this tutorial you’ll define functions, give parameters default and named values, explore local versus shared scope, write recursive functions, and pass functions around as first-class values — each as a complete, runnable program.
Defining and Calling Functions
A function declares its return type, a name, a parenthesized parameter list, and a body in braces. Use return to hand a value back to the caller; a function that returns nothing has the return type void. For short one-liners, the => expression-bodied syntax drops the braces and the return keyword entirely.
Create a file named Program.cs:
| |
These are local functions — declared directly inside the top-level program. C# lets you call them before they appear in the file, because the compiler reads all declarations in a scope before running any statements.
Run it:
| |
Expected output:
=== Functions ===
Add(3, 4) = 7
Square(5) = 25
Default and Named Arguments
A parameter can carry a default value, letting callers omit it when the default is fine. Named arguments let you pass a value by writing parameterName: value, which both documents the call and lets you skip over earlier optional parameters without supplying placeholders.
Create a file named Program.cs:
| |
The last call is where named arguments earn their keep: FormatPrice(100m, decimals: 0) overrides only decimals while leaving currency at its default "USD", with no need to repeat the currency in the call.
Run it with the same Docker command shown above. Expected output:
25
1024
USD 19.95
EUR 8.5
USD 100
Variable Scope
A variable declared inside a function is local: it exists only while that call runs and is invisible to the rest of the program. Each call gets its own fresh copies. When you need a value that several functions share, put it in a class as a const or static member rather than reaching for mutable global state.
Create a file named Program.cs:
| |
If you tried to read subtotal outside CalculateTotal, the program would fail to compile — the name simply does not exist in that scope. That is the static type system protecting you from a common class of bugs.
Run it with the same Docker command. Expected output:
32.4
Tax rate is 0.08
Recursion
A recursive function calls itself, breaking a problem into a smaller version of the same problem until it hits a base case that stops the recursion. C# handles recursion naturally, and the type checker verifies that the parameter and return types line up at every level of the call chain.
Create a file named Program.cs:
| |
Factorial returns long because factorials grow quickly and would overflow a 32-bit int for larger inputs. Every recursive function needs a base case — here n <= 1 and n < 2 — otherwise it would call itself forever.
Run it with the same Docker command. Expected output:
Factorial(5) = 120
Fibonacci(10) = 55
Higher-Order Functions, Lambdas, and LINQ
This is where C#’s functional side shows through. Functions are first-class values: the Func<...> delegate type represents a function that returns a value, and Action<...> represents one that returns nothing. A lambda (x => x * 2) creates such a function inline. A higher-order function is simply one that takes or returns another function — and the entire LINQ library is built on exactly this idea.
Create a file named Program.cs:
| |
ApplyTwice(doubler, 5) calls doubler on 5 to get 10, then on 10 to get 20. The Where and Select calls hand a lambda to LINQ, which applies it to each element — letting you parameterize behavior, not just data.
Run it with the same Docker command. Expected output:
doubler(8) = 16
multiply(6, 7) = 42
ApplyTwice(doubler, 5) = 20
HELLO
Evens: 2, 4
Squares: 1, 4, 9, 16, 25
Running with Docker
Each example in this tutorial is a complete program. Save it as Program.cs and run it with the official .NET SDK image — no local install required.
| |
The command preserves your Program.cs, scaffolds a temporary project around it, compiles, and runs it.
Key Concepts
- Functions vs methods — a function declared inside a class is a method; modern top-level programs let you write local functions that read like standalone functions. Both share the same return-type/name/parameters/body shape.
- Return types are explicit and checked — every function declares what it returns (or
voidfor nothing), and the strong, static type system catches mismatched arguments and missing returns at compile time. - Expression-bodied syntax — the
=>form (int Square(int n) => n * n;) is a concise alternative to a full braced body for single-expression functions. - Default and named arguments — parameters can carry default values, and named arguments (
decimals: 0) let callers override just the arguments they care about while keeping the other defaults. - Local scope by default — variables inside a function exist only for that call; share values through
constorstaticmembers on a class rather than mutable globals. - Recursion needs a base case — a function that calls itself must have a stopping condition, or it will recurse indefinitely; choose a return type wide enough (like
long) to avoid overflow. - Functions are first-class values —
Func<>andAction<>delegates plus lambdas let you store functions in variables and pass them to higher-order functions, the foundation of LINQ’sWhere,Select, and friends.
Running Today
All examples can be run using Docker:
docker pull mcr.microsoft.com/dotnet/sdk:9.0
Comments
Loading comments...
Leave a Comment