No Only Private Ctors
ID |
csharp.no_only_private_ctors |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
CSharp |
Tags |
api-design, code_smell, instantiation |
Description
Reports classes whose every declared instance constructor is private, so the class can
neither be instantiated nor derived from, while it still declares instance members that no
code can reach. A constructor with no access modifier counts as private, since that is the
C# default for class members.
Legitimate uses of a private constructor are excluded:
-
staticandabstractclasses, andpartialclasses whose other part may expose the factory; -
classes with a primary constructor, with any non-private constructor, or with no declared instance constructor at all — whether none is declared or the only one is
static; -
classes that actually call their own private constructor — a
new ThisClass(…)anywhere the private constructor is reachable, which covers singletons, smart enums and nested builders; the C# 9 target-typed form (Instance = new();,Instance ⇒ new();) is recognised too; -
classes deriving from a native handle base such as
SafeHandle, whose instances the runtime materialises through P/Invoke marshalling; -
classes whose members are all
static— those are reported bycsharp.static_classwith the more useful advice.
Rationale
Nothing outside the class can call a private constructor, and no derived class can chain to
it, so an instance can only be created by code that sits inside the class or its container.
When no such code ever calls it, the class’s instance methods, properties and events are
unreachable: they read as live API but can never run. This normally means one of two things
went wrong — the constructor was supposed to be public (or protected for a base class),
or the members were supposed to be static.
public class Report // FLAW - nothing can build a Report
{
private Report() { }
public string Title { get; set; }
public void Render() { }
}
public class Session // OK - static factory hands out instances
{
private Session() { }
public static Session Open() { return new Session(); }
public void Close() { }
}
public class Cache // OK - singleton idiom
{
private static readonly Cache instance = new Cache();
private Cache() { }
public static Cache Instance => instance;
public void Clear() { }
}
public class TargetTypedSingleton // OK - C# 9 target-typed new()
{
public static readonly TargetTypedSingleton Instance = new();
private TargetTypedSingleton() { }
public void Clear() { }
}
public class Suit // OK - smart enum builds its own instances
{
public static readonly Suit Hearts = new Suit("hearts");
private Suit(string name) { Name = name; }
public string Name { get; }
}
internal sealed class NativeHandle : SafeHandle // OK - the runtime creates it via P/Invoke
{
private NativeHandle() : base(IntPtr.Zero, true) { }
protected override bool ReleaseHandle() { return true; }
}
public static class Paths // OK - static class
{
public static string Join(string a, string b) { return a + "/" + b; }
}
Remediation
Decide which of the two intents applies. If callers are meant to create instances, widen the
constructor to public (or protected when the class is a base type). If the class is a
pure helper, declare it static class and make its members static. If instances are meant
to be handed out under control, add the static factory or singleton accessor that does so.