Perché l'ereditarietà e le interfacce sono limitate ai membri dell'istanza?

3

Dichiarazione di non responsabilità: penso che le regole siano quasi le stesse nella maggior parte dei linguaggi OO, ma dato che sono più familiare con C #, mi riferirò a questo linguaggio specifico.

Penso che l'uso di attributi e riflessioni potrebbe essere ampiamente ridotto se i membri statici applicassero gli stessi principi delle istanze OO.

Le proprietà e i metodi statici possono essere "sovrascritti" dai decedent utilizzando la nuova parola chiave, ma non possono essere forzati ad implementarne uno tramite un'interfaccia, né la parola chiave 'base' può essere utilizzata (solo il nome di base è supportato esplicitamente ). Per peggiorare le cose, i membri non possono essere referenziati tramite un'istanza, ma devono sempre essere specificatamente qualificati con il tipo esatto.

I costruttori hanno anche un supporto limitato di "ereditarietà", si può scegliere quale costruttore di base è chiamato, ma non si può forzare un oggetto a supportare un costruttore che prende determinati parametri definendolo in un'interfaccia.

Il suggerimento migliore che ho trovato per rispondere a questa domanda è questo in overflow dello stack (risposta a una domanda diversa ).

Nella sua risposta Jörg indica tre principi che si applicano all'Object Orientation:

  • Messaggi,
  • conservazione locale e protezione e occultamento del processo dello stato e
  • estrema rilegatura in ritardo di tutte le cose.

Ora capisco perfettamente che se vuoi passare una classe piena di stato da un metodo a un altro come un oggetto, questi principi hanno senso. Tuttavia, a mio parere, il principio di ereditarietà, i metodi di base prioritari con funzionalità alternative o aggiuntive potrebbero anche essere utili per la logica aziendale senza stato.

Un esempio in cui forzare un oggetto a implementare un determinato costruttore sarebbe utile è con la de-serializzazione. Ad esempio, dove il costruttore deve accettare un nodo XML. In C # è implementabile dinamicamente usando il reflection, ma avere errori in fase di compilazione ed evitare il boilerplate di riflessione aggiungerebbe molto valore!

Il metodo statico / interfacce membro sarebbe utile in situazioni in cui alcuni aspetti di un valore sono specifici del tipo, senza alcuna relazione con l'istanza. Ad esempio, una classe può avere un DisplayName, Unità, Peso, Fattore o altri aspetti che sono uguali in tutte le istanze del tipo specifico, indipendentemente dal valore dell'istanza. Gli attributi (o le annotazioni) possono essere utilizzati per sfruttarli senza la necessità di creare un'istanza, tuttavia, anche in questo caso è un codice aggiuntivo e non c'è modo di imporre l'implementazione con errori in fase di compilazione.

Ammetto che ci sono molti modi semplici per aggirare questi punti, non sto cercando soluzioni alternative. Restituire un valore statico in una proprietà di istanza è abbastanza comune, e non è tanto un overhead per creare un'istanza di un oggetto solo per leggere gli aspetti specifici del tipo, ma mi piacerebbe conoscere lo sfondo logico sul perché statico le cose sono state sistematicamente escluse. Ho già sentito parlare di "stack virtuale", ma non so come funziona tecnicamente (e non trovo alcuna spiegazione semplice in rete).

Le ragioni della natura tecnica o filosofica?

Modifica: elaborazione su interfacce statiche:

Un esempio di come penso che un'interfaccia statica possa fornire un controllo strongmente scritto, controllato, alternativo alla riflessione:

public staticInterface MyStaticInterface
{
    constructor(XmlNode node);
    string DisplayName { get; }
    int Weight { get; }
}

L'utilizzo:

public void FillNodesToolBox() 
{
    MyStaticInterface[] nodes = 
        assembly.GetTypes().Where(t => t.Implements<MyStaticInterface>());

    for (int i = nodes.Length - 1; i >= 0 ; i--)
    {
        Console.WriteLine(string.Concat(
            nodes[i].DisplayName, " (", nodes[i].Weight.ToString(), ")"));
    }
}

Guardando indietro, in realtà sembra che il mio oggetto Type sia ereditato da System.Type, aggiungendo alcune proprietà, che è diverso da un membro statico ...

string.Empty != typeof(string).Emty (quest'ultimo non verrà compilato).

Forse sto iniziando a trovare la risposta, ma sembra che stia camminando in un cerchio paradossale, non è ancora abbastanza "istantaneo" ...

    
posta Louis Somers 27.03.2014 - 01:26
fonte

4 risposte

2

Semplicemente non può funzionare.

Diciamo che hai una classe A che definisce un metodo statico "virtuale". Quindi hai le classi B e C che derivano da A ed entrambe ignorano questo metodo statico. Quindi si chiama A.StaticMethod (). Quale dei B o C dovrebbe essere chiamato?

Il motivo per cui i metodi virtuali sono legati all'istanza è perché il tipo dell'istanza specifica quale implementazione del metodo virtuale viene chiamata. Se non si dispone di queste informazioni, non è possibile chiamare il metodo corretto.

Inoltre, se provi a sostenere che devi chiamare il metodo statico su un tipo in cui è implementato (ad esempio, B.StaticMethod () o C.StaticMethod ()), allora quella non è una chiamata al metodo virtuale e può già implementato in C # senza problemi.

Now I fully understand that if you want to pass on a state-full class from one method to another as an object, these principles make sense. However in my opinion the principle of inheritance, overriding base methods with alternative or additional functionality, could also be beneficial to stateless business logic.

La logica aziendale potrebbe sembrare apolide dal punto di vista dell'astrazione. Quindi progetti le tue interfacce o classi astratte attorno a questo. Ma poi, ti rendi conto che vuoi parametrizzare un comportamento specifico in fase di runtime. Con i metodi virtuali d'istanza, questo è facile. Basta creare un nuovo sottotipo che contenga lo stato specifica questa parametrizzazione. Niente ti impedisce di avere istanze senza stato con i soli metodi, ma avere la capacità di aggiungere lo stato senza modificare l'astrazione è un enorme vantaggio di OOP. Avendo i metodi statici, stai rimuovendo questo vantaggio.

Se pensi davvero che la programmazione "stateless" sia migliore per il tuo progetto, potrebbe essere una buona idea guardare alla programmazione funzionale, che è tutta una questione di metodi di composizione con mantenimento dello stato minimo.

    
risposta data 27.03.2014 - 07:55
fonte
2

I motivi per cui un'interfaccia non può dettare una firma del costruttore o la presenza di metodi statici sono principalmente tecnici.

Quando si chiama il metodo I::foo dell'interfaccia I , l'ambiente di runtime ha bisogno di un po 'di metadati per sapere se è effettivamente X::foo o Y::foo che dovrebbe essere invocato. Questi metadati sono specifici per l'oggetto su cui il metodo è invocato e quindi collegato a quell'oggetto.

Il problema con i costruttori e i metodi statici è che sono invocati quando non esiste ancora alcun oggetto disponibile. Ciò significa che non ci sono metadati disponibili per l'ambiente runtime per determinare da quale classe richiamare il costruttore o il metodo statico.

    
risposta data 27.03.2014 - 09:40
fonte
1

Le interfacce di motivo concettuale non possono dichiarare "membri statici astratti" che lo scopo di un'interfaccia è specificare cosa può essere fatto con un oggetto , non con un tipo . Ad esempio, l'interfaccia .NET IList<T> è un tipo che rappresenta oggetti che si comportano come elenchi.

Ora ha senso specificare un elenco di operazioni che possono essere eseguite su un tipo piuttosto che su un oggetto. Ma una cosa del genere è chiamata typeclass , non un'interfaccia.

Typeclasses

Se C # supporta i typeclass, la dichiarazione di un typeclass potrebbe essere simile a questa:

typeclass CMonoidalList<T> of TList : IList<T>
{
    TList(IEnumerable<T> contents); // a constructor
    TList Append(TList suffix); // an instance method
}

Una classe che implementa questa classe di prodotto potrebbe assomigliare a questa:

class MyIntList : CMonoidalList<int>
{
    public MyIntList(IEnumerable<int> contents) { ... }
    public MyIntList Append(MyIntList suffix) { ... }
    ...
}

E un metodo che usa questa classe di caratteri sarebbe quindi simile a questo:

public static TList AppendString<TList>(TList list, string suffix)
    where TList : CMonoidalList<char>
{
    TList suffixList = new TList(suffix);
    return list.Append(suffixList);
}

Il problema con i typeclass

Lo svantaggio dei typeclass, rispetto alle interfacce, è che i typeclass non hanno senso come tipi come fanno le interfacce. non ha senso scrivere codice che considera CMonoidalList come un tipo:

public static CMonoidalList<T> AppendBackwards<T>(
        CMonoidalList<T> suffix, CMonoidalList<T> prefix)
{
    return prefix.Append(suffix);
}

Questo codice non ha senso perché Append richiede sia prefix che suffix per essere lo stesso tipo che implementa CMonoidalList<T> , ma l'elenco di argomenti di AppendBackwards non riesce a esprimi questo.

Questo significa che i typeclass sono meno convenienti da usare delle interfacce, perché devi usare where ovunque. Inoltre, le interfacce sono un concetto importante nella programmazione orientata agli oggetti, mentre le typeclass non lo sono.

Le interfacce possono essere migliori delle typeclass

Ogni typeclass che puoi pensare fa ha un'interfaccia corrispondente. L'interfaccia corrispondente al tuo MyStaticInterface sarà:

public interface MyClassyInterface<T>
{
    T New(XmlNode node);
    string DisplayName { get; }
    int Weight { get; }
}

Questa interfaccia ha in realtà un vantaggio rispetto alla tua classe di tipizzazione: puoi avere più "implementazioni" di essa per un singolo "tipo di implementazione" T . A volte potresti voler avere un sacco di diversi DisplayNames e Weights senza dover scrivere una classe separata per ognuno. Quando lo fai, avere un'interfaccia invece di un typeclass ti tornerà utile.

Uno svantaggio di questo è che ora devi effettivamente passare attorno alle implementazioni dell'interfaccia, invece di ottenerle automaticamente dal tipo di implementazione. Un altro svantaggio è il fatto che è possibile avere più "implementazioni" per un singolo "tipo di implementazione", anche se si desidera limitare le cose a un'implementazione per tipo!

Altre lingue

La maggior parte delle lingue non ha tipecass, presumibilmente perché sono considerate costose da implementare (dato che, in generale, le più caratteristiche sono costose da implementare) e non fornirebbero molto di più beneficio.

Haskell ha esemplari completi a tutti gli effetti, insieme a tutti i loro vantaggi e svantaggi. I programmatori Haskell usano molto spesso le classificazioni e generalmente non considerano gli svantaggi come un grosso problema.

Scala non ha typeclass, ma ha caratteristiche che ti permettono di "falsificarlo". Se vuoi scrivere un "typeclass" in Scala, lo converti in un'interfaccia (come ho descritto sopra) e scrivi che . Quando si desidera avere una "implementazione del typeclass", si scrive un'implementazione dell'interfaccia e quindi si dichiara che l'implementazione è "implicita". Quindi non sarà necessario esplicitamente passare attorno alle implementazioni dell'interfaccia; il compilatore di Scala proverà a farlo accadere in modo completamente automatico. solitamente funziona.

    
risposta data 08.06.2017 - 23:00
fonte
0

Constructors also have limited "inheritance" support, one can choose which base constructor is called, but you cannot force an object to support a constructor that takes certain parameters by defining it in an interface.

Un'interfaccia per definizione non ha alcuna conoscenza del suo tipo sottostante. Tecnicamente non c'è nemmeno bisogno di essere un tipo sottostante; un'interfaccia è semplicemente una raccolta di funzioni. Anche se i linguaggi OOP tradizionali hanno un meccanismo integrato per le interfacce e si aspettano di "collegarli" alle classi, si potrebbe anche prendere un gruppo di delegati / lambda / funzioni con firme note in qualche tipo di contenitore e che sarebbe un interfaccia anche.

One example where forcing an object to implement a certain constructor would be helpful is with de-serialization. For example where the constructor should accept an XML node.

Secondo me questa è una cattiva idea. Cosa succede quando vuoi serializzare / deserializzare la classe in un modo diverso? O più modi contemporaneamente? Una classe dovrebbe avere solo una ragione per cambiare, e sapere come serializzarsi dà una seconda ragione. Certo, la serializzazione è un caso interessante perché richiede l'incapsulamento, ma sento ancora che è meglio gestirlo esternamente.

Static method/member interfaces would be helpful in situations where some aspects of a value are type specific, without any relation to the instance. For example a class could have a DisplayName, Unit, Weight, Factor or other aspects that are the same across all instances of the specific type, regardless of the instance value.

Un campo statico indica esattamente una cosa per definizione; è per questo che non è legato a nessuna istanza di una classe. Qualcosa non può essere statico e sovrascrivibile allo stesso tempo. Pensa a cosa significa scavalcare qualcosa; avresti qualcosa che sa come individuare il campo a cui stai tentando di accedere. Ciò renderebbe qualsiasi cosa tu stia cercando di ottenere un campo di istanza. Potrebbe non essere un campo istanza delle istanze della classe, ma dovresti trasformare la classe stessa in un oggetto.

Come nota finale, l'ereditarietà è già un meccanismo problematico. Non è intrinsecamente componibile dal momento che puoi ereditare solo da una cosa alla volta (altrimenti apri una diversa lattina di worm) e se non imponi restrizioni su cosa può essere ignorato, le modifiche alla classe base che sarebbero sicure quando considerato isolatamente può rompere le sottoclassi. Essere in grado di sovrascrivere i campi statici sarebbe di valore molto dubbio.

    
risposta data 27.03.2014 - 04:17
fonte

Leggi altre domande sui tag