No Cast Interface To Concrete
ID |
csharp.no_cast_interface_to_concrete |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
CSharp |
Tags |
abstraction, cast, code_smell, coupling |
Description
Reports a downcast from an interface reference to the concrete class or struct behind it, written
either as (MyClass)value or as value as MyClass.
Rationale
A cast like this is a design signal: the code holds the interface because that is what it was handed, but it wants something the interface does not offer. The honest fix is to add the member to the interface, or to declare the concrete type in the signature so callers know what is required. Reaching past the abstraction at the point of use ties that line to one implementation, which is the coupling the interface existed to prevent.
It is also a failure waiting for a second implementation. The compiler accepts the downcast on
trust; the moment a different implementation, a decorator or a test double travels the same path,
(MyClass)value throws InvalidCastException. The as form is worse in one respect: it yields
null instead of failing, so the error surfaces later and somewhere else.
Reporting is confined to types declared in the file under analysis, and only when the source resolves to an interface and the target to a class or struct. Interface-to-interface conversions, upcasts from a concrete type, and expressions whose type cannot be resolved are not reported.
public interface IShape
{
double Area();
}
public class Circle : IShape
{
public double Radius;
public double Area() { return 3.14 * Radius * Radius; }
}
public interface ISolid : IShape
{
double Volume();
}
public class Report
{
public double RadiusOf(IShape shape)
{
Circle circle = (Circle)shape; // FLAW — the abstraction lacks what the caller needs
return circle.Radius;
}
public double VolumeOf(IShape shape)
{
ISolid solid = (ISolid)shape; // OK, interface to interface
return solid.Volume();
}
public double AreaOf(Circle circle)
{
IShape shape = circle; // OK, no downcast is needed
return shape.Area();
}
}
Limitations
Both the interface and the concrete type it is cast down to must be declared in the file under analysis, because that resolution does not cross files. In a codebase organized around the usual interface/implementation split — the interface in one file, each implementation in its own — the two types this rule needs to see are rarely declared together, so most real casts of this shape fall outside what it can catch. A clean run says only that no such cast collided with a same-file declaration, not that the codebase has none.
Remediation
Add the member the caller needs to the interface, so no cast is required. Where the member belongs
only to one implementation, take the concrete type as the parameter type and let the signature
state the dependency. Where the code genuinely handles several shapes of value, use pattern
matching (if (shape is Circle circle)) so the non-matching case is handled explicitly instead of
throwing.