Подтвердить что ты не робот

Почему значение спецификации базового класса не может быть рекурсивно зависеть от самого себя в С#?

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

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!

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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