← Open in the full interactive course (progress tracking, search & more)

A single method can be generic on its own — even inside a class that isn't generic at all.

So far, every type parameter you've written has belonged to a class — Box<T>, Repository<T>, Pair<T1, T2>. But what if you just need one method to work generically, inside an otherwise perfectly ordinary, non-generic class? Making the whole class generic for the sake of one method would be overkill. C# has a lighter-weight tool for exactly this: the generic method.

In this lesson, you'll learn how to declare type parameters directly on a method (not the class), how the compiler infers those type arguments automatically at the call site most of the time, and when a method needs its own type parameter versus reusing its containing generic class's.

What Is It?

The Simple Explanation

A generic method is a method that declares its own type parameter, separate from — and independent of — whatever class it lives in. The type parameter only exists for the duration of that one method call; it has nothing to do with the class as a whole.

The Technical Definition

You declare a generic method by placing type parameters in angle brackets between the method name and its parameter list: public T Method<T>(T value). The type parameter is scoped to that single method — it can be used in the method's parameters, return type, and body, but nowhere else in the class. Most of the time, you never have to specify the type argument explicitly at the call site — the compiler figures it out through type inference, by looking at the arguments you actually pass.

public class Printer // ← an ordinary, non-generic class { // Print<T> is a generic method — T belongs to this method only public void Print<T>(T value) => Console.WriteLine(value); } var printer = new Printer(); printer.Print(42); // T inferred as int printer.Print("hello"); // T inferred as string printer.Print(3.14); // T inferred as double

Why Does It Exist?

The Problem

Suppose you have a plain, non-generic utility class — say, a class full of small helper methods — and just one of those methods needs to work with any type, such as a method that swaps the values of two variables:

// Without generics, you'd need one overload per type you care about public class Swapper { public void Swap(ref int a, ref int b) { (a, b) = (b, a); } public void Swap(ref string a, ref string b) { (a, b) = (b, a); } public void Swap(ref double a, ref double b) { (a, b) = (b, a); } // ...and one more for every other type you'll ever need to swap }

Making the entire Swapper class generic (class Swapper<T>) would be a poor fit here too — Swapper doesn't logically "belong to" one particular T the way Repository<T> belongs to one entity type. It's the method, not the class, that needs the flexibility.

The Solution

public class Swapper { public void Swap<T>(ref T a, ref T b) => (a, b) = (b, a); } var swapper = new Swapper(); int x = 1, y = 2; swapper.Swap(ref x, ref y); // T inferred as int Console.WriteLine($"{x}, {y}"); // 2, 1 string first = "A", second = "B"; swapper.Swap(ref first, ref second); // T inferred as string Console.WriteLine($"{first}, {second}"); // B, A

One method, one definition, works for every type — without dragging the whole class into being generic, and without a mountain of near-identical overloads.

Big Picture

WHERE DOES THE TYPE PARAMETER LIVE?
On the class
class Repository<T>
Every instance is "locked in" to one T for its whole lifetime — Repository<Product> only ever holds Product.
On the method
void Print<T>(T value)
T is decided fresh, per call — the same Printer instance can print an int, then a string, then a Product.
Class-level T = fixed per instance. Method-level T = fresh per call. Choose based on which lifetime matches your problem.

How It Works

FROM DECLARATION TO A FULLY-INFERRED CALL
1. DECLARE THE TYPE PARAMETER ON THE METHOD
public T First<T>(T[] items) => items[0];
2. CALL IT WITHOUT SPECIFYING THE TYPE — LET INFERENCE DO THE WORK
int[] numbers = [10, 20, 30];
int firstNumber = First(numbers);   // compiler infers T = int from the argument
3. SPECIFY THE TYPE ARGUMENT EXPLICITLY WHEN INFERENCE CAN'T (OR SHOULDN'T)
T CreateDefault<T>() => default!;

// No argument to infer T from — must be explicit
int defaultInt = CreateDefault<int>();
string? defaultText = CreateDefault<string>();

Simple Example

public static class ArrayHelpers // a plain static class — not generic itself { public static T Last<T>(T[] items) => items[^1]; public static bool Contains<T>(T[] items, T target) where T : IEquatable<T> => items.Any(item => item.Equals(target)); } int[] numbers = [3, 7, 12, 5]; int lastNumber = ArrayHelpers.Last(numbers); // T inferred as int → 5 bool hasSeven = ArrayHelpers.Contains(numbers, 7); // T inferred as int → true string[] names = ["Ana", "Ben", "Cara"]; string lastName = ArrayHelpers.Last(names); // T inferred as string → Cara

Code → Meaning → Result:

Real-World Example

A very common real-world case: an event system where the publisher class isn't generic — it just routes messages — but the method that raises a specific kind of event needs to be generic, so the same publisher can handle many different event payload types.

public record OrderPlacedEvent(int OrderId, decimal Total); public record UserRegisteredEvent(string Email); public class EventBus // an ordinary, non-generic class { private readonly Dictionary<Type, List<Action<object>>> _handlers = []; public void Subscribe<TEvent>(Action<TEvent> handler) { Type eventType = typeof(TEvent); if (!_handlers.TryGetValue(eventType, out var list)) _handlers[eventType] = list = []; list.Add(payload => handler((TEvent)payload)); } public void Publish<TEvent>(TEvent eventPayload) { if (_handlers.TryGetValue(typeof(TEvent), out var list)) foreach (var handler in list) handler(eventPayload!); } } var bus = new EventBus(); bus.Subscribe<OrderPlacedEvent>(e => Console.WriteLine($"Order {e.OrderId} placed for {e.Total:C}")); bus.Subscribe<UserRegisteredEvent>(e => Console.WriteLine($"Welcome email queued for {e.Email}")); bus.Publish(new OrderPlacedEvent(1001, 49.99m)); // Order 1001 placed for $49.99 bus.Publish(new UserRegisteredEvent("ana@example.com")); // Welcome email queued for ana@example.com

Notice EventBus itself is completely non-generic — it has no <T> on the class. Only Subscribe<TEvent> and Publish<TEvent> are generic, and each call independently infers its own TEvent from the payload you pass. This is exactly the shape real event/messaging systems in .NET use.

Analogy

A Fax Machine vs a Custom-Fitted Envelope

A generic class is like a custom-fitted envelope made for one specific document size — once made, that envelope only ever holds that size document. A generic method is more like a fax machine: the machine itself doesn't care what document you feed it — a letter, a contract, a photo — it figures out how to handle whatever's put in front of it, fresh, every single time, without being "configured" ahead of time for one document type.

Repository<T> is the envelope — one instance, one type, for its whole lifetime. Print<T>(T value) is the fax machine — the same instance handles a different T on every call, with no lasting commitment to any of them.

Under the Hood

HOW TYPE INFERENCE ACTUALLY WORKS
1. THE COMPILER MATCHES ARGUMENT TYPES TO THE METHOD SIGNATURE
2. INFERENCE CAN FAIL WHEN THERE'S NOT ENOUGH INFORMATION
3. GENERIC METHODS ARE JIT-INSTANTIATED JUST LIKE GENERIC CLASSES

Common Confusion

1. A generic method inside a generic class can use its own separate type parameter

If Repository<T> has a method like public TResult Convert<TResult>(Func<T, TResult> converter), that method has two type parameters in play: the class's own T (already fixed once the class is instantiated) and a brand-new TResult, scoped only to that one method call. Reusing the same letter for both (calling the second one T too) would actually hide the class's T inside that method — always give a method-level type parameter a distinct name if the class already has its own.

2. "Generic method" doesn't require a generic class, and vice versa

These are two orthogonal decisions. You can have a non-generic class with generic methods (this lesson's EventBus), a generic class with non-generic methods (a Repository<T> method that always returns int, unrelated to T), or both at once. Choose independently based on whether the flexibility belongs to the whole object's lifetime, or just to one call.

3. Explicit type arguments are sometimes clearer even when inference would work

Even when the compiler could infer T on its own, writing it explicitly (Last<int>(numbers)) is occasionally worth doing for readability at a call site where the type isn't obvious from context — it's never wrong to be explicit, only sometimes unnecessary.

Common Mistakes

Mistake 1 — Making the whole class generic when only one method needs it

Overkill — forces every caller to specify a type argument for the whole class, even though only one method actually uses it:

public class Printer<T> // unnecessary — nothing else in the class needs T { public void Print(T value) => Console.WriteLine(value); } var intPrinter = new Printer<int>(); var stringPrinter = new Printer<string>(); // now you need a separate instance per type!

Correct — make just the method generic, keep the class ordinary:

public class Printer { public void Print<T>(T value) => Console.WriteLine(value); } var printer = new Printer(); printer.Print(42); // one instance handles any T printer.Print("hello");

Mistake 2 — Reusing the class's type parameter name for a method-level one

public T Convert<T>(Func<T, T> f) inside a class already declaring T — the method's T silently shadows the class's T, which is confusing and easy to misread. Give the method's own type parameter a distinct, descriptive name, like TResult.

Mistake 3 — Expecting inference to work when a type parameter only appears in the return type

var value = CreateDefault(); — this won't compile if T only appears as CreateDefault<T>()'s return type; there's no argument for the compiler to infer from. Supply it explicitly: var value = CreateDefault<int>();

When Should I Use It?

Use a generic method when

Use a generic class instead when

Rule of thumb: Ask "does this type need to stay the same across every call on this object, or can it change from one call to the next?" A type that should stay fixed for the object's lifetime belongs on the class. A type that's free to vary per call belongs on the method.

Mental Model

Class-level T = fixed once, for the object's whole lifetime
Method-level T = decided fresh, on every single call
Type inference = the compiler works out T from your arguments — no <T> typing needed, most of the time

Remember:
· A non-generic class can still have generic methods.
· Give a method's own type parameter a distinct name if its containing class already has one.
· Inference needs a real argument to look at — a type parameter used only in a return type must be supplied explicitly.

Key Takeaway


Check Your Understanding

You've learned how a single method can be generic on its own. Let's check your understanding.

1. Can a completely non-generic class contain a generic method?

Show answer

Correct: B

Why B is correct: As demonstrated by Printer and EventBus, an entirely ordinary, non-generic class can contain one or more generic methods — the type parameter belongs to the method, not the class.

Why A is incorrect: This gets the relationship backwards — generic methods are independent of whether the class itself is generic.

Why C is incorrect: Both instance methods and static methods can be generic — static is unrelated to this capability.

Why D is incorrect: Generic methods commonly do have parameters — in fact, parameters using T are exactly what makes type inference possible.

Reinforcement: "Generic class" and "generic method" are independent, orthogonal choices — you can mix and match based on what the problem actually needs.

2. Given public T Last<T>(T[] items) => items[^1];, why does Last(numbers) compile without writing Last<int>(numbers)?

Show answer

Correct: B

Why B is correct: Type inference examines the actual argument (numbers, an int[]) and matches it against the parameter's declared type (T[]) to conclude T = int — entirely at compile time.

Why A is incorrect: Type parameters absolutely matter — they're checked and (for value types) specialized at compile/JIT time, not ignored.

Why C is incorrect: There's no such default — T would be inferred as string just as readily if you passed a string[] instead.

Why D is incorrect: This code compiles perfectly fine without the explicit argument — that's precisely what type inference enables.

Reinforcement: Type inference is a compile-time convenience — it doesn't change what the compiler checks, only how much you have to type.

3. Why does T CreateDefault<T>() require an explicit type argument at every call site, unlike T Last<T>(T[] items)?

Show answer

Correct: B

Why B is correct: Type inference works by examining the types of arguments you pass in. CreateDefault<T>() takes no arguments at all, so there's nothing for the compiler to look at — you must state the type argument yourself.

Why A is incorrect: Being static has no bearing on whether inference can work — the issue is purely about whether T appears in a parameter.

Why C is incorrect: Method naming has no effect on type inference whatsoever.

Why D is incorrect: This lesson specifically covers a case (a type parameter used only in a return type) where an explicit type argument genuinely is required.

Reinforcement: Inference needs at least one parameter that actually uses the type parameter — no usable parameter means no inference is possible.

4. You're designing a small utility class with one method that needs to work with any type, and every other member of the class has nothing to do with that type. What's the better design, per this lesson?

Show answer

Correct: B

Why B is correct: This is exactly the scenario the Printer/Swapper examples demonstrated — when the flexibility is genuinely scoped to one method, a generic method keeps the rest of the class simple and non-generic, and avoids forcing every caller to pick a type argument for the whole class.

Why A is incorrect: This was the "Mistake 1" example — it needlessly forces separate instances per type, even though nothing else in the class needs that.

Why C is incorrect: Duplicating per type reintroduces exactly the maintenance burden generics exist to eliminate.

Why D is incorrect: This throws away compile-time type safety and, for value types, introduces boxing — the exact problems the "Generics" lesson opened with.

Reinforcement: Match the scope of the type parameter to the scope of the actual need — method-level flexibility calls for a generic method, not a generic class.

You can now write flexible, type-safe methods without over-generalizing an entire class. Next: why unconstrained T is surprisingly limited, and the constraints that fix that.


dotnetmadeeasy.com — Learn C# and .NET, the right way.