Domanda

Una cosa bella dei metodi anonimi è che posso usare variabili locali nel contesto chiamante. C'è qualche motivo per cui questo non funziona per i parametri out e i risultati delle funzioni?

function ReturnTwoStrings (out Str1 : String) : String;
begin
  ExecuteProcedure (procedure
                    begin
                      Str1 := 'First String';
                      Result := 'Second String';
                    end);
end;

Esempio molto artificiale ovviamente, ma mi sono imbattuto in alcune situazioni in cui questo sarebbe stato utile.

Quando provo a compilare questo, il compilatore si lamenta che "non è in grado di catturare i simboli". Inoltre, ho ricevuto un errore interno una volta quando ho provato a farlo.

EDIT Ho appena realizzato che funziona con parametri normali come

... (List : TList)

Non è problematico come gli altri casi? Chi garantisce che il riferimento punta ancora a un oggetto vivo ogni volta che viene eseguito il metodo anonimo?

È stato utile?

Soluzione

I parametri var e out e la variabile Risultato non possono essere acquisiti perché la sicurezza di questa operazione non può essere verificata staticamente. Quando la variabile Risultato è di tipo gestito, come una stringa o un'interfaccia, l'archiviazione viene effettivamente allocata dal chiamante e un riferimento a questo archivio viene passato come parametro implicito; in altre parole, la variabile Risultato, a seconda del suo tipo, è proprio come un parametro out.

La sicurezza non può essere verificata per il motivo menzionato da Jon. La chiusura creata da un metodo anonimo può sopravvivere all'attivazione del metodo in cui è stata creata e allo stesso modo sopravvivere all'attivazione del metodo che ha chiamato il metodo in cui è stata creata. Pertanto, qualsiasi parametro var o out o variabile del risultato acquisito potrebbe finire orfano, e qualsiasi scrittura effettuata all'interno della chiusura in futuro danneggerebbe lo stack.

Ovviamente Delphi non funziona in un ambiente gestito e non ha le stesse restrizioni di sicurezza come ad es. C #. La lingua potrebbe farti fare quello che vuoi. Tuttavia, risulterebbe difficile diagnosticare i bug in situazioni in cui è andato storto. Il cattivo comportamento si manifesterebbe come variabili locali in un valore che cambia di routine senza una causa prossima visibile; sarebbe anche peggio se il riferimento al metodo fosse chiamato da un altro thread.

Sarebbe abbastanza difficile eseguire il debug. Anche i punti di interruzione della memoria hardware sarebbero uno strumento relativamente scarso, poiché lo stack viene modificato frequentemente. Uno dovrebbe attivare i punti di interruzione della memoria hardware in modo condizionale dopo aver colpito un altro punto di interruzione (ad esempio al momento dell'inserimento del metodo). Il debugger Delphi può farlo, ma rischierei di supporre che la maggior parte delle persone non sia a conoscenza della tecnica.

Aggiorna : rispetto alle aggiunte alla tua domanda, la semantica del passaggio dei riferimenti di istanza in base al valore è leggermente diversa tra i metodi che contengono una chiusura (e cattura il parametro 0 e i metodi che non contengono una chiusura. Entrambi i metodi possono conservare un riferimento all'argomento passato per valore; i metodi che non catturano il parametro possono semplicemente aggiungere il riferimento a un elenco o memorizzarlo in un campo privato.

La situazione è diversa con i parametri passati per riferimento perché le aspettative del chiamante sono diverse. Un programmatore che fa questo:

procedure GetSomeString(out s: string);
// ...
GetSomeString(s);

sarebbe estremamente sorpreso se GetSomeString dovesse mantenere un riferimento alla variabile s trasmessa. D'altra parte:

procedure AddObject(obj: TObject);
// ...
AddObject(TObject.Create);

Non sorprende che AddObject mantenga un riferimento, poiché il nome stesso implica che sta aggiungendo il parametro a un archivio con stato. Indipendentemente dal fatto che tale archivio con stato abbia la forma di una chiusura o meno, è un dettaglio di implementazione del metodo AddObject .

Altri suggerimenti

Il problema è che la tua variabile Str1 non è " posseduta " da ReturnTwoStrings, in modo che il tuo metodo anonimo non possa catturarlo.

Il motivo per cui non è in grado di catturarlo è che il compilatore non conosce il proprietario finale (da qualche parte nello stack delle chiamate per chiamare ReturnTwoStrings), quindi non può determinare da dove catturarlo.

Modifica: (aggiunto dopo un commento di Smasher )

Il nucleo dei metodi anonimi è che catturano le variabili (non i loro valori).

Allen Bauer (CodeGear) spiega un po 'di più sull'acquisizione di variabili nel suo blog .

C'è una domanda C # sull'elusione del problema pure.

Il parametro out e il valore restituito sono irrilevanti dopo il ritorno della funzione: come ti aspetteresti che si comporti il ??metodo anonimo se lo acquisissi e lo eseguissi in seguito? (In particolare, se si utilizza il metodo anonimo per creare un delegato ma non lo si esegue mai, il parametro out e il valore restituito non verrebbero impostati al momento della restituzione della funzione.)

I parametri di uscita sono particolarmente difficili: la variabile che gli alias dei parametri di uscita potrebbero non esistere al momento della successiva chiamata del delegato. Ad esempio, supponiamo di essere in grado di acquisire il parametro out e restituire il metodo anonimo, ma il parametro out è una variabile locale nella funzione chiamante ed è nello stack. Se il metodo chiamante fosse restituito dopo aver archiviato il delegato da qualche parte (o restituito), cosa sarebbe successo quando il delegato fosse stato finalmente chiamato? Dove scriverebbe quando viene impostato il valore del parametro out?

Lo inserisco in una risposta separata perché la tua EDIT rende la tua domanda davvero diversa.

Probabilmente estenderò questa risposta più tardi dato che ho un po 'di fretta per arrivare a un cliente.

La modifica indica che è necessario ripensare i tipi di valore, i tipi di riferimento e l'effetto di var, out, const e nessun contrassegno di parametro.

Facciamo prima i tipi di valore.

I valori dei tipi di valore vivono nello stack e hanno un comportamento di copia su assegnazione. (Proverò a includerne un esempio in seguito).

Se non si ha la marcatura dei parametri, il valore effettivo passato a un metodo (procedura o funzione) verrà copiato nel valore locale di quel parametro all'interno del metodo. Quindi il metodo non funziona sul valore passato ad esso, ma su una copia.

Quando hai out, var o const, allora non ha luogo alcuna copia: il metodo farà riferimento al valore effettivo passato. Per var, consentirà di modificare quel valore effettivo, per const non lo consentirà. Per il resto, non sarai in grado di leggere il valore effettivo, ma sarai comunque in grado di scrivere il valore effettivo.

I valori dei tipi di riferimento vivono nell'heap, quindi per loro non importa se hai un parametro out, var, const o no: quando cambi qualcosa, cambi il valore sull'heap.

Per i tipi di riferimento, si ottiene comunque una copia quando non si ha alcun parametro di marcatura, ma si tratta di una copia di un riferimento che punta ancora al valore sull'heap.

È qui che i metodi anonimi si complicano: fanno un'acquisizione variabile. (Probabilmente Barry può spiegarlo ancora meglio, ma ci proverò) Nel caso modificato, il metodo anonimo acquisirà la copia locale dell'elenco. Il metodo anonimo funzionerà su quella copia locale e dal punto di vista del compilatore tutto è dandy.

Tuttavia, il punto cruciale della tua modifica è la combinazione di "funziona con parametri normali" e "che garantisce che il riferimento punta ancora a un oggetto vivo ogni volta che viene eseguito il metodo anonimo".

Questo è sempre un problema con i parametri di riferimento, non importa se usi metodi anonimi o meno.

Ad esempio questo:

procedure TMyClass.AddObject(Value: TObject);
begin
  FValue := Value;
end;

procedure TMyClass.DoSomething();
begin
  ShowMessage(FValue.ToString());
end;

Chi garantisce che quando qualcuno chiama DoSomething, l'istanza in cui punta FValue esiste ancora? La risposta è che devi garantirlo tu stesso non chiamando DoSomething quando l'istanza di FValue è morta. Lo stesso vale per la tua modifica: non dovresti chiamare il metodo anonimo quando l'istanza sottostante è morta.

Questa è una delle aree in cui le soluzioni di conteggio dei riferimenti o di raccolta dei rifiuti semplificano la vita: lì l'istanza verrà mantenuta in vita fino a quando l'ultimo riferimento ad essa non sarà scomparso (il che potrebbe far sì che l'istanza viva più a lungo di quanto inizialmente previsto!) .

Quindi, con la tua modifica, la tua domanda in realtà cambia da metodi anonimi alle implicazioni dell'uso di parametri tipizzati di riferimento e della gestione della vita in generale.

Spero che la mia risposta ti aiuti ad andare in quella zona.

- Jeroen

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top