Learn how classes define behavior: instance and static methods, parameters, return values, overloading, parameter modifiers (
ref,out,in,params), extension methods, and expression-bodied members.
Level: 🟢 Beginner Prerequisites: 05. Properties
- What is a Method?
- Instance Methods
- Static Methods
- Parameters
- Return Values
- Method Overloading
ref,out,inparams- Extension Methods
- Expression-Bodied Methods
- Real-World Example
- Interview Questions
By the end of this chapter you will be able to:
- Declare methods and read their signatures.
- Distinguish instance from static methods.
- Pass arguments by value, by reference (
ref), as outputs (out) and as read-only references (in). - Accept a variable number of arguments with
params. - Overload methods correctly and predict which overload is called.
- Write extension methods and understand what they really are.
- Use expression-bodied syntax appropriately.
A method is a named block of code inside a class or struct that performs an action. Methods define an object's behavior.
[access] [static] ReturnType MethodName(ParameterList)
{
// body
return value; // if ReturnType is not void
}
public int Add(int a, int b)
{
return a + b;
}| Part | Example | Meaning |
|---|---|---|
| Access modifier | public |
Who can call it |
| Return type | int |
What it gives back (void if nothing) |
| Name | Add |
PascalCase, usually a verb |
| Parameters | int a, int b |
Inputs |
| Body | { return a + b; } |
The work |
The method signature is the name plus the number, types and modifiers (ref/out/in) of its parameters. The return type is not part of the signature.
Belong to an object and can use that object's state through the implicit this.
public class Counter
{
private int _count;
public void Increment() => _count++; // reads and changes this object's state
public int GetCount() => _count;
}
var c = new Counter();
c.Increment();
c.Increment();
Console.WriteLine(c.GetCount()); // 2Belong to the type, are called through the type name, and have no this. They cannot directly access instance members.
public class Calculator
{
public static int Max(int a, int b) => a > b ? a : b;
}
Console.WriteLine(Calculator.Max(7, 4)); // 7More on static members in 13. Static Members.
Parameters are the inputs a method declares; arguments are the values the caller passes.
static string Greet(string name, string greeting = "Hello") // 'greeting' is optional
=> $"{greeting}, {name}!";
Console.WriteLine(Greet("Sam")); // Hello, Sam!
Console.WriteLine(Greet("Sam", "Welcome")); // Welcome, Sam!
Console.WriteLine(Greet(greeting: "Hi", name: "Maya")); // Hi, Maya! (named arguments)| Feature | Rule |
|---|---|
| Optional parameters | Need a compile-time constant default; must come after required ones |
| Named arguments | Pass by name in any order; improves readability |
| Default passing | By value: the method receives a copy of the argument |
For reference types, "by value" means the reference is copied, so the method can change the object but not the caller's variable. See 03. Objects.
static int Square(int n) => n * n; // returns one value
static void Log(string message) => Console.WriteLine(message); // returns nothing (void)
// Multiple values with a tuple
static (int Min, int Max) MinMax(int[] values)
{
int min = values[0], max = values[0];
foreach (var v in values)
{
if (v < min) min = v;
if (v > max) max = v;
}
return (min, max);
}
var (lo, hi) = MinMax(new[] { 4, 9, 1, 7 });
Console.WriteLine($"{lo} {hi}"); // 1 9returnends the method immediately (early return is fine and often clearer).- A non-
voidmethod must return a value on every code path.
Several methods can share a name if their signatures differ (number, types or order of parameters).
public class Printer
{
public void Print(int value) => Console.WriteLine($"int: {value}");
public void Print(double value) => Console.WriteLine($"double: {value}");
public void Print(string value) => Console.WriteLine($"string: {value}");
public void Print(string a, string b) => Console.WriteLine($"two strings: {a}, {b}");
}
var p = new Printer();
p.Print(5); // int: 5
p.Print(5.5); // double: 5.5
p.Print("hi"); // string: hi
p.Print("a", "b"); // two strings: a, bValid overloads differ by: parameter count, parameter types, or parameter order of different types.
Not valid: differing only by return type, parameter names, or ref vs out vs in against each other.
Overloading is resolved by the compiler at compile time (compile-time polymorphism, see 10. Polymorphism).
| Modifier | Meaning | Caller must initialize? | Method must assign? | Keyword at call site |
|---|---|---|---|---|
| (none) | Pass a copy | Yes | n/a | No |
ref |
Pass by reference, read and write | Yes | No | Required |
out |
Pass by reference, method outputs a value | No | Yes (before returning) | Required |
in |
Pass by reference, read-only | Yes | n/a (cannot modify) | Optional |
By value (default):
static void AddOne(int n) => n++; // changes only the local copy
int x = 5;
AddOne(x);
Console.WriteLine(x); // 5ref: two-way:
static void AddOne(ref int n) => n++;
int x = 5;
AddOne(ref x);
Console.WriteLine(x); // 6
static void Swap(ref int a, ref int b)
{
(a, b) = (b, a);
}out: extra outputs, the Try... pattern:
static bool TryDivide(int a, int b, out int result)
{
if (b == 0)
{
result = 0; // must assign on every path
return false;
}
result = a / b;
return true;
}
if (TryDivide(10, 2, out int quotient))
Console.WriteLine(quotient); // 5
if (!TryDivide(1, 0, out _)) // '_' discards an output you do not need
Console.WriteLine("Cannot divide by zero");The framework uses the same pattern: int.TryParse("42", out int n).
in: read-only reference, for large structs:
public readonly struct Vector3
{
public double X { get; }
public double Y { get; }
public double Z { get; }
public Vector3(double x, double y, double z) { X = x; Y = y; Z = z; }
}
static double Length(in Vector3 v) => Math.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z);
Console.WriteLine(Length(new Vector3(1, 2, 2))); // 3in avoids copying large value types while guaranteeing the method cannot modify them. For small structs and reference types it brings no benefit.
params lets a method accept any number of arguments as an array.
static int Sum(params int[] numbers)
{
int total = 0;
foreach (var n in numbers) total += n;
return total;
}
Console.WriteLine(Sum()); // 0
Console.WriteLine(Sum(1, 2, 3)); // 6
Console.WriteLine(Sum(new[] { 4, 5 })); // 9 (an array can also be passed directly)Rules: only one params parameter per method, and it must be last. Newer C# versions also allow params with other collection types such as ReadOnlySpan<T>.
An extension method adds a method to an existing type without modifying or inheriting it. It is a static method in a static class whose first parameter is marked this.
public static class StringExtensions
{
public static string Truncate(this string text, int maxLength)
=> text.Length <= maxLength ? text : text[..maxLength] + "...";
public static int WordCount(this string text)
=> text.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}
Console.WriteLine("Hello, world".Truncate(5)); // Hello...
Console.WriteLine("one two three".WordCount()); // 3What really happens: the compiler rewrites "one two three".WordCount() into StringExtensions.WordCount("one two three").
Key facts:
- Extension methods are not real members of the type; they cannot access its
privatemembers. - An actual instance method wins over an extension method with the same signature.
- The extension's namespace must be imported (
using) to be visible. - LINQ is built entirely from extension methods on
IEnumerable<T>.
For methods whose body is a single expression, use =>.
public int Square(int n) => n * n;
public string Describe() => $"{Name} ({Age})";
public void Log(string message) => Console.WriteLine(message);Use them for short, obvious logic. Prefer a regular block body when the method needs several statements.
- Methods are how objects do things; without them a class is just a data bag.
- Good method design keeps code readable, reusable and testable.
- Choosing the right parameter kind (
ref,out,in, value) communicates intent and avoids bugs. - Overloading gives callers a simple, consistent API (
Print(int),Print(string)). - Extension methods let you add convenience behavior to types you do not own.
public class Sample
{
// Instance method
public int Add(int a, int b) { return a + b; }
// Static method
public static int Max(int a, int b) => a > b ? a : b;
// Optional + named parameters
public string Greet(string name, string greeting = "Hello") => $"{greeting}, {name}";
// ref / out / in
public void Increment(ref int value) => value++;
public bool TryParse(string text, out int number) => int.TryParse(text, out number);
public double Length(in Vector3 v) => Math.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z);
// params
public int Sum(params int[] numbers) { /* ... */ return 0; }
// Overloads
public void Print(int value) { }
public void Print(string value) { }
}
// Extension method (separate static class)
public static class Extensions
{
public static bool IsEmpty(this string s) => s.Length == 0;
}public class Calculator
{
private int _lastResult;
public int LastResult => _lastResult;
// Instance method: updates this object's state
public int Add(int a, int b)
{
_lastResult = a + b;
return _lastResult;
}
// Static method: needs no object
public static int Max(int a, int b) => a > b ? a : b;
}
var calc = new Calculator();
Console.WriteLine(calc.Add(2, 3)); // uses an instance
Console.WriteLine(calc.LastResult);
Console.WriteLine(Calculator.Max(7, 4)); // uses the typeOutput
5
5
7
A PriceCalculator that combines overloads, params, an out parameter (Try... pattern) and expression-bodied methods.
public class PriceCalculator
{
private readonly decimal _taxRate;
public PriceCalculator(decimal taxRate) => _taxRate = taxRate;
// Overloads: same name, different signatures
public decimal Total(decimal price) => Total(price, 1);
public decimal Total(decimal price, int quantity)
=> price * quantity * (1 + _taxRate);
// params: any number of prices
public decimal TotalOf(params decimal[] prices)
{
decimal sum = 0m;
foreach (var price in prices)
sum += price;
return sum * (1 + _taxRate);
}
// Try-pattern with out
public bool TryApplyCoupon(decimal total, string code, out decimal discounted)
{
if (code == "SAVE10")
{
discounted = total * 0.90m;
return true;
}
discounted = total;
return false;
}
}
var calc = new PriceCalculator(0.10m);
Console.WriteLine($"{calc.Total(20m):F2}");
Console.WriteLine($"{calc.Total(20m, 3):F2}");
Console.WriteLine($"{calc.TotalOf(10m, 5m, 5m):F2}");
if (calc.TryApplyCoupon(calc.Total(20m, 3), "SAVE10", out var discounted))
Console.WriteLine($"Coupon applied: {discounted:F2}");
if (!calc.TryApplyCoupon(66m, "BAD", out _))
Console.WriteLine("Coupon invalid");Output
22.00
66.00
22.00
Coupon applied: 59.40
Coupon invalid
The Total(decimal) overload delegates to the more general one, so the logic lives in one place.
When you call an overloaded method, the compiler:
- Collects all methods with that name that are accessible.
- Keeps those whose parameters can accept the arguments (including implicit conversions).
- Chooses the best match (exact type match beats a conversion, non-
paramsform beatsparamsexpansion, and so on). - Reports an ambiguity error if no single best match exists.
p.Print(5); // Print(int) : exact match
p.Print(5.5); // Print(double) : exact match
p.Print('a'); // Print(int) : char converts implicitly to intBy value ref / out / in
┌────────┐ copy ┌────────┐
│ caller │ ──────► [n] │ caller │ ◄──────┐
│ x = 5 │ (local) │ x = 5 │ │ same variable,
└────────┘ └────────┘ │ accessed through a reference
method ┘
- By value: the method works on its own copy. For a reference type, the copy is the reference, so the object is shared but the caller's variable is not reassigned.
ref/out/in: the method works on the caller's variable itself.
An instance method receives a hidden first argument, this, that refers to the object. A static method receives none.
c.Increment(); // conceptually: Counter.Increment(this: c)
Calculator.Max(7, 4); // no 'this'The compiler copies the default value into each call site at compile time (like const, see 04. Fields). If you change a default in a library, callers keep the old value until recompiled. For public APIs that evolve, prefer overloads.
flowchart TD
A[Method needs input] --> B{Method must change the caller's variable?}
B -- No --> C{Large readonly struct, copy would be costly?}
C -- Yes --> D["in"]
C -- No --> E["pass by value (default)"]
B -- Yes --> F{Does the method need the incoming value?}
F -- Yes --> G["ref"]
F -- No --> H["out"]
classDiagram
class Calculator {
-int _lastResult
+int LastResult
+Add(int a, int b) int
+Max(int a, int b)$ int
}
class PriceCalculator {
-decimal _taxRate
+PriceCalculator(decimal taxRate)
+Total(decimal price) decimal
+Total(decimal price, int quantity) decimal
+TotalOf(decimal[] prices) decimal
+TryApplyCoupon(decimal total, string code, decimal discounted) bool
}
flowchart LR
Call["calc.Total(20m, 3)"] --> Resolve{Overload resolution}
Resolve -->|"1 argument"| O1["Total(decimal)"]
Resolve -->|"2 arguments"| O2["Total(decimal, int)"]
O1 --> O2
O2 --> R["return price * quantity * (1 + tax)"]
(Larger diagrams live in /diagrams.)
| value | ref |
out |
in |
params |
|
|---|---|---|---|---|---|
| Passes | Copy | Reference | Reference | Read-only reference | Array/collection of values |
| Method can modify caller's variable | ❌ | ✅ | ✅ | ❌ | ❌ |
| Caller must initialize first | ✅ | ✅ | ❌ | ✅ | n/a |
| Callee must assign | ❌ | ❌ | ✅ | ❌ | n/a |
| Keyword needed at call | ❌ | ✅ | ✅ | optional | ❌ |
| Typical use | Most parameters | Update/swap | Try... results, extra outputs |
Big readonly structs | Variable argument count |
| Aspect | Instance | Static | Extension |
|---|---|---|---|
| Declared in | Any class/struct | Any class/struct | Static class |
| Called on | An object | The type name | An object (syntax), static call (reality) |
Has this |
✅ | ❌ | Receives it as the first parameter |
| Access to private members | ✅ | Of its own type only | ❌ |
| Needs an object | ✅ | ❌ | ✅ (the extended value) |
| Can be overridden | ✅ (if virtual) |
❌ | ❌ |
| Use for | Behavior using object state | Stateless utilities | Adding helpers to types you do not own |
| Overloading | Overriding | |
|---|---|---|
| Same name | ✅ | ✅ |
| Signature | Different | Same |
| Where | Same class (or inherited) | Derived class replaces base virtual method |
| Resolved | Compile time | Run time |
| Covered in | This chapter, 10 | 10 |
- ❌ Expecting a value parameter to change the caller's variable.
AddOne(x)leavesxunchanged. Fix: return the result, or useref/out. - ❌ Confusing
refandout.refneeds an initialized variable and may read it;outneed not be initialized but must be assigned before returning. - ❌ Forgetting to assign an
outparameter on every path, including early returns. The compiler rejects it. - ❌ Overloading by return type only.
int Get()andstring Get()do not compile. Fix: rename or change parameters. - ❌ Ambiguous overloads with optional parameters. Two overloads that both accept the call cause an error or surprising choices. Fix: simplify, or use distinct names.
- ❌ Placing
paramsanywhere but last, or using more than one. Fix: oneparams, last. - ❌ Too many parameters. Long lists are error-prone. Fix: group related values into a type, or split the method.
- ❌ Boolean flag parameters like
Save(data, true, false). Unreadable. Fix: separate methods or an enum/options object. - ❌ Many
outparameters instead of a return type. Fix: return a tuple or a small type; keepoutmainly for theTry...pattern. - ❌ Assuming extension methods can reach private members, or expecting them to override instance methods. They cannot.
- ❌ Using
inwith small types or reference types and expecting a speedup. Fix: reserveinfor large readonly structs. - ❌ Relying on optional-parameter defaults in public library APIs that may change later. Fix: use overloads.
- Name methods with PascalCase verbs (
CalculateTotal,SendEmail);boolmethods read like questions (IsValid,CanShip). - Keep methods short and focused on one task.
- Keep parameter lists short (aim for four or fewer).
- Use guard clauses and early returns to reduce nesting.
- Prefer return values over
ref/out; useoutmainly forTryXxxpatterns. - Return tuples or small types instead of several
outparameters. - Use named arguments when a call has several same-typed or boolean arguments.
- Make a method
staticif it does not use instance state. - Put shared logic in one overload and let the others call it.
- Use extension methods sparingly, in clearly named static classes and namespaces.
- Use
=>for one-expression methods only.
Q1. What is a method signature?
The method name together with the number, types, order and modifiers (ref, out, in) of its parameters. The return type and parameter names are not part of the signature.
Q2. What is the difference between an instance method and a static method?
An instance method belongs to an object, receives this, and can use instance state. A static method belongs to the type, is called through the type name, has no this, and cannot directly access instance members.
Q3. What is method overloading, and what can differ between overloads?
Defining multiple methods with the same name but different signatures: a different number, types or order of parameters. Return type alone is not enough.
Q4. What is the difference between `ref` and `out`?
Both pass an argument by reference. With ref, the variable must be initialized before the call and the method may read and write it. With out, the variable need not be initialized, but the method must assign it before returning. Both keywords are required at the call site.
Q5. What does the `in` modifier do?
It passes an argument by read-only reference. The method cannot modify it. It avoids copying large structs; the in keyword at the call site is optional.
Q6. What does `params` do, and what are its restrictions?
It allows a caller to pass a variable number of arguments, which the method receives as an array (or another supported collection type). A method can have only one params parameter and it must be the last parameter.
Q7. What is an extension method?
A static method in a static class whose first parameter is prefixed with this, letting it be called with instance syntax on the extended type. It does not modify the type, cannot access private members, and an actual instance method with the same signature takes precedence.
Q8. If you pass an object to a method by value, can the method change the object?
Yes, it can change the object's state, because the copied reference points to the same object. It cannot make the caller's variable point to a different object unless the parameter is ref or out.
Q9. What is the `Try...` pattern?
A method such as int.TryParse(text, out int value) that returns a bool indicating success and delivers the result through an out parameter, avoiding exceptions for expected failures.
Q10. Can you overload a method by changing only the return type?
No. The compiler cannot choose an overload from the return type alone, so the signature must differ in its parameters.
Q11. What is the difference between overloading and overriding?
Overloading uses the same name with different parameters, resolved at compile time. Overriding replaces a base class virtual/abstract method with the same signature in a derived class, resolved at run time.
Q12. What are the risks of optional parameters?
Default values are compiled into the caller, so changing them in a library does not affect already-compiled callers until they are rebuilt. They can also create ambiguity with overloads. For evolving public APIs, overloads are safer.
| # | Problem | Difficulty | Solution |
|---|---|---|---|
| 1 | Create a MathHelper class with static methods Square, Cube and IsEven. |
🟢 | View → |
| 2 | Create a Counter class with instance methods Increment, Decrement and Reset, and a way to read the value. |
🟢 | View → |
| 3 | Overload a Print method for int, double, string and bool. Call each and note which overload runs. |
🟢 | View → |
| 4 | Write Swap(ref int a, ref int b) and demonstrate it. Then explain why it fails without ref. |
🟢 | View → |
| 5 | Write TryDivide(int a, int b, out int result) that returns false for division by zero. |
🟡 | View → |
| 6 | Write Average(params double[] values) that handles an empty call safely. |
🟡 | View → |
| 7 | Write a method returning a tuple (int Min, int Max, double Average) for an array. |
🟡 | View → |
| 8 | Create extension methods IsPalindrome(this string) and Capitalize(this string). |
🟡 | View → |
| 9 | Show that passing an object by value lets a method change the object, but reassigning the parameter does not affect the caller. Then fix it with ref. |
🟡 | View → |
| 10 | Design a ShippingCalculator with overloads and one shared core method; make each overload delegate to it. |
🟠 | View → |
Starter files: /exercises/06-methods
- A method defines behavior; its signature is name + parameter types/modifiers (not return type).
- Instance methods use object state; static methods belong to the type.
- Arguments are passed by value by default; for reference types the reference is copied.
- Use
ref(read/write),out(method outputs),in(read-only reference) deliberately. paramsaccepts a variable number of arguments; it must be the only and lastparamsparameter.- Overloading is resolved at compile time by signature, never by return type alone.
- Extension methods are static methods with
thison the first parameter; they add syntax, not true members. - Keep methods small, focused and well named.
⬅ Previous: Properties • 🏠 Main README • Next: Constructors ➡