Одна из целей разработки таких фреймворков, как Java и .NET, - сделать так, чтобы код, скомпилированный для работы с одной версией предварительно скомпилированной библиотеки, одинаково хорошо работал с последующими версиями этой библиотеки, даже если эти последующие версии добавляют новые функции. В то время как обычная парадигма в таких языках, как C или C ++, заключается в распространении статически связанных исполняемых файлов, которые содержат все необходимые им библиотеки, парадигма в .NET и Java заключается в распространении приложений в виде наборов компонентов, которые «связаны» во время выполнения .
COM-модель, которая предшествовала .NET, пыталась использовать этот общий подход, но в действительности у нее не было наследования - вместо этого каждое определение класса эффективно определяло как класс, так и интерфейс с тем же именем, которое содержало все его открытые члены. , Экземпляры были типа класса, в то время как ссылки были типа интерфейса. Объявленный класс как производный от другого был эквивалентен объявлению класса как реализующего интерфейс другого, и требовал, чтобы новый класс повторно реализовал все открытые члены классов, из которых один произошел. Если Y и Z являются производными от X, а затем W происходит от Y и Z, не имеет значения, будут ли Y и Z реализовывать элементы X по-разному, потому что Z не сможет использовать их реализации - ему придется определить его своя. W может инкапсулировать экземпляры Y и / или Z и связывать свои реализации методов X с их помощью, но не будет никакой двусмысленности относительно того, что должны делать методы X - они будут делать то, что явно указано в коде Z.
Сложность в Java и .NET заключается в том, что коду разрешено наследовать элементы и иметь к ним доступ неявно относится к родительским элементам. Предположим, что у каждого были классы W-Z, связанные как выше:
class X { public virtual void Foo() { Console.WriteLine("XFoo"); }
class Y : X {};
class Z : X {};
class W : Y, Z // Not actually permitted in C#
{
public static void Test()
{
var it = new W();
it.Foo();
}
}
Казалось бы, W.Test()
должен создать экземпляр W, вызывающий реализацию виртуального метода Foo
, определенного в X
. Предположим, однако, что Y и Z на самом деле были в отдельно скомпилированном модуле, и хотя они были определены, как указано выше, когда X и W компилировались, позже они были изменены и перекомпилированы:
class Y : X { public override void Foo() { Console.WriteLine("YFoo"); }
class Z : X { public override void Foo() { Console.WriteLine("ZFoo"); }
Теперь, каким должен быть эффект вызова W.Test()
? Если бы перед распространением программу нужно было статически связать, то на этапе статического связывания можно было бы обнаружить, что, хотя программа не имела неоднозначности до того, как Y и Z были изменены, изменения в Y и Z сделали вещи двусмысленными, и компоновщик мог отказаться постройте программу, если или пока не решена такая неоднозначность. С другой стороны, вполне возможно, что человек, у которого есть и W, и новые версии Y и Z, - это тот, кто просто хочет запустить программу и не имеет исходного кода ни для одной из них. Когда запускается W.Test()
, больше не будет понятно, что должен делать W.Test()
, но пока пользователь не попытается запустить W с новой версией Y и Z, ни одна часть системы не сможет распознать, что существует проблема (если W не считался нелегитимным даже до изменения Y и Z).