Comprensione della serializzazione


38

Sono un ingegnere informatico e dopo una discussione con alcuni colleghi, mi sono reso conto di non avere una buona conoscenza della serializzazione del concetto. A quanto ho capito, la serializzazione è il processo di conversione di alcune entità, come un oggetto in OOP, in una sequenza di byte, in modo che detta entità possa essere memorizzata o trasmessa per l'accesso successivo (il processo di "deserializzazione").

Il problema che ho è: non sono tutte le variabili (siano esse primitive into composte) già rappresentate da una sequenza di byte? (Certo che lo sono, perché sono memorizzati in registri, memoria, disco, ecc.)

Quindi cosa rende la serializzazione un argomento così profondo? Per serializzare una variabile, non possiamo semplicemente prendere questi byte in memoria e scriverli in un file? Quali difficoltà ho perso?


21
La serializzazione può essere banale per oggetti contigui . Quando il valore dell'oggetto è rappresentato come un grafico puntatore , le cose diventano molto più complicate, specialmente se detto grafico ha dei loop.
chi

1
@chi: la tua prima frase è un po 'fuorviante dato che la contiguità è irrilevante. Potresti avere un grafico che sembra essere continuo nella memoria e che comunque non ti aiuterebbe a serializzarlo poiché devi ancora (a) rilevare che effettivamente sembra contiguo e (b) sistemare i puntatori all'interno. Direi solo la seconda parte di quello che hai detto.
Mehrdad,

@Mehrdad Concordo sul fatto che il mio commento non è del tutto preciso, per i motivi che hai citato. Forse senza puntatore / puntatore è una migliore distinzione (anche se non del tutto accurata)
chi

7
Devi anche preoccuparti della rappresentazione sull'hardware. Se serializzo un int 4 bytessul mio PDP-11 e quindi provo a leggere quegli stessi quattro byte in memoria sul mio macbook, non hanno lo stesso numero (a causa di Endianes). Quindi devi normalizzare i dati in una rappresentazione che puoi decodificare (questa è la serializzazione). Il modo in cui serializzi i dati ha anche velocità / flessibilità di compromessi leggibili da uomo / macchina.
Martin York,

Cosa succede se si utilizza Entity Framework con molte proprietà di navigazione profondamente connesse? In un caso, potresti voler serializzare una proprietà di navigazione, ma in un altro lasciala nulla (perché ricaricherai quell'oggetto reale dal database in base all'ID che si trova nell'oggetto padre serializzato). Questo è solo un esempio. Ci sono molti.
ErikE

Risposte:


40

Se si dispone di una struttura di dati complicata, la sua rappresentazione in memoria potrebbe essere normalmente sparsa in tutta la memoria. (Pensa ad un albero binario, per esempio.)

Al contrario, quando vuoi scriverlo sul disco, probabilmente vuoi avere una rappresentazione come una sequenza (si spera breve) di byte contigui. Questo è ciò che la serializzazione fa per te.


27

Il problema che ho è: non tutte le variabili (siano esse primitive come int o oggetti compositi) sono già rappresentate da una sequenza di byte? (Certo che lo sono, perché sono memorizzati in registri, memoria, disco, ecc.)

Quindi cosa rende la serializzazione un argomento così profondo? Per serializzare una variabile, non possiamo semplicemente prendere questi byte in memoria e scriverli in un file? Quali difficoltà ho perso?

Considera un grafico a oggetti in C con nodi definiti come questo:

struct Node {
    struct Node* parent;
    struct Node* someChild;
    struct Node* anotherLink;

    int value;
    char* label;
};

//

struct Node nodes[10] = {0};
nodes[5].parent = nodes[0];
nodes[0].someChild = calloc( 1, sizeof(struct Node) );
nodes[5].anotherLink = nodes[3];
for( size_t i = 3; i < 7; i++ ) {
    nodes[i].anotherLink = calloc( 1, sizeof(struct Node) );
}

In fase di esecuzione l'intero Nodegrafico oggetto verrebbe sparso nello spazio di memoria e lo stesso nodo potrebbe essere puntato da molti nodi diversi.

Non è possibile semplicemente scaricare la memoria su un file / flusso / disco e chiamarlo serializzato perché i valori del puntatore (che sono indirizzi di memoria) non possono essere de-serializzati (perché tali posizioni di memoria potrebbero già essere occupate quando si carica il dump in memoria). Un altro problema con il semplice dumping della memoria è che finirai per archiviare tutti i tipi di dati irrilevanti e lo spazio inutilizzato: su x86 un processo ha fino a 4GiB di spazio di memoria e un sistema operativo o MMU ha solo un'idea generale di quale memoria sia effettivamente significativo o meno (basato sulle pagine di memoria assegnate a un processo), quindi avendoNotepad.exe 4 GB di byte grezzi sul mio disco ogni volta che voglio salvare un file di testo sembra un po 'dispendioso.

Un altro problema riguarda il controllo delle versioni: cosa succede se si serializza il Nodegrafico il giorno 1, quindi il giorno 2 si aggiunge un altro campo Node(come un altro valore del puntatore o un valore di base), quindi il giorno 3 si deserializza il file da giorno 1?

Devi anche considerare altre cose, come l'endianità. Uno dei motivi principali per cui i file MacOS e IBM / Windows / PC erano incompatibili tra loro negli anni '80 e '90 nonostante apparentemente fatti dagli stessi programmi (Word, Photoshop, ecc.) Era perché su valori interi multi-byte x86 / PC sono stati salvati in ordine little-endian, ma ordine big-endian su Mac - e il software non è stato realizzato tenendo conto della portabilità multipiattaforma. Oggi le cose vanno meglio grazie a una migliore formazione degli sviluppatori e al nostro mondo informatico sempre più eterogeneo.


2
Scaricare tutto nello spazio di memoria del processo sarebbe anche orribile per motivi di sicurezza. Una notte di programma ha in memoria sia 1) alcuni dati pubblici sia 2) password, nonce segreto o chiave privata. Quando si serializza il primo, non si desidera rivelare alcuna informazione sul secondo.
chi

8
Una nota molto interessante su questo argomento: perché i formati di file di Microsoft Office sono così complicati?
colpisce il

15

Il trucco è in realtà già descritto nella parola stessa: " serializzazione ".

La domanda è fondamentalmente: come posso rappresentare un grafico diretto ciclicamente interconnesso interconnesso di oggetti arbitrariamente complessi come una sequenza lineare di byte?

Pensaci: una sequenza lineare è un po 'come un grafico diretto degenerato in cui ogni vertice ha esattamente un bordo in entrata e in uscita (tranne il "primo vertice" che non ha bordo in entrata e l' "ultimo vertice" che non ha bordo in uscita) . E un byte è ovviamente meno complesso di un oggetto .

Quindi, sembra ragionevole che mentre passiamo da un grafico arbitrariamente complesso a un "grafico" molto più limitato (in realtà solo un elenco) e da oggetti arbitrariamente complessi a byte semplici, le informazioni andranno perse, se lo facciamo in modo ingenuo e non t codifica le informazioni "estranee" in qualche modo. Ed è esattamente ciò che fa la serializzazione: codificare le informazioni complesse in un semplice formato lineare.

Se hai familiarità con YAML , potresti dare un'occhiata alle funzioni di ancoraggio e alias che ti consentono di rappresentare l'idea che "lo stesso oggetto potrebbe apparire in luoghi diversi" in una serializzazione.

Ad esempio se hai il seguente grafico:

A → B → D
↓       ↑
C ––––––+

Potresti rappresentarlo come un elenco di percorsi lineari in YAML in questo modo:

- [&A A, B, &D D]
- [*A, C, *D]

Potresti anche rappresentarlo come un elenco di adiacenza, o una matrice di adiacenza, o come una coppia il cui primo elemento è un insieme di nodi e il cui secondo elemento è un insieme di coppie di nodi, ma in tutte quelle rappresentazioni, devi avere un modo di riferirsi avanti e indietro a nodi esistenti , ad esempio puntatori , che generalmente non si trovano in un file o in un flusso di rete. Alla fine, tutto ciò che hai sono i byte.

(Quale BTW significa che anche il file di testo YAML sopra riportato deve essere "serializzato", ecco a cosa servono le varie codifiche dei caratteri e i formati di trasferimento Unicode ... non è strettamente "serializzazione", solo codifica, perché il file di testo è già seriale / elenco lineare di punti di codice, ma puoi vedere alcune somiglianze.)


13

Le altre risposte riguardano già grafici di oggetti complessi, ma vale la pena sottolineare che anche la serializzazione delle primitive non è banale.

Usando i nomi di tipo primitivo C per concretezza, considera:

  1. Serializzo a long. Qualche tempo dopo lo deserializzo, ma ... su una piattaforma diversa, e ora longè int64_tpiuttosto che l' int32_tho memorizzato. Quindi, devo stare molto attento alle dimensioni esatte di ogni tipo che immagazzino, o memorizzare alcuni metadati che descrivono il tipo e le dimensioni di ogni campo.

    Si noti che questa piattaforma diversa potrebbe essere la stessa piattaforma dopo una ricompilazione futura.

  2. Serializzo un int32_t. Qualche tempo dopo lo deserializzo, ma ... su una piattaforma diversa, e ora il valore è corrotto. Purtroppo ho salvato il valore su una piattaforma big-endian e l'ho caricato su una piattaforma little-endian. Ora devo stabilire una convenzione per il mio formato o aggiungerne altri metadati che descrivono l'endiannness di ogni file / flusso / qualunque cosa. E, naturalmente, eseguono effettivamente le conversioni appropriate.

  3. Serializzo una stringa. Questa volta una piattaforma utilizza chare UTF-8 e una wchar_te UTF-16.

Quindi, direi che la serializzazione di qualità ragionevole non è banale nemmeno per i primitivi nella memoria contigua. Ci sono molte decisioni di codifica che devi documentare o descrivere con metadati incorporati.

I grafici a oggetti aggiungono un altro livello di complessità.


6

Ci sono molti aspetti:

Leggibilità con lo stesso programma

Il tuo programma ha memorizzato i tuoi dati in qualche modo come byte nella memoria. Ma potrebbe essere arbitrariamente sparpagliato su registri diversi, con puntatori che vanno avanti e indietro tra i suoi pezzi più piccoli [modifica: come commentato, fisicamente i dati sono più probabili nella memoria principale rispetto a un registro dati, ma ciò non toglie il problema del puntatore] . Basti pensare a un elenco intero collegato. Ogni elemento della lista potrebbe essere memorizzato in un posto completamente diverso e tutto ciò che tiene insieme la lista sono i puntatori da un elemento a quello successivo. Se dovessi prendere quei dati così come sono e provare a copiarli su un'altra macchina che esegue lo stesso programma, potresti riscontrare problemi:

  1. Innanzitutto, il registro indica che i tuoi dati sono archiviati su una macchina potrebbe essere già utilizzato per qualcosa di completamente diverso su un'altra macchina (qualcuno sta navigando nello scambio di stack e il browser ha già mangiato tutta quella memoria). Quindi, se si sostituisce semplicemente quei registri, arrivederci browser. Pertanto, è necessario riorganizzare i puntatori nella struttura per adattarli agli indirizzi disponibili sulla seconda macchina. Lo stesso problema si presenta quando si tenta di caricare nuovamente i dati sullo stesso computer in un secondo momento.
  2. Che cosa succede se un componente esterno punta nella tua struttura o la tua struttura ha puntatori a dati esterni, non hai trasmesso? Segfault ovunque! Questo sarebbe diventato un incubo di debug.

Leggibilità da un altro programma

Supponiamo che tu riesca ad allocare gli indirizzi giusti su un altro computer, affinché i tuoi dati si adattino. Se i tuoi dati vengono elaborati da un programma separato su quella macchina (lingua diversa), quel programma potrebbe avere una comprensione di base totalmente diversa dei dati. Supponi di avere oggetti C ++ con puntatori, ma la tua lingua di destinazione non supporta nemmeno i puntatori a quel livello. Ancora una volta, si finisce con un modo pulito per indirizzare quei dati nel secondo programma. Si finisce con alcuni dati binari in memoria, ma poi, è necessario scrivere un codice aggiuntivo che avvolge i dati e in qualche modo li traduca in qualcosa con cui la lingua di destinazione può funzionare. Sembra deserializzazione, solo che ora il tuo punto di partenza è uno strano oggetto sparso nella tua memoria principale, che è diverso per le diverse lingue di origine, invece di un file con una struttura ben definita. La stessa cosa, ovviamente, se si tenta di interpretare direttamente il file binario che include i puntatori, è necessario scrivere parser per ogni possibile modo in cui un'altra lingua potrebbe rappresentare i dati in memoria.

Leggibilità di un essere umano

Due dei più importanti linguaggi di serializzazione moderni per la serializzazione basata sul web (xml, json) sono facilmente comprensibili da un essere umano. Invece di una pila binaria di sostanza appiccicosa, la struttura e il contenuto effettivi dei dati sono chiari anche senza un programma per leggere i dati. Questo ha molteplici vantaggi:

  • debug più semplice -> se c'è un problema nella pipeline del servizio, basta guardare i dati che escono da un servizio e verificarne il senso (come primo passo); puoi anche vedere direttamente se i dati sembrano come pensi che dovrebbero, quando scrivi l'interfaccia di esportazione in primo luogo.
  • archivibilità: se hai i tuoi dati come un puro mucchio binario di goo e perdi il programma che dovrebbe interpretarli, perdi i dati (o dovrai spendere un po 'di tempo per trovare effettivamente qualcosa lì); se i tuoi dati serializzati sono leggibili dall'uomo, puoi facilmente usarli come archivio o programmare il tuo importatore per un nuovo programma
  • la natura dichiarativa dei dati serializzati in tal modo significa anche che è totalmente indipendente dal sistema informatico e dal suo hardware; potresti caricarlo in un computer quantistico costruito in modo totalmente diverso o infettare un'intelligenza artificiale aliena con fatti alternativi in ​​modo che voli accidentalmente nel prossimo sole (Emmerich se leggi questo, un riferimento sarebbe carino, se usi quell'idea per il prossimo 4 luglio film)

I miei dati sono probabilmente principalmente nella memoria principale, non nei registri. Se i miei dati rientrano nei registri, la serializzazione è a malapena un problema. Penso che tu abbia frainteso cosa sia un registro.
David Richerby,

In effetti, ho usato il termine registro troppo vagamente qui. Ma il punto principale è che i tuoi dati possono contenere puntatori allo spazio degli indirizzi per identificare i propri componenti o fare riferimento ad altri dati. Non importa se si tratta di un registro fisico o di un indirizzo virtuale nella memoria principale.
Frank Hopkins,

No, hai usato il termine "registrati" in modo completamente errato. Le cose che chiamate registri si trovano in una parte completamente diversa della gerarchia della memoria rispetto ai registri effettivi.
David Richerby,

6

Oltre a ciò che hanno detto le altre risposte:

A volte vuoi serializzare cose che non sono dati puri.

Ad esempio, pensa a un handle di file o a una connessione a un server. Anche se l'handle o il socket del file è un int, questo numero non ha senso alla successiva esecuzione del programma. Per ricreare correttamente gli oggetti che contengono handle per tali cose, è necessario riaprire i file e ricreare le connessioni e decidere cosa fare in caso di errore.

Oggigiorno molte lingue supportano la memorizzazione di funzioni anonime all'interno degli oggetti, ad esempio un onBlah()gestore in Javascript. Ciò è impegnativo perché tale codice può contenere riferimenti a ulteriori dati che a loro volta devono essere serializzati. (E poi c'è il problema della serializzazione del codice in un modo multipiattaforma, che è ovviamente più facile per le lingue interpretate.) Tuttavia, anche se solo un sottoinsieme della lingua può essere supportato, può ancora dimostrarsi abbastanza utile. Non molti meccanismi di serializzazione tentano di serializzare il codice, ma vedi serialize-javascript .

Nei casi in cui si desidera serializzare un oggetto ma contiene qualcosa che non è supportato dal meccanismo di serializzazione, è necessario riscrivere il codice in modo da aggirare il problema. Ad esempio, è possibile utilizzare enum al posto di funzioni anonime quando esiste un numero limitato di funzioni possibili.

Spesso si desidera che i dati serializzati siano concisi.

Se invii dati in rete o addirittura li memorizzi su disco, può essere importante mantenere le dimensioni ridotte. Uno dei modi più semplici per raggiungere questo obiettivo è di eliminare le informazioni che possono essere ricostruite (ad esempio, scartando cache, tabelle hash e rappresentazioni alternative degli stessi dati).

Ovviamente, il programmatore deve selezionare manualmente ciò che deve essere salvato e ciò che deve essere scartato e assicurarsi che le cose vengano ricostruite quando l'oggetto viene ricreato.

Pensa all'atto di salvare un gioco. Gli oggetti possono contenere molti puntatori a dati grafici, dati audio e altri oggetti. Ma la maggior parte di queste cose può essere caricata dai file di dati di gioco e non deve essere archiviata in un file di salvataggio. Scartarlo può essere laborioso, quindi spesso rimangono poche cose. Ho modificato in modo esadecimale alcuni file di salvataggio nel mio tempo e ho scoperto dati chiaramente ridondanti, come le descrizioni testuali degli articoli.

A volte lo spazio non è importante ma la leggibilità lo è, nel qual caso è possibile utilizzare un formato ASCII (eventualmente JSON o XML).


3

Definiamo che cos'è effettivamente una sequenza di byte. Una sequenza di byte è costituita da un numero intero non negativo chiamato lunghezza e una funzione / corrispondenza arbitraria che mappa qualsiasi numero intero i che sia almeno zero e inferiore alla lunghezza a un valore di byte (un numero intero compreso tra 0 e 255).

Molti degli oggetti che gestisci in un programma tipico non sono in quella forma, perché gli oggetti sono in realtà costituiti da molte allocazioni di memoria diverse che si trovano in luoghi diversi nella RAM e potrebbero essere separati l'uno dall'altro da milioni di byte di cose non importa. Basti pensare a un elenco collegato di base: ogni nodo nell'elenco è una sequenza di byte, sì, ma i nodi si trovano in molte posizioni diverse nella memoria del computer e sono collegati con puntatori. Oppure pensa a una semplice struttura che ha un puntatore a una stringa di lunghezza variabile.

Il motivo per cui vogliamo serializzare le strutture di dati in una sequenza di byte è di solito perché vogliamo memorizzarli su disco o inviarli a un sistema diverso (ad es. Tramite la rete). Se si tenta di memorizzare un puntatore su disco o di inviarlo a un altro sistema, sarà piuttosto inutile perché il programma che legge quel puntatore avrà a disposizione un diverso set di aree di memoria.


1
Non sono sicuro che sia un'ottima definizione di una sequenza. Molte persone definiscono una sequenza come, beh, una sequenza: una linea di cose una dopo l'altra. Per tua definizione, int seq(int i) { if (0 <= i < length) return i+1; else return -1;}è una sequenza. Quindi come lo memorizzerò su disco?
David Richerby

1
Se la lunghezza è 4, memorizzo un file a quattro byte con contenuto: 1, 2, 3, 4.
David Grayson

1
@DavidRicherby La sua definizione equivale a "una linea di cose una dopo l'altra", è solo una definizione più matematica e precisa della tua definizione intuitiva. Nota che la tua funzione non è una sequenza perché per avere una sequenza hai bisogno di quella funzione e di un altro numero intero chiamato lunghezza.
user253751

1
@FreshAir Il mio punto è che la sequenza è 1, 2, 3, 4, 5. La cosa che ho scritto è una funzione . Una funzione non è una sequenza.
David Richerby,

1
Un modo semplice per scrivere una funzione su disco è quello che ho già proposto: per ogni possibile input, memorizzare l'output. Penso che forse non lo capisci ancora, ma non sono sicuro di cosa dire. Sapevi che nei sistemi embedded è comune convertire funzioni costose come sinin una tabella di ricerca, che è una sequenza di numeri? Sapevi che la tua funzione è la stessa di quella per gli input a cui teniamo? int seq(n) { int a[] = [1, 2, 3, 4]; return a[n]; } Perché dici esattamente che il mio file a quattro byte è una rappresentazione inadeguata?
David Grayson,

2

Le complessità riflettono le complessità di dati e oggetti stessi. Questi oggetti possono essere oggetti del mondo reale o solo oggetti per computer. La risposta è nel nome. La serializzazione è la rappresentazione lineare di oggetti multidimensionali. Esistono molti problemi diversi dalla RAM frammentata.

Se è possibile appiattire 12 matrici tridimensionali e alcuni codici di programma, la serializzazione consente anche di trasferire un intero programma (e dati) tra computer. I protocolli di calcolo distribuito come RMI / CORBA utilizzano ampiamente la serializzazione per trasferire dati e programmi.

Prendi in considerazione la tua bolletta del telefono. Potrebbe essere un singolo oggetto, composto da tutte le chiamate (elenco di stringhe), importo da pagare (numero intero) e paese. Oppure la tua bolletta telefonica potrebbe essere capovolta da quanto sopra e consistere in telefonate discrete e dettagliate collegate al tuo nome. Ogni appiattimento apparirà diverso, riflettendo il modo in cui la tua compagnia telefonica ha scritto quella versione del suo software e il motivo per cui i database orientati agli oggetti non sono mai decollati.

Alcune parti di una struttura potrebbero non essere nemmeno nella memoria. Se la memorizzazione nella cache è lenta, alcune parti di un oggetto potrebbero fare riferimento solo a un file su disco e vengono caricate solo quando si accede a quella parte di quel particolare oggetto. Ciò è comune nei quadri di persistenza grave. I BLOB sono un buon esempio. Getty Images potrebbe memorizzare un'enorme immagine multi-megabyte di Fidel Castro e alcuni metadati come il nome dell'immagine, il costo del noleggio e l'immagine stessa. Potresti non voler caricare ogni volta l'immagine di 200 MB in memoria, a meno che tu non lo guardi davvero. Serializzato, l'intero file richiederebbe oltre 200 MB di spazio di archiviazione.

Alcuni oggetti non possono nemmeno essere serializzati. Nella terra della programmazione Java, è possibile avere un oggetto di programmazione che rappresenta lo schermo grafico o una porta seriale fisica. Non esiste un vero concetto di serializzazione di nessuno dei due. Come invieresti la tua porta a qualcun altro su una rete?

Alcune cose come password / chiavi di crittografia non devono essere archiviate o trasmesse. Possono essere etichettati come tali (volatili / transitori, ecc.) E il processo di serializzazione li salterà ma possono vivere nella RAM. Omettere questi tag è il modo in cui le chiavi di crittografia vengono inviate / archiviate inavvertitamente in ASCII normale.

Questa e l'altra risposta è il motivo per cui è complicato.


2

Il problema che ho è: non tutte le variabili (siano esse primitive come int o oggetti compositi) sono già rappresentate da una sequenza di byte?

Sì. Il problema qui è il layout di quei byte. Un sempliceint può essere lungo 2, 4 o 8 bit. Può essere in endian grande o piccolo. Può essere senza segno, firmato con il complemento di 1 o anche in alcuni bit super esotici come il negabinary.

Se intscarichi semplicemente il binario dalla memoria e lo chiami "serializzato", devi collegare praticamente tutto il computer, il sistema operativo e il programma affinché sia ​​deserializzabile. O almeno una descrizione precisa di essi.

Quindi cosa rende la serializzazione un argomento così profondo? Per serializzare una variabile, non possiamo semplicemente prendere questi byte in memoria e scriverli in un file? Quali difficoltà ho perso?

La serializzazione di un oggetto semplice sta praticamente scrivendo secondo alcune regole. Tali regole sono molte e non sempre ovvie. Ad esempio un xs:integerin XML è scritto in base-10. Non base-16, non base-9, ma 10. Non è un presupposto nascosto, è una regola reale. E tali regole rendono la serializzazione una serializzazione. Perché, praticamente, non ci sono regole sulla disposizione dei bit del tuo programma in memoria .

Era solo la punta di un iceberg. Facciamo un esempio di una sequenza di quei primitivi più semplici: una C struct. Potresti pensarlo

struct {
short width;
short height;
long count;
}

ha un layout di memoria definito su un determinato computer + sistema operativo? Beh, non lo fa. A seconda delle #pragma packimpostazioni correnti , il compilatore riempie i campi. Con le impostazioni predefinite della compilazione a 32 bit, entrambi shortssaranno riempiti a 4 byte, quindi structin realtà avranno 3 campi di 4 byte in memoria. Quindi ora, non devi solo specificare che shortè lungo 16 bit, è un numero intero, scritto nel complemento 1 negativo, big o little endian. È inoltre necessario annotare le impostazioni di imballaggio della struttura con cui è stato compilato il programma.

Questo è praticamente ciò che riguarda la serializzazione: creare un insieme di regole e attenersi a esse.

Tali regole possono quindi essere espanse per accettare strutture ancora più sofisticate (come elenchi a lunghezza variabile o dati non lineari), funzionalità aggiuntive come leggibilità umana, controllo delle versioni, compatibilità con le versioni precedenti e correzione degli errori, ecc. Ma anche scrivere un singolo intè già abbastanza complicato se si voglio solo assicurarti che sarai in grado di rileggerlo in modo affidabile.

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.