C#idiomatic C#
generics-and-interfacesidioms

Idioms

Generics and interfaces

Type parameters and interfaces are how C# states a requirement, and both work best when the statement is exact.

Generics let one method serve many types, and interfaces let one type serve many callers. These are the decisions that keep both honest: what a constraint promises, what an interface exposes, and when a cast or an object parameter means the type was thrown away.

Constrain the type parameter instead of casting inside

Without a constraint the compiler knows only the members of System.Object, so a method that needs more has to cast, and a cast moves the failure from the compiler to the running program. where T : IComparable<T> states the capability the method uses, and every call site is checked against that statement.

Preferred

csharp
static bool Greater<T>(T left, T right) where T : IComparable<T> =>
    left.CompareTo(right) > 0;

Avoid

csharp
static bool Greater<T>(T left, T right) =>
    ((IComparable<T>)left).CompareTo(right) > 0;

Corpus: Constraints on type parameters · Generic types and methods

Choose `struct`, `class`, or `unmanaged` for what the method does

where T : struct promises a non-nullable value type and implies new(); where T : class promises a reference type, non-nullable in a nullable context; where T : notnull accepts either and only warns when a nullable type arrives. unmanaged adds the guarantee that the type holds no references, which is what sizeof and pointer code need. A struct constraint is not enough for that work, because it still allows fields that are references, so the compiler warns that the type may be managed.

Preferred

csharp
static unsafe int SizeOf<T>() where T : unmanaged => sizeof(T);

Avoid

csharp
static unsafe int SizeOf<T>() where T : struct => sizeof(T);

Corpus: Constraints on type parameters · Unmanaged types

Write `default` instead of `default(T)`IDE0034

The literal and the operator produce the same value: null for a reference type, a zeroed value for a value type. The style rule prefers the literal, which takes its type from the context, so the operator is left for the places where nothing else in the expression names the type.

Preferred

csharp
static T Zero<T>() where T : struct => default;

Avoid

csharp
static T Zero<T>() where T : struct => default(T);

Corpus: Default value expressions · IDE0034: Simplify 'default' expression

Use the variance an interface already declares

out T keeps T in output positions, which is why IEnumerable<Derived> converts to IEnumerable<Base>; in T keeps it in input positions, which is why IEqualityComparer<Base> converts to IEqualityComparer<Derived>. Variance belongs to the interface, not to the classes that implement it: List<T> implements the covariant IEnumerable<T> and is invariant itself, so List<Derived> still does not convert to List<Base>. Value types take no part in it, so IEnumerable<int> is not IEnumerable<object>.

Preferred

csharp
IEnumerable<Employee> employees = LoadEmployees();
IEnumerable<Person> people = employees;

Avoid

csharp
List<Employee> employees = LoadEmployees();
List<Person> people = employees;

Corpus: Covariance and contravariance in generics · Variance in generic interfaces

Take `T` instead of `object`

A parameter of type object throws the type away at the boundary, so the body casts back to what it needs and a mismatch surfaces when the cast runs. A type parameter carries the type through the call and back out, and a generic collection of value types stores them without boxing.

Preferred

csharp
static string NameOf<T>(T person) where T : Person => person.Name;

Avoid

csharp
static string NameOf(object person) => ((Person)person).Name;

Corpus: Generic types and methods · Generic types overview

Interfaces add capability, base classes carry stateCA1040

A class can implement many interfaces but inherit one base class, so an interface is the way to add a capability across unrelated hierarchies, and an abstract class is the place for shared fields, constructors, and non-public members. An interface with no members states no contract, so a marker for a group of types belongs in an attribute instead.

Preferred

csharp
abstract class Entity { protected int Id; }
sealed class Order : Entity, IComparable<Order>
{
    public int CompareTo(Order? other) => other is null ? 1 : Id.CompareTo(other.Id);
}

Avoid

csharp
public interface IEntity { }

Corpus: Interfaces: define behavior for multiple types · CA1040: Avoid empty interfaces

Implement interface members explicitly when names collide

One public member satisfies two interfaces that declare the same signature, so both callers arrive in the same code. An explicit implementation puts the interface name in front of the member: each interface gets its own body, nothing is added to the class surface, and the member is reachable only through the interface. Explicit members take no access modifier.

Preferred

csharp
void IControl.Paint() => DrawControl();
void ISurface.Paint() => DrawSurface();

Avoid

csharp
public void Paint() => DrawSurface();

Corpus: Explicit interface implementation · Interfaces: define behavior for multiple types

Treat default interface members as an upgrade tool

A default implementation lets an interface that is already in use gain a member without breaking the classes that implement it, and an implementer with different rules replaces the body. The member is not inherited by the class, so a caller needs a reference of the interface type to reach it, and two derived interfaces that override the same default make the call ambiguous for a class that implements both. Keeping each interface on one capability is what keeps those defaults apart.

Preferred

csharp
public interface ICustomer
{
    decimal Discount() => 0m;
}
decimal Apply(ICustomer customer) => customer.Discount();

Avoid

csharp
SampleCustomer customer = new();
customer.Discount();

Corpus: Safely update interfaces using default interface methods · Create mixin types using default interface methods

Do not return `List<T>` from a public APICA1002

List<T> is built for performance, not for inheritance: it has no virtual members, so its behavior cannot change later without breaking callers bound to the concrete type. Expose a type meant for the surface instead: ReadOnlyCollection<T> when callers may not change the contents, or Collection<T>, IList<T>, and ICollection<T> when they may. The rule looks at externally visible properties, return values, and parameters.

Preferred

csharp
public ReadOnlyCollection<Order> Orders => _orders.AsReadOnly();

Avoid

csharp
public List<Order> Orders => _orders;

Corpus: CA1002: Do not expose generic lists · When to use generic collections

Put operators in interfaces with static abstract members

A static abstract member of an interface is supplied by every implementing type, which is what lets a type parameter use an operator in its own body: a constraint to INumber<T> carries the + that the method calls. Use static virtual when one body fits most implementers and a few need their own. Constrain to the narrowest interface the algorithm uses, since a wide one leaves out types that support only part of it.

Preferred

csharp
static T Sum<T>(IEnumerable<T> values) where T : INumber<T> =>
    values.Aggregate(T.Zero, (total, value) => total + value);

Avoid

csharp
static int Sum(IEnumerable<int> values) => values.Sum();
static double Sum(IEnumerable<double> values) => values.Sum();

Corpus: Explore static virtual members in interfaces · Generic math

Reach for `IComparer<T>` when the ordering has a name

IComparer<T> is a type: it can sit in a field, arrive through a constructor, or be handed to SortedDictionary<TKey, TValue> and to the sorting and searching methods of the collection classes. Comparison<T> is a delegate, which is the right shape for an ordering written at the one call that needs it. When the same rule appears at a second call site, give it a comparer so it exists once.

Preferred

csharp
sealed class ByName : Comparer<Person>
{
    public override int Compare(Person? x, Person? y) =>
        string.CompareOrdinal(x?.Name, y?.Name);
}

Avoid

csharp
people.Sort((a, b) => string.CompareOrdinal(a.Name, b.Name));
orders.Sort((a, b) => string.CompareOrdinal(a.Name, b.Name));

Corpus: Generic interfaces · Comparisons and sorts within collections · Generic delegates for manipulating arrays and lists

Spell out the type argument when inference has nothing to work with

The compiler infers type arguments from the arguments of the call, never from the return type or from a constraint, so a method whose only mention of T is its result cannot be called without one. Writing it is also how the intended reading is stated when the argument alone leaves two readings open.

Preferred

csharp
static T[] Empty<T>() => Array.Empty<T>();
var rows = Empty<Row>();

Avoid

csharp
static T[] Empty<T>() => Array.Empty<T>();
var rows = Empty();

Corpus: Generic methods · Generic types and methods

A ref struct cannot be a type argument

Span<T> and ReadOnlySpan<T> are ref struct types, which live on the stack, so they cannot be type arguments for List<T>, Task<T>, or any other generic type that keeps its instances on the heap. From C# 13 a ref struct can be a type argument when the type parameter declares allows ref struct, and even then the parameter cannot be boxed or used after the call. A buffer that has to outlive the method or cross an await becomes Memory<T>.

Preferred

csharp
private readonly List<Memory<byte>> _chunks = new();

Avoid

csharp
private readonly List<Span<byte>> _chunks = new();

Corpus: ref struct types · Memory<T> and Span<T> usage guidelines

Use `Func<T>` and `Action<T>` before declaring a delegate

The built-in families cover the shapes a method parameter usually needs, with up to sixteen inputs and the return type last for Func<T>. A delegate type earns its declaration when the name says something the signature does not, as with an event the codebase refers to by name; one declared per method is a type callers have to learn for nothing.

Preferred

csharp
static void Retry(int attempts, Action operation)
{
    for (int i = 0; i < attempts; i++) operation();
}

Avoid

csharp
delegate void RetryOperation();
static void Retry(int attempts, RetryOperation operation);

Corpus: Lambda expressions, delegates, and events · Generic delegates for manipulating arrays and lists