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.
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.
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 doubleSuppose 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.
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, AOne method, one definition, works for every type — without dragging the whole class into being generic, and without a mountain of near-identical overloads.
class Repository<T>T for its whole lifetime — Repository<Product> only ever holds Product.
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.
T = fixed per instance. Method-level T = fresh per call. Choose based on which lifetime matches your problem.
public T First<T>(T[] items) => items[0];
<T> goes right after the method name — this scopes T to this single method only.int[] numbers = [10, 20, 30];
int firstNumber = First(numbers); // compiler infers T = int from the argument
int[]) and works backward to determine T must be int — you never had to type <int> yourself.T CreateDefault<T>() => default!;
// No argument to infer T from — must be explicit
int defaultInt = CreateDefault<int>();
string? defaultText = CreateDefault<string>();
T at all (like CreateDefault<T>() above), there's nothing for the compiler to infer from, so you must supply the type argument explicitly.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 → CaraCode → Meaning → Result:
ArrayHelpers itself is a plain, non-generic static class — only Last<T> and Contains<T> carry their own type parameters.int[] and string[], with the compiler inferring T fresh at every call site.where T : IEquatable<T> is a constraint — it's needed here so .Equals can be called meaningfully; constraints are the subject of the next two lessons.)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.comNotice 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.
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.
T Last<T>(T[] items), calling Last(numbers) where numbers is int[] means the compiler pattern-matches T[] against int[], concluding T = int. This happens entirely at compile time — there is no runtime "guessing."T CreateDefault<T>()), there are no arguments for the compiler to examine, so inference has nothing to work from — you must supply <T> explicitly at the call site.T, forcing you to resolve the ambiguity explicitly.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.
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.
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.
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"); 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.
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>();
Print(42) then Print("hi") on the same instance).Repository<Product> should always hold Product, call after call.T across many calls, not just within one method.<T> typing needed, most of the time<T> explicitly at the call site.T when the type should stay fixed for the object's entire lifetime; choose method-level T when it's free to vary from call to call.Publish<TEvent>, LINQ's Select<TSource,TResult> (later in this course), and countless utility methods.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?
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)?
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)?
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?
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.