2009-03-16 10 views
11

Per un po 'di tempo, ho cercato di comprendere il motivo per cui alcune classi "cannibali" possono compilare.Classi di cannibali

Prima di continuare, forse dovrei spiegare quello che chiamo una classe "cannibale". Non sono sicuro se ho appena inventato quel termine o se è stato in giro per un po 'o anche se sto usando correttamente, ma che non è importante in questo momento.

Fondamentalmente io chiamo una classe di cannibali una classe che si consuma. In altre parole una classe la cui interfaccia dichiara membri del proprio tipo. Ad esempio:

class Foo 
{ 
    public Foo SomeFoo; 
} 

Come si può vedere sopra, la classe Foo ha un membro di tipo Foo (stesso).

Ora, la prima volta che ho visto questo (poco tempo fa) non avevo intenzione di compilare, ma con mia sorpresa lo compilava. Il motivo per cui non ho fatto questo sarebbe compilato perché a me questo urla qualche tipo di incubo ricorsivo.

A complicare le cose un po 'più, ho deciso di provare la stessa cosa ma facendo la classe una struttura come ad esempio:

struct Foo 
{ 
    public Foo SomeFoo; 
} 

Purtroppo, questo non viene compilato, invece si ottiene l'errore: membro Struct 'Foo.SomeFoo' di tipo 'Foo' provoca un ciclo nel layout struct

per me, un errore di compilazione ha più senso che nessun errore, ma sono sicuro che ci siano la maggior parte una spiegazione logica a questo comportamento così ho mi stavo chiedendo se qualcuno di voi ragazzi potesse e xplain questo comportamento.

Grazie.

+1

LOL Classe cannibale. Questo mi ha fatto ridere :) –

+0

Non ho mai sentito chiamare Cannibal di solito Self Referencing ma mi piace Cannibal molto meglio. – JoshBerke

risposta

24

Il motivo non è possibile progettare una struttura come questo è perché le strutture devono essere inizializzato quando vengono assegnati con alcuni valori di default. Così, quando si dispone di una struttura Foo come te descritto e si crea un Foo ...

Foo x; // x = default(Foo) 

chiama il costruttore predefinito per Foo. Tuttavia, tale costruttore predefinito deve fornire un valore predefinito per il campo Foo.SomeFoo.

Bene, come lo trova? Deve chiamare default(Foo). Quale, per costruire un Foo, deve dare un valore predefinito per il campo Foo.SomeFoo ... Come hai immaginato, questo è un incubo ricorsivo.

Poiché è possibile creare un riferimento null a una classe e astenersi dal creare immediatamente un'istanza della classe, non ci sono problemi; puoi chiamare new Foo() e Foo.SomeFoo sarà nullo. Nessuna ricorsione necessaria.

ADDENDUM: Quando ci pensi, se sei ancora confuso, penso che le altre risposte abbiano un altro buon modo di considerarlo (lo stesso problema essenziale, un approccio diverso) - quando il programma alloca la memoria per un Foo, come è possibile farlo?Cosa è sizeof(Foo) quando Foo contiene alcune cose e quindi un altro intero Foo? Non puoi farlo.

Invece, se fosse una classe, sarebbe sufficiente allocare un paio di byte per un riferimento a uno , non uno Foo effettivo e non ci sono problemi.

+0

Per riepilogare: class = tipo di riferimento, struct = tipo di valore. Non è possibile eseguire il layout della mappa di memoria di un tipo che contiene se stesso. Tuttavia, i tipi di riferimento conterranno solo il riferimento alla memoria, non la struttura dei dati effettiva. Pertanto, non è necessario sapere in che modo tale tipo è strutturato in fase di compilazione. – spoulson

0

Quindi come si chiama un'implementazione Singleton una classe canibal?

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

     return _Instance; 
    } 
    } 
} 
+0

-1, no, l'istanza in statico e eviterebbe completamente questo problema. – Samuel

1
class Foo 
{ 
    public Foo SomeFoo; 
} 

Il SomeFoo in questo esempio solo un link - e non fa il ricorsiva creare problemi.
E a causa di ciò, possono esistere classi canibal.

12

La differenza sta nel fatto che Foo è un tipo di riferimento. Il layout della memoria di classe avrà un puntatore a un'istanza di Foo e andrà bene.

Nel caso della struttura, si dispone fondamentalmente di un layout di memoria leggermente ricicente, che non può funzionare.

Ora, se la classe tenta di creare un'istanza di Foo nel proprio costruttore, si verificherà un problema di overflow dello stack.

+0

Questa è la risposta corretta. La differenza tra tipi di valore e tipi di riferimento, che è la differenza tra le strutture e le classi. Il problema del valore predefinito è una conseguenza di ciò. –

0

Come hanno detto è un tipo di valore, quindi non può contenere se stesso, in quanto non può funzionare (pensateci).

È possibile comunque effettuare le seguenti operazioni:

unsafe struct Foo 
{ 
    public Foo* SomeFoo; 
} 
5

Essi sono chiamati tipi ricorsivi, e sono abbastanza comuni. Un albero, ad esempio, è comunemente implementato in termini di "nodi" che si riferiscono ad altri nodi. Non vi è nulla di paradossale a questo proposito, purché siano fare riferimento a l'un l'altro ma non contenere a vicenda.

Come un modo per avvolgere la testa intorno a questo, il quadrato di un intero è sempre un altro numero intero. Niente di strano, è solo un riferimento o una relazione tra i due numeri interi. Ma non è possibile avere una stringa che contenga una copia completa di se stessa come sottostringa. Questa è la distinzione.

+0

Ho appena inventato una cosa che contiene una copia di se stessa ... Penso che la chiamerò un frattale! :) –

+0

Ah, ma i frattali non contengono una copia completa di se stessi. Invece, porzioni di un frattale possono essere messe in corrispondenza con il tutto sotto qualche mappatura, fino ad alcuni epsilon. – MarkusQ

1

Ok, capisco.

Quindi, per portare a casa il punto, se ho capito bene, al compilatore non interessa se il membro è di tipo Foo, Bar o qualsiasi altra cosa. Tutto quello che il compilatore deve sapere è il numero di byte che ha bisogno di allocare per quella variabile membro.

Quindi, se questo era stato compilato per un sistema operativo a 32 bit, quindi suppongo che il compilatore è tecnicamente cambiando la dichiarazione del tipo Foo a qualcosa di simile:

class Foo 
{ 
    public Int32 SomeFoo; 
} 

Invece di “Pippo” il compilatore è davvero vedendo 32 bit (tipo di).

Grazie.

+0

in definitiva, per quanto riguarda la memoria, sì. –

1

capire perché questo è consentito insieme a come può essere utile, un'implementazione lista concatenata classico è un grande esempio, check out questo tutorial

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

Vedete, essi definiscono una classe nodo come segue:

public class LinkedList 
{ 

    Node firstNode; 

    public class Node 
    { 

     Node previous; 
     Node next; 
     int value; 
    } 
} 

dove ogni nodo punta al nodo precedente e successivo, così pensare ad esso come un collegamento a un altro oggetto è un buon inizio.

Se non hai ancora fatto un elenco collegato, lo consiglio vivamente perché è un buon exersize.