Mi è stato detto che le eccezioni dovrebbero essere utilizzate solo in casi eccezionali. Come faccio a sapere se il mio caso è eccezionale?

94

Il mio caso specifico qui è che l'utente può passare una stringa nell'applicazione, l'applicazione la analizza e la assegna agli oggetti strutturati. A volte l'utente può digitare qualcosa di non valido. Ad esempio, il loro contributo può descrivere una persona, ma possono dire che la loro età è "mela". Il comportamento corretto in questo caso è il rollback della transazione e per segnalare all'utente che si è verificato un errore e dovranno riprovare. Potrebbe esserci un obbligo di segnalare ogni errore che possiamo trovare nell'input, non solo il primo.

In questo caso, ho sostenuto che dovremmo lanciare un'eccezione. Era in disaccordo, dicendo: "Le eccezioni dovrebbero essere eccezionali: è previsto che l'utente possa inserire dati non validi, quindi questo non è un caso eccezionale" Non sapevo davvero come argomentare quel punto, perché per definizione della parola, lui sembra giusto

Ma, a mio avviso, questo è il motivo per cui le eccezioni sono state inventate in primo luogo. Un tempo aveva per ispezionare il risultato per vedere se si è verificato un errore. Se non sei riuscito a controllare, le cose brutte potrebbero accadere senza che te ne accorga.

Senza eccezioni, ogni livello dello stack deve controllare il risultato dei metodi che chiamano e se un programmatore dimentica di controllare uno di questi livelli, il codice potrebbe procedere accidentalmente e salvare dati non validi (ad esempio). Sembra più incline agli errori in questo modo.

Ad ogni modo, sentiti libero di correggere qualsiasi cosa abbia detto qui. La mia domanda principale è se qualcuno dice che le eccezioni dovrebbero essere eccezionali, come faccio a sapere se il mio caso è eccezionale?

    
posta Daniel Kaplan 24.01.2013 - 09:48
fonte

13 risposte

81

Sono state inventate eccezioni per facilitare la gestione degli errori con meno ingombro del codice. Dovresti usarli nei casi in cui rendono più semplice la gestione degli errori con meno ingombri di codice. Questo business delle "eccezioni solo per circostanze eccezionali" deriva da un periodo in cui la gestione delle eccezioni era considerata un risultato di performance inaccettabile. Questo non è più il caso nella grande maggioranza del codice, ma le persone continuano a ribellarsi alla regola senza ricordare il motivo alla base.

Specialmente in Java, che è forse il linguaggio più rispettoso delle eccezioni mai concepito, non dovresti sentirti a disagio nell'usare le eccezioni quando semplifica il tuo codice. In effetti, la classe Integer di Java non ha un mezzo per verificare se una stringa è un intero valido senza potenzialmente lanciare un NumberFormatException .

Inoltre, anche se non puoi fare affidamento su solo per la convalida dell'interfaccia utente, tieni presente se l'interfaccia utente è progettata correttamente, ad esempio utilizzando uno spinner per immettere valori numerici brevi, quindi un valore non numerico trasformarlo nel back end sarebbe davvero una condizione eccezionale.

    
risposta data 24.01.2013 - 18:18
fonte
71

Quando dovrebbe essere lanciata un'eccezione? Quando si tratta di codice, penso che la seguente spiegazione sia molto utile:

Un'eccezione si verifica quando un membro non riesce a completare l'attività che deve eseguire come indicato dal suo nome . (Jeffry Richter, CLR via C #)

Perché è utile? Suggerisce che dipende dal contesto in cui qualcosa dovrebbe essere gestito come eccezione o meno. A livello di chiamate di metodo, il contesto è dato da (a) il nome, (b) la firma del metodo e (b) il codice client, che utilizza o si prevede che utilizzi il metodo.

Per rispondere alla tua domanda dovresti dare un'occhiata al codice, dove viene elaborato l'input dell'utente. Potrebbe assomigliare a questo:

public void Save(PersonData personData) { … }

Il nome del metodo suggerisce che è stata fatta una validazione? No. In questo caso, un PersonData non valido dovrebbe generare un'eccezione.

Supponiamo che la classe abbia un altro metodo che assomiglia a questo:

public ValidationResult Validate(PersonData personData) { … }

Il nome del metodo suggerisce che è stata fatta una validazione? Sì. In questo caso, PersonData non deve generare eccezioni.

Per mettere insieme le cose, entrambi i metodi suggeriscono che il codice client dovrebbe assomigliare a questo:

ValidationResult validationResult = personRegister.Validate(personData);
if (validationResult.IsValid())
{
    personRegister.Save(personData)
}
else
{
    // Throw an exception? To answer this look at the context!
    // That is: (a) Method name, (b) signature and
    // (c) where this method is (expected) to be used.
}

Quando non è chiaro se un metodo dovrebbe generare un'eccezione, allora forse è dovuto un nome o firma del metodo scarsamente scelti. Forse il design della classe non è chiaro. A volte è necessario modificare la progettazione del codice per ottenere una risposta chiara alla domanda se deve essere lanciata o meno un'eccezione.

    
risposta data 24.01.2013 - 14:05
fonte
30

Penso sempre a cose come l'accesso al server del database o un'API web quando si pensa alle eccezioni. Ti aspetti che il server / web API funzioni, ma in casi eccezionali potrebbe non farlo (il server non funziona). Una richiesta web potrebbe essere rapida in genere, ma in circostanze eccezionali (carico elevato) potrebbe scadere. Questo è fuori dal tuo controllo.

I dati di input degli utenti sono sotto il tuo controllo, dal momento che puoi controllare che cosa mandano e fare ciò che ti piace. Nel tuo caso, convaliderei l'input dell'utente prima ancora di provare a salvarlo. E tendo a convenire che gli utenti che forniscono dati non validi dovrebbero essere previsti e la tua app dovrebbe tenerne conto convalidando l'input e fornendo il messaggio di errore user-friendly.

Detto questo, io uso le eccezioni nella maggior parte dei miei setter di modelli di dominio, dove non dovrebbe esserci assolutamente alcuna possibilità che i dati non validi vadano a finire. Tuttavia, questa è un'ultima linea di difesa, e tendo a costruire i miei moduli di input con ricche regole di convalida, in modo che non ci sia praticamente alcuna possibilità di attivare quell'eccezione del modello di dominio. Quindi, quando un setter si aspetta una cosa e ne riceve un'altra, si tratta di una situazione eccezionale, che non avrebbe dovuto accadere in circostanze normali.

EDIT (qualcos'altro da considerare):

Quando invii i dati forniti dall'utente al db, sai in anticipo cosa dovresti e non dovresti entrare nelle tue tabelle. Ciò significa che i dati possono essere convalidati rispetto ad alcuni formati previsti. Questo è qualcosa che puoi controllare. Ciò che non è possibile controllare è il mancato funzionamento del server nel mezzo della query. Quindi sai che la query è ok e i dati sono filtrati / convalidati, provi la query e fallisce ancora, questa è una situazione eccezionale.

Analogamente alle richieste web, non puoi sapere se la richiesta scadrà o non riesci a connetterti prima di provare a inviarlo. Quindi questo garantisce anche un approccio try / catch, dal momento che non puoi chiedere al server se funzionerà qualche millisecondo dopo quando invierai la richiesta.

    
risposta data 24.01.2013 - 10:03
fonte
29

Exceptions should be exceptional: It's expected that the user may input invalid data, so this isn't an exceptional case

Su quell'argomento:

  • Si prevede che un file non possa esistere, quindi non è eccezionale caso.
  • Si prevede che la connessione al server possa andare persa, quindi non è un caso eccezionale
  • Si prevede che il file di configurazione possa essere confuso, quindi non è un caso eccezionale
  • Ci si aspetta che a volte la tua richiesta cada, quindi non è un caso eccezionale

Qualsiasi eccezione che si cattura, devi aspettarti perché, beh, hai deciso di prenderlo. E così con questa logica, non dovresti mai lanciare alcuna eccezione che hai in realtà intenzione di prendere.

Quindi penso che "le eccezioni dovrebbero essere eccezionali" è una terribile regola empirica.

Ciò che dovresti fare dipende dalla lingua. Lingue diverse hanno diverse convenzioni su quando devono essere lanciate le eccezioni. Python, ad esempio, lancia eccezioni per tutto e quando in Python seguo l'esempio. Il C ++, d'altro canto, lancia relativamente poche eccezioni, e lì seguo l'esempio. Puoi trattare C ++ o Java come Python e generare eccezioni per tutto, ma il tuo funzionamento è in disaccordo con il modo in cui il linguaggio si aspetta di essere usato.

Preferisco l'approccio di Python, ma penso che sia una cattiva idea metterci dentro altre lingue.

    
risposta data 24.01.2013 - 15:57
fonte
15

Riferimento

Da The Pragmatic Programmer:

We believe that exceptions should rarely be used as part of a program's normal flow; exceptions should be reserved for unexpected events. Assume that an uncaught exception will terminate your program and ask yourself, "Will this code still run if I remove all the exception handlers?" If the answer is "no," then maybe exceptions are being used in nonexceptional circumstances.

Continuano a esaminare l'esempio dell'apertura di un file per la lettura e il file non esiste - dovrebbe sollevare un'eccezione?

If the file should have been there, then an exception is warranted. [...] On the other hand, if you have no idea whether the file should exist or not, then it doesn't seem exceptional if you can't find it, and an error return is appropriate.

Successivamente, discutono sul motivo per cui hanno scelto questo approccio:

[A]n exception represents an immediate, nonlocal transfer of control - it's a kind of cascading goto. Programs that use exceptions as part of their normal processing suffer from all the readability and maintainability problems of classic spaghetti code. These programs break encapsulation: routines and their callers are more tightly coupled via exception handling.

Riguardo alla tua situazione

La tua domanda si riduce a "Gli errori di convalida dovrebbero generare eccezioni?" La risposta è che dipende da dove sta avvenendo la convalida.

Se il metodo in questione si trova all'interno di una sezione del codice in cui si presume che i dati di input siano già stati convalidati, i dati di input non validi dovrebbero generare un'eccezione; se il codice è progettato in modo tale che questo metodo riceva l'input esatto inserito da un utente, ci si può aspettare che i dati non siano validi e un'eccezione non dovrebbe essere sollevata.

    
risposta data 24.01.2013 - 20:00
fonte
10

C'è molta pontificazione filosofica qui, ma in generale, le condizioni eccezionali sono semplicemente quelle condizioni che non puoi o non vuoi gestire (oltre alla pulizia, segnalazione di errori e simili) senza l'intervento dell'utente. In altre parole, sono condizioni irrecuperabili.

Se si consegna un programma a un percorso di file, con l'intenzione di elaborare quel file in qualche modo, e il file specificato da quel percorso non esiste, si tratta di una condizione eccezionale. Non puoi fare nulla a riguardo nel tuo codice, oltre a segnalarlo all'utente e permetterle di specificare un percorso file diverso.

    
risposta data 24.01.2013 - 16:24
fonte
7

Ci sono due dubbi che dovresti prendere in considerazione:

  1. si discute di un singolo problema: chiamiamolo Assigner poiché questa preoccupazione è di assegnare input a oggetti strutturati e si esprime il vincolo che i suoi input siano validi

  2. un'interfaccia utente ben implementata ha una preoccupazione aggiuntiva: la convalida dell'input e dell'amplificazione utente; feedback costruttivo sugli errori (chiamiamo questa parte Validator )

Dal punto di vista del componente Assigner , lanciare un'eccezione è assolutamente ragionevole, dal momento che hai espresso un vincolo che è stato violato.

Dal punto di vista dell'esperienza utente , l'utente non dovrebbe parlare direttamente con questo Assigner in primo luogo. Dovrebbero parlarci tramite il Validator .

Ora, in Validator , l'input utente non valido è non un caso eccezionale, è davvero il caso che ti interessi di più. Quindi qui un'eccezione non sarebbe appropriata, e questo è anche il luogo in cui desideri identificare gli errori tutti invece di salvarli sul primo.

Noterai che non ho menzionato il modo in cui queste preoccupazioni sono implementate. Sembra che tu stia parlando di Assigner e il tuo collega sta parlando di Validator+Assigner combinato. Una volta che ti rendi conto che sei due preoccupazioni distinte (o separabili), almeno puoi discuterne sensatamente.

Per indirizzare il commento di Renan, sto solo assumendo che una volta identificati i due distinti dubbi, è ovvio quali casi dovrebbero essere considerati eccezionali in ogni contesto.

In effetti, se non è ovvio se qualcosa dovrebbe essere considerato eccezionale, direi che probabilmente non hai finito di identificare i dubbi indipendenti nella tua soluzione.

Credo che sia la risposta diretta a

... how do I know if my case is exceptional?

continua a semplificare finché non è ovvio . Quando hai una pila di concetti semplici che capisci bene, puoi ragionare chiaramente sulla loro reinserzione in codice, classi, librerie o altro.

    
risposta data 24.01.2013 - 12:24
fonte
4

Altri hanno risposto bene, ma ancora qui è la mia risposta breve. L'eccezione è una situazione in cui qualcosa nell'ambiente ha torto, che non puoi controllare e il tuo codice non può andare avanti. In questo caso dovrai anche informare l'utente cosa è andato storto, perché non puoi andare oltre, e qual è la risoluzione.

    
risposta data 24.01.2013 - 15:14
fonte
2

Le eccezioni dovrebbero rappresentare condizioni che è probabile che il codice di chiamata immediato non sia pronto per essere gestito, anche se il metodo di chiamata potrebbe. Si consideri, ad esempio, il codice che sta leggendo alcuni dati da un file, può legittimamente assumere che ogni file valido terminerà con un record valido e non è richiesto per estrarre le informazioni da un record parziale.

Se la routine dei dati di lettura non utilizzava eccezioni ma indicava semplicemente se la lettura era riuscita, il codice chiamante doveva apparire come:

temp = dataSource.readInteger();
if (temp == null) return null;
field1 = (int)temp;
temp = dataSource.readInteger();
if (temp == null) return null;
field2 = (int)temp;
temp = dataSource.readString();
if (temp == null) return null;
field3 = temp;

ecc. spendendo tre righe di codice per ogni utile pezzo di lavoro. Al contrario, se readInteger genera un'eccezione incontrando la fine di un file e se il chiamante può semplicemente passare l'eccezione, il codice diventa:

field1 = dataSource.readInteger();
field2 = dataSource.readInteger();
field3 = dataSource.readString();

Molto più semplice e più pulito, con un'enfasi molto maggiore sul caso in cui le cose funzionano normalmente. Si noti che nei casi in cui il chiamante immediato vorrebbe essere in attesa di gestire una condizione, un metodo che restituisce un codice di errore sarà spesso più utile di uno che genera un'eccezione. Ad esempio, per totalizzare tutti gli interi in un file:

do
{
  temp = dataSource.tryReadInteger();
  if (temp == null) break;
  total += (int)temp;
} while(true);

vs

try
{
  do
  {
    total += (int)dataSource.readInteger();
  }
  while(true);
}
catch endOfDataSourceException ex
{ // Don't do anything, since this is an expected condition (eventually)
}

Il codice che chiede gli interi si aspetta che una di quelle chiamate fallisca. Avere il codice utilizza un ciclo infinito che verrà eseguito fino a quando ciò accade è molto meno elegante rispetto all'utilizzo di un metodo che indica i guasti tramite il suo valore di ritorno.

Poiché le classi spesso non conoscono le condizioni che i loro clienti non si aspetteranno o non si aspettano, è spesso utile offrire due versioni di metodi che potrebbero fallire in modi che alcuni chiamanti si aspetteranno e altri non lo faranno. Ciò consentirà di utilizzare tali metodi in modo pulito con entrambi i tipi di chiamanti. Si noti inoltre che anche i metodi "try" dovrebbero generare eccezioni in caso di situazioni che il chiamante probabilmente non si aspetta. Ad esempio, tryReadInteger non dovrebbe generare un'eccezione se incontra una condizione di fine file pulita (se il chiamante non se lo aspettava, il chiamante avrebbe utilizzato readInteger ). D'altra parte, probabilmente dovrebbe generare un'eccezione se i dati non possono essere letti perché, ad es. la memory stick che conteneva era scollegata. Mentre tali eventi dovrebbero sempre essere riconosciuti come una possibilità, è improbabile che il codice di chiamata immediato sia pronto a fare qualcosa di utile in risposta; non dovrebbe certamente essere riportato allo stesso modo di una condizione di fine file.

    
risposta data 24.01.2013 - 19:30
fonte
2

Non sono mai stato un grande fan del consiglio che dovresti solo fare eccezioni in casi eccezionali, in parte perché non dice nulla (è come dire che dovresti mangiare solo cibo che è commestibile), ma anche perché è molto soggettivo, e spesso non è chiaro cosa costituisca un caso eccezionale e cosa no.

Tuttavia, ci sono buoni motivi per questo consiglio: lanciare e catturare eccezioni è lento, e se si sta eseguendo il codice nel debugger in Visual Studio con esso impostato per notificare all'utente quando viene generata un'eccezione, si può finire per essere spammato da dozzine se non centinaia di messaggi molto prima di arrivare al problema.

Quindi come regola generale, se:

  • il tuo codice è privo di errori e
  • i servizi da cui dipende sono tutti disponibili e
  • il tuo utente sta utilizzando il tuo programma nel modo in cui è stato concepito per essere utilizzato (anche se alcuni degli input forniti non sono validi)

allora il tuo codice non dovrebbe mai generare un'eccezione, anche una che viene catturata in seguito. Per intercettare dati non validi, puoi utilizzare i validatori a livello di interfaccia utente o codice come Int32.TryParse() nel livello di presentazione.

Per qualsiasi altra cosa, dovresti attenersi al principio che un'eccezione significa che il tuo metodo non può fare quello che dice il nome. In generale non è una buona idea usare i codici di ritorno indicare un errore (a meno che il nome del metodo indichi chiaramente che lo fa, ad esempio TryParse() ) per due motivi. Innanzitutto, la risposta predefinita a un codice di errore è ignorare la condizione di errore e continuare a prescindere; in secondo luogo, si può finire fin troppo facilmente con alcuni metodi usando codici di ritorno e altri metodi usando le eccezioni, e dimenticando quale è quale. Ho persino visto codebase in cui due diverse implementazioni intercambiabili della stessa interfaccia prendono qui approcci diversi.

    
risposta data 25.01.2013 - 14:29
fonte
2

La cosa più importante nella scrittura del software è renderlo leggibile. Tutte le altre considerazioni sono secondarie, incluso renderlo efficiente e correggerlo. Se è leggibile, il resto può essere curato nella manutenzione, e se non è leggibile, allora è meglio buttarlo via. Pertanto, dovresti generare eccezioni quando migliora la leggibilità.

Quando scrivi un algoritmo, pensa alla persona che in futuro la leggerà. Quando vieni in un posto dove potrebbe esserci un potenziale problema, chiediti se il lettore vuole vedere come gestisci il problema ora , o il lettore preferirebbe semplicemente andare avanti con l'algoritmo?

Mi piace pensare a una ricetta per la torta al cioccolato. Quando ti dice di aggiungere le uova, ha una scelta: può presumere che tu abbia uova e andare avanti con la ricetta, o può iniziare una spiegazione su come puoi ottenere le uova se non hai uova. Potrebbe riempire un intero libro con tecniche per la caccia ai polli selvatici, tutto per aiutarti a preparare una torta. Va bene, ma la maggior parte delle persone non vorrebbe leggere quella ricetta. La maggior parte delle persone preferirebbe semplicemente presumere che le uova siano disponibili e andare avanti con la ricetta. Questo è un giudizio che gli autori devono fare quando scrivono le ricette.

Non ci possono essere regole garantite su ciò che rende una buona eccezione e quali problemi devono essere gestiti immediatamente, perché richiede di leggere la mente del tuo lettore. Il meglio che tu possa fare è una regola empirica, e "le eccezioni sono solo per circostanze eccezionali" è piuttosto buona. Di solito, quando un lettore legge il tuo metodo, sta cercando il metodo che farà il 99% delle volte, e preferirebbe non avere quel genere di casi bizzarri come gli utenti che inseriscono input illegali e altre cose che non succede quasi mai. Vogliono vedere il flusso normale del software disposto direttamente, un'istruzione dopo l'altra, come se i problemi non accadessero mai. Capire il tuo programma sarà abbastanza difficile senza dover affrontare costantemente le tangenti per affrontare ogni piccolo problema che potrebbe emergere.

    
risposta data 19.11.2013 - 08:35
fonte
2

There may be a requirement to report on every error we can find in the input, not just the first.

Ecco perché non puoi lanciare un'eccezione qui. Un'eccezione interrompe immediatamente il processo di convalida. Quindi ci sarebbe molto da fare per risolvere il problema.

Un cattivo esempio:

Metodo di convalida per la classe Dog utilizzando le eccezioni:

void validate(Set<DogValidationException> previousExceptions) {
    if (!DOG_NAME_PATTERN.matcher(this.name).matches()) {
        DogValidationException disallowedName = new DogValidationException(Problem.DISALLOWED_DOG_NAME);
        if (!previousExceptions.contains(disallowedName)){
            throw disallowedName;
        }
    }
    if (this.legs < 4) {
        DogValidationException invalidDog = new DogValidationException(Problem.LITERALLY_INVALID_DOG);
        if (!previousExceptions.contains(invalidDog)){
            throw invalidDog;
        }
    }
    // etc.
}

Come chiamarlo:

Set<DogValidationException> exceptions = new HashSet<DogValidationException>();
boolean retry;
do {
    retry = false;
    try {
        dog.validate(exceptions);
    } catch (DogValidationException e) {
        exceptions.add(e);
        retry = true;
    }
} while (retry);

if(exceptions.isEmpty()) {
    dogDAO.beginTransaction();
    dogDAO.save(dog);
    dogDAO.commitAndCloseTransaction();
} else {
    // notify user to fix the problems
}

Il problema qui è che il processo di validazione, per ottenere tutti gli errori, richiederebbe saltare le eccezioni già trovate. Quanto sopra potrebbe funzionare, ma questo è un chiaro uso scorretto delle eccezioni . Il tipo di convalida che ti è stato richiesto dovrebbe essere prima toccato dal database. Quindi non c'è bisogno di ripristinare nulla. E, i risultati della convalida sono probabilmente errori di validazione (si spera che siano zero, comunque).

L'approccio migliore è:

Chiamata al metodo:

Set<Problem> validationResults = dog.validate();
if(validationResults.isEmpty()) {
    dogDAO.beginTransaction();
    dogDAO.save(dog);
    dogDAO.commitAndCloseTransaction();
} else {
    // notify user to fix the problems
}

Metodo di convalida:

Set<Problem> validate() {
    Set<Problem> result = new HashSet<Problem>();
    if(!DOG_NAME_PATTERN.matcher(this.name).matches()) {
        result.add(Problem.DISALLOWED_DOG_NAME);
    }
    if(this.legs < 4) {
        result.add(Problem.LITERALLY_INVALID_DOG);
    }
    // etc.
    return result;
}

Perché? Ci sono un sacco di motivi e la maggior parte delle ragioni sono state evidenziate nelle altre risposte. Per dirla semplice: È molto più semplice leggere e capire dagli altri. Secondo, vuoi mostrare le tracce dello stack dell'utente per spiegargli che ha impostato il suo dog in modo errato?

Se , durante il commit nel secondo esempio, si verifica ancora un errore , anche se il tuo validatore ha convalidato il dog con zero problemi, quindi il lancio un'eccezione è la cosa giusta . Ad esempio: Nessuna connessione al database, la voce del database è stata modificata da qualcun altro nel frattempo o simile.

    
risposta data 15.02.2016 - 10:31
fonte
-3

Questa risposta è la migliore trovata dalle guide dei sistemi operativi e dalle guide alla programmazione della CPU. In realtà, questa risposta è molto semplice e vi è una differenza fisica tra le affermazioni e le eccezioni. Non trovo alcuna filosofia dietro la risposta "effettiva". La leggibilità può venire dopo la ragione fisica, ma questa parte non è discutibile, e cioè questi:

Le eccezioni si verificano quando una funzione trasmette un errore a una funzione di chiamata. Fondamentalmente se il tuo programma non può gestire qualcosa, ma il sistema operativo lo può, lo "getterà" sul sistema operativo (anche la funzione chiamante ei genitori). Se le istruzioni non trasmettono errori alle funzioni di chiamata.

Le eccezioni contrassegneranno fisicamente il registro delle eccezioni / registro di debug sull'hardware. L'intera macchina saprà che deve interrompere ciò che sta facendo per correggere il tuo processo.

E questo è tutto. Nessuna filosofia, niente argomenti, niente stili. Ora "Eccezionale" ha una vera definizione di "Calling-process dovrebbe gestire l'errore". Ora che siamo consapevoli delle differenze fisiche tra If e Try / Catch, la filosofia può venire dopo. Ma questa è la vera differenza reale, per quanto la mia ricerca ha trovato dall'architettura ai concetti OS

    
risposta data 20.12.2017 - 22:17
fonte

Leggi altre domande sui tag