Versioning is an after-thought in most languages, but not in C#.
"Versioning" actually has two different meanings. A new version of a component is "source compatible" with a previous version if code that depends on the previous version can, when recompiled, work with the new version. In contrast, for a "binary compatible" component, a program that depended on the old version can, without recompilation, work with the new version.
Most languages do not support binary compatibility at all, and many do little to facilitate source compatibility. In fact, some languages contain flaws that make it impossible, in general, to evolve a class over time without breaking some client code.
As an example, consider the situation of a base class author who ships a class named Base
. In this first version, Base
contains no F
method. A component named Derived
derives from Base
, and introduces an F
. This Derived
class, along with the class Base
that it depends on, is released to customers, who deploy to numerous clients and servers.
// Author A namespace A { class Base // version 1 { } } // Author B namespace B { class Derived: A.Base { public virtual void F() { System.Console.WriteLine("Derived.F"); } } }
So far, so good. But now the versioning trouble begins. The author of Base
produces a new version, and adds its own F
method.
// Author A namespace A { class Base // version 2 { public virtual void F() { // added in version 2 System.Console.WriteLine("Base.F"); } } }
This new version of Base
should be both source and binary compatible with the initial version. (If it weren’t possible to simply add a method then a base class could never evolve.) Unfortunately, the new F
in Base
makes the meaning of Derived
’s F
is unclear. Did Derived
mean to override Base
’s F
? This seems unlikely, since when Derived
was compiled, Base
did not even have an F
! Further, if Derived
’s F
does override Base
’s F
, then does Derived
’s F
adhere to the contract specified by Base
? This seems even more unlikely, since it is pretty darn difficult for Derived
’s F
to adhere to a contract that didn’t exist when it was written. For example, the contract of Base
’s F
might require that overrides of it always call the base. Derived
’s F
could not possibly adhere to such a contract since it cannot call a method that does not yet exist.
In practice, will name collisions of this kind actually occur? Let’s consider the factors involved. First, it is important to note that the authors are working completely independently – possibly in separate corporations – so no collaboration is possible. Second, there may be many derived classes. If there are more derived classes, then name collisions are more likely to occur. Imagine that the base class is Form
, and that all VB, VC++ and C# developers are creating derived classes – that’s a lot of derived classes. Finally, name collisions are more likely if the base class is in a specific domain, as authors of both a base class and its derived classes are likely to choose names from this domain.
C# addresses this versioning problem by requiring developers to clearly state their intent. In the original code example, the code was clear, since Base
did not even have an F
. Clearly, Derived
’s F
is intended as a new method rather than an override of a base method, since no base method named F
exists.
// Author A namespace A { class Base { } } // Author B namespace B { class Derived: A.Base { public virtual void F() { System.Console.WriteLine("Derived.F"); } } }
If Base
adds an F
and ships a new version, then the intent of a binary version of Derived
is still clear – Derived
’s F
is semantically unrelated, and should not be treated as an override.
However, when Derived
is recompiled, the meaning is unclear – the author of Derived
may intend its F
to override Base
’s F
, or to hide it. Since the intent is unclear, the C# compiler produces a warning, and by default makes Derived
’s F
hide Base
’s F
– duplicating the semantics for the case in which Derived
is not recompiled. This warning alerts Derived
’s author to the presence of the F
method in Base
. If Derived
’s F
is semantically unrelated to Base
’s F
, then Derived
’s author can express this intent – and, in effect, turn off the warning – by using the new
keyword in the declaration of F
.
// Author A namespace A { class Base // version 2 { public virtual void F() { // added in version 2 System.Console.WriteLine("Base.F"); } } } // Author B namespace B { class Derived: A.Base // version 2a: new { new public virtual void F() { System.Console.WriteLine("Derived.F"); } } }
On the other hand, Derived
’s author might investigate further, and decide that Derived
’s F
should override Base
’s F
, and clearly specify this intent through specification of the override
keyword, as shown below.
// Author A namespace A { class Base // version 2 { public virtual void F() { // added in version 2 System.Console.WriteLine("Base.F"); } } } // Author B namespace B { class Derived: A.Base // version 2b: override { public override void F() { base.F(); System.Console.WriteLine("Derived.F"); } } }
The author of Derived
has one other option, and that is to change the name of F
, thus completely avoiding the name collision. Though this change would break source and binary compatibility for Derived
, the importance of this compatibility varies depending on the scenario. If Derived
is not exposed to other programs, then changing the name of F
is likely a good idea, as it would improve the readability of the program – there would no longer be any confusion about the meaning of F
.