No Changed Default In Override
ID |
csharp.no_changed_default_in_override |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
CSharp |
Tags |
code_smell, default-value, inheritance, surprising-behaviour |
Description
Reports a parameter of an override whose default value differs from the default declared for
the same parameter by the method being overridden. Adding a default the base method does not
declare, and dropping one it does, both count as a difference.
An implicit interface implementation — class C : I { public void M(int x = 2) } against
I.M(int x = 1) — is checked the same way, and is the sharpest form of the trap: c.M() and
((I) c).M() are both everyday spellings, and they now pass different arguments.
An explicit interface implementation (void I.M(int x)) is treated the other way round. It is
reachable only through the interface, so the interface’s default always applies and leaving it out
is the idiom — that is not reported. A default written on such a member is reported instead,
because it can never be used; the compiler makes the same point with CS1066.
Only supertypes declared in the same file are compared, so a base class from another assembly or another file is never reported on.
Rationale
A default value is not dispatched. The compiler fills the missing argument in at the call site, reading the default from the static type of the receiver, and the value the override declares never enters that decision. The result is that the same call takes different arguments depending on how the variable holding the object was declared:
public class Notifier
{
public virtual void Send(string message, int retries = 3) { }
public virtual void Flush(int timeout = 30) { }
}
public class EmailNotifier : Notifier
{
public override void Send(string message, int retries = 5) { } // FLAW - the 5 is never used
public override void Flush(int timeout = 30) { } // OK - same default
}
public interface IChannel
{
void Publish(string topic, int quality = 1);
}
public class Implicit : IChannel
{
public void Publish(string topic, int quality = 7) { } // FLAW - c.Publish() sends 7, ((IChannel) c).Publish() sends 1
}
public class Explicit : IChannel
{
void IChannel.Publish(string topic, int quality) { } // OK - the interface default always applies
}
// Notifier n = new EmailNotifier();
// n.Send("hi"); // retries = 3, from the base declaration
// EmailNotifier e = new EmailNotifier();
// e.Send("hi"); // retries = 5, from the override
Two spellings of one call quietly disagree, and nothing in the derived class hints at it — the override looks like it is in charge of its own signature. The same trap applies to a default added on an override, and to a default dropped from it: in the first case the value is dead except for callers holding the derived type, in the second the base default silently reappears.
Remediation
Declare the same default value the base method declares, and change it in the base declaration when the value itself needs to change. When the derived type genuinely needs different behaviour for the shorter call, remove the default and add an overload that forwards with the value the derived type wants — an overload is resolved on the static type too, but it says so in the signature instead of hiding it in a default.