Quando è il test del tipo OK?


53

Supponendo una lingua con un tipo di sicurezza intrinseca (ad esempio, non JavaScript):

Dato un metodo che accetta un SuperType, sappiamo che nella maggior parte dei casi in cui potremmo essere tentati di eseguire prove di tipo per scegliere un'azione:

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    o.doSomethingA()
  } else {
    o.doSomethingB();
  }
}

Di solito dovremmo, se non sempre, creare un singolo metodo scavalcabile sul SuperTypee fare questo:

public void DoSomethingTo(SuperType o) {
  o.doSomething();
}

... in cui a ogni sottotipo viene data la propria doSomething()implementazione. Il resto della nostra applicazione può quindi essere adeguatamente ignaro del fatto che un dato SuperTypesia davvero un SubTypeAo un SubTypeB.

Meraviglioso.

Tuttavia, ci sono ancora is aoperazioni simili a quelle nella maggior parte, se non in tutte, le lingue sicure di tipo. E ciò suggerisce una potenziale necessità di test espliciti del tipo.

Così, in quali situazioni, se del caso, dovrebbero abbiamo o devono abbiamo eseguire test di tipo esplicito?

Perdona la mia distrazione o mancanza di creatività. So di averlo fatto prima; ma, onestamente, è stato tanto tempo fa che non riesco a ricordare se quello che ho fatto è stato buono! E nella memoria recente, non credo di aver riscontrato la necessità di testare tipi al di fuori del mio cowboy JavaScript.



4
Vale la pena sottolineare che né Java né C # avevano generici nella loro prima versione. Per utilizzare i contenitori è stato necessario eseguire il cast da e verso Object.
Doval,

6
"Controllo del tipo" si riferisce quasi sempre al controllo se il codice rispetta le regole di tipizzazione statiche della lingua. Quello che vuoi dire è generalmente chiamato test di tipo .


3
Questo è il motivo per cui mi piace scrivere C ++ e lasciare RTTI disattivato. Quando uno non può letteralmente testare i tipi di oggetto in fase di esecuzione, costringe lo sviluppatore ad aderire a un buon design OO per quanto riguarda la domanda che viene posta qui.

Risposte:


48

"Mai" è la risposta canonica a "quando va bene il test di tipo?" Non c'è modo di provare o confutare questo; fa parte di un sistema di credenze su ciò che rende "buon design" o "buon design orientato agli oggetti". È anche hokum.

A dire il vero, se hai un set integrato di classi e anche più di una o due funzioni che richiedono quel tipo di test diretto del tipo, probabilmente lo stai facendo male. Ciò di cui hai davvero bisogno è un metodo implementato in modo diverso nei SuperTypesuoi sottotipi. Questo è parte integrante della programmazione orientata agli oggetti ed esistono tutte le classi di ragione e l'ereditarietà.

In questo caso, la verifica esplicita del tipo è errata non perché la verifica del tipo sia intrinsecamente errata, ma perché il linguaggio ha già un modo pulito, estensibile e idiomatico di ottenere la discriminazione del tipo e non è stato utilizzato. Invece, sei tornato a un linguaggio primitivo, fragile, non estensibile.

Soluzione: utilizzare il linguaggio. Come hai suggerito, aggiungi un metodo a ciascuna delle classi, quindi lascia che gli algoritmi di ereditarietà standard e di selezione del metodo determinino il caso applicabile. Oppure, se non riesci a modificare i tipi di base, esegui la sottoclasse e aggiungi il tuo metodo lì.

Questo per quanto riguarda la saggezza convenzionale e alcune risposte. Alcuni casi in cui ha senso il test esplicito del tipo:

  1. È una tantum. Se hai dovuto fare molta discriminazione per tipo, potresti estenderne i tipi o la sottoclasse. Ma tu no. Hai solo uno o due posti in cui hai bisogno di test espliciti, quindi non vale la pena tornare indietro e lavorare attraverso la gerarchia di classi per aggiungere le funzioni come metodi. Oppure non vale la pena lo sforzo pratico di aggiungere il tipo di generalità, test, revisioni del progetto, documentazione o altri attributi delle classi di base per un utilizzo così semplice e limitato. In tal caso, l'aggiunta di una funzione che esegue test diretti è razionale.

  2. Non puoi modificare le classi. Pensi alla sottoclasse, ma non puoi. Molte classi in Java, ad esempio, sono designate final. Cerchi di public class ExtendedSubTypeA extends SubTypeA {...} inserire a e il compilatore ti dice, senza mezzi termini, che ciò che stai facendo non è possibile. Spiacente, grazia e raffinatezza del modello orientato agli oggetti! Qualcuno ha deciso che non puoi estendere i loro tipi! Sfortunatamente, molte delle librerie standard lo sono finale creare classi finalè una guida comune alla progettazione. Una funzione end-run è ciò che ti resta disponibile.

    A proposito, questo non si limita alle lingue tipicamente statiche. Linguaggio dinamico Python ha un numero di classi base che, sotto le copertine implementate in C, non possono essere realmente modificate. Come Java, include la maggior parte dei tipi standard.

  3. Il tuo codice è esterno. Stai sviluppando con classi e oggetti che provengono da una gamma di server di database, motori middleware e altre basi di codice che non puoi controllare o regolare. Il tuo codice è solo un modesto consumatore di oggetti generati altrove. Anche se potresti sottoclassare SuperType, non sarai in grado di ottenere quelle librerie dalle quali dipendi per generare oggetti nelle tue sottoclassi. Ti consegneranno istanze dei tipi che conoscono, non delle tue varianti. Questo non è sempre il caso ... a volte sono progettati per essere flessibili e istanziano dinamicamente istanze di classi che vengono alimentate. Oppure forniscono un meccanismo per registrare le sottoclassi che vuoi che le loro fabbriche costruiscano. I parser XML sembrano particolarmente bravi a fornire tali punti di ingresso; vedi ad es o lxml in Python . Ma la maggior parte delle basi di codice non fornisce tali estensioni. Ti restituiranno le classi con cui sono stati costruiti e di cui sono a conoscenza. In genere non ha senso delegare i loro risultati ai risultati personalizzati solo per poter utilizzare un selettore di tipi orientato agli oggetti. Se hai intenzione di fare una discriminazione di tipo, dovrai farlo in modo relativamente rozzo. Il codice di test del tipo sembra quindi abbastanza appropriato.

  4. Generici poveri / spedizione multipla. Vuoi accettare una varietà di tipi diversi per il tuo codice e ritieni che avere una serie di metodi molto specifici per tipo non sia elegante. public void add(Object x)sembra logico, ma non un array di addByte, addShort, addInt, addLong, addFloat, addDouble, addBoolean, addChar, e addStringvarianti (per citarne alcuni). Avere funzioni o metodi che prendono un super-tipo elevato e quindi determinano cosa fare in base al tipo-tipo-non ti vinceranno il Purity Award al Booch-Liskov Design Symposium annuale, ma lasceranno cadere il La denominazione ungherese ti darà un'API più semplice. In un certo senso, il tuo is-aois-instance-of test sta simulando la distribuzione generica o multipla in un contesto linguistico che non lo supporta nativamente.

    Il supporto linguistico integrato sia per i generici che per la tipizzazione delle anatre riduce la necessità del controllo del tipo rendendo più probabile "fare qualcosa di grazioso e appropriato". La selezione multipla di invio / interfaccia vista in lingue come Julia e Go sostituisce allo stesso modo i test di tipo diretti con meccanismi integrati per la selezione basata sul tipo di "cosa fare". Ma non tutte le lingue supportano queste. Java, ad esempio, è generalmente a invio singolo e i suoi modi di dire non sono super-amichevoli per evitare di scrivere.

    Ma anche con tutte queste caratteristiche di discriminazione di tipo - eredità, generici, tipizzazione anatra e invio multiplo - a volte è solo conveniente avere una singola routine consolidata che rende il fatto che stai facendo qualcosa in base al tipo di oggetto chiaro e immediato. Nella metaprogrammazione l' ho trovato essenzialmente inevitabile. Se ricorrere a domande dirette sul tipo costituisce "pragmatismo in azione" o "codice sporco" dipenderà dalla vostra filosofia e convinzioni progettuali.


Se un'operazione non può essere eseguita su un tipo particolare, ma può essere eseguita su due tipi diversi in cui sarebbe implicitamente convertibile, il sovraccarico è appropriato solo se entrambe le conversioni producono risultati identici. Altrimenti si finisce con comportamenti scadenti come in .NET: (1.0).Equals(1.0f)cedere true [l'argomento promuove a double], ma (1.0f).Equals(1.0)cedere false [l'argomento promuove a object]; in Java, Math.round(123456789*1.0)produce 123456789, ma Math.round(123456789*1.0)produce 123456792 [l'argomento promuove floatpiuttosto che double].
supercat,

Questo è l'argomento classico contro il cast / escalation di tipo automatico. Risultati negativi e paradossali, almeno in casi limite. Sono d'accordo, ma non sono sicuro di come intendi relazionarmi con la mia risposta.
Jonathan Eunice,

Stavo rispondendo al tuo punto n. 4, che sembrava sostenere il sovraccarico piuttosto che usare metodi con nomi diversi con tipi diversi.
supercat,

2
@supercat Incolpare la mia scarsa vista, ma le due espressioni Math.roundsembrano identiche a me. Qual è la differenza?
Lily Chung,

2
@IstvanChung: Ooops ... quest'ultimo avrebbe dovuto essere Math.round(123456789)[indicativo di cosa potrebbe accadere se qualcuno riscrivesse Math.round(thing.getPosition() * COUNTS_PER_MIL)per restituire un valore di posizione non getPositionintlong
scalato

25

La situazione principale di cui abbia mai avuto bisogno era quando si confrontano due oggetti, come in un equals(other)metodo, che potrebbe richiedere algoritmi diversi a seconda del tipo esatto di other. Anche allora, è abbastanza raro.

L'altra situazione in cui l'ho trovata, ancora una volta molto raramente, è dopo la deserializzazione o l'analisi, dove a volte ne hai bisogno per lanciare in modo sicuro un tipo più specifico.

Inoltre, a volte hai solo bisogno di un trucco per aggirare il codice di terze parti che non controlli. È una di quelle cose che non vuoi davvero usare regolarmente, ma sono contento che sia lì quando ne hai davvero bisogno.


1
Fui momentaneamente deliziato dal caso di deserializzazione, avendo ricordato erroneamente di averlo usato lì; ma ora non riesco a immaginare come avrei fatto! So di aver fatto alcune strane ricerche di tipo per quello; ma non sono sicuro che ciò costituisca una prova del tipo . Forse la serializzazione è un parallelo più stretto: dover interrogare oggetti per il loro tipo concreto.
svidgen,

1
La serializzazione è di solito fattibile usando il polimorfismo. Quando si deserializza, spesso si fa qualcosa del genere BaseClass base = deserialize(input), perché non si conosce ancora il tipo, quindi lo si fa if (base instanceof Derived) derived = (Derived)baseper memorizzarlo come tipo derivato esatto.
Karl Bielefeldt,

1
L'uguaglianza ha senso. Nella mia esperienza, tali metodi hanno spesso una forma del tipo “se questi due oggetti hanno lo stesso tipo concreto, restituiscono se tutti i loro campi sono uguali; altrimenti, restituisce false (o non confrontabili) ".
Jon Purdy,

2
In particolare, il fatto che ti ritrovi a testare il tipo è un buon indicatore del fatto che i confronti di uguaglianza polimorfica sono un nido di vipere.
Steve Jessop,

12

Il caso standard (ma si spera raro) assomiglia a questo: se nella seguente situazione

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    DoSomethingA((SubTypeA) o )
  } else {
    DoSomethingB((SubTypeB) o );
  }
}

le funzioni DoSomethingAo DoSomethingBnon possono essere facilmente implementate come funzioni membro dell'albero ereditario di SuperType/ SubTypeA/ SubTypeB. Ad esempio, se

  • i sottotipi fanno parte di una libreria che non è possibile modificare o
  • se aggiungere il codice per DoSomethingXXXquella libreria significherebbe introdurre una dipendenza proibita.

Si noti che spesso ci sono situazioni in cui è possibile aggirare questo problema (ad esempio, creando un wrapper o un adattatore per SubTypeAe SubTypeB, o cercando di reimplementare DoSomethingcompletamente in termini di operazioni di base di SuperType), ma a volte queste soluzioni non valgono la seccatura o rendono cose più complicate e meno estensibili rispetto all'esame esplicito del tipo.

Un esempio del mio lavoro di ieri: ho avuto una situazione in cui avrei parallelizzato l'elaborazione di un elenco di oggetti (di tipo SuperType, con esattamente due diversi sottotipi, in cui è estremamente improbabile che ce ne saranno mai di più). La versione non parallela conteneva due loop: un loop per gli oggetti del sottotipo A, chiamando DoSomethingAe un secondo loop per gli oggetti del sottotipo B, chiamata DoSomethingB.

I metodi "DoSomethingA" e "DoSomethingB" sono entrambi calcoli che richiedono molto tempo, usando informazioni di contesto che non sono disponibili nell'ambito dei sottotipi A e B. (quindi non ha senso implementarli come funzioni membro dei sottotipi). Dal punto di vista del nuovo "ciclo parallelo", rende le cose molto più semplici trattandole in modo uniforme, quindi ho implementato una funzione simile a quella DoSomethingTodall'alto. Tuttavia, esaminare le implementazioni di "DoSomethingA" e "DoSomethingB" mostra che funzionano in modo molto diverso internamente. Quindi cercare di implementare un "DoSomething" generico estendendolo SuperTypecon molti metodi astratti non funzionerà davvero, o significherebbe sovrascrivere completamente le cose.


2
Qualche possibilità che tu possa aggiungere un piccolo esempio concreto per convincere il mio cervello nebbioso che questo scenario non è inventato?
svidgen,

Per essere chiari, non sto dicendo che sia inventato. Ho il sospetto di essere solo estremamente impazzito oggi.
svidgen,

@svidgen: questo è ben lungi dall'essere inventato, in realtà ho riscontrato queste situazioni oggi (anche se non posso pubblicare questo esempio qui perché contiene interni aziendali). E sono d'accordo con le altre risposte che l'uso dell'operatore "is" dovrebbe essere un'eccezione e fatto solo in rari casi.
Doc Brown,

Penso che la tua modifica, che ho trascurato, dia un buon esempio. ... Ci sono casi in cui questo è OK anche quando si esegue il controllo SuperTypeed è sottoclassi?
svidgen,

6
Ecco un esempio concreto: il contenitore di livello superiore in una risposta JSON da un servizio Web può essere un dizionario o un array. In genere, hai qualche strumento che trasforma il JSON in oggetti reali (ad esempio NSJSONSerializationin Obj-C), ma non vuoi semplicemente fidarti che la risposta contenga il tipo che ti aspetti, quindi prima di usarlo lo controlli (ad es. if ([theResponse isKindOfClass:[NSArray class]])...) .
Caleb,

5

Come lo chiama lo zio Bob:

When your compiler forgets about the type.

In uno dei suoi episodi di Clean Coder ha fornito un esempio di una chiamata di funzione che viene utilizzata per restituire Employees. Managerè un sottotipo di Employee. Supponiamo di avere un servizio applicativo che accetta un ManagerID e lo convoca in ufficio :) La funzione getEmployeeById()restituisce un super-tipo Employee, ma voglio verificare se in questo caso d'uso viene restituito un manager.

Per esempio:

var manager = employeeRepository.getEmployeeById(empId);
if (!(manager is Manager))
   throw new Exception("Invalid Id specified.");
manager.summon();

Qui sto controllando per vedere se il dipendente restituito dalla query è in realtà un manager (cioè mi aspetto che sia un manager e se altrimenti fallisce velocemente).

Non è il miglior esempio, ma dopo tutto è lo zio Bob.

Aggiornare

Ho aggiornato l'esempio per quanto posso ricordare dalla memoria.


1
Perché l' Managerimplementazione di summon()non getta l'eccezione in questo esempio?
svidgen,

@svidgen forse la CEOconvocazione può Manager.
user253751

@svidgen, Non sarebbe poi così chiaro che employeeRepository.getEmployeeById (empId) dovrebbe restituire un manager
Ian

@Ian Non lo vedo come un problema. Se il codice chiamante chiede un Employee, dovrebbe preoccuparsi solo di ottenere qualcosa che si comporta come un Employee. Se diverse sottoclassi Employeehanno autorizzazioni, responsabilità diverse, ecc., Cosa rende la verifica del tipo un'opzione migliore di un sistema di autorizzazioni reale?
svidgen,

3

Quando il controllo del tipo è corretto?

Mai.

  1. Avendo il comportamento associato a quel tipo in qualche altra funzione, stai violando il principio aperto chiuso , perché sei libero di modificare il comportamento esistente del tipo cambiando la isclausola, o (in alcune lingue, o in base al scenario) perché non è possibile estendere il tipo senza modificare gli interni della funzione facendo il iscontrollo.
  2. Ancora più importante, i iscontrolli sono un segnale forte che stai violando il principio di sostituzione di Liskov . Tutto ciò che funziona SuperTypedovrebbe essere completamente ignaro di quali sottotipi potrebbero esserci.
  3. Stai associando implicitamente qualche comportamento al nome del tipo. Questo rende il tuo codice più difficile da mantenere perché questi contratti impliciti sono distribuiti in tutto il codice e non è garantito che vengano applicati universalmente e coerentemente come lo sono i membri della classe.

Detto questo, i iscontrolli possono essere meno negativi di altre alternative. Mettere tutte le funzionalità comuni in una classe base è pesante e spesso porta a problemi peggiori. Usare una singola classe che ha una bandiera o enum per quale "tipo" l'istanza è ... è peggio che orribile, dal momento che stai diffondendo l'elusione del sistema di tipi a tutti i consumatori.

In breve, dovresti sempre considerare i controlli del tipo come un forte odore di codice. Ma come per tutte le linee guida, ci saranno momenti in cui sei costretto a scegliere tra quale violazione delle linee guida è la meno offensiva.


3
C'è un avvertimento minore: l' implementazione di tipi di dati algebrici in lingue che non li supportano. Tuttavia, l'uso dell'ereditarietà e del typechecking è puramente un dettaglio di implementazione confuso lì; l'intento non è quello di introdurre sottotipi, ma di classificare i valori. Lo sollevo solo perché gli ADT sono utili e non sono mai un qualificatore molto forte, ma per il resto sono pienamente d'accordo; instanceofperde i dettagli dell'implementazione e rompe l'astrazione.
Doval,

17
"Mai" è una parola che non mi piace molto in un simile contesto, specialmente quando contraddice ciò che scrivi di seguito.
Doc Brown,

10
Se per "mai" intendi effettivamente "a volte", allora hai ragione.
Caleb,

2
OK significa ammissibile, accettabile, ecc., Ma non necessariamente ideale o ottimale. Contrasto con non OK : se qualcosa non va bene, non dovresti farlo affatto. Come indichi, la necessità di controllare il tipo di qualcosa può essere un'indicazione di problemi più profondi nel codice, ma ci sono momenti in cui è l'opzione più conveniente, meno cattiva, e in tali situazioni è ovviamente OK usare gli strumenti a tua disposizione disposizione. (Se fosse facile evitarli in tutte le situazioni, probabilmente non sarebbero lì in primo luogo.) La domanda si riduce identificando quelle situazioni e non aiuta mai .
Caleb,

2
@supercat: un metodo dovrebbe richiedere il tipo meno specifico possibile che soddisfi i suoi requisiti . IEnumerable<T>non promette che esiste un "ultimo" elemento. Se il tuo metodo ha bisogno di un tale elemento, dovrebbe richiedere un tipo che garantisca l'esistenza di uno. E i sottotipi di quel tipo possono quindi fornire implementazioni efficienti del metodo "Last".
cao

2

Se hai una base di codice di grandi dimensioni (oltre 100.000 righe di codice) e sei vicino alla spedizione o stai lavorando in una succursale che in seguito dovrà essere unita, e quindi ci sono molti costi / rischi per cambiare molto.

A volte hai quindi la possibilità di un grande rifrattore del sistema o di alcuni semplici "test di tipo" localizzati. Questo crea un debito tecnico che dovrebbe essere rimborsato al più presto, ma spesso non lo è.

(È impossibile trovare un esempio, poiché qualsiasi codice che è abbastanza piccolo da essere usato come esempio, è anche abbastanza piccolo da rendere chiaramente visibile il design migliore.)

O in altre parole, quando l'obiettivo è quello di essere pagato il tuo salario piuttosto che ottenere "voti positivi" per la pulizia del tuo design.


L'altro caso comune è il codice dell'interfaccia utente, ad esempio quando si mostra un'interfaccia utente diversa per alcuni tipi di dipendenti, ma chiaramente non si desidera che i concetti dell'interfaccia utente sfuggano a tutte le classi del "dominio".

Puoi utilizzare il "test del tipo" per decidere quale versione dell'interfaccia utente mostrare o avere una tabella di ricerca elaborata che converta da "classi di dominio" a "classi dell'interfaccia utente". La tabella di ricerca è solo un modo per nascondere il "test del tipo" in un unico posto.

(Il codice di aggiornamento del database può avere gli stessi problemi del codice dell'interfaccia utente, tuttavia si tende ad avere solo un set di codice di aggiornamento del database, ma è possibile avere molte schermate diverse che devono adattarsi al tipo di oggetto mostrato.)


Il modello visitatore è spesso un buon modo per risolvere il caso dell'interfaccia utente.
Ian Goldby,

@IanGoldby, concordato a volte può essere, comunque stai ancora facendo "test di tipo", un po 'nascosto.
Ian,

Nascosto nello stesso senso in cui è nascosto quando chiami un normale metodo virtuale? O intendi qualcos'altro? Il modello visitatore che ho usato non ha istruzioni condizionali che dipendono dal tipo. È tutto fatto dalla lingua.
Ian Goldby,

@IanGoldby, intendevo nascosto nel senso che può rendere più difficile da capire il codice WPF o WinForms. Mi aspetto che per alcune interfacce Web di base funzionerebbe molto bene.
Ian,

2

L'implementazione di LINQ utilizza un sacco di controllo del tipo per possibili ottimizzazioni delle prestazioni e quindi un fallback per IEnumerable.

L'esempio più ovvio è probabilmente il metodo ElementAt (piccolo estratto del sorgente .NET 4.5):

public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, int index) { 
    IList<TSource> list = source as IList<TSource>;

    if (list != null) return list[index];
    // ... and then an enumerator is created and MoveNext is called index times

Ma ci sono molti posti nella classe Enumerable in cui viene utilizzato un modello simile.

Quindi forse l'ottimizzazione delle prestazioni per un sottotipo comunemente usato è un uso valido. Non sono sicuro di come avrebbe potuto essere progettato meglio.


Avrebbe potuto essere progettato meglio fornendo un mezzo conveniente attraverso il quale le interfacce possono fornire implementazioni predefinite e quindi IEnumerable<T>includendo molti metodi come quelli in List<T>, insieme a una Featuresproprietà che indica quali metodi possono funzionare bene, lentamente o per niente, così come le varie ipotesi che un consumatore può tranquillamente formulare in merito alla raccolta (ad esempio, le sue dimensioni e / o i suoi contenuti esistenti non cambieranno mai [un tipo potrebbe supportare Addpur garantendo che i contenuti esistenti siano immutabili]).
supercat

Ad eccezione degli scenari in cui un tipo potrebbe dover contenere una di due cose completamente diverse in momenti diversi e i requisiti si escludono reciprocamente in modo chiaro (e quindi l'utilizzo di campi separati sarebbe ridondante), in genere ritengo che la necessità di provare a trasmettere sia un segno quei membri che avrebbero dovuto far parte di un'interfaccia di base, non lo erano. Questo non vuol dire che il codice che utilizza un'interfaccia che avrebbe dovuto includere alcuni membri ma non dovrebbe usare try-casting per aggirare l'omissione dell'interfaccia di base, ma quelle interfacce di scrittura di base dovrebbero ridurre al minimo la necessità dei client di provare-cast.
supercat

1

C'è un esempio che emerge spesso negli sviluppi dei giochi, in particolare nel rilevamento delle collisioni, che è difficile da affrontare senza l'uso di una qualche forma di test del tipo.

Supponiamo che tutti gli oggetti di gioco derivino da una classe base comune GameObject. Ogni oggetto ha una forma rigida collisione corpo CollisionShapeche può fornire un'interfaccia comune (dire Posizione interrogazione, orientamento, ecc) ma le forme di collisione effettivi saranno tutti sottoclassi concrete come Sphere, Box, ConvexHull, ecc memorizzare informazioni specifiche per il tipo di oggetto geometrico (vedi qui per un esempio reale)

Ora, per verificare una collisione, devo scrivere una funzione per ogni coppia di tipi di forme di collisione:

detectCollision(Sphere, Sphere)
detectCollision(Sphere, Box)
detectCollision(Sphere, ConvexHull)
detectCollision(Box, ConvexHull)
...

che contengono la matematica specifica necessaria per eseguire un'intersezione di quei due tipi geometrici.

Ad ogni "tick" del mio loop di gioco devo controllare le coppie di oggetti per verificare eventuali collisioni. Ma ho accesso solo a se GameObjecte ai loro corrispondenti CollisionShape. Chiaramente ho bisogno di conoscere tipi concreti per sapere quale funzione di rilevamento delle collisioni chiamare. Nemmeno il doppio invio (che logicamente non è diverso dal controllo del tipo comunque) può aiutare qui *.

In pratica in questa situazione i motori fisici che ho visto (Bullet e Havok) si basano su prove di tipo in una forma o nell'altra.

Non sto dicendo che questo è necessariamente una buona soluzione, è solo che può essere il migliore di un piccolo numero di possibili soluzioni a questo problema

* Tecnicamente è possibile utilizzare il doppio dispacciamento in un modo orrendo e complicato che richiederebbe combinazioni N (N + 1) / 2 (dove N è il numero di tipi di forma che hai) e offuscherebbe solo ciò che stai realmente facendo quale sta scoprendo contemporaneamente i tipi delle due forme, quindi non considero questa una soluzione realistica.


1

A volte non si desidera aggiungere un metodo comune a tutte le classi perché non è in realtà la loro responsabilità eseguire quel compito specifico.

Ad esempio, vuoi disegnare alcune entità ma non vuoi aggiungere il codice di disegno direttamente ad esse (il che ha senso). Nelle lingue che non supportano la spedizione multipla potresti finire con il seguente codice:

void DrawEntity(Entity entity) {
    if (entity instanceof Circle) {
        DrawCircle((Circle) entity));
    else if (entity instanceof Rectangle) {
        DrawRectangle((Rectangle) entity));
    } ...
}

Questo diventa problematico quando questo codice appare in diversi punti e devi modificarlo ovunque quando aggiungi un nuovo tipo di Entità. In tal caso, è possibile evitarlo utilizzando il modello Visitatore, ma a volte è meglio mantenere le cose semplici e non progettarle troppo. Queste sono le situazioni in cui il test del tipo è OK.


0

L'unica volta che uso è in combinazione con la riflessione. Ma anche in questo caso il controllo dinamico è per lo più, non codificato in modo rigido per una classe specifica (o codificato solo per classi speciali come Stringo List).

Per controllo dinamico intendo:

boolean checkType(Type type, Object object) {
    if (object.isOfType(type)) {

    }
}

e non codificato

boolean checkIsManaer(Object object) {
    if (object instanceof Manager) {

    }
}

0

Test di tipo e casting del tipo sono due concetti strettamente correlati. Così strettamente correlato che mi sento fiducioso nel dire che non dovresti mai fare un test di tipo a meno che il tuo intento non sia quello di digitare il cast dell'oggetto in base al risultato.

Quando si pensa al design ideale orientato agli oggetti, i test di tipo (e il casting) non dovrebbero mai avvenire. Ma spero che ormai abbiate capito che la programmazione orientata agli oggetti non è l'ideale. A volte, specialmente con un codice di livello inferiore, il codice non può rimanere fedele all'ideale. Questo è il caso di ArrayLists in Java; poiché non sanno in fase di runtime quale classe viene archiviata nella matrice, creano Object[]array e li lanciano staticamente nel tipo corretto.

È stato sottolineato che una comune necessità di digitare test (e digitare casting) deriva dal Equalsmetodo, che nella maggior parte delle lingue dovrebbe prendere una decisione Object. L'implementazione dovrebbe avere alcuni controlli dettagliati per verificare se i due oggetti sono dello stesso tipo, il che richiede di essere in grado di testare di che tipo sono.

Anche i test di tipo vengono spesso riflessi. Spesso avrai metodi che restituiscono Object[]o qualche altro array generico e vuoi estrarre tutti gli Foooggetti per qualsiasi motivo. Questo è un uso perfettamente legittimo di prove e casting di tipo.

In generale, il test di tipo è negativo quando abbina inutilmente il codice a come è stata scritta un'implementazione specifica. Questo può facilmente portare alla necessità di un test specifico per ogni tipo o combinazione di tipi, ad esempio se si desidera trovare l'intersezione di linee, rettangoli e cerchi e la funzione di intersezione ha un algoritmo diverso per ogni combinazione. Il tuo obiettivo è quello di mettere tutti i dettagli specifici di un tipo di oggetto nello stesso posto di quell'oggetto, perché ciò faciliterà la gestione e l'estensione del codice.


1
ArrayListsnon conosco la classe che viene archiviata in fase di runtime perché Java non aveva generici e quando furono finalmente introdotti Oracle optò per la retrocompatibilità con il codice generics-less. equalsha lo stesso problema ed è comunque una decisione di progettazione discutibile; confronti di uguaglianza non hanno senso per ogni tipo.
Doval,

1
Tecnicamente, le raccolte Java non trasmettono i loro contenuti a nulla. Il compilatore inserisce i dattiloscritti nella posizione di ciascun accesso: ad es.String x = (String) myListOfStrings.get(0)

L'ultima volta che ho guardato il sorgente Java (che potrebbe essere stato 1.6 o 1.5) c'è stato un cast esplicito nel sorgente ArrayList. Genera un avviso del compilatore (soppresso) per una buona ragione, ma è comunque consentito. Suppongo che si possa dire che, a causa dell'implementazione dei generici, è sempre stato Objectfino a quando non si è avuto accesso comunque; i generici in Java forniscono solo il cast implicito reso sicuro dalle regole del compilatore.
Meustrus,

0

È accettabile nel caso in cui si debba prendere una decisione che coinvolge due tipi e questa decisione è incapsulata in un oggetto al di fuori di quella gerarchia di tipi. Ad esempio, supponiamo che tu stia pianificando quale oggetto verrà successivamente elaborato in un elenco di oggetti in attesa di elaborazione:

abstract class Vehicle
{
    abstract void Process();
}

class Car : Vehicle { ... }
class Boat : Vehicle { ... }
class Truck : Vehicle { ... }

Ora diciamo che la nostra logica di business è letteralmente "tutte le auto hanno la precedenza su barche e camion". L'aggiunta di una Priorityproprietà alla classe non consente di esprimere questa logica di business in modo pulito perché finirai con questo:

abstract class Vehicle
{
    abstract void Process();
    abstract int Priority { get }
}

class Car : Vehicle { public Priority { get { return 1; } } ... }
class Boat : Vehicle { public Priority { get { return 2; } } ... }
class Truck : Vehicle { public Priority { get { return 2; } } ... }

Il problema è che ora per capire l'ordinamento prioritario devi guardare tutte le sottoclassi, o in altre parole hai aggiunto l'accoppiamento alle sottoclassi.

Ovviamente dovresti trasformare le priorità in costanti e metterle in una classe da sole, il che aiuta a pianificare insieme la logica aziendale:

static class Priorities
{
    public const int CAR_PRIORITY = 1;
    public const int BOAT_PRIORITY = 2;
    public const int TRUCK_PRIORITY = 2;
}

Tuttavia, in realtà l'algoritmo di pianificazione è qualcosa che potrebbe cambiare in futuro e potrebbe eventualmente dipendere da qualcosa di più del semplice tipo. Ad esempio, potrebbe dire "i camion con un peso superiore a 5000 kg hanno una priorità speciale su tutti gli altri veicoli". Ecco perché l'algoritmo di pianificazione appartiene alla sua stessa classe ed è una buona idea ispezionare il tipo per determinare quale dovrebbe essere il primo:

class VehicleScheduler : IScheduleVehicles
{
    public Vehicle WhichVehicleGoesFirst(Vehicle vehicle1, Vehicle vehicle2)
    {
        if(vehicle1 is Car) return vehicle1;
        if(vehicle2 is Car) return vehicle2;
        return vehicle1;
    }
}

Questo è il modo più semplice per implementare la logica di business e ancora il più flessibile ai cambiamenti futuri.


Non sono d'accordo con il tuo esempio particolare, ma sarei d'accordo con il principio di avere un riferimento privato che può contenere tipi diversi con significati diversi. Quello che suggerirei come esempio migliore sarebbe un campo che può contenere null, a Stringo a String[]. Se il 99% degli oggetti avrà bisogno esattamente di una stringa, l'incapsulamento di ogni stringa in una costruzione separata String[]può aggiungere un notevole sovraccarico di memoria. La gestione del caso a stringa singola utilizzando un riferimento diretto a un Stringrichiederà più codice, ma consente di risparmiare spazio di archiviazione e può rendere le cose più veloci.
supercat

0

Il test del tipo è uno strumento, usalo saggiamente e può essere un potente alleato. Usalo male e il tuo codice inizierà a puzzare.

Nel nostro software abbiamo ricevuto messaggi sulla rete in risposta alle richieste. Tutti i messaggi deserializzati condividevano una classe base comune Message.

Le classi stesse erano molto semplici, solo il payload come proprietà e routine di C # tipizzate per il marshalling e il loro smarsing (In effetti ho generato la maggior parte delle classi usando i modelli t4 dalla descrizione XML del formato del messaggio)

Il codice sarebbe qualcosa del tipo:

Message response = await PerformSomeRequest(requestParameter);

// Server (out of our control) would send one message as response, but 
// the actual message type is not known ahead of time (it depended on 
// the exact request and the state of the server etc.)
if (response is ErrorMessage)
{ 
    // Extract error message and pass along (for example using exceptions)
}
else if (response is StuffHappenedMessage)
{
    // Extract results
}
else if (response is AnotherThingHappenedMessage)
{
    // Extract another type of result
}
// Half a dozen other possible checks for messages

Certo, si potrebbe sostenere che l'architettura del messaggio potrebbe essere progettata meglio ma è stata progettata molto tempo fa e non per C #, quindi è quello che è. Qui i test di tipo hanno risolto un problema reale per noi in un modo non troppo malandato.

Vale la pena notare che C # 7.0 sta ottenendo la corrispondenza del modello (che per molti aspetti è il test di tipo sugli steroidi) non può essere tutto negativo ...


0

Prendi un parser JSON generico. Il risultato di un'analisi riuscita è un array, un dizionario, una stringa, un numero, un valore booleano o un valore nullo. Può essere uno di quelli. E gli elementi di un array o i valori in un dizionario possono essere di nuovo uno di questi tipi. Poiché i dati vengono forniti al di fuori del programma, devi accettare qualsiasi risultato (ovvero devi accettarlo senza arresti anomali; puoi rifiutare un risultato che non è quello che ti aspetti).


Bene, alcuni deserializzatori JSON tenteranno di creare un'istanza di un albero di oggetti se la struttura contiene informazioni sul tipo per "campi di inferenza". Ma si. Penso che questa sia la direzione in cui la risposta di Karl B era diretta.
svidgen
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.