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
static bool Greater<T>(T left, T right) where T : IComparable<T> =>
left.CompareTo(right) > 0;Avoid
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
static unsafe int SizeOf<T>() where T : unmanaged => sizeof(T);Avoid
static unsafe int SizeOf<T>() where T : struct => sizeof(T);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
static T Zero<T>() where T : struct => default;Avoid
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
IEnumerable<Employee> employees = LoadEmployees();
IEnumerable<Person> people = employees;Avoid
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
static string NameOf<T>(T person) where T : Person => person.Name;Avoid
static string NameOf(object person) => ((Person)person).Name;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
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
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
void IControl.Paint() => DrawControl();
void ISurface.Paint() => DrawSurface();Avoid
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
public interface ICustomer
{
decimal Discount() => 0m;
}
decimal Apply(ICustomer customer) => customer.Discount();Avoid
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
public ReadOnlyCollection<Order> Orders => _orders.AsReadOnly();Avoid
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
static T Sum<T>(IEnumerable<T> values) where T : INumber<T> =>
values.Aggregate(T.Zero, (total, value) => total + value);Avoid
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
sealed class ByName : Comparer<Person>
{
public override int Compare(Person? x, Person? y) =>
string.CompareOrdinal(x?.Name, y?.Name);
}Avoid
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
static T[] Empty<T>() => Array.Empty<T>();
var rows = Empty<Row>();Avoid
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
private readonly List<Memory<byte>> _chunks = new();Avoid
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
static void Retry(int attempts, Action operation)
{
for (int i = 0; i < attempts; i++) operation();
}Avoid
delegate void RetryOperation();
static void Retry(int attempts, RetryOperation operation);Corpus: Lambda expressions, delegates, and events · Generic delegates for manipulating arrays and lists