Почему объявление поля с дублированным вложенным типом в родовом классе приводит к увеличению исходного кода?
Сценарий очень редок, но довольно прост: вы определяете общий класс, а затем создаете вложенный класс, который наследуется от внешнего класса и определяет ассоциативное поле (типа self) внутри вложенного. Фрагмент кода проще, чем описание:
class Outer<T>
{
class Inner : Outer<Inner>
{
Inner field;
}
}
после декомпиляции IL, код С# выглядит так:
internal class Outer<T>
{
private class Inner : Outer<Outer<T>.Inner>
{
private Outer<Outer<T>.Inner>.Inner field;
}
}
Это кажется достаточно справедливым, но когда вы меняете объявление типа поля, все становится сложнее. Поэтому, когда я изменяю объявление поля на
Inner.Inner field;
После декомпиляции это поле будет выглядеть так:
private Outer<Outer<Outer<T>.Inner>.Inner>.Inner field;
Я понимаю, что классная "натура" и наследование не совсем ладят друг с другом, но почему мы наблюдаем такое поведение? Является ли объявление типа Inner.Inner
измененным типом вообще? Существуют ли в этом контексте типы Inner.Inner
и Inner
?
Когда все становится очень сложно
Вы можете увидеть декомпилированный исходный код для класса ниже. Он действительно огромный и имеет общую длину 12159 символов.
class X<A, B, C>
{
class Y : X<Y, Y, Y>
{
Y.Y.Y.Y.Y.Y y;
}
}
Наконец, этот класс:
class X<A, B, C, D, E>
{
class Y : X<Y, Y, Y, Y, Y>
{
Y.Y.Y.Y.Y.Y.Y.Y.Y y;
}
}
приводит к сборке 27.9 MB (29,302,272 bytes)
и Total build time: 00:43.619
Используемые инструменты
Компиляция выполняется под компиляторами С# 5 и С# 4. Декомпиляция выполняется dotPeek. Конфигурации сборки: Release
и Debug
Ответы
Ответ 1
Ядро вашего вопроса, почему Inner.Inner
- это другой тип, чем Inner
. Как только вы это понимаете, ваши наблюдения за временем компиляции и сгенерированным размером IL-кода следуют легко.
Первое, что нужно отметить, это то, что когда у вас есть это объявление
public class X<T>
{
public class Y { }
}
Существует бесконечное число типов, связанных с именем Y
. Для каждого аргумента общего типа T
существует один аргумент T
, поэтому X<int>.Y
отличается от X<object>.Y
, и, что важно для более поздних версий, X<X<T>>.Y
- это другой тип, чем X<T>.Y
для всех T
. Вы можете проверить это для разных типов T
.
Следующее, что нужно отметить, это то, что в
public class A
{
public class B : A { }
}
Существует множество способов обращения к вложенному типу B
. Один из них - A.B
, другой - A.B.B
и т.д. Оператор typeof(A.B) == typeof(A.B.B)
возвращает true
.
Когда вы объединяете эти два, то, как вы это сделали, происходит что-то интересное. Тип Outer<T>.Inner
не совпадает с типом Outer<T>.Inner.Inner
. Outer<T>.Inner
является подклассом Outer<Outer<T>.Inner>
, а Outer<T>.Inner.Inner
является подклассом Outer<Outer<Outer<T>.Inner>.Inner>
, который мы установили ранее, поскольку он отличается от Outer<T>.Inner
. Поэтому Outer<T>.Inner.Inner
и Outer<T>.Inner
относятся к разным типам.
При генерации IL, компилятор всегда использует полностью квалифицированные имена для типов. Вы умело нашли способ ссылаться на типы с именами, длина которых растет с экспоненциальными скоростями. Поэтому при увеличении общей арности Outer
или добавлении дополнительных уровней .Y
в поле field
в Inner
размер выходного IL и время компиляции растут так быстро.
Ответ 2
Это не ответ!
Есть много аспектов вашего вопроса. Немного: Если тип содержит (из-за наследования, не разрешенного иначе) вложенный тип с тем же именем, что и сам тип, и если это имя используется внутри типа, то какое имя имеет название?
Трудно выразить словами, но вот пример, который я имею в виду:
namespace N
{
class Mammal
{
// contains nested type of an unfortunate name
internal interface Giraffe
{
}
}
class Giraffe : Mammal
{
Giraffe g; // what the fully qualified name of the type of g?
}
}
Примечание: Это легко! Нет дженериков! Нет проблем с классом, наследующим свой собственный содержащий класс.
Вопрос в том, какой тип g
? Это N.Giraffe
(класс), или это N.Giraffe.Giraffe
(интерфейс)? Правильный ответ - последний. Поскольку для определения значения для имени Giraffe
сначала выполняется поиск членов текущего типа (в этом случае он находит интерфейс). Только если совпадение не найдено, вы переходите к членам текущего пространства имен (где будет найден текущий тип).
Ответ 3
Наследование из генератора, параметризованного текущим типом, часто называется Любопытно повторяющийся шаблон шаблона и обескуражен Эрик Липперт, ранее в команде компилятора С#.
В вашем случае вложенный класс Outer<A>.Inner
определяется как наследующий от Outer<Inner>
. Это означает, что вложенный класс содержит через наследование определение вложенного класса. Это дает бесконечное определение вложенных классов: Outer<A>.Inner.Inner.Inner...
Теперь, в вашем исходном определении
class Inner : Outer<Inner>
{
Inner field;
}
поле объявляется как тип Inner
, который в этой области относится к текущему типу. Когда вы изменили его на Inner.Inner
, первый Inner
ссылался на текущий тип, а второй .Inner
ссылался на вложенный класс, полученный через наследование.
В качестве примера, позвольте расширить определение класса Inner, чтобы включить то, что исходит из наследования (и переименовать, чтобы сделать вещи немного яснее):
//original
class Outer<A>
{
class Inner1 //: Outer<Inner1>
{
Inner1 field;
//from inheritance
class Inner2 : Outer<Inner1.Inner2>
{
Inner2 field;
}
}
}
//modified for Inner.Inner
class Outer<A>
{
class Inner1 //: Outer<Inner1>
{
Inner1.Inner2 field;
//from inheritance
class Inner2 : Outer<Inner1.Inner2>
{
Inner2.Inner3 field;
}
}
}
Итак, вернитесь к своим вопросам:
Почему мы наблюдаем такое поведение? Является ли объявление типа Inner.Inner измененным? Существуют ли внутренние и внутренние типы Inner.Inner? В этом контексте?
Вы изменили определение типа класса Inner, чтобы его поле было другого типа. Экземпляр Outer<A>.Inner
, возможно, (я не проверял) был бы применим к другому внутреннему типу, но они являются определениями типа 2.