Modern C# · Lesson 7

Generic methods

Preserve a caller’s type through reusable logic.

One operation can preserve several concrete types

The identity operation returns the value it receives without changing the value’s type. Writing it with a type parameter expresses that relationship once: an input of T produces an output of the same T. The sample uses an integer and a string to show that reuse does not require weakening the signature to object. A useful generic API preserves meaningful relationships between types, rather than merely replacing a concrete type name with a letter to make a method look more reusable.

Follow type inference at both call sites

At Identity(42), the compiler infers the type argument as int, and the returned value prints as 42. At Identity("rune"), it infers string, and the result prints as rune. The method body simply returns its parameter in both cases. The output order follows the two sequential WriteLine calls; no collection, background work, or conversion pipeline is involved. You can make the type arguments explicit while exploring, but doing so should not change these results.

T Identity<T>(T value) => value;

The signature is a promise about capabilities

An unconstrained type parameter does not grant every operation a concrete type might support. Identity can return its argument because that operation is valid for any accepted T. It cannot simply add one to T, read a Count property, or call an arbitrary application method. Those operations would need an appropriate capability contract, such as a constraint or a supplied function. The compiler checks the generic body against its declared guarantees rather than assuming every future caller will happen to pass a convenient type.

Identity does not mean cloning

Passing a class instance through Identity returns the same reference value; it does not construct a second independent object. Passing a struct returns a value copy under the ordinary calling convention, but references inside that value remain references. Consequently the generic identity operation cannot promise deep independence from its input. This is a common confusion when a helper’s mathematical-looking name sounds stronger than its actual signature. Explain both the preserved type and the ownership behavior before using a generic helper as part of a snapshot or copying API.

Practice: preserve the inferred result type

Assign the integer result to an int variable and the string result to a string variable, then use an operation specific to each type outside Identity. For the string, inspect Length; for the integer, add a fixed amount. Next deliberately try an incompatible assignment and inspect the compiler diagnostic with Check. This separates the generic method’s minimal body from the concrete operations available after each call has produced a result whose type the compiler knows.

  • Predict the inferred T for every call before adding an explicit type argument as a verification step.
  • Restore the valid assignments after examining a diagnostic so the original example remains runnable.

Practice: compare a value and a reference input

As an independent exercise, pass a mutable list through Identity, keep both variables, and append an item through the returned reference. Explain why both variables observe the change. Then pass a small struct with an integer field, modify the returned value, and compare the original field. These experiments show why one generic implementation can preserve type safety while the concrete type still determines copying and aliasing behavior. Avoid introducing a conversion to object merely to bypass the distinction.

  • Use a new list and a new struct for the experiment so earlier mutations cannot affect the comparison.
  • If a new generic operation needs comparison or construction, state that requirement explicitly rather than guessing an implementation.

Measure reuse by the relationship it expresses

A well-chosen generic method removes repetition while retaining a precise contract for its callers. Identity is intentionally simple enough to expose that contract without unrelated algorithms. More elaborate generic code should still make clear which types relate, which capabilities are required, and whether the result shares storage with the input. The browser runtime can execute these ordinary language features, but that does not imply every reflection-heavy or native-dependent generic library is available. Keep claims about language typing separate from claims about platform libraries.

  • Baseline output is 42 followed by rune.
  • Type inference is determined separately at the integer and string call sites.
  • Generic type safety does not imply cloning, immutability, or unrestricted operations on T.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

Console.WriteLine(Identity(42));
Console.WriteLine(Identity("rune"));
T Identity<T>(T value) => value;

Expected output

42
rune
Try this
A type parameter describes what stays related across a call.

Further reading