Devi disporre di oggetti e impostarli su null?


310

Devi eliminare gli oggetti e impostarli su null o il garbage collector li ripulirà quando escono dal campo di applicazione?


4
Sembra esserci un consenso sul fatto che non è necessario impostare l'oggetto su null, ma è necessario eseguire Dispose ()?
CJ7,


9
Il mio consiglio è sempre di disporre se un oggetto implementa IDisposable. Usa un blocco usando ogni volta. Non fare ipotesi, non lasciarlo al caso. Tuttavia, non è necessario impostare nulla su null. Un oggetto è appena uscito dal campo di applicazione.
peter

11
@peter: non usare "con" blocchi con WCF proxy client: msdn.microsoft.com/en-us/library/aa355056.aspx
nlawalker

9
Tuttavia, potresti voler impostare alcuni riferimenti su null all'interno del tuo Dispose()metodo! Questa è una sottile variazione su questa domanda, ma è importante perché l'oggetto che viene smaltito non può sapere se sta "andando fuori campo" (la chiamata Dispose()non è una garanzia). Altro qui: stackoverflow.com/questions/6757048/…
Kevin P. Rice,

Risposte:


239

Gli oggetti verranno ripuliti quando non vengono più utilizzati e quando il Garbage Collector lo ritiene opportuno. A volte, potrebbe essere necessario impostare un oggetto per nullfarlo uscire dall'ambito (come un campo statico il cui valore non è più necessario), ma in generale non è necessario impostarlo null.

Per quanto riguarda lo smaltimento degli oggetti, sono d'accordo con @Andre. Se l'oggetto è IDisposable, è una buona idea smaltirlo quando non è più necessario, specialmente se l'oggetto utilizza risorse non gestite. Il mancato smaltimento delle risorse non gestite comporterà perdite di memoria .

È possibile utilizzare l' usingistruzione per disporre automaticamente un oggetto quando il programma lascia l'ambito usingdell'istruzione.

using (MyIDisposableObject obj = new MyIDisposableObject())
{
    // use the object here
} // the object is disposed here

Che è funzionalmente equivalente a:

MyIDisposableObject obj;
try
{
    obj = new MyIDisposableObject();
}
finally
{
    if (obj != null)
    {
        ((IDisposable)obj).Dispose();
    }
}

4
Se obj è un tipo di riferimento, il blocco finally è equivalente a:if (obj != null) ((IDisposable)obj).Dispose();
Randy supporta Monica

1
@Tuzo: grazie! Modificato per riflettere questo.
Zach Johnson,

2
Un'osservazione riguardante IDisposable. La mancata eliminazione di un oggetto generalmente non causerà una perdita di memoria su alcuna classe ben progettata. Quando si lavora con risorse non gestite in C #, è necessario disporre di un finalizzatore che rilascerà comunque le risorse non gestite. Ciò significa che invece di deallocare le risorse quando dovrebbe essere eseguito, verrà rinviato a quando il garbage collector finalizza l'oggetto gestito. Può comunque causare molti altri problemi (come i blocchi non rilasciati). Dovresti smaltire un IDisposableperò!
Aidiakapi,

@RandyLevy Hai un riferimento per questo? Grazie
Basic

Ma la mia domanda è Dispose () ha bisogno di implementare qualche logica? Deve fare qualcosa? O internamente quando Dispose () è chiamato segnali GC che è buono per andare? Ho controllato il codice sorgente per TextWriter per esempio e Dispose non ha implementazione.
Mihail Georgescu,

137

Gli oggetti non escono mai dall'ambito in C # come in C ++. Vengono gestiti automaticamente dal Garbage Collector quando non vengono più utilizzati. Questo è un approccio più complicato del C ++ in cui l'ambito di una variabile è del tutto deterministico. Il Garbage Collector di CLR passa attivamente attraverso tutti gli oggetti che sono stati creati e determina se vengono utilizzati.

Un oggetto può "uscire dall'ambito" in una funzione ma se il suo valore viene restituito, GC controllerebbe se la funzione chiamante mantiene o meno il valore restituito.

L'impostazione dei riferimenti agli oggetti su nullnon è necessaria poiché la garbage collection funziona determinando quali oggetti fanno riferimento a altri oggetti.

In pratica, non devi preoccuparti della distruzione, funziona e va benissimo :)

Disposedeve essere chiamato su tutti gli oggetti che implementano IDisposablequando hai finito di lavorare con loro. Normalmente useresti un usingblocco con quegli oggetti in questo modo:

using (var ms = new MemoryStream()) {
  //...
}

EDIT su ambito variabile. Craig ha chiesto se l'ambito variabile ha alcun effetto sulla durata dell'oggetto. Per spiegare correttamente quell'aspetto di CLR, dovrò spiegare alcuni concetti di C ++ e C #.

Portata variabile effettiva

In entrambe le lingue la variabile può essere utilizzata solo nello stesso ambito in cui è stata definita: classe, funzione o un blocco di istruzioni racchiuso tra parentesi graffe. La sottile differenza, tuttavia, è che in C #, le variabili non possono essere ridefinite in un blocco nidificato.

In C ++, questo è perfettamente legale:

int iVal = 8;
//iVal == 8
if (iVal == 8){
    int iVal = 5;
    //iVal == 5
}
//iVal == 8

In C #, tuttavia, viene visualizzato un errore del compilatore:

int iVal = 8;
if(iVal == 8) {
    int iVal = 5; //error CS0136: A local variable named 'iVal' cannot be declared in this scope because it would give a different meaning to 'iVal', which is already used in a 'parent or current' scope to denote something else
}

Questo ha senso se si osserva MSIL generato: tutte le variabili utilizzate dalla funzione sono definite all'inizio della funzione. Dai un'occhiata a questa funzione:

public static void Scope() {
    int iVal = 8;
    if(iVal == 8) {
        int iVal2 = 5;
    }
}

Di seguito è l'IL generato. Si noti che iVal2, che è definito all'interno del blocco if, è effettivamente definito a livello di funzione. In pratica ciò significa che C # ha solo un ambito di classe e livello di funzione per quanto riguarda la durata variabile.

.method public hidebysig static void  Scope() cil managed
{
  // Code size       19 (0x13)
  .maxstack  2
  .locals init ([0] int32 iVal,
           [1] int32 iVal2,
           [2] bool CS$4$0000)

//Function IL - omitted
} // end of method Test2::Scope

Ambito C ++ e durata dell'oggetto

Ogni volta che una variabile C ++, allocata nello stack, esce dall'ambito, viene distrutta. Ricorda che in C ++ puoi creare oggetti nello stack o nell'heap. Quando li crei nello stack, una volta che l'esecuzione lascia l'ambito, vengono espulsi dallo stack e vengono distrutti.

if (true) {
  MyClass stackObj; //created on the stack
  MyClass heapObj = new MyClass(); //created on the heap
  obj.doSomething();
} //<-- stackObj is destroyed
//heapObj still lives

Quando gli oggetti C ++ vengono creati sull'heap, devono essere esplicitamente distrutti, altrimenti si tratta di una perdita di memoria. Nessun problema con le variabili dello stack però.

Durata dell'oggetto C #

In CLR, gli oggetti (ovvero i tipi di riferimento) vengono sempre creati sull'heap gestito. Ciò è ulteriormente rafforzato dalla sintassi della creazione di oggetti. Considera questo frammento di codice.

MyClass stackObj;

In C ++ questo creerebbe un'istanza MyClassnello stack e chiamerebbe il suo costruttore predefinito. In C # creerebbe un riferimento alla classe MyClassche non punta a nulla. L'unico modo per creare un'istanza di una classe è utilizzando l' newoperatore:

MyClass stackObj = new MyClass();

In un certo senso, gli oggetti C # sono molto simili agli oggetti creati utilizzando la newsintassi in C ++: sono creati nell'heap ma, diversamente dagli oggetti C ++, sono gestiti dal runtime, quindi non devi preoccuparti di distruggerli.

Poiché gli oggetti sono sempre nell'heap, il fatto che i riferimenti agli oggetti (ovvero i puntatori) escano dall'ambito diventa discutibile. Ci sono più fattori coinvolti nel determinare se un oggetto deve essere raccolto oltre alla semplice presenza di riferimenti all'oggetto.

Riferimenti per oggetti C #

Jon Skeet ha confrontato i riferimenti a oggetti in Java con pezzi di stringa collegati al fumetto, che è l'oggetto. La stessa analogia si applica ai riferimenti agli oggetti C #. Indicano semplicemente una posizione dell'heap che contiene l'oggetto. Pertanto, impostandolo su null non ha alcun effetto immediato sulla durata dell'oggetto, il fumetto continua a esistere, fino a quando il GC non lo "pop".

Continuando verso l'analogia del palloncino, sembrerebbe logico che una volta che il palloncino non ha stringhe attaccate ad esso, possa essere distrutto. In effetti, questo è esattamente il modo in cui gli oggetti contati di riferimento funzionano in linguaggi non gestiti. Solo che questo approccio non funziona molto bene per i riferimenti circolari. Immagina due palloncini che sono attaccati insieme da una corda ma nessuno dei due palloncini ha una corda per nient'altro. In base a semplici regole di conteggio degli ref, entrambi continuano ad esistere, anche se l'intero gruppo di palloncini è "orfano".

Gli oggetti .NET sono molto simili ai palloncini ad elio sotto un tetto. Quando il tetto si apre (GC esegue) - i palloncini non utilizzati galleggiano via, anche se potrebbero esserci gruppi di palloncini legati insieme.

.NET GC utilizza una combinazione di GC generazionale e mark and sweep. L'approccio generazionale prevede il runtime che favorisce l'ispezione degli oggetti che sono stati allocati più di recente, poiché è più probabile che siano inutilizzati e contrassegna e sweep comporta il runtime che passa attraverso l'intero grafico degli oggetti e risolve se ci sono gruppi di oggetti che non sono utilizzati. Questo affronta adeguatamente il problema della dipendenza circolare.

Inoltre, .NET GC funziona su un altro thread (il cosiddetto thread finalizzatore) in quanto ha un bel po 'da fare e farlo sul thread principale interromperà il programma.


1
@Igor: Andando "al di fuori dell'ambito" intendo che il riferimento all'oggetto è fuori contesto e non è possibile fare riferimento all'ambito corrente. Sicuramente questo succede ancora in C #.
CJ7,

@Craig Johnston, non confondere l'ambito variabile utilizzato dal compilatore con durata variabile determinata dal runtime: sono diversi. Una variabile locale potrebbe non essere "attiva" anche se è ancora nell'ambito.
Randy supporta Monica

1
@Craig Johnston: vedi blogs.msdn.com/b/ericgu/archive/2004/07/23/192842.aspx : "non esiste alcuna garanzia che una variabile locale rimarrà attiva fino alla fine di un ambito se non lo è Il runtime è libero di analizzare il codice che ha e determinare quali non ci sono ulteriori usi di una variabile oltre un certo punto, e quindi non mantenere quella variabile viva oltre quel punto (cioè non trattarla come una radice ai fini di GC). "
Randy supporta Monica il

1
@Tuzo: True. Ecco a cosa serve GC.KeepAlive.
Steven Sudit,

1
@Craig Johnston: No e Sì. No perché il runtime .NET lo gestisce per te e fa un buon lavoro. Sì perché il compito del programmatore non è scrivere codice che (solo) compila ma scrivere codice che viene eseguito . A volte aiuta a sapere cosa sta facendo il runtime sotto le coperte (ad es. Risoluzione dei problemi). Si potrebbe sostenere che è il tipo di conoscenza che aiuta a separare i buoni programmatori dai grandi programmatori.
Randy supporta Monica

18

Come altri hanno già detto, vuoi sicuramente chiamare Disposese la classe implementa IDisposable. Prendo una posizione abbastanza rigida su questo. Alcuni sostengono che potrebbe chiamare Disposein DataSet, per esempio, è inutile perché essi smontati e vide che non ha fatto nulla di significativo. Ma penso che ci siano errori in questo argomento.

Leggi questo per un interessante dibattito di persone rispettate sull'argomento. Quindi leggi il mio ragionamento qui perché penso che Jeffery Richter sia nel campo sbagliato.

Ora, se devi o meno impostare un riferimento null. La risposta è no. Vorrei illustrare il mio punto con il seguente codice.

public static void Main()
{
  Object a = new Object();
  Console.WriteLine("object created");
  DoSomething(a);
  Console.WriteLine("object used");
  a = null;
  Console.WriteLine("reference set to null");
}

Quindi, quando ritieni che l'oggetto a cui fa riferimento asia idoneo per la raccolta? Se hai detto dopo la chiamata a a = nullallora ti sbagli. Se hai detto che dopo che il Mainmetodo è completo, allora sbagli. La risposta corretta è che può essere ritirato durante la chiamata a DoSomething. È giusto. È idoneo prima che il riferimento sia impostato su nulle forse anche prima del DoSomethingcompletamento della chiamata . Questo perché il compilatore JIT è in grado di riconoscere quando i riferimenti agli oggetti non vengono più sottoposti a dereferenziazione anche se sono ancora rootati.


3
Cosa succede se aun campo membro privato è in una classe? Se anon è impostato su null, il GC non ha modo di sapere se averrà riutilizzato in qualche metodo, giusto? Pertanto anon verranno raccolti fino a quando l'intera classe contenente non verrà raccolta. No?
Kevin P. Rice,

4
@Kevin: corretto. Se afosse un membro della classe e la classe contenente afosse ancora radicata e in uso, anche quella resterebbe in sospeso. Questo è uno scenario in cui impostarlo su nullpotrebbe essere utile.
Brian Gideon,

1
Il tuo punto si lega a una ragione per cui Disposeè importante: non è possibile invocare Dispose(o qualsiasi altro metodo non in -ineabile) su un oggetto senza un riferimento radicato ad esso; chiamare Disposedopo che uno è stato fatto usando un oggetto assicurerà che un riferimento radicato continui ad esistere per tutta la durata dell'ultima azione eseguita su di esso. Abbandonare tutti i riferimenti a un oggetto senza chiamare Disposepuò ironicamente avere l'effetto di causare il rilascio occasionale delle risorse dell'oggetto troppo presto .
supercat

Questo esempio e questa spiegazione non sembrano definitivi sul duro suggerimento di non impostare mai i riferimenti su null. Voglio dire, ad eccezione del commento di Kevin, un riferimento impostato su null dopo che è stato eliminato sembra piuttosto benigno , quindi qual è il danno? Mi sto perdendo qualcosa?
Dathompson,

13

Non è mai necessario impostare gli oggetti su null in C #. Il compilatore e il runtime si occuperanno di capire quando non sono più nell'ambito.

Sì, è necessario disporre di oggetti che implementano IDisposable.


2
Se hai un riferimento di lunga durata (o anche statico) a un oggetto di grandi dimensioni, devi wantannullarlo non appena hai finito in modo che sia libero di essere recuperato.
Steven Sudit,

12
Se hai mai "finito" non dovrebbe essere statico. Se non è statico, ma "longevo", dovrebbe comunque uscire dal campo di applicazione subito dopo averlo fatto. La necessità di impostare i riferimenti su null indica un problema con la struttura del codice.
EMP

Puoi avere un oggetto statico con cui hai finito. Considerare: una risorsa statica che viene letta dal disco in un formato intuitivo e quindi analizzata in un formato adatto all'uso del programma. Puoi finire con una copia privata dei dati non elaborati che non ha più scopi. (Esempio nel mondo reale: l'analisi è una routine a due passaggi e quindi non può semplicemente elaborare i dati mentre vengono letti.)
Loren Pechtel,

1
Quindi non dovrebbe memorizzare alcun dato non elaborato in un campo statico se utilizzato solo temporaneamente. Certo, puoi farlo, semplicemente non è una buona pratica proprio per questo motivo: devi quindi gestirlo manualmente.
EMP

2
Lo eviti memorizzando i dati grezzi in una variabile locale nel metodo che li elabora. Il metodo restituisce i dati elaborati, che vengono conservati, ma il locale per i dati non elaborati non rientra nell'ambito di applicazione quando il metodo viene chiuso e viene automaticamente GC.
EMP,

11

Sono d'accordo con la risposta comune qui che sì, dovresti disporre e no, generalmente non dovresti impostare la variabile su null ... ma volevo sottolineare che lo smaltimento NON riguarda principalmente la gestione della memoria. Sì, può aiutare (e talvolta lo fa) con la gestione della memoria, ma il suo scopo principale è darti il ​​rilascio deterministico di risorse scarse.

Ad esempio, se si apre una porta hardware (seriale ad esempio), un socket TCP / IP, un file (in modalità di accesso esclusivo) o persino una connessione al database, è stato impedito a qualsiasi altro codice di utilizzare tali elementi fino al loro rilascio. Dispose generalmente rilascia questi elementi (insieme a GDI e ad altri handle "os" ecc. Di cui sono disponibili 1000, ma sono comunque limitati nel complesso). Se non si chiama dipose sull'oggetto proprietario e si rilasciano esplicitamente queste risorse, quindi provare ad aprire nuovamente la stessa risorsa in futuro (o un altro programma lo fa) quel tentativo di apertura fallirà perché l'oggetto non esposto, non raccolto ha ancora l'oggetto aperto . Naturalmente, quando il GC raccoglie l'oggetto (se il modello Dispose è stato implementato correttamente) la risorsa verrà rilasciata ... ma non sai quando sarà, quindi non non so quando è sicuro riaprire quella risorsa. Questo è il problema principale che Dispose risolve. Naturalmente, rilasciando questi handle spesso si libera anche la memoria, e mai rilasciandoli potrebbe non rilasciare mai quella memoria ... quindi tutte le chiacchiere su perdite di memoria o ritardi nella pulizia della memoria.

Ho visto esempi reali di ciò che causano problemi. Ad esempio, ho visto applicazioni Web ASP.Net che alla fine non riescono a connettersi al database (anche se per brevi periodi di tempo o fino al riavvio del processo del server Web) perché il "pool di connessioni del server sql è pieno" ... vale a dire , così tante connessioni sono state create e non esplicitamente rilasciate in un periodo di tempo così breve che non è possibile creare nuove connessioni e molte delle connessioni nel pool, sebbene non attive, fanno ancora riferimento a oggetti non individuati e non raccolti e così possono " essere riutilizzato. Disporre correttamente le connessioni al database ove necessario garantisce che questo problema non si verifichi (almeno a meno che non si disponga di un accesso simultaneo molto elevato).


11

Se l'oggetto implementa IDisposable, quindi sì, dovresti eliminarlo. L'oggetto potrebbe essere sospeso su risorse native (handle di file, oggetti del sistema operativo) che non potrebbero essere liberati immediatamente altrimenti. Ciò può portare alla fame di risorse, problemi di blocco dei file e altri bug sottili che potrebbero altrimenti essere evitati.

Vedi anche Implementazione di un metodo di smaltimento su MSDN.


Ma il collezionista di garabage non chiamerà Dispose ()? In tal caso, perché dovresti chiamarlo?
CJ7,

A meno che non lo chiami esplicitamente, non vi è alcuna garanzia che Disposeverrà chiamata. Inoltre, se il tuo oggetto è trattenuto da una risorsa scarsa o sta bloccando una risorsa (ad esempio un file), ti consigliamo di liberarlo il prima possibile. Aspettare che il GC faccia questo non è ottimale.
Chris Schmich,

12
GC non chiamerà mai Dispose (). GC potrebbe chiamare un finalizzatore che per convenzione dovrebbe ripulire le risorse.
Adrianm,

@adrianm: non mightchiamare, ma willchiamare.
leppie,

2
@leppie: i finalizzatori non sono deterministici e potrebbero non essere chiamati (ad es. quando l'appdomain viene scaricato). Se hai bisogno di una finalizzazione deterministica, devi implementare quello che penso sia chiamato gestore critico. Il CLR ha una gestione speciale di questi oggetti per garantire che siano finalizzati (ad es. Prejits il codice di finalizzazione per gestire la memoria insufficiente)
adrianm,

9

Se implementano l'interfaccia IDisposable, è necessario eliminarli. Il garbage collector si prenderà cura di tutto il resto.

EDIT: la cosa migliore è usare il usingcomando quando si lavora con oggetti usa e getta:

using(var con = new SqlConnection("..")){ ...

5

Quando un oggetto viene implementato, IDisposableè necessario chiamare Dispose(o Close, in alcuni casi, chiamare Dispose per te).

Normalmente non è necessario impostare gli oggetti null, poiché il GC saprà che un oggetto non verrà più utilizzato.

C'è un'eccezione quando imposto gli oggetti su null. Quando recupero molti oggetti (dal database) su cui devo lavorare e li memorizzo in una raccolta (o matrice). Quando il "lavoro" è finito, ho impostato l'oggetto su null, perché il GC non sa che ho finito di lavorarci.

Esempio:

using (var db = GetDatabase()) {
    // Retrieves array of keys
    var keys = db.GetRecords(mySelection); 

    for(int i = 0; i < keys.Length; i++) {
       var record = db.GetRecord(keys[i]);
       record.DoWork();
       keys[i] = null; // GC can dispose of key now
       // The record had gone out of scope automatically, 
       // and does not need any special treatment
    }
} // end using => db.Dispose is called

4

Normalmente, non è necessario impostare i campi su null. Consiglio sempre di disporre delle risorse non gestite.

Per esperienza, ti consiglio anche di fare quanto segue:

  • Annulla l'iscrizione agli eventi se non ti servono più.
  • Impostare qualsiasi campo contenente un delegato o un'espressione su null se non è più necessario.

Mi sono imbattuto in alcuni problemi molto difficili da trovare che erano il risultato diretto di non seguire i consigli di cui sopra.

Un buon posto per farlo è in Dispose (), ma prima di solito è meglio.

In generale, se esiste un riferimento a un oggetto, il Garbage Collector (GC) potrebbe impiegare un paio di generazioni in più per capire che un oggetto non è più in uso. Per tutto il tempo l'oggetto rimane in memoria.

Questo potrebbe non essere un problema finché non scopri che la tua app utilizza molta più memoria di quanto ti aspetti. Quando ciò accade, collegare un profiler di memoria per vedere quali oggetti non vengono ripuliti. L'impostazione dei campi che fanno riferimento ad altri oggetti su null e l'eliminazione delle raccolte disponibili può davvero aiutare il GC a capire quali oggetti può rimuovere dalla memoria. Il GC recupererà più rapidamente la memoria utilizzata, rendendo la tua app molto meno affamata e più veloce.


1
Cosa intendi con "eventi e delegati": cosa dovrebbe essere "ripulito" con questi?
CJ7,

@Craig - Ho modificato la mia risposta. Spero che questo lo chiarisca un po '.
Marnix van Valen,

3

Chiama sempre smaltimento. Non vale il rischio. Le applicazioni per grandi aziende gestite dovrebbero essere trattate con rispetto. Non è possibile formulare ipotesi, altrimenti tornerà a morderti.

Non ascoltare leppie.

Molti oggetti in realtà non implementano IDisposable, quindi non devi preoccuparti di loro. Se escono veramente dal campo di applicazione, verranno liberati automaticamente. Inoltre non mi sono mai imbattuto nella situazione in cui ho dovuto impostare qualcosa su null.

Una cosa che può succedere è che molti oggetti possono essere tenuti aperti. Ciò può aumentare notevolmente l'utilizzo della memoria dell'applicazione. A volte è difficile capire se si tratta effettivamente di una perdita di memoria o se l'applicazione sta facendo un sacco di cose.

Gli strumenti del profilo di memoria possono aiutare in cose del genere, ma può essere difficile.

Inoltre, annulla sempre la sottoscrizione agli eventi che non sono necessari. Prestare attenzione anche con i collegamenti e i controlli WPF. Non è una situazione normale, ma mi sono imbattuto in una situazione in cui avevo un controllo WPF che era associato a un oggetto sottostante. L'oggetto sottostante era grande e occupava una grande quantità di memoria. Il controllo WPF veniva sostituito con una nuova istanza e quella vecchia era ancora in giro per qualche motivo. Ciò ha causato una grande perdita di memoria.

In hindsite il codice è stato scritto male, ma il punto è che vuoi assicurarti che le cose che non vengono utilizzate vadano fuori dallo scopo. Quello ci è voluto molto tempo per trovarlo con un profiler di memoria in quanto è difficile sapere quali elementi della memoria sono validi e cosa non dovrebbe esserci.


2

Anch'io devo rispondere. JIT genera tabelle insieme al codice dalla sua analisi statica di utilizzo variabile. Le voci della tabella sono le "Radici GC" nel frame dello stack corrente. Man mano che il puntatore dell'istruzione avanza, quelle voci della tabella diventano non valide e quindi pronte per la garbage collection. Pertanto: se si tratta di una variabile con ambito, non è necessario impostarla su null: il GC raccoglierà l'oggetto. Se è un membro o una variabile statica, è necessario impostarlo su null

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.