Qual è il vero sovraccarico di try / catch in C #?


94

Quindi, so che try / catch aggiunge un po 'di overhead e quindi non è un buon modo per controllare il flusso del processo, ma da dove viene questo overhead e qual è il suo impatto effettivo?

Risposte:


52

Non sono un esperto di implementazioni linguistiche (quindi prendilo con le pinze), ma penso che uno dei costi maggiori sia srotolare lo stack e memorizzarlo per la traccia dello stack. Sospetto che ciò accada solo quando viene lanciata l'eccezione (ma non lo so), e in tal caso, questo sarebbe un costo nascosto di dimensioni adeguate ogni volta che viene lanciata un'eccezione ... quindi non è come se stessi saltando da un posto nel codice a un altro, c'è molto da fare.

Non penso che sia un problema fintanto che si utilizzano eccezioni per comportamenti ECCEZIONALI (quindi non il tipico percorso previsto attraverso il programma).


33
Più precisamente: provare costa poco, pescare costa poco, lanciare è costoso. Se eviti di provare a prendere, il lancio è comunque costoso.
Programmatore Windows

4
Hmmm - il mark-up non funziona nei commenti. Per riprovare - le eccezioni sono per errori, non per "comportamento eccezionale" o condizioni: blogs.msdn.com/kcwalina/archive/2008/07/17/…
HTTP 410

2
@ Programmatore di Windows Statistiche / fonte per favore?
Kapé

100

Tre punti da sottolineare qui:

  • In primo luogo, c'è poca o NESSUNA penalizzazione delle prestazioni nell'avere effettivamente blocchi try-catch nel codice. Questo non dovrebbe essere preso in considerazione quando si cerca di evitare di averli nella propria applicazione. Il risultato positivo entra in gioco solo quando viene generata un'eccezione.

  • Quando viene lanciata un'eccezione in aggiunta alle operazioni di srotolamento dello stack, ecc. Che si verificano, che altri hanno menzionato, dovresti essere consapevole che un intero gruppo di cose relative a runtime / riflessione accade per popolare i membri della classe di eccezione come la traccia dello stack oggetto e i vari membri del tipo ecc.

  • Credo che questo sia uno dei motivi per cui il consiglio generale se hai intenzione di rilanciare l'eccezione è semplicemente throw;piuttosto che lanciare di nuovo l'eccezione o costruirne una nuova poiché in quei casi tutte le informazioni sullo stack vengono raccolte mentre nel semplice buttalo è tutto conservato.


41
Inoltre: quando rilanci un'eccezione come "throw ex", perdi la traccia dello stack originale e la sostituisci con la traccia dello stack CURRENT; raramente ciò che si vuole. Se si "lancia" solo la traccia dello stack originale nell'eccezione viene conservata.
Eddie,

@Eddie Orthrow new Exception("Wrapping layer’s error", ex);
binki

21

Stai chiedendo informazioni sull'overhead dell'utilizzo di try / catch / latest quando le eccezioni non vengono generate o sull'overhead dell'utilizzo delle eccezioni per controllare il flusso del processo? Quest'ultimo è in qualche modo simile all'uso di un candelotto di dinamite per accendere la candela di compleanno di un bambino, e l'overhead associato rientra nelle seguenti aree:

  • È possibile aspettarsi ulteriori mancati riscontri nella cache a causa dell'eccezione generata che accede ai dati residenti normalmente non nella cache.
  • È possibile aspettarsi errori di pagina aggiuntivi a causa dell'eccezione generata che accede a codice e dati non residenti normalmente non presenti nel working set dell'applicazione.

    • ad esempio, il lancio dell'eccezione richiederà al CLR di trovare la posizione dei blocchi finalmente e catch in base all'IP corrente e all'IP di ritorno di ogni frame finché l'eccezione non viene gestita più il blocco del filtro.
    • costi di costruzione aggiuntivi e risoluzione dei nomi per creare i frame per scopi diagnostici, inclusa la lettura dei metadati, ecc.
    • entrambi gli elementi sopra in genere accedono al codice e ai dati "freddi", quindi sono probabili errori di pagina rigida se si dispone di una pressione della memoria:

      • il CLR cerca di mettere codice e dati che vengono usati raramente lontano dai dati che vengono usati frequentemente per migliorare la località, quindi questo funziona contro di te perché stai forzando il freddo a essere caldo.
      • il costo degli errori di pagina rigidi, se ce ne sono, farà impallidire tutto il resto.
  • Le tipiche situazioni di cattura sono spesso profonde, quindi gli effetti di cui sopra tenderebbero ad essere amplificati (aumentando la probabilità di errori di pagina).

Per quanto riguarda l'impatto effettivo del costo, questo può variare molto a seconda di cos'altro sta succedendo nel tuo codice in quel momento. Jon Skeet ha un buon riassunto qui , con alcuni link utili. Tendo ad essere d'accordo con la sua affermazione secondo cui se arrivi al punto in cui le eccezioni danneggiano in modo significativo la tua prestazione, hai problemi in termini di utilizzo delle eccezioni oltre la semplice prestazione.


6

Nella mia esperienza il sovraccarico più grande è effettivamente lanciare un'eccezione e gestirla. Una volta ho lavorato a un progetto in cui è stato utilizzato un codice simile al seguente per verificare se qualcuno aveva il diritto di modificare un oggetto. Questo metodo HasRight () è stato utilizzato ovunque nel livello di presentazione ed è stato spesso chiamato per centinaia di oggetti.

bool HasRight(string rightName, DomainObject obj) {
  try {
    CheckRight(rightName, obj);
    return true;
  }
  catch (Exception ex) {
    return false;
  }
}

void CheckRight(string rightName, DomainObject obj) {
  if (!_user.Rights.Contains(rightName))
    throw new Exception();
}

Quando il database di test è diventato più pieno di dati di test, questo ha portato a un rallentamento molto visibile durante l'apertura di nuovi moduli, ecc.

Quindi l'ho riformattato nel modo seguente, che - secondo le misurazioni successive di quick 'n dirty - è circa 2 ordini di grandezza più veloce:

bool HasRight(string rightName, DomainObject obj) {
  return _user.Rights.Contains(rightName);
}

void CheckRight(string rightName, DomainObject obj) {
  if (!HasRight(rightName, obj))
    throw new Exception();
}

Quindi, in breve, l'utilizzo delle eccezioni nel flusso di processo normale è circa due ordini di grandezza più lento rispetto all'utilizzo di un flusso di processo simile senza eccezioni.


1
Perché dovresti lanciare un'eccezione qui? Potresti gestire il caso di non avere i diritti sul posto.
ThunderGr

@ThunderGr è in realtà quello che ho cambiato, rendendolo due ordini di grandezza più veloce.
Tobi

5

Contrariamente alle teorie comunemente accettate, try/ catchpuò avere implicazioni significative sulle prestazioni, e questo è se viene generata un'eccezione o meno!

  1. Disabilita alcune ottimizzazioni automatiche (in base alla progettazione) e in alcuni casi inserisce il codice di debug , come ci si può aspettare da un aiuto per il debug . Ci saranno sempre persone che non sono d'accordo con me su questo punto, ma la lingua lo richiede e lo smontaggio lo mostra, quindi queste persone sono per definizione deliranti .
  2. Può avere un impatto negativo sulla manutenzione. Questo è in realtà il problema più significativo qui, ma poiché la mia ultima risposta (che si concentrava quasi interamente su di essa) è stata eliminata, cercherò di concentrarmi sul problema meno significativo (la microottimizzazione) rispetto al problema più significativo ( la macroottimizzazione).

Il primo è stato trattato in un paio di post di blog da Microsoft MVP nel corso degli anni e confido che potresti trovarli facilmente, ma StackOverflow si preoccupa così tanto dei contenuti, quindi fornirò collegamenti ad alcuni di essi come prova di riempimento :

C'è anche questa risposta che mostra la differenza tra il codice disassemblato con e senza l'utilizzo di try/ catch.

Sembra così ovvio che ci sia un sovraccarico che è palesemente osservabile nella generazione del codice e che sembra persino essere riconosciuto da persone che Microsoft apprezza! Eppure sto ripetendo Internet ...

Sì, ci sono dozzine di istruzioni MSIL extra per una banale riga di codice, e questo non copre nemmeno le ottimizzazioni disabilitate, quindi tecnicamente è una microottimizzazione.


Ho postato una risposta anni fa che è stata cancellata in quanto si concentrava sulla produttività dei programmatori (l'ottimizzazione macro).

Questo è un peccato perché è probabile che nessun risparmio di pochi nanosecondi qua e là di tempo della CPU compensi molte ore accumulate di ottimizzazione manuale da parte degli esseri umani. Per cosa paga di più il tuo capo: un'ora del tuo tempo o un'ora con il computer acceso? A che punto stacciamo la spina e ammettiamo che è ora di comprare un computer più veloce ?

Chiaramente, dovremmo ottimizzare le nostre priorità , non solo il nostro codice! Nella mia ultima risposta ho attinto alle differenze tra due frammenti di codice.

Utilizzando try/ catch:

int x;
try {
    x = int.Parse("1234");
}
catch {
    return;
}
// some more code here...

Non utilizzando try/ catch:

int x;
if (int.TryParse("1234", out x) == false) {
    return;
}
// some more code here

Considera dal punto di vista di uno sviluppatore di manutenzione, che è più probabile che ti faccia perdere tempo, se non nella profilazione / ottimizzazione (trattata sopra) che probabilmente non sarebbe nemmeno necessaria se non fosse per il try/ catchproblema, quindi nello scorrimento codice sorgente ... Uno di questi ha quattro righe extra di immondizia boilerplate!

Man mano che sempre più campi vengono introdotti in una classe, tutta questa immondizia standard si accumula (sia nel codice sorgente che nel codice disassemblato) ben oltre i livelli ragionevoli. Quattro righe in più per campo, e sono sempre le stesse righe ... Non ci è stato insegnato a evitare di ripeterci? Suppongo che potremmo nascondere il try/ catchdietro qualche astrazione fatta in casa, ma ... allora potremmo anche evitare le eccezioni (cioè l'uso Int.TryParse).

Questo non è nemmeno un esempio complesso; Ho visto tentativi di istanziare nuove classi in try/ catch. Considera che tutto il codice all'interno del costruttore potrebbe quindi essere squalificato da alcune ottimizzazioni che altrimenti verrebbero applicate automaticamente dal compilatore. Quale modo migliore per dare origine alla teoria secondo cui il compilatore è lento , al contrario del compilatore che fa esattamente quello che gli viene detto di fare ?

Supponendo che venga lanciata un'eccezione da tale costruttore e che di conseguenza venga attivato qualche bug, il povero sviluppatore di manutenzione deve quindi rintracciarlo. Potrebbe non essere un compito così facile, poiché a differenza del codice spaghetti di goto nightmare, try/ catchpuò causare confusione in tre dimensioni , poiché potrebbe spostarsi in alto nello stack non solo in altre parti dello stesso metodo, ma anche in altre classi e metodi , che sarà osservato dallo sviluppatore della manutenzione, nel modo più duro ! Eppure ci viene detto che "goto è pericoloso", eh!

Alla fine menziono, try/ catchha il suo vantaggio che è progettato per disabilitare le ottimizzazioni ! È, se vuoi, un aiuto per il debugging ! Questo è ciò per cui è stato progettato ed è ciò che dovrebbe essere usato come ...

Immagino che anche questo sia un punto positivo. Può essere utilizzato per disabilitare le ottimizzazioni che altrimenti potrebbero paralizzare algoritmi di passaggio di messaggi sicuri e sani per le applicazioni multithread e per rilevare possibili condizioni di competizione;) Questo è l'unico scenario a cui riesco a pensare di usare try / catch. Anche questo ha delle alternative.


Cosa fare ottimizzazioni try, catche finallydisabilitare?

AKA

Come stai try, catche finallyutili come il debug aiuti?

sono barriere in scrittura. Questo viene dallo standard:

12.3.3.13 Istruzioni try-catch

Per una dichiarazione stmt del modulo:

try try-block
catch ( ... ) catch-block-1
... 
catch ( ... ) catch-block-n
  • Lo stato di assegnazione definita di v all'inizio del blocco try è uguale allo stato di assegnazione definita di v all'inizio di stmt .
  • Lo stato di assegnazione definita di v all'inizio di catch-block-i (per ogni i ) è uguale allo stato di assegnazione definita di v all'inizio di stmt .
  • Lo stato di assegnazione definito di v al punto finale di stmt è definitivamente assegnato se (e solo se) v è assegnato definitivamente al punto finale di try-block e ogni catch-block-i (per ogni i da 1 a n ).

In altre parole, all'inizio di ogni tryaffermazione:

  • tutte le assegnazioni fatte agli oggetti visibili prima di inserire l' tryistruzione devono essere complete, il che richiede un thread lock per iniziare, rendendolo utile per il debug delle race condition!
  • al compilatore non è consentito:
    • eliminare le assegnazioni di variabili inutilizzate che sono state definitivamente assegnate prima trydell'istruzione
    • riorganizzare o unire i suoi compiti interni (cioè vedere il mio primo collegamento, se non l'hai già fatto).
    • sollevare le assegnazioni su questa barriera, ritardare l'assegnazione a una variabile che sa non verrà utilizzata fino a tardi (se non del tutto) o spostare preventivamente le assegnazioni successive in avanti per rendere possibili altre ottimizzazioni ...

Una storia simile vale per ogni catchaffermazione; supponiamo che all'interno della tua tryistruzione (o un costruttore o una funzione che invoca, ecc.) tu assegni a quella variabile altrimenti inutile (diciamo, garbage=42;), il compilatore non può eliminare quell'istruzione, non importa quanto sia irrilevante per il comportamento osservabile del programma . L'assegnazione deve essere completata prima di catchinserire il blocco.

Per quello che vale, finally racconta una storia altrettanto degradante :

12.3.3.14 Istruzioni di prova

Per una dichiarazione di prova stmt del modulo:

try try-block
finally finally-block

• Lo stato di assegnazione definito di v all'inizio di try-block è uguale allo stato di assegnazione definita di v all'inizio di stmt .
• Lo stato di assegnazione definita di v all'inizio del blocco alla fine è uguale allo stato di assegnazione definita di v all'inizio di stmt .
• Lo stato di assegnazione definito di v al punto finale di stmt è assegnato definitivamente se (e solo se): o v è assegnato definitivamente al punto finale del blocco try o vè definitivamente assegnato al punto finale del blocco finale Se viene effettuato un trasferimento del flusso di controllo (come un'istruzione goto ) che inizia all'interno del blocco try e termina all'esterno del blocco try , allora anche v è considerato definitivamente assegnato su quello controllare il trasferimento del flusso se v è definitivamente assegnato al punto finale del blocco finale . (Questo non è un solo se, se v è assegnato definitivamente per un altro motivo in questo trasferimento del flusso di controllo, allora è ancora considerato assegnato definitivamente.)

12.3.3.15 Istruzioni Try-catch-latest

Analisi assegnazione definitiva per una prova - fermo - finalmente dichiarazione del modulo:

try try-block
catch ( ... ) catch-block-1
... 
catch ( ... ) catch-block-n
finally finally-block

è fatto come se l'istruzione fosse un'istruzione try - latest che racchiude un'istruzione try - catch :

try { 
    try   
    try-block
    catch ( ... ) catch-block-1
    ...   
    catch ( ... ) catch-block-n
} 
finally finally-block

3

Per non parlare del fatto che si trovi all'interno di un metodo chiamato di frequente, può influenzare il comportamento generale dell'applicazione.
Ad esempio, considero l'uso di Int32.Parse come una cattiva pratica nella maggior parte dei casi poiché genera eccezioni per qualcosa che può essere catturato facilmente altrimenti.

Quindi, per concludere tutto ciò che è scritto qui:
1) Usa i blocchi try..catch per rilevare errori imprevisti - quasi nessuna penalizzazione delle prestazioni.
2) Non usare eccezioni per errori esclusi se puoi evitarlo.


3

Ho scritto un articolo su questo un po 'di tempo fa perché c'erano molte persone che lo chiedevano in quel momento. Puoi trovarlo e il codice di prova su http://www.blackwasp.co.uk/SpeedTestTryCatch.aspx .

Il risultato è che c'è una piccola quantità di overhead per un blocco try / catch ma così piccola che dovrebbe essere ignorata. Tuttavia, se stai eseguendo blocchi try / catch in cicli che vengono eseguiti milioni di volte, potresti prendere in considerazione di spostare il blocco all'esterno del ciclo, se possibile.

Il problema di prestazioni chiave con i blocchi try / catch è quando si rileva effettivamente un'eccezione. Ciò può aggiungere un notevole ritardo alla tua applicazione. Ovviamente, quando le cose vanno male, la maggior parte degli sviluppatori (e molti utenti) riconoscono la pausa come un'eccezione che sta per accadere! La chiave qui non è utilizzare la gestione delle eccezioni per le normali operazioni. Come suggerisce il nome, sono eccezionali e dovresti fare tutto il possibile per evitare che vengano lanciati. Non dovresti usarli come parte del flusso previsto di un programma che funziona correttamente.


2

L' anno scorso ho scritto un post su questo argomento. Controlla. La conclusione è che non c'è quasi alcun costo per un blocco try se non si verifica alcuna eccezione - e sul mio laptop, un'eccezione era di circa 36μs. Potrebbe essere inferiore a quanto ti aspettavi, ma tieni presente che quei risultati erano su uno stack poco profondo. Inoltre, le prime eccezioni sono molto lente.


Non sono riuscito a raggiungere il tuo blog (la connessione è scaduta; stai usando try/ catchtroppo? Eh eh), ma sembra che tu stia discutendo con le specifiche della lingua e alcuni MS MVP che hanno anche scritto blog sull'argomento, fornendo misurazioni al contrario dei tuoi consigli ... Sono aperto al suggerimento che la ricerca che ho fatto sia sbagliata, ma dovrò leggere il tuo post sul blog per vedere cosa dice.
autistico

Oltre al post sul blog di @ Hafthor, ecco un altro post sul blog con codice scritto appositamente per testare le differenze di prestazioni di velocità. In base ai risultati, se si verifica un'eccezione anche solo il 5% delle volte, il codice di gestione delle eccezioni viene eseguito complessivamente 100 volte più lentamente rispetto al codice di gestione delle eccezioni. L'articolo si rivolge specificamente al try-catchblocco vs tryparse()metodi, ma il concetto è lo stesso.

2

È molto più facile scrivere, eseguire il debug e mantenere il codice privo di messaggi di errore del compilatore, messaggi di avviso di analisi del codice ed eccezioni accettate di routine (in particolare eccezioni che vengono lanciate in un punto e accettate in un altro). Poiché è più semplice, il codice sarà in media scritto meglio e con meno bug.

Per me, quel programmatore e il sovraccarico di qualità sono l'argomento principale contro l'utilizzo di try-catch per il flusso di processo.

Il sovraccarico del computer delle eccezioni è insignificante in confronto e solitamente minimo in termini di capacità dell'applicazione di soddisfare i requisiti di prestazioni del mondo reale.


@Ritchard T, perché? In confronto al programmatore e al sovraccarico di qualità è insignificante.
ThunderGr

1

Mi piace molto il post sul blog di Hafthor e, per aggiungere i miei due centesimi a questa discussione, vorrei dire che è sempre stato facile per me fare in modo che DATA LAYER generi un solo tipo di eccezione (DataAccessException). In questo modo il mio BUSINESS LAYER sa quale eccezione aspettarsi e la rileva. Quindi, a seconda di ulteriori regole di business (ad esempio se il mio oggetto di business partecipa al flusso di lavoro, ecc.), Posso lanciare una nuova eccezione (BusinessObjectException) o procedere senza ripetere / lanciare.

Direi di non esitare a usare try..catch ogni volta che è necessario e usalo con saggezza!

Ad esempio, questo metodo partecipa a un flusso di lavoro ...

Commenti?

public bool DeleteGallery(int id)
{
    try
    {
        using (var transaction = new DbTransactionManager())
        {
            try
            {
                transaction.BeginTransaction();

                _galleryRepository.DeleteGallery(id, transaction);
                _galleryRepository.DeletePictures(id, transaction);

                FileManager.DeleteAll(id);

                transaction.Commit();
            }
            catch (DataAccessException ex)
            {
                Logger.Log(ex);
                transaction.Rollback();                        
                throw new BusinessObjectException("Cannot delete gallery. Ensure business rules and try again.", ex);
            }
        }
    }
    catch (DbTransactionException ex)
    {
        Logger.Log(ex);
        throw new BusinessObjectException("Cannot delete gallery.", ex);
    }
    return true;
}

2
David, vuoi racchiudere la chiamata a "DeleteGallery" in un blocco try / catch?
Rob il

Poiché DeleteGallery è una funzione booleana, mi sembra che lanciare un'eccezione non sia utile. Ciò richiederebbe che la chiamata a DeleteGallery sia racchiusa in un blocco try / catch. Un if (! DeleteGallery (theid)) {// handle} mi sembra più significativo. in quell'esempio specifico.
ThunderGr

1

Possiamo leggere in Programming Languages ​​Pragmatics di Michael L. Scott che i compilatori di oggi non aggiungono alcun overhead nel caso comune, questo significa che non si verificano eccezioni. Quindi ogni lavoro viene eseguito in fase di compilazione. Ma quando viene lanciata un'eccezione in fase di esecuzione, il compilatore deve eseguire una ricerca binaria per trovare l'eccezione corretta e questo accadrà per ogni nuovo lancio che hai fatto.

Ma le eccezioni sono eccezioni e questo costo è perfettamente accettabile. Se provi a eseguire la gestione delle eccezioni senza eccezioni e utilizzi invece codici di errore di ritorno, probabilmente avrai bisogno di un'istruzione if per ogni subroutine e ciò incorrerà in un sovraccarico in tempo reale. Sai che un'istruzione if viene convertita in poche istruzioni di assemblaggio, che verranno eseguite ogni volta che entri nelle tue sub-routine.

Mi dispiace per il mio inglese, spero che ti aiuti. Queste informazioni si basano sul libro citato, per ulteriori informazioni fare riferimento al Capitolo 8.5 Gestione delle eccezioni.


Il compilatore è fuori immagine in fase di esecuzione. Ci deve essere un sovraccarico per i blocchi try / catch in modo che il CLR possa gestire le eccezioni. C # viene eseguito su .NET CLR (una macchina virtuale). Mi sembra che l'overhead del blocco stesso sia minimo quando non ci sono eccezioni, ma il costo del CLR che gestisce l'eccezione è molto significativo.
ThunderGr

-4

Analizziamo uno dei maggiori costi possibili di un blocco try / catch quando viene utilizzato dove non dovrebbe essere necessario:

int x;
try {
    x = int.Parse("1234");
}
catch {
    return;
}
// some more code here...

Ed ecco quello senza try / catch:

int x;
if (int.TryParse("1234", out x) == false) {
    return;
}
// some more code here

Senza contare lo spazio vuoto insignificante, si potrebbe notare che questi due pezzi di codice equivalenti hanno quasi esattamente la stessa lunghezza in byte. Quest'ultimo contiene 4 byte in meno di rientro. È una brutta cosa?

Per aggiungere la beffa al danno, uno studente decide di eseguire il loop mentre l'input può essere analizzato come un int. La soluzione senza try / catch potrebbe essere qualcosa del tipo:

while (int.TryParse(...))
{
    ...
}

Ma come appare quando si usa try / catch?

try {
    for (;;)
    {
        x = int.Parse(...);
        ...
    }
}
catch
{
    ...
}

I blocchi di prova / cattura sono modi magici per sprecare il rientro e ancora non sappiamo nemmeno il motivo per cui ha fallito! Immagina come si sente la persona che esegue il debug, quando il codice continua ad essere eseguito oltre un grave difetto logico, invece di fermarsi con un simpatico errore di eccezione evidente. I blocchi try / catch sono la convalida / sanificazione dei dati di un uomo pigro.

Uno dei costi minori è che i blocchi try / catch disabilitano effettivamente alcune ottimizzazioni: http://msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx . Immagino che anche questo sia un punto positivo. Può essere utilizzato per disabilitare le ottimizzazioni che altrimenti potrebbero paralizzare algoritmi di passaggio di messaggi sicuri e sani per le applicazioni multithread e per rilevare possibili condizioni di competizione;) Questo è l'unico scenario a cui riesco a pensare di usare try / catch. Anche questo ha delle alternative.


1
Sono abbastanza sicuro che TryParse provi {int x = int.Parse ("xxx"); return true;} catch {return false; } internamente. Il rientro non è una preoccupazione nella domanda, solo prestazioni e spese generali.
ThunderGr

@ThunderGr In alternativa, leggi la nuova risposta che ho pubblicato. Contiene più collegamenti, uno dei quali è un'analisi dell'enorme aumento delle prestazioni quando si evita Int.Parsea favore di Int.TryParse.
autistico
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.