Классы каннибалов

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

Прежде чем продолжить, возможно, мне следует объяснить, что я называю "каннибалом". Не уверен, что я только что придумал этот термин или если он был вокруг какое-то время или даже если я правильно его использую, но это не важно сейчас.

Я в основном называю класс каннибала классом, который потребляет себя. Другими словами, класс, интерфейс которого объявляет члены своего типа. Например:

class Foo
{
    public Foo SomeFoo; 
}

Как вы можете видеть выше, класс Foo имеет член типа Foo (сам).

Теперь, когда я впервые увидел это (время назад), я не собирался компилироваться, но, к моему удивлению, он компилируется. Причина, по которой я не делал этого, собирался, потому что для меня это кричит какой-то рекурсивный кошмар.

Чтобы усложнить ситуацию немного, я решил попробовать одно и то же, но создав класс struct, например:

struct Foo
{
    public Foo SomeFoo; 
}

К сожалению, это не компилируется, вместо этого вы получаете ошибку: Элемент Struct 'Foo.SomeFoo' типа 'Foo' вызывает цикл в структуре структуры

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

Спасибо.

Ответ 1

Причина, по которой вы не можете создать такую ​​структуру, заключается в том, что структуры должны быть инициализированы, когда они выделены с некоторыми значениями по умолчанию. Итак, когда у вас есть структура Foo, как вы описали, и вы создаете Foo...

Foo x; // x = default(Foo)

он вызывает конструктор по умолчанию для Foo. Однако этот конструктор по умолчанию должен указывать значение по умолчанию для поля Foo.SomeFoo.

Ну, как это найти? Он должен вызвать default(Foo). Который, чтобы построить Foo, должен дать значение по умолчанию для поля Foo.SomeFoo... Как вы догадались, это рекурсивный кошмар.

Поскольку вы можете создать нулевую ссылку на класс и воздержаться от фактического создания экземпляра класса немедленно, нет проблем; вы можете вызвать new Foo(), а Foo.SomeFoo будет просто нулевым. Не требуется рекурсия.

ДОБАВЛЕНИЕ. Когда вы думаете об этом, если вы все еще смущены, я думаю, что другие ответы имеют еще один хороший способ рассмотреть его (та же самая существенная проблема, другой подход) - когда программа выделяет память для Foo, как она должна это делать? Что sizeof(Foo), когда Foo содержит некоторые вещи, а затем еще одно целое Foo? Вы не можете этого сделать.

Вместо этого, если бы это был класс, ему просто нужно было бы выделить пару байтов для ссылки на Foo, а не на фактический Foo, и проблем не было.

Ответ 2

Различие заключается в том, что Foo является ссылочным типом. В макете памяти класса будет указатель на экземпляр Foo, и это будет нормально.

В случае структуры у вас есть в основном мгновенно recusrsive макет памяти, который не может работать.

Теперь, если ваш класс пытается создать экземпляр Foo в своем собственном конструкторе, у вас будет проблема с переполнением стека.

Ответ 3

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

Как один из способов обернуть голову вокруг этого, квадрат целого числа всегда является другим целым числом. Ничего странного в этом, это просто ссылка или связь между двумя целыми числами. Но вы не можете иметь строку, содержащую полную копию самого себя в качестве подстроки. Это различие.

Ответ 4

class Foo
{
    public Foo SomeFoo; 
}

SomeFoo в этом примере просто ссылка - и не создает проблему рекурсивного создания.
И из-за этого могут существовать классы canibal.

Ответ 5

Хорошо, я вижу.

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

Итак, если это было скомпилировано для 32-разрядной ОС, я предполагаю, что компилятор технически меняет объявление типа Foo на что-то вроде:

class Foo
{
    public Int32 SomeFoo; 
}

Вместо "Foo" компилятор действительно видит 32 бита (вид).

Спасибо.

Ответ 6

Чтобы понять, почему это разрешено вместе с тем, как это может быть полезно, классическая реализация связанных списков - отличный пример, посмотрите этот учебник

http://cyberkruz.vox.com/library/post/c-tutorial-linked-list.html

Понимаете, они определяют класс node следующим образом:

public class LinkedList
{

    Node firstNode;

    public class Node
    {

        Node previous;
        Node next;
        int value;
    }
}

Где каждый node указывает на предыдущий и следующий node, поэтому думать об этом как о связи с другим объектом - хорошее начало.

Если вы еще не сделали Связанный список, я настоятельно рекомендую его, так как это хороший exersize.

Ответ 7

Итак, вы называете реализацию Singleton классом canibal?

public class ShopSettings
{
  public static ShopSettings Instance
  {
    get
    {
      if (_Instance == null)
      {
        _Instance = new ShopSettings();
      }

      return _Instance;
    }
  }
}

Ответ 8

Как они сказали, это ценностный тип, поэтому он не может содержать себя, поскольку он просто не работает (подумайте об этом).

Однако вы можете сделать следующее:

unsafe struct Foo
{
  public Foo* SomeFoo;
}