Perché le lingue OOP non offrono una funzionalità per clonare un genitore in una classe figlio?

3

Considera le seguenti esempio:

public class MyParentClass {
   public int MyInt { get; set; }
}

public class MyChildClass : MyParentClass
{
}

public class AnotherClass
{
   public MyChildClass GetChildClassFromParentClass(MyParentClass parent)
   {
       return new MyChildClass() { MyInt = parent.MyInt };
   }
}

Mi chiedo perché non è possibile clonare direttamente la classe genitore nel suo figlio senza la fase manuale di copiare tutti i suoi valori, dal momento che MyParentClass e MyChildClass condividono questa 'relazione'.

Forse una funzione del genere non sarebbe in grado di garantire la coerenza di MyChildClass a causa delle dipendenze iniettate mancanti, ma questa funzione linguistica potrebbe almeno consentire questa clonazione se esiste un costruttore parametrico definito per il figlio.

Vorrei essere chiaro So che questa funzione non è una buona idea e sicuramente esistono scenari che rendono questa funzionalità linguistica non fattibile o sicura. Ma mi piacerebbe sapere quali sono questi scenari.

Nota secondaria: credo che questa non sia una domanda basata sull'opinione pubblica, dal momento che nessun linguaggio OOP mainstream sembra offrire questa funzione, quindi devono esserci argomenti oggettivi contro di essa. *

    
posta JᴀʏMᴇᴇ 12.01.2018 - 17:39
fonte

5 risposte

8

È possibile clonare facilmente un genitore in un bambino?

Alcune lingue consentono ciò specificando il costruttore della classe base. Qui in C ++:

class MyParentClass {
public: 
    int MyInt;
};

class MyChildClass : public: MyParentClass
{
public: 
    MyChildClass () = default; 
    MyChildClass (const MyParentClass& x) : MyParentClass(x) {}  // <== you need to tell   
};

La logica di costruzione dovrebbe prima costruire il genitore usando una costruzione di copia basata su x. Quindi tutto il contenuto del genitore verrebbe copiato (compresi i valori privati). Ovviamente, se il genitore avesse un membro non copiabile, questo fallirebbe alla compilazione.

La tua funzione membro nell'altra classe sarà simile a:

MyChildClass GetChildClassFromParentClass(MyParentClass parent)
{
   return MyChildClass(parent);
}

Demo

Ci sono dei motivi per non fornire questo comportamento per impostazione predefinita?

Semanticamente, questo non ha sempre senso. Prendi l'esempio di un bambino Square e un genitore astratto Shape : la forma non ha in linea di principio informazioni sufficienti per creare un'istanza di Square .

Il downcasting da una classe generale a una più specializzata viene spesso guardato con sospetto. E copiare per default una classe genitore in una classe figlia sarebbe una sorta di downcasting per impostazione predefinita.

Inoltre, questa caratteristica del linguaggio combinata con altre funzionalità esistenti sarebbe una grande fonte di errore . Se oggi scrivessi qualcosa del tipo:

 Shape x; 
 Circle c = new Circle(radius, center);  
 Square s; 
 x = c;  // ok - polymorphism would do 
 s = c;  /// OUCH !!!

Si otterrebbe un errore di mancata corrispondenza di tipo perché è assurdo assegnare un cerchio a un quadrato.

Ma se la tua funzione "da genitore a figlio" fosse abilitata per impostazione predefinita nella lingua, l'istruzione potrebbe essere compilata:

 s = c;   // would compile but with a different semantic 

Infatti c è un Shape e un Shape potrebbe essere copiato in un Square in base alla funzione desiderata. E scommetto che ci sono grandi probabilità che nella maggior parte dei casi questo sia semplicemente sbagliato e che il disallineamento debba essere portato all'attenzione del programmatore in fase di compilazione!

    
risposta data 12.01.2018 - 20:38
fonte
4

Ciò che descrivi può essere fatto tramite i costruttori.

return new MyChildlClass(10);

MyChildClass dovrebbe quindi fornire un costruttore che chiama il suo costruttore genitore:

public class MyChildClass : MyParentClass
{
    public MyChildClass(int myInt) : base(myInt);
}

Quindi puoi myChildClass.MyInt poiché lo hai contrassegnato come pubblico.

Se vuoi un parametro meno costruttore per il bambino, puoi fare qualcosa di simile:

 public MyChildClass() : base(10);

Quindi, ora abbiamo lo stato / comportamento genitore / figlio predefinito, altrimenti useremo l'altro costruttore per definire il comportamento / stato per il genitore.

    
risposta data 12.01.2018 - 18:09
fonte
3

Se potessi clonare una classe base in una classe figlia, potresti lanciare qualsiasi cosa come qualsiasi cosa. Puoi lanciare un List<int> come object e poi clonare object in ConcurrentQueue<HttpRequest> o Exception .

Ecco un esempio che mostra un problema nel clonare una base nel suo figlio:

public class ClassWithNoValue
{

}

public class ClassWithValue : ClassWithNoValue
{
    public int Value { get; private set; }

    public void SetValue(int value)
    {
        if (value > 0) throw new ArgumentException($"{nameof(value)} must be negative!");
        Value = value;
    }
}

Se potessi clonare ClassWithNoValue in ClassWithValue ignorerai il controllo entro ClassWithValue che impedisce un valore non valido. Value sarebbe zero, perché cos'altro sarebbe diverso da quello predefinito? Scriviamo classi per assicurarci che non possano entrare in uno stato non valido, ma tale "upcloning" andrebbe in giro.

Aiuta se visualizziamo la relazione tra classi base in modo diverso. Esternamente, la relazione è più pronunciata perché una classe può essere ridotta a una classe base.

Ma a parte questo sono classi completamente separate. Una classe base nasconde i suoi interni dalle sue classi ereditate e viceversa. L'unica differenza nella relazione tra classi ereditate è che una classe base può scegliere per condividere determinati dettagli solo con le sue classi ereditate.

Anche i metodi astratti e virtuali non condividono nulla tra le classi. Per una classe base avere tali metodi è essenzialmente la stessa di una classe base che richiede certe dipendenze fornite nel suo costruttore in modo che non possa essere istanziata senza di essi. I metodi virtuali sono come argomenti facoltativi del costruttore con i valori predefiniti specificati internamente.

    
risposta data 12.01.2018 - 23:26
fonte
0

Fondamentalmente, non c'è alcun motivo per cui non possa essere fatto. Per funzionare come ci si aspetterebbe normalmente, dovresti utilizzare una strategia di copia su scrittura (COW).

Cioè, hai due figli, ognuno dei quali ci si aspetta di avere ciò che si comporta come la propria copia dell'oggetto genitore. Finché gli oggetti padre di entrambi contengono gli stessi valori, i due possono condividere l'accesso allo stesso oggetto. Anche se concettualmente abbiamo due oggetti separati, entrambi contengono gli stessi valori, quindi l'accesso allo stesso oggetto (per lo più - vedi sotto) è lo stesso come se avessimo due oggetti separati.

Quando uno degli oggetti figli tenta di cambiare parte (o tutti) dei dati nell'oggetto padre, abbiamo un problema però - se entrambi i bambini contengono riferimenti a un genitore comune, la modifica di un valore genitore in un figlio cambia anche quel valore nell'altro bambino - sicuramente non ciò che normalmente ci aspetteremmo. Per far fronte a ciò, creiamo copie separate del genitore in ogni bambino se e solo se / quando l'una o l'altra scrive sull'oggetto secondario genitore - cioè, se si modifica una parte dal genitore, allora (e solo quindi) creiamo due copie separate e ne modifichiamo una (ma lasciamo l'altra sola).

Tuttavia ci sono ancora alcuni problemi con questo. Uno riguarda i tempi. Qualcuno potrebbe aspettarsi che la creazione di un nuovo oggetto sia piuttosto lenta e assegnando a (per esempio) un int è veloce. Se usiamo la copia su write, creare il nuovo oggetto può essere più veloce, ma il primo assegnamento (anche il più banale) alla parte genitore di un oggetto figlio può essere quasi arbitrariamente lento (cioè, può implicare la copia dell'intero oggetto genitore, che potrebbe essere abbastanza grande).

Potrebbe sorgere un'altra possibile violazione delle aspettative se si utilizzavano due oggetti figlio in thread separati e entrambi condividevano l'accesso a un singolo oggetto padre. Per esempio, su una macchina NUMA, un utente potrebbe aspettarsi che ogni thread abbia accesso "locale" al suo oggetto padre, ma se i due thread fossero su nodi separati si potrebbe invece ottenere un accesso più lento a un nodo remoto.

Allo stesso tempo, non credo che una simile funzione debba necessariamente riflettersi nelle specifiche del linguaggio. Una specifica del linguaggio normalmente pone requisiti sul comportamento osservabile dei programmi. Il punto di usare qualcosa come una strategia COW sarebbe che il comportamento osservabile di un programma non è influenzato. È puramente una strategia di implementazione che non ha alcun effetto sull'interfaccia. I potenziali "problemi" citati sopra (e forse alcuni altri) sono fondamentalmente esempi di potenziali conflitti tra ciò che una specifica del linguaggio potrebbe definire un comportamento "osservabile", e un comportamento che un utente ragionevolmente normale di quel linguaggio potrebbe pensare si qualifica come "osservabile".

    
risposta data 12.01.2018 - 18:08
fonte
0

no mainstream OOP language seems to offer this feature, so there must be objective arguments against it.

Non necessariamente. Le lingue non sono progettate partendo da un pool di "tutte le funzionalità possibili" e rimuovendo esplicitamente le funzionalità considerate problematiche. Piuttosto, la progettazione della lingua inizia in genere da un nucleo minimo o da una lingua precedente e vengono aggiunte solo nuove funzionalità che vengono considerate come valori significativi.

Quindi è abbastanza probabile che nessuno abbia mai pensato di aggiungere questa funzione in primo luogo.

    
risposta data 13.01.2018 - 09:26
fonte

Leggi altre domande sui tag