Spiegazione dell'algoritmo LINQ Aggregate


722

Questo potrebbe sembrare zoppo, ma non sono stato in grado di trovare una spiegazione davvero buona di Aggregate.

Bene significa breve, descrittivo, completo con un piccolo esempio chiaro.

Risposte:


1015

La definizione più semplice da comprendere Aggregateè che esegue un'operazione su ciascun elemento dell'elenco tenendo conto delle operazioni precedenti. Vale a dire che esegue l'azione sul primo e sul secondo elemento e porta avanti il ​​risultato. Quindi opera sul risultato precedente e sul terzo elemento e prosegue. eccetera.

Esempio 1. Sommare i numeri

var nums = new[]{1,2,3,4};
var sum = nums.Aggregate( (a,b) => a + b);
Console.WriteLine(sum); // output: 10 (1+2+3+4)

Questo aggiunge 1e 2da fare 3. Quindi aggiunge 3(risultato del precedente) e 3(elemento successivo in sequenza) da creare 6. Quindi aggiunge 6e 4da fare 10.

Esempio 2. Creare un CSV da una matrice di stringhe

var chars = new []{"a","b","c", "d"};
var csv = chars.Aggregate( (a,b) => a + ',' + b);
Console.WriteLine(csv); // Output a,b,c,d

Funziona più o meno allo stesso modo. Concatenare auna virgola e bdi fare a,b. Quindi concatena a,b con una virgola e cper creare a,b,c. e così via.

Esempio 3. Moltiplicare i numeri usando un seme

Per completezza, c'è un sovraccarico di Aggregatecui prende un valore seed.

var multipliers = new []{10,20,30,40};
var multiplied = multipliers.Aggregate(5, (a,b) => a * b);
Console.WriteLine(multiplied); //Output 1200000 ((((5*10)*20)*30)*40)

Proprio come negli esempi sopra, questo inizia con un valore di 5e lo moltiplica per il primo elemento della sequenza che 10dà un risultato di 50. Questo risultato viene portato avanti e moltiplicato per il numero successivo nella sequenza 20per dare un risultato di 1000. Questo continua attraverso i restanti 2 elementi della sequenza.

Esempi live: http://rextester.com/ZXZ64749
Documenti: http://msdn.microsoft.com/en-us/library/bb548651.aspx


appendice

L'esempio 2, sopra, usa la concatenazione di stringhe per creare un elenco di valori separati da una virgola. Questo è un modo semplicistico per spiegare l'uso di Aggregatequale era l'intenzione di questa risposta. Tuttavia, se si utilizza questa tecnica per creare effettivamente una grande quantità di dati separati da virgola, sarebbe più appropriato utilizzare a StringBuilder, e questo è del tutto compatibile con l' Aggregateutilizzo del sovraccarico seeded per avviare il file StringBuilder.

var chars = new []{"a","b","c", "d"};
var csv = chars.Aggregate(new StringBuilder(), (a,b) => {
    if(a.Length>0)
        a.Append(",");
    a.Append(b);
    return a;
});
Console.WriteLine(csv);

Esempio aggiornato: http://rextester.com/YZCVXV6464


11
Un'altra spiegazione per la prima descrizione è che la funzione fornita combina sempre i primi due membri fino a quando l'array viene ridotto a un elemento. Così [1,2,3,4]sarà [3,3,4]allora [6,4]e alla fine [10]. Ma invece di restituire una matrice di un singolo valore ottieni semplicemente il valore stesso.
David Raab,

2
Posso interrompere / uscire anticipatamente da una funzione di aggregazione? Ad esempio, chars.Aggregate ((a, b) => {if (a == 'a') interrompe l'intero aggregato restituisce a + ',' + b})
Jeff Tian,

13
@JeffTian - Suggerirei di concatenare un TakeWhilequindi un Aggregate- questo è il dovere delle estensioni enumerabili - sono facilmente concatenabili. Quindi alla fine TakeWhile(a => a == 'a').Aggregate(....). Vedi questo esempio: rextester.com/WPRA60543
Jamiec,

2
Come sidenote sull'addendum, l'intero blocco potrebbe essere facilmente sostituito da var csv = string.Join(",", chars)(non c'è bisogno di aggregatori o costruttori di stringhe) - ma sì, so che il punto della risposta era di dare un esempio di utilizzo dell'aggregato, quindi è bello. Ma volevo ancora menzionare che non è raccomandato solo per unire le stringhe, c'è già un metodo dedicato per questo ....
T_D

2
un altro uso comune (finora l'unico che ho mai visto nel codice di produzione) è quello di ottenere articoli min o max comevar biggestAccount = Accounts.Aggregate((a1, a2) => a1.Amount >= a2.Amount ? a1 : a2);
Franck

133

Dipende in parte dal sovraccarico di cui stai parlando, ma l'idea di base è:

  • Inizia con un seme come "valore corrente"
  • Scorrere la sequenza. Per ogni valore nella sequenza:
    • Applicare una funzione specificata dall'utente da trasformare (currentValue, sequenceValue)in(nextValue)
    • Impostato currentValue = nextValue
  • Restituisci la finale currentValue

Puoi trovare utile il Aggregatepost nella mia serie Edulinq - include una descrizione più dettagliata (compresi i vari sovraccarichi) e le implementazioni.

Un semplice esempio sta usando Aggregatein alternativa a Count:

// 0 is the seed, and for each item, we effectively increment the current value.
// In this case we can ignore "item" itself.
int count = sequence.Aggregate(0, (current, item) => current + 1);

O forse sommando tutte le lunghezze delle stringhe in una sequenza di stringhe:

int total = sequence.Aggregate(0, (current, item) => current + item.Length);

Personalmente raramente trovo Aggregateutile - i metodi di aggregazione "su misura" di solito sono abbastanza buoni per me.


6
@Jon Esistono variazioni asincrone di aggregato che dividono gli elementi in un albero in modo che il lavoro possa essere suddiviso tra i core? Sembra che il design del metodo sia coerente con i concetti di "ridurre" o "piega", ma non so se lo stia facendo davvero sotto il cofano o semplicemente scorrendo l'elenco degli elementi.
AaronLS

@Jon: l'edulink sopra menzionato non funziona puoi reindirizzarmi sul link giusto. E puoi per favore essere più specifico sul termine funzioni di aggregazione "su misura" che hai usato nella tua risposta.
Koushik,

1
@Koushik: ho corretto il link nel post. Per funzioni di aggregazione "su misura" intendo cose come Max / Min / Count / Sum.
Jon Skeet,

62

L' aggregato super corto funziona come piega in Haskell / ML / F #.

Leggermente più lungo .Max (), .Min (), .Sum (), .Average () tutto scorre sugli elementi in una sequenza e li aggrega usando la rispettiva funzione di aggregazione. .Aggregate () è un aggregatore generalizzato in quanto consente allo sviluppatore di specificare lo stato iniziale (noto anche come seed) e la funzione aggregata.

So che hai chiesto una breve spiegazione, ma ho pensato che mentre gli altri davano un paio di risposte brevi, ho pensato che potresti essere interessato a uno leggermente più lungo

Versione lunga con codice Un modo per illustrare cosa potrebbe essere mostrare come implementare la Deviazione standard del campione una volta usando foreach e una volta usando .Aggregate. Nota: non ho dato priorità alle prestazioni qui, quindi ho ripetutamente ripetuto più volte la raccolta

Innanzitutto una funzione di supporto utilizzata per creare una somma di distanze quadratiche:

static double SumOfQuadraticDistance (double average, int value, double state)
{
    var diff = (value - average);
    return state + diff * diff;
}

Quindi campione Deviazione standard utilizzando ForEach:

static double SampleStandardDeviation_ForEach (
    this IEnumerable<int> ints)
{
    var length = ints.Count ();
    if (length < 2)
    {
        return 0.0;
    }

    const double seed = 0.0;
    var average = ints.Average ();

    var state = seed;
    foreach (var value in ints)
    {
        state = SumOfQuadraticDistance (average, value, state);
    }
    var sumOfQuadraticDistance = state;

    return Math.Sqrt (sumOfQuadraticDistance / (length - 1));
}

Quindi una volta usando .Aggregate:

static double SampleStandardDeviation_Aggregate (
    this IEnumerable<int> ints)
{
    var length = ints.Count ();
    if (length < 2)
    {
        return 0.0;
    }

    const double seed = 0.0;
    var average = ints.Average ();

    var sumOfQuadraticDistance = ints
        .Aggregate (
            seed,
            (state, value) => SumOfQuadraticDistance (average, value, state)
            );

    return Math.Sqrt (sumOfQuadraticDistance / (length - 1));
}

Si noti che queste funzioni sono identiche ad eccezione di come viene calcolato sumOfQuadraticDistance:

var state = seed;
foreach (var value in ints)
{
    state = SumOfQuadraticDistance (average, value, state);
}
var sumOfQuadraticDistance = state;

Contro:

var sumOfQuadraticDistance = ints
    .Aggregate (
        seed,
        (state, value) => SumOfQuadraticDistance (average, value, state)
        );

Quindi, ciò che fa .Aggregate è che incapsula questo modello di aggregatore e mi aspetto che l'implementazione di .Aggregate sia simile a questa:

public static TAggregate Aggregate<TAggregate, TValue> (
    this IEnumerable<TValue> values,
    TAggregate seed,
    Func<TAggregate, TValue, TAggregate> aggregator
    )
{
    var state = seed;

    foreach (var value in values)
    {
        state = aggregator (state, value);
    }

    return state;
}

L'uso delle funzioni di deviazione standard sarebbe simile al seguente:

var ints = new[] {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
var average = ints.Average ();
var sampleStandardDeviation = ints.SampleStandardDeviation_Aggregate ();
var sampleStandardDeviation2 = ints.SampleStandardDeviation_ForEach ();

Console.WriteLine (average);
Console.WriteLine (sampleStandardDeviation);
Console.WriteLine (sampleStandardDeviation2);

A parer mio

Quindi .Aggregate aiuta la leggibilità? In generale, adoro LINQ perché penso .Where, .Select, .OrderBy e così via aiuta notevolmente la leggibilità (se si evita il .Select gerarchico incorporato). L'aggregato deve essere in Linq per motivi di completezza, ma personalmente non sono così convinto che. Aggregate aggiunge leggibilità rispetto a un predicatore ben scritto.


+1 Eccellente! Ma i metodi di estensione SampleStandardDeviation_Aggregate()e SampleStandardDeviation_ForEach()non possono essere private(per impostazione predefinita in assenza di un qualificatore di accesso), quindi avrebbero dovuto essere maturati da uno dei due publico internal, mi sembra
Fulproof

Cordiali saluti: Se ricordo bene i metodi di estensione nel mio esempio faceva parte della stessa classe che li usava ==> lavori privati ​​in questo caso.
Solo un altro metaprogramma il

39

Un'immagine vale più di mille parole

Promemoria:
Func<X, Y, R>è una funzione con due input di tipo Xe Y, che restituisce un risultato di tipo R.

Enumerable.Aggregate ha tre sovraccarichi:


Sovraccarico 1:

A Aggregate<A>(IEnumerable<A> a, Func<A, A, A> f)

Aggregate1

Esempio:

new[]{1,2,3,4}.Aggregate((x, y) => x + y);  // 10


Questo sovraccarico è semplice, ma presenta le seguenti limitazioni:

  • la sequenza deve contenere almeno un elemento,
    altrimenti la funzione genererà un InvalidOperationException.
  • elementi e risultato devono essere dello stesso tipo.



Sovraccarico 2:

B Aggregate<A, B>(IEnumerable<A> a, B bIn, Func<B, A, B> f)

Aggregate2

Esempio:

var hayStack = new[] {"straw", "needle", "straw", "straw", "needle"};
var nNeedles = hayStack.Aggregate(0, (n, e) => e == "needle" ? n+1 : n);  // 2


Questo sovraccarico è più generale:

  • deve essere fornito un valore seed ( bIn).
  • la raccolta può essere vuota,
    in questo caso la funzione produrrà il valore seme come risultato.
  • gli elementi e il risultato possono avere tipi diversi.



Sovraccarico 3:

C Aggregate<A,B,C>(IEnumerable<A> a, B bIn, Func<B,A,B> f, Func<B,C> f2)


Il terzo sovraccarico non è molto utile IMO.
Lo stesso può essere scritto in modo più succinto usando il sovraccarico 2 seguito da una funzione che trasforma il suo risultato.


Le illustrazioni sono adattate da questo eccellente post sul blog .


Questa sarebbe un'ottima risposta .... su una domanda su Haskel. Ma non c'è sovraccarico di Aggegatein .net che richiede a Func<T, T, T>.
Jamiec,

4
Sì, c'è . Lo usi nella tua risposta!
3dGrabber,

1
Voto perché descrivi attentamente cosa succede quando la sequenza è vuota. Sia N il numero di elementi nella sorgente. Osserviamo che il sovraccarico che non richiede a seed, applica la funzione accumulatore N -1 volte; mentre gli altri sovraccarichi (che non diano seed) applicare la funzione di accumulatore N volte.
Jeppe Stig Nielsen

17

L'aggregato viene sostanzialmente utilizzato per raggruppare o sommare i dati.

Secondo MSDN "Funzione aggregata Applica una funzione accumulatore su una sequenza".

Esempio 1: aggiungere tutti i numeri in un array.

int[] numbers = new int[] { 1,2,3,4,5 };
int aggregatedValue = numbers.Aggregate((total, nextValue) => total + nextValue);

* importante: il valore aggregato iniziale per impostazione predefinita è l'elemento 1 nella sequenza della raccolta. vale a dire: il valore iniziale della variabile totale sarà 1 per impostazione predefinita.

spiegazione variabile

totale: conterrà il valore di somma (valore aggregato) restituito dalla funzione

nextValue: è il valore successivo nella sequenza di array. Questo valore viene quindi aggiunto al valore aggregato, cioè totale.

Esempio 2: aggiungere tutti gli elementi in un array. Impostare anche il valore iniziale dell'accumulatore per iniziare ad aggiungere con da 10.

int[] numbers = new int[] { 1,2,3,4,5 };
int aggregatedValue = numbers.Aggregate(10, (total, nextValue) => total + nextValue);

spiegazione degli argomenti:

il primo argomento è l'iniziale (valore iniziale, ovvero valore seme) che verrà utilizzato per iniziare l'aggiunta con il valore successivo nella matrice.

il secondo argomento è una funzione che è una funzione che richiede 2 int.

1.totale: sarà lo stesso di prima del valore di riepilogo (valore aggregato) restituito dalla funzione dopo il calcolo.

2.nextValue:: è il valore successivo nella sequenza di array. Questo valore viene quindi aggiunto al valore aggregato, cioè totale.

Anche il debug di questo codice ti darà una migliore comprensione di come funzionano gli aggregati.


7

Ho imparato molto dalla risposta di Jamiec .

Se l'unica necessità è generare una stringa CSV, puoi provare questo.

var csv3 = string.Join(",",chars);

Ecco un test con 1 milione di stringhe

0.28 seconds = Aggregate w/ String Builder 
0.30 seconds = String.Join 

Il codice sorgente è qui


Quando ho eseguito lo stesso codice in dotnetfiddle.net come indicato nel collegamento, ho ricevuto "Errore irreversibile: limite di utilizzo della memoria superato" per "string.Join" ma Aggregate ha sempre funzionato come previsto. Quindi credo che questo non sia raccomandato per usare String.Join
Manish Jain il

Strano? Quando ho commentato il primo cronometro che era per Aggregate; quindi non visualizzo alcun "Errore irreversibile: limite di utilizzo della memoria superato". Spiega per favore! Link: dotnetfiddle.net/6YyumS
Manish Jain,

dotnetfiddle.net ha un limite di memoria quando si raggiunge l'arresto dell'esecuzione. se sposti il ​​codice aggregato prima del codice String.Join, potresti ricevere un errore per aggregato.
Rm558

7

Oltre a tutte le ottime risposte qui, l'ho anche usato per guidare un oggetto attraverso una serie di passaggi di trasformazione.

Se una trasformazione è implementata come a Func<T,T>, è possibile aggiungere diverse trasformazioni a List<Func<T,T>>e utilizzare Aggregateper percorrere un'istanza di Togni passaggio.

Un esempio più concreto

Volete prendere un stringvalore e guidarlo attraverso una serie di trasformazioni di testo che potrebbero essere costruite programmaticamente.

var transformationPipeLine = new List<Func<string, string>>();
transformationPipeLine.Add((input) => input.Trim());
transformationPipeLine.Add((input) => input.Substring(1));
transformationPipeLine.Add((input) => input.Substring(0, input.Length - 1));
transformationPipeLine.Add((input) => input.ToUpper());

var text = "    cat   ";
var output = transformationPipeLine.Aggregate(text, (input, transform)=> transform(input));
Console.WriteLine(output);

Questo creerà una catena di trasformazioni: Rimuovi spazi iniziali e finali -> rimuovi il primo carattere -> rimuovi l'ultimo carattere -> converti in maiuscolo. I passaggi di questa catena possono essere aggiunti, rimossi o riordinati in base alle esigenze, per creare qualsiasi tipo di pipeline di trasformazione richiesta.

Il risultato finale di questa specifica pipeline è che " cat "diventa "A".


Questo può diventare molto potente quando ti rendi conto che Tpuò essere qualsiasi cosa . Questo potrebbe essere usato per trasformazioni di immagini, come i filtri, usando BitMapcome esempio;


4

Definizione

Il metodo aggregato è un metodo di estensione per raccolte generiche. Il metodo aggregato applica una funzione a ciascun elemento di una raccolta. Non solo applica una funzione, ma prende il suo risultato come valore iniziale per la successiva iterazione. Di conseguenza, otterremo un valore calcolato (min, max, avg o altro valore statistico) da una raccolta.

Pertanto, il metodo aggregato è una forma di implementazione sicura di una funzione ricorsiva.

Sicuro , perché la ricorsione ripeterà ogni elemento di una raccolta e non possiamo ottenere alcuna sospensione del ciclo infinito da una condizione di uscita errata. Ricorsivo , poiché il risultato della funzione corrente viene utilizzato come parametro per la chiamata di funzione successiva.

Sintassi:

collection.Aggregate(seed, func, resultSelector);
  • seed - valore iniziale di default;
  • func - la nostra funzione ricorsiva. Può essere un'espressione lambda, un delegato di Func o un tipo di funzione TF (T risultato, T nextValue);
  • resultSelector - può essere una funzione come func o un'espressione per calcolare, trasformare, cambiare, convertire il risultato finale.

Come funziona:

var nums = new[]{1, 2};
var result = nums.Aggregate(1, (result, n) => result + n); //result = (1 + 1) + 2 = 4
var result2 = nums.Aggregate(0, (result, n) => result + n, response => (decimal)response/2.0); //result2 = ((0 + 1) + 2)*1.0/2.0 = 3*1.0/2.0 = 3.0/2.0 = 1.5

Uso pratico:

  1. Trova Factorial da un numero n:

int n = 7;
var numbers = Enumerable.Range(1, n);
var factorial = numbers.Aggregate((result, x) => result * x);

che sta facendo la stessa cosa di questa funzione:

public static int Factorial(int n)
{
   if (n < 1) return 1;

   return n * Factorial(n - 1);
}
  1. Aggregate () è uno dei metodi di estensione LINQ più potenti, come Select () e Where (). Possiamo usarlo per sostituire Sum (), Min (). Funzionalità Max (), Avg () o per modificarla implementando il contesto di aggiunta:
    var numbers = new[]{3, 2, 6, 4, 9, 5, 7};
    var avg = numbers.Aggregate(0.0, (result, x) => result + x, response => (double)response/(double)numbers.Count());
    var min = numbers.Aggregate((result, x) => (result < x)? result: x);
  1. Utilizzo più complesso dei metodi di estensione:
    var path = @“c:\path-to-folder”;

    string[] txtFiles = Directory.GetFiles(path).Where(f => f.EndsWith(“.txt”)).ToArray<string>();
    var output = txtFiles.Select(f => File.ReadAllText(f, Encoding.Default)).Aggregate<string>((result, content) => result + content);

    File.WriteAllText(path + summary.txt”, output, Encoding.Default);

    Console.WriteLine(“Text files merged into: {0}”, output); //or other log info

Prima risposta abbastanza buona. Molto bene! Peccato che sia una domanda così vecchia o avresti avuto molti voti positivi
Jamiec,

1

Questa è una spiegazione sull'uso di Aggregateun'API fluente come Linq Sorting.

var list = new List<Student>();
var sorted = list
    .OrderBy(s => s.LastName)
    .ThenBy(s => s.FirstName)
    .ThenBy(s => s.Age)
    .ThenBy(s => s.Grading)
    .ThenBy(s => s.TotalCourses);

e vediamo che vogliamo implementare una funzione di ordinamento che accetta una serie di campi, questo è molto semplice usando al Aggregateposto di un for-loop, come questo:

public static IOrderedEnumerable<Student> MySort(
    this List<Student> list,
    params Func<Student, object>[] fields)
{
    var firstField = fields.First();
    var otherFields = fields.Skip(1);

    var init = list.OrderBy(firstField);
    return otherFields.Skip(1).Aggregate(init, (resultList, current) => resultList.ThenBy(current));
}

E possiamo usarlo in questo modo:

var sorted = list.MySort(
    s => s.LastName,
    s => s.FirstName,
    s => s.Age,
    s => s.Grading,
    s => s.TotalCourses);

1

Ognuno ha dato la sua spiegazione. La mia spiegazione è così.

Il metodo aggregato applica una funzione a ciascun elemento di una raccolta. Ad esempio, abbiamo la raccolta {6, 2, 8, 3} e la funzione Aggiungi (operatore +) fa (((6 + 2) +8) +3) e restituisce 19

var numbers = new List<int> { 6, 2, 8, 3 };
int sum = numbers.Aggregate(func: (result, item) => result + item);
// sum: (((6+2)+8)+3) = 19

In questo esempio viene passato il metodo denominato Aggiungi anziché l'espressione lambda.

var numbers = new List<int> { 6, 2, 8, 3 };
int sum = numbers.Aggregate(func: Add);
// sum: (((6+2)+8)+3) = 19

private static int Add(int x, int y) { return x + y; }

0

Una definizione breve ed essenziale potrebbe essere questa: il metodo di estensione Linq Aggregate consente di dichiarare una sorta di funzione ricorsiva applicata agli elementi di un elenco, i cui operandi sono due: gli elementi nell'ordine in cui sono presenti nell'elenco, un elemento alla volta e il risultato della precedente iterazione ricorsiva o niente se non ancora ricorsione.

In questo modo è possibile calcolare il fattoriale dei numeri o concatenare stringhe.


0

Aggregato utilizzato per sommare le colonne in un array intero multidimensionale

        int[][] nonMagicSquare =
        {
            new int[] {  3,  1,  7,  8 },
            new int[] {  2,  4, 16,  5 },
            new int[] { 11,  6, 12, 15 },
            new int[] {  9, 13, 10, 14 }
        };

        IEnumerable<int> rowSums = nonMagicSquare
            .Select(row => row.Sum());
        IEnumerable<int> colSums = nonMagicSquare
            .Aggregate(
                (priorSums, currentRow) =>
                    priorSums.Select((priorSum, index) => priorSum + currentRow[index]).ToArray()
                );

Seleziona con indice viene utilizzato all'interno della funzione di aggregazione per sommare le colonne corrispondenti e restituire un nuovo array; {3 + 2 = 5, 1 + 4 = 5, 7 + 16 = 23, 8 + 5 = 13}.

        Console.WriteLine("rowSums: " + string.Join(", ", rowSums)); // rowSums: 19, 27, 44, 46
        Console.WriteLine("colSums: " + string.Join(", ", colSums)); // colSums: 25, 24, 45, 42

Ma contare il numero di tracce in un array booleano è più difficile poiché il tipo accumulato (int) differisce dal tipo sorgente (bool); qui è necessario un seme per utilizzare il secondo sovraccarico.

        bool[][] booleanTable =
        {
            new bool[] { true, true, true, false },
            new bool[] { false, false, false, true },
            new bool[] { true, false, false, true },
            new bool[] { true, true, false, false }
        };

        IEnumerable<int> rowCounts = booleanTable
            .Select(row => row.Select(value => value ? 1 : 0).Sum());
        IEnumerable<int> seed = new int[booleanTable.First().Length];
        IEnumerable<int> colCounts = booleanTable
            .Aggregate(seed,
                (priorSums, currentRow) =>
                    priorSums.Select((priorSum, index) => priorSum + (currentRow[index] ? 1 : 0)).ToArray()
                );

        Console.WriteLine("rowCounts: " + string.Join(", ", rowCounts)); // rowCounts: 3, 1, 2, 2
        Console.WriteLine("colCounts: " + string.Join(", ", colCounts)); // colCounts: 3, 2, 1, 2
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.