There is a certain set of features in any programming language for which you just need to know how they are implemented. For example,
closures ; This is not beyond a complex concept, but knowledge of how this beast is structured allows certain conclusions to be drawn regarding the behavior of closures with loop variables. The same goes for calling virtual methods in the constructor of the base class: there is no one correct solution and you just need to know what the language developers have decided and whether the method of the successor will be called (as in Java or C #), or the “polymorphic” behavior in the constructor does not work and the base class method will be called (as in C ++).
Another type of problem that does not have an ideal solution is the combination of overloading (overloading) and overriding (overriding) of the method. Let's consider the following example in C #. Suppose we have a pair of classes,
Base and
Derived , with the virtual method
Foo ( int) and the non-virtual method
Foo ( object) in the class
Derived :
')
class Base { public virtual void Foo(int i) { Console.WriteLine("Base.Foo(int)"); } } class Derived : Base { public override void Foo(int i) { Console.WriteLine("Derived.Foo(int)"); } public void Foo(object o) { Console.WriteLine("Derived.Foo(object)"); } }
The question is which method will be called in the following case:
int i = 42; Derived d = new Derived(); d.Foo(i);
The first and very reasonable assumption is that the
Derived method will be called
. Foo ( int) , because 42 is an
int , and the class
Derived contains the method
Foo ( int) . However, in reality this is not the case and the
Derived method will be called
. Foo ( object) .
Of course, smart people will immediately get into the specification and give the following conclusion: the compiler, they say, treats the declaration and method redefinition in different ways and he, damn, first looks for a suitable method in the class of the current variable (ie, in the class
Derived ) will be found (even if implicit type conversions are required), it will calm down and consider base classes (ie, the
Base class), even if there is a more appropriate version of the method, which will be overridden by the heir.
However, in this case, it’s not just the fact that the declaration and redefinition of methods is interpreted differently and that the methods of the base class are “methods” of the second kind, and the compiler analyzes them secondarily, how many reasons that the compiler (or rather its developers) decided implement exactly this behavior.
To answer the question of how logical the current behavior is, let's take a step back and consider the following case. Suppose that in our class hierarchy there is only one method
Foo ( object) , and it is located in the class
Derived :
class Base {} class Derived : Base { public void Foo(object o) { Console.WriteLine("Derived.Foo(object)"); } }
Yes, not very useful class hierarchy, but nonetheless. The most important thing about it is that no one will raise questions about which call to
Foo will be called in the following case (there is only one variant): new Derived (). Foo (42).
But let's assume that the development of the
Base and
Derived classes is done by different organizations or at least different developers. Since the developer of the
Base class doesn’t really know what the
Derived class developer is doing, at one point he can add the
Foo method to the base class without the knowledge of the heir class developers:
class Base { public virtual void Foo(int i) { Console.WriteLine("Base.Foo(int)"); } }
If we follow the common sense that was said in our answer to the original question, then we have a more appropriate overload of the
Foo method and the following code: new Derived (). Foo (42) should now call the base class method and output
Base. Foo ( int) . However, how logical is it that without the knowledge of the developer of the class
Derived after changes in the base class, a well-tested code suddenly stops working? Of course, one could say that in this case, let's not call the base class method and call it only if there is an overload in the
Derived class. But this behavior will be even stranger.
This problem is known in wide circles of readers of the C # language specification and Eric Lippert’s blog as the problem of
“fragile base classes” (brittle base classes syndrome), which most developers of programming languages ​​try to solve. In this particular case, it is solved by the fact that the
compiler first analyzes the methods directly declared in the class of the variable used and only in the absence of a suitable method considers the methods declared in the base classes.What about other programming languages?
Yes, it would be very interesting to learn how this problem is solved in other programming languages, for example, in C ++, Java or, maybe, in Eiffel (in the opinion of many of the most advanced OO programming language).
Let me start from the end, because it will be a little easier. In Eiffel, the problem is solved very simply: in spite of the many labor of OO chips in Eiffel, there is simply no method overload and you cannot declare a method with the same name as the base class method in the heir. This means that the diagnosis of this problem is postponed at compile time and simply does not exist at run time. (By the way, although this sounds ridiculous, it is a very effective way of dealing with many problems; the same Eiffel successfully solves a number of non-trivial problems simply because it does not allow them. And although this approach is far from ideal, sometimes it may well be applied to solving many problems in the domain area: sometimes it’s easier to prohibit some possibility for the user rather than kill him for half a year to solve it).
NOTE
In fact, this trick is not only used in the Eiffel language; for example, in C # there is some problem with
virtual events , which is solved in VB.NET quite elegantly - virtual events are
simply forbidden in it.
In Java and C ++, things are somewhat different and this is connected, first of all, with the way in which these languages ​​declare the method redefinition in the class of the heir. In these languages, a different approach to virtuality is used by default (in Java, all methods are virtual by default, and in C ++ the method must be explicitly declared explicitly), but initially they used the same approach to redefining virtual methods in the heir class:
class Base { public: virtual void Foo(Integer& i) {} }; class Derived : public Base { public:
To override the method in Java and C ++ languages, no additional keywords are required: it is sufficient to implement a method with the same signature in the derived class. And since from the point of view of syntax, the redefined method is no different from the declaration of a new method (compare two methods of the
Derived class), then the behavior here will
not be the
same as in C #:
Integer i; Derived *pd = new Derived; pd->Foo(i);
In this case, as we initially expected, the
Foo ( Integer &) method will be called.
In the Java language and in the C ++ language, the programmer was later able to more accurately convey his intentions from the point of view of overriding methods in the successor. In Java, starting from the 5th version, a special annotation appeared -
Override , and in C ++ 11 a new keyword “override” appeared. However, for obvious reasons, the behavior in these languages ​​remained unchanged.
NOTEBy the way, for more details on what's new in C ++ 11 compared to the previous standard, you can find in the translation of the Bjarne Straustrup
FAQ :
C ++ 11 FAQ .
The truth is that the similarities between Java and C ++ end there. If you comment out the
Foo ( Integer &) method in the
Derived class, then C ++ will
call Derived :: Foo ( Object &) (that is, a more appropriate base class method will not be considered as a candidate), and in Java it will be called
Base. Foo ( Integer) .
Conclusion
Overload resolution is an interesting thing in and of itself (
here’s one of Nikov’s etudes as a confirmation), but it is even more complicated if you add inheritance to it. On the one hand, the current behavior in C # may seem to be wrong, but if you weigh the pros and cons, it will be quite logical and not so bad.
In any case, regardless of the programming language used, there will be one advice: if possible, it’s better not to mix method overloading and redefining them (remember what kind of zoo we got in three rather popular programming languages).