Наследование и внутреннее представление
Я получил следующий вопрос:
class Alpha<X>
where X : class
{}
class Bravo<T, U>
where T : class
where U : T
{
Alpha<U> alpha;
}
При компиляции этого кода выдается ошибка, в которой говорится, что тип U не может использоваться в качестве аргумента типа для Alpha, поскольку U не является ссылочным типом. Но ведь U является ссылочным типом, поскольку тип U ограничен типом T, а тип T ограничен ссылочными типами. Является ли это ошибкой компилятора?
Конечно же, нет. Bravo<object, int> является корректным типом, но в этом случае U не является ссылочным типом. Ограничение типа U говорит лишь о том, что тип U должен наследовать тип T (*). int является наследником object, вот почему это выражение не противоречит ограничению. Все структуры наследуют как минимум двум ссылочным типом, а некоторые из них наследуются от многих других. (Перечисления наследуются от System.Enum, многие структуры реализуют интерфейсы и т.д.)
В этом случае разработчику следует добавить ограничение на ссылочный тип и для типа U.
Но эта простая задача заставила меня глубже задуматься об этой проблеме. Мне кажется, что многие люди не полностью понимают, что в языке C# означает наследование. На самом деле, все очень просто: тип наследника наследует все члены базового класса. Вот и все! Если в базовом классе есть член M, то в наследнике так же есть член M (**).
Меня иногда спрашивают, наследуются ли закрытые члены; конечно нет! Чтобы это означало? Но, да, закрытые члены наследуются, хотя в большинстве случаев это и не имеет значения, поскольку к ним нельзя получить доступ извне. Однако если наследник может получить доступ к закрытым членам базового класса, то становится ясно, что да, закрытые члены наследуются:
class B
{
private int x;
private class D : B
{
Класс D наследуется от класса B, а поскольку вложенные классы имеют доступ к закрытым членам, то класс D без проблем может использовать переменную x.
Меня иногда спрашивают, «но как значимый тип, такой как int, занимающий лишь 32 бита памяти, ни больше, ни меньше, может наследоваться от класса object? Объект занимает значительно больше 32-х бит; он содержит блок синхронизации (sync block), таблицу виртуальных методов и тому подобное». Видимо многие разработчики считают, что наследование имеет какое-то отношение к расположению объекта в памяти. Способ расположения объекта в памяти является деталью реализации, а не частью контракта отношения наследования! Когда мы видим, что int наследуется от класса object, то имеется в виду, что если object содержит член, скажем, ToString, тогда int также его содержит. Когда вызывается метод ToString на переменной с типом object времени компиляции, компилятор генерирует код, который производит поиск нужного метода в таблице виртуальных функций во время выполнения. При вызове метода ToString на переменной типа int, компилятор генерирует код, вызывающий этот метод напрямую, поскольку он знает, что int – это закрытый (sealed) значимый тип, который переопределяет метод ToString. А при упаковке типа int, полученный объект будет располагаться в памяти таким же образом, как и любой другой объект.
Но не существует такого требования, в котором было бы сказано, что int и object всегда должны представляться в памяти одинаково только потому, что один из них наследуется от другого; требуется всего лишь, чтобы компилятор каким-то образом мог генерировать код, не нарушающий отношения наследования.
-------------------------
(*) или являться типом T, или, возможно наследуется от типа, связанным с T некоторым вариантным преобразованием.
(**) Конечно, это не совсем так; существуют несколько крайних случаев. Например, класс «наследующий» некоторый интерфейс должен содержать реализацию всех членов этого интерфейса, но он может реализовывать интерфейс явно (explicitly) и не показывать члены интерфейса, как свои собственные. Это еще одна причина, почему мне не очень нравится, что при описании интерфейсов мы выбрали термин «наследует», а не «реализует». Кроме того, некоторые члены, такие как деструкторы и конструкторы не наследуются.