2009-04-16 13 views
12

Se ho le seguenti interfacce e una classe che li implementa - '! Bad'In Delphi, come è possibile verificare se un riferimento IInterface implementa un'interfaccia derivata ma non esplicitamente supportata?

IBase = Interface ['{82F1F81A-A408-448B-A194-DCED9A7E4FF7}'] 
End; 

IDerived = Interface(IBase) ['{A0313EBE-C50D-4857-B324-8C0670C8252A}'] 
End; 

TImplementation = Class(TInterfacedObject, IDerived) 
End; 

Il codice seguente stampa -

Procedure Test; 
Var 
    A : IDerived; 
Begin 
    A := TImplementation.Create As IDerived; 
    If Supports (A, IBase) Then 
     WriteLn ('Good!') 
    Else 
     WriteLn ('Bad!'); 
End; 

Questo è un po 'fastidioso ma comprensibile. I supporti non possono eseguire il cast in IBase perché IBase non si trova nell'elenco dei GUID supportato da TImplementation. Può essere risolto modificando la dichiarazione -

TImplementation = Class(TInterfacedObject, IDerived, IBase) 

Eppure, anche senza fare che io so già che A implementa IBase perché A è un IDerived ed un IDerived è un IBase. Quindi, se lascio la verifica che posso lanciare un e tutto andrà bene -

Procedure Test; 
Var 
    A : IDerived; 
    B : IBase; 
Begin 
    A := TImplementation.Create As IDerived; 
    B := IBase(A); 
    //Can now successfully call any of B's methods 
End; 

Ma ci imbattiamo in un problema quando abbiamo iniziare a mettere IBases in un contenitore generico - TInterfaceList per esempio. Può contenere solo IInterfaces quindi dobbiamo fare un po 'di casting.

Procedure Test2; 
Var 
    A : IDerived; 
    B : IBase; 
    List : TInterfaceList; 
Begin 
    A := TImplementation.Create As IDerived; 
    B := IBase(A); 

    List := TInterfaceList.Create; 
    List.Add(IInterface(B)); 
    Assert (Supports (List[0], IBase)); //This assertion fails 
    IBase(List[0]).DoWhatever; //Assuming I declared DoWhatever in IBase, this works fine, but it is not type-safe 

    List.Free; 
End; 

Mi piacerebbe molto avere una sorta di affermazione per la cattura di tutti i tipi non corrispondenti - questo genere di cose si possono fare con oggetti utilizzando l'operatore è, ma che non funziona per le interfacce. Per varie ragioni, non voglio aggiungere esplicitamente IBase all'elenco delle interfacce supportate. C'è un modo in cui posso scrivere TImplementation e l'asserzione in modo tale che valuterà il vero iff hard-casting IBase (List [0]) è una cosa sicura da fare?

Edit:

Come è venuto in una delle risposte, sto aggiungendo i due principali motivi che non voglio aggiungere IBase alla lista delle interfacce che implementa TImplementation.

In primo luogo, in realtà non risolve il problema. Se, in Test2, l'espressione:

Supports (List[0], IBase) 

restituisce vero, questo non significa che sia sicuro per eseguire un hard-cast. QueryInterface può restituire un puntatore diverso per soddisfare l'interfaccia richiesta. Per esempio, se TImplementation implementa in modo esplicito sia IBase e IDerived (e IInterface), quindi l'affermazione passeranno con successo:

Assert (Supports (List[0], IBase)); //Passes, List[0] does implement IBase 

Ma immaginate che qualcuno aggiunge erroneamente un elemento alla lista come IInterface

List.Add(Item As IInterface); 

L'asserzione passa ancora - l'elemento implementa ancora IBase, ma il riferimento aggiunto alla lista è solo un IInterface - il suo hard-casting su un IBase non produrrebbe alcunché di ragionevole, quindi l'asserzione non è sufficiente per verificare se il seguente hard -cast è sicuro. L'unico modo che è garantito per il lavoro sarebbe quella di utilizzare un come-fuso o supporti:

(List[0] As IBase).DoWhatever; 

Ma questo è un costo delle prestazioni frustrante, in quanto è destinato ad essere la responsabilità del codice di aggiunta di elementi all'elenco assicurati che siano del tipo IBase - dovremmo essere in grado di assumerlo (da qui l'affermazione da prendere se questa ipotesi è falsa).L'affermazione non è nemmeno necessaria, tranne per cogliere errori successivi se qualcuno cambia alcuni dei tipi. Il codice originale da cui proviene questo problema è anche abbastanza critico dal punto di vista delle prestazioni, quindi un costo per le prestazioni che raggiunge poco (si cattura ancora solo tipi non corrispondenti durante l'esecuzione, ma senza la possibilità di compilare una build di rilascio più veloce) è qualcosa che preferisco evitare .

Il secondo motivo è che voglio essere in grado di confrontare i riferimenti per l'uguaglianza, ma questo non può essere fatto se lo stesso oggetto di implementazione è detenuto da riferimenti diversi con offset VMT diversi.

Modifica 2: Ampliato la modifica sopra riportata con un esempio.

Modifica 3: Nota: la domanda è: come posso formulare l'asserzione in modo che l'hard-cast sia sicuro se passa l'asserzione, non come evitare l'hard-cast. Ci sono modi per fare il passo del cast in modo diverso, o per evitarlo completamente, ma se c'è un costo delle prestazioni di runtime, non posso usarli. Voglio tutto il costo del controllo entro l'asserzione in modo che possa essere compilato in seguito.

Detto questo, se qualcuno può evitare del tutto il problema senza costi di prestazioni e nessun pericolo di controllo del tipo sarebbe fantastico!

+0

Nei miei test l'offset VMT in GetInterfaceTable() era uguale anche se sono stati implementati due GUID diversi: ha senso se un'interfaccia "eredita" dall'altra, quindi la base punta solo alla prima parte della stessa tabella . – mghie

+0

Era in codice win32? Ho scoperto che ogni interfaccia che ho aggiunto esplicitamente a TImplementation ha aumentato la dimensione dell'istanza della classe di 4 byte - un puntatore VMT aggiuntivo. Anche ogni GUID passato ai supporti, anche da interfacce ereditate, restituirebbe un indirizzo univoco. – David

+0

Il motivo per cui lo chiedo è che .net Delphi potrebbe comportarsi in modo diverso - Aggiungerò un tag win32 – David

risposta

12

Una cosa che puoi fare è interrompe le interfacce di tipo casting. Non è necessario farlo per passare da IDerived a IBase e non è necessario passare da IBase a IUnknown.Qualsiasi riferimento a IDerivedè giàIBase, quindi è possibile chiamare i metodi IBase anche senza digitare il cast. Se si esegue una digitazione di tipo inferiore, si lascia che il compilatore lavori di più e rilevi cose che non sono valide.

Il tuo obiettivo dichiarato è di essere in grado di verificare che la cosa che stai uscendo dalla tua lista sia davvero un riferimento IBase. L'aggiunta di IBase come interfaccia implementata consente di raggiungere facilmente questo obiettivo. In questa luce, i tuoi "due principali motivi" per non farlo non contengono acqua.

  1. "Voglio essere in grado di confrontare i riferimenti per l'uguaglianza": Nessun problema. COM richiede che se si chiama QueryInterface due volte con lo stesso GUID sullo stesso oggetto, si ottiene lo stesso puntatore di interfaccia entrambe le volte. Se si dispone di due riferimenti all'interfaccia arbitraria e si as li trasmette entrambi a IBase, i risultati avranno lo stesso valore puntatore se e solo se sono supportati dallo stesso oggetto.

    Dal momento che sembra voler vostra lista per contenere solo IBase valori, e non si dispone di Delphi 2009 in cui un generico TInterfaceList<IBase> sarebbe utile, si può disciplinare te stesso per sempre aggiungere esplicitamente IBase valori all'elenco, mai valori di qualsiasi tipo di discendente. Ogni volta che si aggiunge un elemento al codice lista, l'uso in questo modo:

    List.Add(Item as IBase); 
    

    In questo modo, i duplicati nella lista sono facili da rilevare, e la tua "calchi hard" sono assicurati a lavorare.

  2. "In realtà non risolve il problema": ma lo fa, data la regola sopra.

    Assert(Supports(List[i], IBase)); 
    

    Quando l'oggetto implementa esplicitamente tutte le sue interfacce, è possibile controllare cose del genere. E se hai aggiunto elementi alla lista come ho descritto sopra, è sicuro disabilitare l'asserzione. L'abilitazione dell'asserzione consente di rilevare quando qualcuno ha modificato il codice altrove nel programma per aggiungere un elemento all'elenco in modo errato. L'esecuzione frequente dei test di unità consente di rilevare il problema molto presto anche dopo l'introduzione.

Con i punti di cui sopra in mente, è possibile verificare che tutto ciò che è stato aggiunto alla lista è stato aggiunto correttamente con questo codice:

var 
    AssertionItem: IBase; 

Assert(Supports(List[i], IBase, AssertionItem) 
     and (AssertionItem = List[i])); 
// I don't recall whether the compiler accepts comparing an IBase 
// value (AssertionItem) to an IUnknown value (List[i]). If the 
// compiler complains, then simply change the declaration to 
// IUnknown instead; the Supports function won't notice. 

Se l'asserzione fallisce, allora o si aggiunto qualcosa da l'elenco che non supporta affatto IBase o il riferimento di interfaccia specifico aggiunto per alcuni oggetti non può servire come riferimento IBase. Se l'asserzione passa, allora sai che List[i] ti darà un valore valido IBase.

Si noti che il valore aggiunto all'elenco non deve essere un valore IBase esplicitamente. Dato tue dichiarazioni di tipo di cui sopra, questo è sicuro:

var 
    A: IDerived; 
begin 
    A := TImplementation.Create; 
    List.Add(A); 
end; 

che è sicuro perché le interfacce implementate da TImplementation formano un albero di eredità che degenera in un elenco semplice. Non ci sono rami in cui due interfacce non ereditano l'una dall'altra ma hanno un antenato comune.Se due decendenti di IBase e TImplementation li hanno entrambi implementati, il codice precedente non sarebbe valido perché il riferimento IBase detenuto in A non sarebbe necessariamente il riferimento "canonico" IBase per quell'oggetto. L'asserzione rileverà quel problema, e avresti bisogno di aggiungerlo con List.Add(A as IBase).

Quando si disabilitano le asserzioni, il costo di ottenere i tipi corretti viene pagato solo durante l'aggiunta all'elenco, non durante la lettura dall'elenco. Ho chiamato la variabile AssertionItem per scoraggiare l'utilizzo di tale variabile altrove nella procedura; è lì solo per supportare l'asserzione, e non avrà un valore valido una volta che le asserzioni saranno disabilitate.

+0

Hm, vorrei poter revocare questa risposta più di una volta. :-) –

+0

Anche senza generici puoi scrivere la tua TBaseList (simile a TInterfaceList, per contenere i riferimenti IBase). –

+0

Hai ragione che as-casting posso risolvere tutti i miei problemi, ma con un costo in termini di prestazioni. Anche se aggiungo esplicitamente tutte le interfacce a TImplementation, l'asserzione non garantisce che l'hard-cast sia sicuro, dovrebbe essere un cast. Ho modificato la mia domanda per ampliarla. – David

0

In Test2;

È shound't digitare nuovamente IDerived come IBase da IBase (A), ma con:

Supports(A, IBase, B); 

e aggiungendo alla lista può essere solo:

List.Add(B); 
6

Si trova proprio nel vostro esame e come Per quanto posso dire, non c'è davvero una soluzione diretta al problema che hai incontrato. La ragione sta nella natura dell'ereditarietà tra le interfacce, che ha solo una vaga rassomiglianza di eredità tra le classi. Un'interfaccia ereditata è un'interfaccia completamente nuova, che ha alcuni metodi in comune con l'interfaccia ereditata da, ma nessuna connessione diretta. Quindi, scegliendo di non implementare l'interfaccia della classe base, si sta facendo un'ipotesi specifica che il programma compilato seguirà: TImplementation non implementa IBase. Penso che "l'ereditarietà dell'interfaccia" sia un po 'approssimativa, l'estensione dell'interfaccia ha più senso! Una pratica comune è quella di avere una classe base che implementa l'interfaccia di base e di classi derivate che implementano le interfacce estese, ma nel caso in cui si desideri una classe separata che implementa sia semplicemente elencare tali interfacce. C'è un motivo specifico che vuoi evitare di usare:

TImplementation = Class(TInterfacedObject, IDerived, IBase) 

o semplicemente non ti piace?

ulteriore commento

non si dovrebbe mai, anche di tipo difficile lanciare un'interfaccia. Quando fai "come" su un'interfaccia, regola i puntatori dell'oggetto vtable nel modo giusto ... se fai un cast duro (e hai metodi per chiamare) il tuo codice può facilmente bloccarsi. La mia impressione è che si trattino le interfacce come gli oggetti (usando l'ereditarietà e le trasmissioni allo stesso modo) mentre il loro funzionamento interno è molto diverso!

+0

+1. Si noti che se davvero non si desidera menzionare IBase nell'elenco delle interfacce implementate, è possibile ripetere le funzioni e le procedure di IBase in IDerived che è necessario chiamare, senza cambiare IDerived. – mghie

+0

Ci sono motivi per evitare l'aggiunta di IBase, ora li ho aggiunti alla domanda originale. Avevo paura che le interfacce "ereditate" non fossero realmente connesse, come dici tu. Comunque ho il controllo sull'attore. C'è forse un modo pulito per introdurre la funzionalità che sto cercando? – David