Классы каннибалов
В какой-то момент я пытаюсь обернуть голову вокруг причины, по которой некоторым классам 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;
}