Почему значение спецификации базового класса не может рекурсивно зависеть от самого себя в C #? - PullRequest
17 голосов
/ 07 января 2012

Следующий фрагмент кода C # не компилируется:

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.
{ }

Это поведение является правильным в соответствии со спецификацией C # 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!

И мы возвращаемся к моему первоначальному вопросу: два верхних определения класса не разрешены спецификацией C #.Если бы это определение класса было разрешено, мои деревья двоичного поиска были бы пригодны для использования.Прямо сейчас они просто компилируются: их нельзя использовать.

1 Ответ

21 голосов
/ 07 января 2012

Я боролся с проблемами, которые вы поднимали в течение бесчисленных часов за последние несколько лет. Детальное обсуждение всех поднятых вами вопросов заняло бы у меня несколько часов, чтобы напечатать, поэтому я просто подведу итог:

Во-первых, оказывается, что даже с тем условием «временно предположить, что он является объектом», который мы с Мэдсом добавили, чтобы попытаться ужесточить этот раздел спецификации, этот раздел спецификации все еще недостаточно обоснован. Бит «как связать имя с типом» спецификации предполагает, что все отношения вложенности и наследования известны и непротиворечивы в момент, когда происходит поиск, но, конечно, это не может быть так, поскольку вся причина, по которой мы поиск имени в первую очередь должен определить базовый тип. Если бы у меня были свои заметки со мной, я мог бы дать вам несколько примеров сумасшедших иерархий типов, в которых комбинации обобщенных типов, вложений, интерфейсов и базовых классов ставят компилятор в ситуации, когда то, как вы определяете, что означает данное имя, зависит от порядка, в котором Вы выясняете базовые классы.

Очевидно, что это не очень хорошее место для жизни. Мы не хотим, чтобы смысл программы на C # отличался при изменении порядка классов в файле!

Во-вторых, мы ограничены тем, что может быть представлено в метаданных.

В-третьих, исторически мы были ограничены тем, что может эффективно передаваться в метаданные. Предыдущие версии отправителей метаданных имели проблемы с производительностью или правильностью, если вы пытались создавать производные типы перед базовыми или внутренними типами перед внешними типами. (Я попытался в C # 4 решить эту проблему, написав топологический сортировщик, который нашел бы эффективное упорядочение, если бы оно существовало, но изменение оказалось достаточно сложным и опасным, поэтому мы решили не принимать его до Roslyn. В Roslyn мы используем совершенно другой излучатель.)

В-четвертых, редко встречаются такие топологии в реальном производственном коде; Вы, очевидно, исключение из этого правила.

В-пятых, одна из наших основных целей для языка - сделать его языком «ямы качества», где его особенности приводят к написанию программ, которые являются как правильными, так и понятными. Разрешение видов сумасшедших «любопытно повторяющихся» шаблонов, которые вы видите в шаблонах C ++, явно не является целью команды языка C #. Мы не заинтересованы в предоставлении теоретически полной системы типов; Мы заинтересованы в том, чтобы облегчить представление о том, что Сотрудник - это своего рода Человек.

Все эти факторы работают против того, чтобы сделать цикличности в отношениях базового класса и вложенного класса более законными Как бы мне лично не хотелось создавать хорошо обоснованную систему для разрешения циклических операций в базовых типах таким образом, чтобы не нарушать существующий код, это не является достаточно высоким приоритетом; у нас есть длинный список вещей, которые мы хотим улучшить для Roslyn, и алгоритм разрешения базового класса далек от верха этого списка.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...