A generic method is a method that declares its own type parameter independent of the type it lives in. You write the method once with a placeholder like T, and the compiler stamps out a working version for every element type the callers use, no casting and no duplication. This lesson covers how to declare them, how type inference figures out T at the call site, what happens when inference fails, and how generic methods interact with overload resolution.
The shape of a generic method is the same as a regular method, with one extra piece: a type parameter list in angle brackets right after the method name. Inside the method, that type parameter behaves like any other type.
Pick<T> reads as "Pick, parameterized by some type T." Inside the method, T is the element type of the list and the return type. When the call is Pick<string>(products), the compiler treats T as string for that call, so the method behaves as if you'd written static string Pick(List<string> items). When the call is Pick<decimal>(prices), T becomes decimal. One declaration, two specializations the compiler manages for you.
The _Generic Classes_ lesson covered generic classes, where the type parameter belongs to the class. Generic methods are the same idea moved down one level: the type parameter belongs to the method, and a method can be generic even when its enclosing class isn't.
The naming convention is the same as for generic classes. Use T for a single type parameter. When you need more than one, use descriptive names: TKey, TValue, TInput, TResult, TItem. Single-letter alternatives like U and V show up in older code, and the compiler accepts anything, but readers find descriptive names easier to follow.
Here's the same method written with explicit calls and with type inference, side by side.
Both calls behave identically. The second form drops the <string> because the compiler can deduce T from the argument's static type. Inference is what makes generic methods feel natural in everyday C# code.
When you call a generic method, you usually don't write the type arguments. The compiler infers them from the argument types you pass in. For a method like Pick<T>(List<T> items), the compiler walks the parameter list, sees that the first parameter expects List<T>, observes that the caller passed a List<string>, and concludes T = string. The whole process happens at compile time and produces the same result as if you'd written Pick<string>(...) by hand.
The inference here is doing a small bit of bookkeeping. The compiler sees two parameters that both mention T: List<T> and Func<T, bool>. From List<decimal>, it pins T = decimal. It then checks that the lambda p => p < 5m is compatible with Func<decimal, bool>, and since p infers as decimal and < is defined on decimal, everything lines up.
Type inference is purely a compile-time activity. At runtime, the generic method is called with concrete types just like a hand-written specialization, so there's no inference overhead at runtime.
Inference fails when the compiler can't pin down T from the arguments alone. The common case is a method whose T only appears in the return type.
CreateDefault<T> takes no arguments. There's nothing for the compiler to look at: T only appears in the return type, and the compiler doesn't infer from what you assign the result to. The where T : new() clause is a generic constraint, which says T must have a public parameterless constructor. The _Generic Constraints_ lesson covers constraints in full; we're using one here just to make the example compile.
The fix is to write the type argument explicitly: CreateDefault<List<string>>(). When inference can't help, you fall back to spelling the type out. The error code to remember is CS0411, which always means "the compiler can't figure out one or more of your type arguments."
There's a second case where inference fails: ambiguity. If the compiler sees the type parameter in two places and the two argument types disagree on what T should be, inference also fails.
The first two calls infer T cleanly because both arguments agree. The commented-out call fails because the compiler would need to pick T = int and T = string at the same time, which it won't do. Stating the type argument as object resolves the ambiguity: both 1 and "two" are valid object values, so the call compiles. C# 12 added a small improvement here for collection expressions, but the basic rule has been the same since C# 2: when inference is ambiguous, write the type argument.
The most common place a generic method lives is inside a regular, non-generic class. The class itself doesn't need to know about the type parameter; only the method does. Utility classes like System.Math and helper classes in your own code often follow this shape.
The Helpers class itself is not generic. It declares two static methods that happen to be generic. Each method owns its own T: the T of Swap and the T of FindFirst have nothing to do with each other, and a call to one doesn't fix the other. The class is just a container for related methods, and the type parameters live entirely on the methods.
This pattern is common for generic-method use in everyday C#. LINQ is the largest example: every method on Enumerable (such as Where<T>, Select<TIn, TOut>, and OrderBy<T, TKey>) is a static generic method on a non-generic static class. You almost never write Enumerable.Where<string>(...) explicitly, because inference handles it.
Generic methods that operate on value types do not box. Swap<int>(...) moves the actual int values around without ever wrapping them in object. A non-generic version that worked through object parameters would box every value-type argument and unbox every read, adding allocations and CPU cost. This is one of the main reasons generics exist.
A generic class can declare a method with its own type parameter on top of the class's parameter. The class's T is fixed when the class is constructed; the method's parameter is fresh for every call. The result is "two sets of type parameters" that don't interfere with each other, which is exactly what you need when a method transforms one type into another.
Catalog<Product> pins the class-level T to Product. The method MapTo<U> introduces a separate parameter U, which is chosen per call. The first call infers U = decimal from the lambda's return type; the second infers U = string. The catalog instance and its T don't change between calls, only U does.
If a method's type parameter happens to share a name with the class's, the method's version shadows the class's inside the method body, and the compiler warns. The fix is simple: pick a different name. Using descriptive names like TItem for the class and TResult for the method avoids the collision and reads better.
The diagram below shows how the two type parameters fit together at a call site.
The class's T is pinned when you construct the instance. The method's U is pinned when you call the method. Both parameters are visible inside the method body. The class parameter stays the same across many calls; the method parameter can change each time.
A method can declare more than one type parameter. The common case is a transformation method that maps one type to another, where the input and output types are independent. The standard signature is TResult Map<TInput, TResult>(TInput source, Func<TInput, TResult> fn), and you'll recognize the shape from Enumerable.Select.
The method declares two type parameters in its type-parameter list: <TInput, TResult>. The compiler infers TInput from the list element type and TResult from the lambda's return type. The two parameters move together at the call site even though they're conceptually independent: TInput describes what comes in, and TResult describes what goes out.
The order of the parameters in the angle-bracket list matters only when you write them explicitly. The call Mapper.Map<Product, ProductSummary>(catalog, ...) says "TInput is Product, TResult is ProductSummary," in that order. If you write them swapped, you get a compiler error about argument types not matching.
There's no upper limit on how many type parameters a method can declare, but more than three is rare in practice and usually a signal to rethink the API. Two parameters cover transformations; three cover things like "key, value, result" lookups (TryGet<TKey, TValue, TResult> style). Beyond that, named option records or dedicated types read better than a long parameter list.
Three type parameters carry the data through three stages: the input element type, the running accumulator, and the final shape. Inference handles all three because every parameter type mentions a different T and the compiler can pin each one from the corresponding argument. If even one of the three appeared only in the return position with no input that mentioned it, you'd be back to writing the type arguments by hand.
A method-group conversion to Func<> allocates a small delegate object on the heap. For one-shot calls this is negligible, but inside a hot loop it adds up. Use a stored Func<> field or a static lambda when you'd otherwise create the same delegate millions of times.
A class can have a non-generic method and a generic method with the same name. The compiler picks between them at every call site using its overload resolution rules, and the rules favor non-generic methods when both are applicable.
The first call passes an int, and both overloads are applicable: Add(int) matches directly, and Add<T> matches with T = int. The compiler picks Add(int) because non-generic overloads beat generic ones when both are applicable. The second and third calls have no non-generic match, so the generic overload wins. The fourth call uses explicit type arguments, which forces the compiler to look only at the generic method.
The rule in plain language: a non-generic method that is applicable always wins over a generic method. A more specific overload wins over a less specific one. If two generic overloads tie on specificity, the call is ambiguous and you get an error.
The decision flow is short. The compiler first looks for a non-generic match that takes the exact argument type. If it finds one, that's the call. If it doesn't, it tries the generic overload and runs inference. If inference succeeds, that's the call. If neither works, it's an error.
A subtler case shows up with implicit conversions. If you pass a short to a class that has both Add(int) and Add<T>, the non-generic Add(int) still wins because the implicit conversion short -> int is an exact-enough match in C#'s preference list. The generic version is only chosen when no non-generic overload is applicable at all, even via implicit conversion.
Local functions can be generic too. A local function is a method declared inside another method, and it can carry its own type parameters that are separate from the enclosing method's. This is useful when you want a small helper that's only meaningful inside one method's body, without polluting the class with another public surface.
Reduce<T, TResult> is declared inside Report and is invisible anywhere else. The static keyword on the local function says "this function doesn't capture any local variables from the enclosing method," which means the compiler can emit it as a plain static method without an allocation. Non-static local functions can capture locals, but they may allocate a small closure object to hold the captured state.
Generic local functions are a good fit when:
If the helper grows or starts being reused, promote it to a private static method on the class. The transition is mechanical: the signature stays the same, you just move it out of the method body.
A static local function never allocates. A non-static local function may allocate a closure object the first time it's invoked, if it captures any locals. The JIT often inlines local functions, so the call itself is usually free.
The patterns above show up repeatedly in real code. Here are a handful of small generic methods that combine inference, multiple type parameters, and constraints in ways that come up day to day.
MostExpensive declares two type parameters: T for the element type and TKey for whatever value the selector returns. The constraint where TKey : IComparable<TKey> tells the compiler that whatever TKey is, it must support CompareTo. Constraints are the subject of the _Generic Constraints_ lesson, so for now the takeaway is that they let the compiler verify the operations the method body uses. Without the constraint, calling bestKey.CompareTo(...) wouldn't compile because TKey is otherwise an unknown type with only the methods of object.
GroupBy infers both T and TKey from the list and the lambda. The call CatalogTools.GroupBy(catalog, p => p.Stock > 0) pins T = Product and TKey = bool. The return type is Dictionary<bool, List<Product>>, which the compiler builds from those inferences. notnull on TKey is another constraint, again the topic of the _Generic Constraints_ lesson; for a dictionary key it's needed because the underlying Dictionary<TKey, TValue> doesn't allow null keys.
A few small habits make generic methods easier to read and easier for the compiler to help you with.
Put the most-inferable parameter first. When type inference walks the parameter list, it pins type parameters from the first argument that mentions them. If you have Map<TIn, TOut>(Func<TIn, TOut> fn, List<TIn> source) versus Map<TIn, TOut>(List<TIn> source, Func<TIn, TOut> fn), both compile, but the second reads more naturally because the source data leads and the projection follows. Most LINQ methods follow this order for exactly that reason.
Name type parameters descriptively when there's more than one. T is fine for one. T and U work for two if they're truly symmetric. The moment they have different roles, give them names: TInput, TResult, TKey, TValue, TState. The names show up in IntelliSense and in error messages, and a reader who sees Map<TInput, TResult> knows immediately which is which.
Prefer inference over explicit type arguments at call sites. Writing Pick<Product>(products) when Pick(products) would compile is noise. Explicit type arguments are useful when inference fails or when the implicit choice would be wrong, but in everyday code the compiler does the right thing.
Hide ugly type machinery behind helper methods. If a generic method needs three type parameters and a complicated constraint, callers shouldn't have to spell that out every time. Add a thin wrapper for the common case, and let the wrapper do the explicit work once. This is what Enumerable.OfType<T> does: the underlying logic is general, but the public surface is one method with one type argument.
Don't make a method generic just because you can. If T only ever appears in one place and the method always operates on object, the generic version isn't buying anything. Make it generic when callers would otherwise cast, when value types would otherwise be boxed, or when the API is genuinely meant to work across types.
10 quizzes