Почему значение спецификации базового класса не может быть рекурсивно зависеть от самого себя в С#?
Следующий фрагмент кода С# не компилируется:
public class A
{
public interface B { }
}
public class C
: A,
C.B // Error given here: The type name 'B' does not exist in the type 'C'.
{ }
public class D : C.B // Compiles without problems if we comment out 'C.B' above.
{ }
Это поведение является правильным в соответствии со спецификацией С# 4.0 (пункт 10.1.4.1):
При определении значения спецификации прямого базового класса A класса B прямой базовый класс B временно считается объектом. Интуитивно это гарантирует, что значение спецификации базового класса не может рекурсивно зависеть от самого себя.
Мой вопрос: почему это поведение не допускается?
У Intellisense нет проблем с этим, хотя я знаю, что это мало говорит о том, что после столкновения Visual Studio, когда Intellisense пытается понять какую-то комбинацию злых классов с вариантными дженериками.
Поиск в Интернете по приведенной выше цитате из спецификации ничего не дает, поэтому я предполагаю, что это нигде не было показано.
Почему меня это волнует? Я разработал следующий фрагмент кода:
// The next three classes should really be interfaces,
// but I'm going to override a method later on to prove my point.
// This is a container class, that does nothing except contain two classes.
public class IBagContainer<Bag, Pointer>
where Bag : IBagContainer<Bag, Pointer>.IBag
where Pointer : IBagContainer<Bag, Pointer>.IPointer
{
// This could be an interface for any type of collection.
public class IBag
{
// Insert some object, and return a pointer object to it.
// The pointer object could be used to speed up certain operations,
// so you don't have to search for the object again.
public virtual Pointer Insert(object o) { return null; }
}
// This is a pointer type that points somewhere insice an IBag.
public class IPointer
{
// Returns the Bag it belongs to.
public Bag GetSet() { return null; }
}
}
// This is another container class, that implements a specific type of IBag.
public class BinarySearchTreeContainer<Tree, Node> : IBagContainer<Tree, Node>
where Tree : BinarySearchTreeContainer<Tree, Node>.BinarySearchTree
where Node : BinarySearchTreeContainer<Tree, Node>.BinarySearchTreeNode
{
// This is your basic binary search tree.
public class BinarySearchTree : IBagContainer<Tree, Node>.IBag
{
// We can search for objects we've put in the tree.
public Node Search(object o) { return null; }
// See what I did here? Insert doesn't return a Pointer or IPointer,
// it returns a Node! Covariant return types!
public override Node Insert(object o) { return null; }
}
// A node in the binary tree. This is a basic example of an IPointer.
public class BinarySearchTreeNode : IBagContainer<Tree, Node>.IPointer
{
// Moar covariant return types!
public override Tree GetSet() { return null; }
// If we maintain next and prev pointers in every node,
// these operations are O(1). You can't expect every IBag
// to support these operations.
public Node GetNext() { return null; }
public Node GetPrev() { return null; }
}
}
Вот, мы достигли ковариантных типов возврата! Однако есть одна небольшая деталь.
Попробуйте создать экземпляр BinarySearchTree. Для этого нам нужно указать BinarySearchTreeContainer.BinarySearchTree для некоторых подходящих классов Tree и Node. Для дерева мы хотели бы использовать BinarySearchTree, для которого нам нужно указать BinarySearchTreeContainer.BinarySearchTree... И мы застряли.
Это, по существу, любопытно повторяющийся шаблон шаблона (CRTP). К сожалению, мы не можем исправить это, как в CRTP:
public class BinarySearchTreeContainer
: BinarySearchTreeContainer
<BinarySearchTreeContainer.BinarySearchTree,
BinarySearchTreeContainer.BinarySearchTreeNode> { }
public class IBagContainer
: IBagContainer
<IBagContainer.IBag,
IBagContainer.IPointer> { }
(...)
BinarySearchTreeContainer.BinarySearchTree tree
= new BinarySearchTreeContainer.BinarySearchTree();
tree.Search(null);
IBagContainer.IBag bag = tree; // No cast!
//bag.Search(null); // Invalid!
//BinarySearchTreeContainer.BinarySearchTreeNode node
// = bag.Insert(null); // Invalid!
И мы вернемся к моему первоначальному вопросу: определение двух верхних классов не допускается спецификацией С#. Если это определение класса было разрешено, мои двоичные деревья поиска могли бы использоваться. Прямо сейчас они просто компилируются: их нельзя использовать.
Ответы
Ответ 1
Я боролся с проблемами, которые вы поднимаете за бесчисленные часы за последние несколько лет. Подробное обсуждение всех вопросов, которые вы поднимаете, займет у меня несколько часов, чтобы напечатать, поэтому я просто обобщу:
Во-первых, оказывается, что даже с тем, что "временно предполагаемый объект", который мы с Mads добавили, чтобы попытаться затянуть этот раздел спецификации, этот раздел спецификации все еще не обоснован. "Как связать имя с типом" бит спецификации предполагает, что все отношения вложенности и наследования известны и согласованы в момент, когда происходит поиск, но, конечно, очевидно, что это не может быть так, поскольку вся причина, по которой мы находимся выполнение поиска имени в первую очередь заключается в определении базового типа. Если бы у меня были мои заметки, я мог бы привести несколько примеров сумасшедших иерархий типов, где комбинации дженериков, вложений, интерфейсов и базовых классов помещают компилятор в ситуации, когда то, как вы определяете, какое именно имя означает, зависит от порядка, в котором вы изучаете базовые классы.
Очевидно, что это нехорошее место. Мы не хотим, чтобы значение программы С# отличалось при повторном заказе классов в файле!
Во-вторых, мы ограничены тем, что может быть представлено в метаданных.
В-третьих, исторически мы были ограничены тем, что можно эффективно испускать в метаданные. Предыдущие версии эмитентов метаданных имели проблемы с производительностью или корректностью, если вы пытались генерировать производные типы перед базовыми типами или внутренними типами перед внешними типами. (Я попытался на С# 4 решить это, написав топологический сортировщик, который найдет эффективный порядок, если он существует, но изменение оказалось достаточно сложным и опасным, что мы решили не принимать изменения до Roslyn. В Roslyn мы используем совершенно другой эмиттер.)
В-четвертых, редко бывает, что подобные типы топологий появляются в реальном коде производства; вы, по-видимому, являетесь исключением из этого правила.
В-пятых, одна из наших основных целей для языка - сделать язык "ям качества", где особенности языка заставляют писать программы, которые являются правильными и понятными. Разрешение типов сумасшедших "любопытных повторяющихся" шаблонов, которые вы видите в шаблонах на С++, явно не является целью команды языка С#. Мы не заинтересованы в предоставлении теоретически полной системы типов; мы заинтересованы в том, чтобы упростить представление о том, что Сотрудник - это своего рода Личность.
Все эти факторы работают против того, чтобы сделать круглые формы в базовом классе и вложенные отношения классов более законными. Насколько мне лично понравилась бы задача создать обоснованную систему для решения круглых точек в базовых типах таким образом, чтобы не нарушить существующий код, это не является достаточно высоким приоритетом; у нас есть длинный список вещей, которые мы хотим улучшить для Roslyn, а алгоритм разрешения базового класса далеко от вершины этого списка.