Qual è la parola chiave di rendimento utilizzata in C #?


829

Nella domanda Come posso esporre solo un frammento di IList <> una delle risposte aveva il seguente frammento di codice:

IEnumerable<object> FilteredList()
{
    foreach(object item in FullList)
    {
        if(IsItemInPartialList(item))
            yield return item;
    }
}

Cosa ci fa la parola chiave yield? L'ho visto referenziato in un paio di posti e un'altra domanda, ma non ho ancora capito cosa effettivamente fa. Sono abituato a pensare alla resa nel senso di una discussione che cede a un'altra, ma qui non sembra rilevante.



14
Questo non è sorprendente. La confusione deriva dal fatto che siamo condizionati a vedere il "ritorno" come un'uscita di funzione mentre preceduto da un "rendimento" non lo è.
Larry,

4
Ho letto i documenti ma temo di non capire ancora :(
Ortund,

Risposte:


737

La yieldparola chiave in realtà fa molto qui.

La funzione restituisce un oggetto che implementa l' IEnumerable<object>interfaccia. Se una funzione chiamante inizia foreachsu questo oggetto, la funzione viene richiamata nuovamente fino a quando "cede". Questo è lo zucchero sintattico introdotto in C # 2.0 . Nelle versioni precedenti dovevi creare il tuo IEnumerablee gli IEnumeratoroggetti per fare cose come questa.

Il modo più semplice per capire un codice come questo è digitare un esempio, impostare alcuni punti di interruzione e vedere cosa succede. Prova a seguire questo esempio:

public void Consumer()
{
    foreach(int i in Integers())
    {
        Console.WriteLine(i.ToString());
    }
}

public IEnumerable<int> Integers()
{
    yield return 1;
    yield return 2;
    yield return 4;
    yield return 8;
    yield return 16;
    yield return 16777216;
}

Quando passi attraverso l'esempio, troverai la prima chiamata a Integers()restituire 1. La seconda chiamata ritorna 2e la linea yield return 1non viene più eseguita.

Ecco un esempio di vita reale:

public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
    using (var connection = CreateConnection())
    {
        using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
        {
            command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return make(reader);
                }
            }
        }
    }
}

113
In questo caso sarebbe più semplice, sto solo usando l'intero qui per mostrare come funziona il rendimento. La cosa bella dell'utilizzo del rendimento è che è un modo molto rapido di implementare il modello iteratore, quindi le cose vengono valutate pigramente.
Mendelt,

111
Vale anche la pena notare che è possibile utilizzare yield break;quando non si desidera restituire altri articoli.
Rory,

7
yieldnon è una parola chiave. Se così fosse, non potrei usare la resa come identificatore come inint yield = 500;
Brandin,

5
@Brandin è perché tutti i linguaggi di programmazione supportano due tipi di parole chiave, vale a dire riservate e contestuali. la resa rientra nella categoria successiva, motivo per cui il compilatore C # non proibisce il codice. Maggiori dettagli qui: ericlippert.com/2009/05/11/reserved-and-contestual-keywords Saresti felice di sapere che ci sono anche parole riservate che non sono riconosciute come parole chiave da una lingua. Per esempio vai a java. Maggiori dettagli qui: stackoverflow.com/questions/2545103/…
RBT

7
'If a calling function starts foreach-ing over this object the function is called again until it "yields"'. non suona bene per me. Ho sempre pensato alla parola chiave c # yield nel contesto di "il raccolto produce un raccolto abbondante", invece di "l'auto cede al pedone".
Zack,

369

Iterazione. Crea una macchina a stati "sotto le coperte" che ricorda dove ti trovavi in ​​ogni ciclo aggiuntivo della funzione e riprende da lì.


210

La resa ha due grandi usi,

  1. Aiuta a fornire iterazioni personalizzate senza creare raccolte temporanee.

  2. Aiuta a eseguire iterazioni con stato. inserisci qui la descrizione dell'immagine

Per spiegare più sopra due punti in modo più dimostrativo, ho creato un semplice video che puoi guardare qui


13
Il video mi aiuta a capire chiaramente il yield. Articolo del progetto di codice di @ ShivprasadKoirala A cosa serve il rendimento di C #? della stessa spiegazione è anche una buona fonte
Dush

Vorrei anche aggiungere come terzo punto il modo yield"veloce" per creare un IEnumerator personalizzato (piuttosto che una classe implementa l'interfaccia IEnumerator).
MrTourkos,

Ho visto il tuo video Shivprasad e ho spiegato chiaramente l'uso della parola chiave yield.
Tore Aurstad,

Grazie per il video !. Molto ben spiegato!
Roblogic,

Ottimo video, ma mi chiedo ... L'implementazione usando la resa è ovviamente più pulita, ma deve essenzialmente creare la propria memoria temporanea o / ed Elenco internamente per tenere traccia dello stato (o piuttosto creare una macchina a stati). Quindi, "Rendimento" sta facendo altro che rendere più semplice l'implementazione e rendere le cose più belle o c'è qualcos'altro? Che ne dici di efficienza, eseguire il codice usando Yield è più o meno efficiente / veloce che senza?
toughQuestions

135

Recentemente Raymond Chen ha anche pubblicato un'interessante serie di articoli sulla parola chiave yield.

Sebbene sia utilizzato nominalmente per implementare facilmente un modello iteratore, ma può essere generalizzato in una macchina a stati. Inutile citare Raymond, l'ultima parte si collega anche ad altri usi (ma l'esempio nel blog di Entin è particolarmente valido, mostrando come scrivere codice sicuro asincrono).


Questo deve essere votato. Dolce come spiega lo scopo dell'operatore e degli interni.
sajidnizami,

3
la parte 1 spiega lo zucchero sintattico del "rendimento". spiegazione eccellente!
Dror Weiss,

99

A prima vista, il rendimento return è uno zucchero .NET per restituire un IEnumerable .

Senza rendimento, tutti gli elementi della raccolta vengono creati contemporaneamente:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        return new List<SomeData> {
            new SomeData(), 
            new SomeData(), 
            new SomeData()
        };
    }
}

Stesso codice utilizzando la resa, restituisce articolo per articolo:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        yield return new SomeData();
        yield return new SomeData();
        yield return new SomeData();
    }
}

Il vantaggio dell'utilizzo del rendimento è che se la funzione che consuma i tuoi dati necessita semplicemente del primo elemento della raccolta, il resto degli articoli non verrà creato.

L'operatore di rendimento consente la creazione di articoli quando richiesto. Questa è una buona ragione per usarlo.


40

yield returnviene utilizzato con gli enumeratori. Ad ogni chiamata della dichiarazione di rendimento, il controllo viene restituito al chiamante ma garantisce che lo stato della chiamata venga mantenuto. Per questo motivo, quando il chiamante enumera l'elemento successivo, continua l'esecuzione nel metodo callee dall'istruzione immediatamente dopo l' yieldistruzione.

Proviamo a capirlo con un esempio. In questo esempio, in corrispondenza di ciascuna riga ho menzionato l'ordine in cui scorre l'esecuzione.

static void Main(string[] args)
{
    foreach (int fib in Fibs(6))//1, 5
    {
        Console.WriteLine(fib + " ");//4, 10
    }            
}

static IEnumerable<int> Fibs(int fibCount)
{
    for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
    {
        yield return prevFib;//3, 9
        int newFib = prevFib + currFib;//6
        prevFib = currFib;//7
        currFib = newFib;//8
    }
}

Inoltre, lo stato viene mantenuto per ogni enumerazione. Supponiamo, ho un'altra chiamata al Fibs()metodo, quindi lo stato verrà ripristinato per questo.


2
set prevFib = 1 - il primo numero di Fibonacci è un "1", non uno "0"
fubo

31

Intuitivamente, la parola chiave restituisce un valore dalla funzione senza lasciarlo, cioè nell'esempio di codice restituisce il itemvalore corrente e quindi riprende il ciclo. Più formalmente, viene utilizzato dal compilatore per generare codice per un iteratore . Gli iteratori sono funzioni che restituiscono IEnumerableoggetti. Il MSDN ha diversi articoli su di loro.


4
Bene, per essere precisi non riprende il ciclo, lo mette in pausa fino a quando il genitore chiama "iterator.next ()".
Alex

8
@jitbit Ecco perché ho usato "intuitivamente" e "più formalmente".
Konrad Rudolph,

31

Un'implementazione dell'elenco o dell'array carica immediatamente tutti gli articoli mentre l'implementazione del rendimento fornisce una soluzione di esecuzione differita.

In pratica, è spesso desiderabile eseguire la quantità minima di lavoro necessaria per ridurre il consumo di risorse di un'applicazione.

Ad esempio, potremmo avere un'applicazione che elabora milioni di record da un database. I seguenti vantaggi possono essere raggiunti quando utilizziamo IEnumerable in un modello pull-based di esecuzione differita:

  • È probabile che la scalabilità, l'affidabilità e la prevedibilità migliorino poiché il numero di record non influisce in modo significativo sui requisiti delle risorse dell'applicazione.
  • È probabile che prestazioni e reattività migliorino poiché l'elaborazione può iniziare immediatamente invece di attendere che venga caricata per prima l'intera raccolta.
  • È probabile che la recuperabilità e l'utilizzo migliorino poiché l'applicazione può essere arrestata, avviata, interrotta o non riuscita. Solo gli elementi in corso andranno persi rispetto al pre-recupero di tutti i dati in cui è stata effettivamente utilizzata solo una parte dei risultati.
  • L'elaborazione continua è possibile in ambienti in cui vengono aggiunti flussi di carico di lavoro costanti.

Ecco un confronto tra costruire prima una raccolta come un elenco rispetto all'utilizzo della resa.

Esempio di elenco

    public class ContactListStore : IStore<ContactModel>
    {
        public IEnumerable<ContactModel> GetEnumerator()
        {
            var contacts = new List<ContactModel>();
            Console.WriteLine("ContactListStore: Creating contact 1");
            contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
            Console.WriteLine("ContactListStore: Creating contact 2");
            contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
            Console.WriteLine("ContactListStore: Creating contact 3");
            contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
            return contacts;
        }
    }

    static void Main(string[] args)
    {
        var store = new ContactListStore();
        var contacts = store.GetEnumerator();

        Console.WriteLine("Ready to iterate through the collection.");
        Console.ReadLine();
    }

Uscita console
ContactListStore: creazione del contatto 1
ContactListStore: creazione del contatto 2
ContactListStore: creazione del contatto 3
Pronta per scorrere la raccolta.

Nota: l'intera raccolta è stata caricata in memoria senza nemmeno chiedere un singolo elemento nell'elenco

Esempio di rendimento

public class ContactYieldStore : IStore<ContactModel>
{
    public IEnumerable<ContactModel> GetEnumerator()
    {
        Console.WriteLine("ContactYieldStore: Creating contact 1");
        yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
        Console.WriteLine("ContactYieldStore: Creating contact 2");
        yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
        Console.WriteLine("ContactYieldStore: Creating contact 3");
        yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
    }
}

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();

    Console.WriteLine("Ready to iterate through the collection.");
    Console.ReadLine();
}

Console Output
Pronto per scorrere la raccolta.

Nota: la raccolta non è stata eseguita affatto. Ciò è dovuto alla natura "esecuzione differita" di IEnumerable. La costruzione di un articolo avverrà solo quando è realmente necessario.

Chiamiamo di nuovo la raccolta e affrontiamo il comportamento quando recuperiamo il primo contatto nella raccolta.

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();
    Console.WriteLine("Ready to iterate through the collection");
    Console.WriteLine("Hello {0}", contacts.First().FirstName);
    Console.ReadLine();
}

Console Output
Pronto per iterare attraverso la raccolta
ContactYieldStore: Creazione contatto 1
Ciao Bob

Bello! È stato creato solo il primo contatto quando il client "ha estratto" l'elemento dalla raccolta.


1
Questa risposta richiede più attenzione! Thx
leon22

@ leon22 assolutamente +2
snr

26

Ecco un modo semplice per comprendere il concetto: L'idea di base è che se vuoi una raccolta che puoi usare " foreach" su, ma raccogliere gli elementi nella raccolta è costoso per qualche motivo (come interrogarli da un database), E spesso non avrai bisogno dell'intera collezione, quindi crei una funzione che costruisce la raccolta un elemento alla volta e lo restituisce al consumatore (che può quindi terminare presto lo sforzo di raccolta).

Pensala in questo modo: vai al banco della carne e vuoi comprare una libbra di prosciutto a fette. Il macellaio prende un prosciutto da 10 libbre sul retro, lo mette sulla macchina affettatrice, affetta il tutto, quindi ti riporta il mucchio di fette e ne misura una libbra. (VECCHIO modo). Con yield, il macellaio porta la macchina affettatrice al bancone e inizia a tagliare e "cedere" ogni fetta sulla bilancia fino a quando misura 1 libbra, quindi la avvolge per te e il gioco è fatto. The Old Way può essere migliore per il macellaio (gli consente di organizzare i suoi macchinari nel modo che preferisce), ma il New Way è chiaramente più efficiente nella maggior parte dei casi per il consumatore.


18

La yieldparola chiave consente di creare un IEnumerable<T>nel modulo su un blocco iteratore . Questo blocco iteratore supporta l' esecuzione differita e se non si ha familiarità con il concetto, potrebbe apparire quasi magico. Tuttavia, alla fine della giornata, è solo il codice che viene eseguito senza strani trucchi.

Un blocco iteratore può essere descritto come zucchero sintattico in cui il compilatore genera una macchina a stati che tiene traccia di quanto è progredita l'enumerazione dell'enumerabile. Per enumerare un enumerabile, spesso si utilizza un foreachciclo. Tuttavia, un foreachciclo è anche zucchero sintattico. Quindi sei due astrazioni rimosse dal codice reale, motivo per cui inizialmente potrebbe essere difficile capire come tutto funzioni insieme.

Supponiamo di avere un blocco iteratore molto semplice:

IEnumerable<int> IteratorBlock()
{
    Console.WriteLine("Begin");
    yield return 1;
    Console.WriteLine("After 1");
    yield return 2;
    Console.WriteLine("After 2");
    yield return 42;
    Console.WriteLine("End");
}

I blocchi di iteratori reali spesso hanno condizioni e loop ma quando si controllano le condizioni e si srotolano i loop, questi finiscono comunque come yieldistruzioni interfogliate con altro codice.

Per enumerare il blocco iteratore foreachviene utilizzato un ciclo:

foreach (var i in IteratorBlock())
    Console.WriteLine(i);

Ecco l'output (nessuna sorpresa qui):

Inizio
1
Dopo 1
2
Dopo 2
42
Fine

Come detto sopra foreachè lo zucchero sintattico:

IEnumerator<int> enumerator = null;
try
{
    enumerator = IteratorBlock().GetEnumerator();
    while (enumerator.MoveNext())
    {
        var i = enumerator.Current;
        Console.WriteLine(i);
    }
}
finally
{
    enumerator?.Dispose();
}

Nel tentativo di districare questo ho creato un diagramma di sequenza con le astrazioni rimosse:

Schema sequenza blocchi iteratore C #

Anche la macchina a stati generata dal compilatore implementa l'enumeratore ma per rendere più chiaro il diagramma, li ho mostrati come istanze separate. (Quando la macchina a stati viene enumerata da un altro thread in realtà si ottengono istanze separate ma qui i dettagli non sono importanti.)

Ogni volta che si chiama il blocco iteratore viene creata una nuova istanza della macchina a stati. Tuttavia, nessuno dei tuoi codici nel blocco iteratore viene eseguito fino a quando non enumerator.MoveNext()viene eseguito per la prima volta. Ecco come funziona l'esecuzione differita. Ecco un esempio (piuttosto sciocco):

var evenNumbers = IteratorBlock().Where(i => i%2 == 0);

A questo punto l'iteratore non è stato eseguito. La Whereclausola crea un nuovo IEnumerable<T>che avvolge il IEnumerable<T>reso da IteratorBlockma questo enumerabile deve ancora essere enumerato. Questo succede quando si esegue un foreachciclo:

foreach (var evenNumber in evenNumbers)
    Console.WriteLine(eventNumber);

Se si enumera l'enumerabile due volte, viene creata una nuova istanza della macchina a stati ogni volta e il blocco iteratore eseguirà lo stesso codice due volte.

Si noti che i metodi di LINQ piace ToList(), ToArray(), First(), Count()ecc userà un foreachciclo per enumerare l'enumerabile. Ad esempio ToList(), enumera tutti gli elementi dell'enumerabile e li memorizza in un elenco. È ora possibile accedere all'elenco per ottenere tutti gli elementi dell'enumerabile senza eseguire nuovamente il blocco iteratore. C'è un compromesso tra l'uso della CPU per produrre gli elementi dell'enumerabile più volte e la memoria per memorizzare gli elementi dell'enumerazione per accedervi più volte quando si usano metodi come ToList().


18

Se lo capisco correttamente, ecco come lo definirei dal punto di vista della funzione che implementa IEnumerable con yield.

  • Eccone uno.
  • Chiama di nuovo se ne hai bisogno di un altro.
  • Ricorderò quello che ti ho già dato.
  • Saprò solo se posso dartene un altro quando chiami di nuovo.

semplice e geniale
Harry,

10

La parola chiave C # yield, per dirla semplicemente, consente a molte chiamate a un corpo di codice, indicato come iteratore, che sa come tornare prima che sia fatto e, quando richiamato, continua da dove era stato interrotto - cioè aiuta un iteratore diventa trasparente per ogni elemento in una sequenza che l'iteratore restituisce nelle chiamate successive.

In JavaScript, lo stesso concetto si chiama Generatori.


La migliore spiegazione ancora. Questi sono anche gli stessi generatori in Python?
petrosmm,

7

È un modo molto semplice e facile per creare un enumerabile per il tuo oggetto. Il compilatore crea una classe che avvolge il tuo metodo e che implementa, in questo caso, IEnumerable <oggetto>. Senza la parola chiave yield, dovresti creare un oggetto che implementa IEnumerable <oggetto>.


5

Sta producendo una sequenza enumerabile. Quello che fa è in realtà creare una sequenza IEnumerable locale e restituirla come risultato del metodo


3

Questo link ha un semplice esempio

Esempi ancora più semplici sono qui

public static IEnumerable<int> testYieldb()
{
    for(int i=0;i<3;i++) yield return 4;
}

Si noti che il rendimento non ritorna dal metodo. Puoi anche mettere un WriteLinedopo ilyield return

Quanto sopra produce un IEnumerable di 4 in 4,4,4,4

Qui con a WriteLine. Aggiungerà 4 all'elenco, stamperà abc, quindi aggiungerà 4 all'elenco, quindi completerà il metodo e quindi tornerà davvero dal metodo (una volta completato il metodo, come accadrebbe con una procedura senza ritorno). Ma questo avrebbe un valore, un IEnumerableelenco di ints, che restituisce al completamento.

public static IEnumerable<int> testYieldb()
{
    yield return 4;
    console.WriteLine("abc");
    yield return 4;
}

Si noti inoltre che quando si utilizza la resa, ciò che si sta restituendo non è dello stesso tipo della funzione. È del tipo di un elemento all'interno IEnumerabledell'elenco.

Si utilizza yield con il tipo restituito dal metodo as IEnumerable. Se il tipo restituito dal metodo è into List<int>e lo usi yield, non verrà compilato. È possibile utilizzare il IEnumerabletipo restituito dal metodo senza rendimento, ma sembra che forse non si possa utilizzare il rendimento senza il IEnumerabletipo restituito dal metodo.

E per farlo funzionare devi chiamarlo in un modo speciale.

static void Main(string[] args)
{
    testA();
    Console.Write("try again. the above won't execute any of the function!\n");

    foreach (var x in testA()) { }


    Console.ReadLine();
}



// static List<int> testA()
static IEnumerable<int> testA()
{
    Console.WriteLine("asdfa");
    yield return 1;
    Console.WriteLine("asdf");
}

nota- se si cerca di capire SelectMany, usa la resa e anche i generici .. questo esempio può aiutare public static IEnumerable<TResult> testYieldc<TResult>(TResult t) { yield return t; }e public static IEnumerable<TResult> testYieldc<TResult>(TResult t) { return new List<TResult>(); }
barlop

Sembra un'ottima spiegazione! Questa potrebbe essere stata la risposta accettata.
pongapundit,

@pongapundit grazie, la mia risposta è sicuramente chiara e semplice, ma non ho usato molto la resa, altri risponditori hanno molta più esperienza e conoscenza dei suoi usi di me. Quello che ho scritto sulla resa qui è stato probabilmente grattandomi la testa cercando di capire alcune delle risposte qui e su quel link dotnetperls! Ma dal momento che non lo so yield returnbene (a parte la semplice cosa che ho menzionato), e non l'ho usato molto e non so molto sui suoi usi, non penso che questo dovrebbe essere quello accettato.
barlop

3

Un punto importante sulla parola chiave Yield è Lazy Execution . Ora, ciò che intendo per Lazy Execution è eseguire quando necessario. Un modo migliore per dirlo è dare un esempio

Esempio: non utilizzare Yield, ovvero nessuna esecuzione pigra.

        public static IEnumerable<int> CreateCollectionWithList()
        {
            var list =  new List<int>();
            list.Add(10);
            list.Add(0);
            list.Add(1);
            list.Add(2);
            list.Add(20);

            return list;
        }

Esempio: utilizzo di Yield ovvero Lazy Execution.

    public static IEnumerable<int> CreateCollectionWithYield()
    {
        yield return 10;
        for (int i = 0; i < 3; i++) 
        {
            yield return i;
        }

        yield return 20;
    }

Ora quando chiamo entrambi i metodi.

var listItems = CreateCollectionWithList();
var yieldedItems = CreateCollectionWithYield();

noterai listItems avrà 5 elementi al suo interno (passa il mouse su listItems durante il debug). Considerando che yieldItems avrà solo un riferimento al metodo e non agli articoli. Ciò significa che non ha eseguito il processo per ottenere elementi all'interno del metodo. Un modo molto efficiente per ottenere dati solo quando necessario. L'implementazione effettiva del rendimento può essere vista in ORM come Entity Framework, NHibernate ecc.


-3

Sta cercando di introdurre un po 'di Ruby Goodness :)
Concetto: questo è un esempio di Ruby Code che stampa ogni elemento dell'array

 rubyArray = [1,2,3,4,5,6,7,8,9,10]
    rubyArray.each{|x| 
        puts x   # do whatever with x
    }

L'implementazione di ciascun metodo dell'array fornisce il controllo al chiamante ('put x') con ogni elemento dell'array presentato in modo ordinato come x. Il chiamante può quindi fare tutto ciò che deve fare con x.

Tuttavia .Net non va fino in fondo qui. C # sembra avere accoppiato la resa con IEnumerable, in un certo modo costringendoti a scrivere un ciclo foreach nel chiamante come visto nella risposta di Mendelt. Un po 'meno elegante.

//calling code
foreach(int i in obCustomClass.Each())
{
    Console.WriteLine(i.ToString());
}

// CustomClass implementation
private int[] data = {1,2,3,4,5,6,7,8,9,10};
public IEnumerable<int> Each()
{
   for(int iLooper=0; iLooper<data.Length; ++iLooper)
        yield return data[iLooper]; 
}

7
-1 Questa risposta non suona bene per me. Sì, C # yieldè accoppiato IEnumerablee C # non ha il concetto Ruby di "blocco". Ma C # ha lambda, che potrebbe consentire l'implementazione di un ForEachmetodo, molto simile a quello di Ruby each. Ciò non significa che sarebbe una buona idea farlo , però.
rsenna,

Meglio ancora: public IEnumerable <int> Each () {int index = 0; dati di rendimento di rendimento [indice ++]; }
ata il
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.