LINQ equivalente de foreach para IEnumerable <T>


716

Me gustaría hacer el equivalente de lo siguiente en LINQ, pero no puedo entender cómo:

IEnumerable<Item> items = GetItems();
items.ForEach(i => i.DoStuff());

¿Cuál es la sintaxis real?


99
Considere "foreach" vs. "ForEach" de Eric Lippert. Proporciona una buena justificación para no usar / suministrar a ForEach().

44
@pst: solía pegar ciegamente estos métodos de extensión en mis proyectos. Gracias por ese articulo.
Robin Maben

2
Hay MoreLINQ que tiene una ForEachextensión .
Uwe Keim

2
Aunque no es muy sexy, encaja en una línea sin crear una copia a través de ToList -foreach (var i in items) i.Dostuff();
James Westgate

1
¡Algunos de estos enlaces están muertos! ¿Cuál fue el artículo que se votó tanto?
Warren

Respuestas:


863

No hay extensión ForEach para IEnumerable; sólo para List<T>. Entonces podrías hacer

items.ToList().ForEach(i => i.DoStuff());

Alternativamente, escriba su propio método de extensión ForEach:

public static void ForEach<T>(this IEnumerable<T> enumeration, Action<T> action)
{
    foreach(T item in enumeration)
    {
        action(item);
    }
}

213
Tenga cuidado con ToList (), porque ToList () crea una copia de la secuencia, lo que podría causar problemas de rendimiento y memoria.
decasteljau

15
Hay algunas razones por las que ".Foreach ()" será mejor. 1. Multi-threading, posible por PLINQ. 2. Excepciones, es posible que desee recopilar todas las excepciones en el bucle y lanzarlo de una vez; y son códigos de ruido.
Dennis C

12
PLINQ usa ForAll, no ForEach, por lo que no usaría ForEach.
user7116

99
Debe devolver un IENumerable<T>en el método de extensión. Me gusta:public static IEnumerable<T> ForAll<T>(this IEnumerable<T> numeration, Action<T> action) { foreach (T item in enumeration) { action(item); } return enumeration; }
Kees C. Bakker

22
Tenga en cuenta que en el primer caso, el desarrollador 1) guarda casi sin escribir y 2) asigna memoria innecesariamente para almacenar toda la secuencia como una lista antes de tirarla. Piensa antes de ti LINQ
Mark Sowul

364

Fredrik ha proporcionado la solución, pero puede valer la pena considerar por qué esto no está en el marco para empezar. Creo que la idea es que los operadores de consulta LINQ no deberían tener efectos secundarios, y que se ajusten a una forma razonablemente funcional de ver el mundo. Claramente, ForEach es exactamente lo contrario: una construcción puramente basada en efectos secundarios.

Eso no quiere decir que sea algo malo, solo pensar en las razones filosóficas detrás de la decisión.


44
Y sin embargo, F # tiene Seq.iter
Stephen Swensen

66
Sin embargo, hay una cosa muy útil que las funciones de LINQ generalmente proporcionan que foreach () no: la función anónima tomada por las funciones de Linq también puede tomar un índice como parámetro, mientras que con foreach debe reconstruir el clásico para (int i .. ) construir. Y los lenguajes funcionales deben permitir la iteración que tenga efectos secundarios; de lo contrario, nunca podría hacer ni un ápice de trabajo con ellos :) Me gustaría señalar que el método funcional típico sería devolver el enumerable original sin cambios.
Walt W

3
@Walt: Todavía no estoy seguro de estar de acuerdo, en parte porque un enfoque verdaderamente funcional típico no usaría foreach como este ... usaría un enfoque funcional más como LINQ, creando una nueva secuencia.
Jon Skeet

12
@Stephen Swensen: sí, F # tiene Seq.iter, pero F # también tiene estructuras de datos inmutables. cuando se utiliza Seq.iter en estos, la Seq original no se modifica; Se devuelve una nueva Seq con los elementos de efecto lateral.
Anders

77
@Anders: Seq.iterno devuelve nada, y mucho menos una nueva secuencia con elementos de efectos secundarios. Realmente se trata solo de los efectos secundarios. Puede estar pensando Seq.map, porque Seq.iteres precisamente equivalente a un ForEachmétodo de extensión en IEnumerabe<_>.
Joel Mueller

39

Actualización 17/07/2012: Aparentemente a partir de C # 5.0, el comportamiento foreachdescrito a continuación ha cambiado y " el uso de una foreachvariable de iteración en una expresión lambda anidada ya no produce resultados inesperados " . Esta respuesta no se aplica a C # ≥ 5.0 .

@John Skeet y todos los que prefieren la palabra clave foreach.

El problema con "foreach" en C # anterior a 5.0 , es que es inconsistente con la forma en que el equivalente "para la comprensión" funciona en otros idiomas, y con cómo esperaría que funcionara (la opinión personal aquí se menciona solo porque otros han mencionado su opinión sobre la legibilidad). Consulte todas las preguntas sobre " Acceso al cierre modificado ", así como " Cierre sobre la variable de bucle considerado dañino ". Esto es solo "dañino" debido a la forma en que se implementa "foreach" en C #.

Tome los siguientes ejemplos utilizando el método de extensión funcionalmente equivalente al de la respuesta de @Fredrik Kalseth.

public static class Enumerables
{
    public static void ForEach<T>(this IEnumerable<T> @this, Action<T> action)
    {
        foreach (T item in @this)
        {
            action(item);
        }
    }
}

Disculpas por el ejemplo excesivamente inventado. Solo estoy usando Observable porque no es del todo descabellado hacer algo como esto. Obviamente, hay mejores formas de crear este observable, solo estoy tratando de demostrar un punto. Por lo general, el código suscrito al observable se ejecuta de forma asíncrona y potencialmente en otro hilo. Si usa "foreach", esto podría producir resultados muy extraños y potencialmente no deterministas.

La siguiente prueba con el método de extensión "ForEach" pasa:

[Test]
public void ForEachExtensionWin()
{
    //Yes, I know there is an Observable.Range.
    var values = Enumerable.Range(0, 10);

    var observable = Observable.Create<Func<int>>(source =>
                            {
                                values.ForEach(value => 
                                    source.OnNext(() => value));

                                source.OnCompleted();
                                return () => { };
                            });

    //Simulate subscribing and evaluating Funcs
    var evaluatedObservable = observable.ToEnumerable().Select(func => func()).ToList();

    //Win
    Assert.That(evaluatedObservable, 
        Is.EquivalentTo(values.ToList()));
}

Lo siguiente falla con el error:

Esperado: equivalente a <0, 1, 2, 3, 4, 5, 6, 7, 8, 9> Pero fue: <9, 9, 9, 9, 9, 9, 9, 9, 9, 9>

[Test]
public void ForEachKeywordFail()
{
    //Yes, I know there is an Observable.Range.
    var values = Enumerable.Range(0, 10);

    var observable = Observable.Create<Func<int>>(source =>
                            {
                                foreach (var value in values)
                                {
                                    //If you have resharper, notice the warning
                                    source.OnNext(() => value);
                                }
                                source.OnCompleted();
                                return () => { };
                            });

    //Simulate subscribing and evaluating Funcs
    var evaluatedObservable = observable.ToEnumerable().Select(func => func()).ToList();

    //Fail
    Assert.That(evaluatedObservable, 
        Is.EquivalentTo(values.ToList()));
}

¿Qué dice la advertencia del resharper?
reggaeguitar

1
@reggaeguitar No he usado C # en 7 u 8 años, desde antes de que se lanzara C # 5.0, lo que cambió el comportamiento descrito aquí. En el momento en que dio esta advertencia stackoverflow.com/questions/1688465/… . Sin embargo, dudo que todavía advierta sobre esto.
Drstevens

34

Puede usar la FirstOrDefault()extensión, que está disponible para IEnumerable<T>. Al regresar falsedel predicado, se ejecutará para cada elemento, pero no le importará que en realidad no encuentre una coincidencia. Esto evitará los ToList()gastos generales.

IEnumerable<Item> items = GetItems();
items.FirstOrDefault(i => { i.DoStuff(); return false; });

11
Estoy de acuerdo también Puede ser un pequeño truco inteligente, pero a primera vista, este código no está claro qué está haciendo, ciertamente en comparación con un bucle foreach estándar.
Connell

26
Tuve que votar en contra porque, aunque técnicamente correcto, tu yo futuro y todos tus sucesores te odiarán para siempre porque en lugar de foreach(Item i in GetItems()) { i.DoStuff();}tomar más personajes y hacerlo extremadamente confuso
Mark Sowul

14
Ok, pero probablemente un truco más legible:items.All(i => { i.DoStuff(); return true; }
nawfal

55
Sé que esta es una publicación bastante antigua, pero también tuve que rechazarla. Estoy de acuerdo en que es un truco inteligente, pero no es mantenible. A primera vista, parece que está haciendo algo solo con el primer elemento de la lista. Esto podría causar fácilmente más errores en el futuro si los codificadores no entienden cómo funciona y qué se supone que está haciendo.
Lee

44
Esta es la programación de efectos secundarios y en mi humilde opinión se desaconseja.
Anish

21

Tomé el método de Fredrik y modifiqué el tipo de retorno.

De esta manera, el método admite la ejecución diferida como otros métodos LINQ.

EDITAR: Si esto no estaba claro, cualquier uso de este método debe terminar con ToList () o cualquier otra forma de forzar el método para que funcione en el enumerable completo. De lo contrario, la acción no se realizaría.

public static IEnumerable<T> ForEach<T>(this IEnumerable<T> enumeration, Action<T> action)
{
    foreach (T item in enumeration)
    {
        action(item);
        yield return item;
    }
}

Y aquí está la prueba para ayudar a verlo:

[Test]
public void TestDefferedExecutionOfIEnumerableForEach()
{
    IEnumerable<char> enumerable = new[] {'a', 'b', 'c'};

    var sb = new StringBuilder();

    enumerable
        .ForEach(c => sb.Append("1"))
        .ForEach(c => sb.Append("2"))
        .ToList();

    Assert.That(sb.ToString(), Is.EqualTo("121212"));
}

Si elimina ToList () al final, verá que la prueba falla ya que StringBuilder contiene una cadena vacía. Esto se debe a que ningún método obligó a ForEach a enumerar.


Su implementación alternativa de ForEaches interesante, sin embargo, no coincide con el comportamiento de List.ForEach, cuya firma es public void ForEach(Action<T> action). Tampoco coincide con el comportamiento de la Observable.ForEachextensión IObservable<T>, cuya firma es public static void ForEach<TSource>(this IObservable<TSource> source, Action<TSource> onNext). FWIW, el ForEachequivalente en las colecciones de Scala, incluso las que son perezosas, también tienen un tipo de retorno que es equivalente a vacío en C #.
Drstevens

Esta es la mejor respuesta: ejecución diferida + api fluido.
Alexander Danilov

3
Esto funciona exactamente igual que llamar Selectcon ToList. El propósito de ForEaches no tener que llamar ToList. Debe ejecutarse de inmediato.
t3chb0t

3
¿Cuál sería el beneficio de tener este "ForEach", cuya ejecución se aplaza, frente a una que se ejecuta de inmediato? El aplazamiento (que es inconveniente en cómo se usará ForEach) no mejora el hecho de que ForEach solo realiza un trabajo útil si la Acción tiene efectos secundarios [la queja habitual que se hace contra un operador de Linq]. Entonces, ¿cómo hace este cambio ayuda? Además, el "ToList ()" agregado para forzarlo a ejecutarse no tiene ningún propósito útil. Es un accidente esperando a suceder. El punto de "ForEach" son sus efectos secundarios. Si QUIERES una enumeración devuelta, SELECT tiene más sentido.
ToolmakerSteve

20

Mantenga sus efectos secundarios fuera de mi IEnumerable

Me gustaría hacer el equivalente de lo siguiente en LINQ, pero no puedo entender cómo:

Como otros han señalado aquí y en el extranjero IEnumerable, se espera que LINQ y los métodos estén libres de efectos secundarios.

¿Realmente quiere "hacer algo" a cada elemento en el IEnumerable? Entonces foreaches la mejor opción. La gente no se sorprende cuando ocurren efectos secundarios aquí.

foreach (var i in items) i.DoStuff();

Apuesto a que no quieres un efecto secundario

Sin embargo, en mi experiencia, generalmente no se requieren efectos secundarios. La mayoría de las veces hay una simple consulta LINQ esperando ser descubierta acompañada de una respuesta de StackOverflow.com de Jon Skeet, Eric Lippert o Marc Gravell que explica cómo hacer lo que quiere.

Algunos ejemplos

Si en realidad solo está agregando (acumulando) algún valor, entonces debe considerar el Aggregatemétodo de extensión.

items.Aggregate(initial, (acc, x) => ComputeAccumulatedValue(acc, x));

Quizás desee crear uno nuevo a IEnumerablepartir de los valores existentes.

items.Select(x => Transform(x));

O tal vez desee crear una tabla de búsqueda:

items.ToLookup(x, x => GetTheKey(x))

La lista (juego de palabras no del todo previsto) de posibilidades sigue y sigue.


77
Ah, entonces Seleccionar es Mapa y Agregar es Reducir.
Darrel Lee

17

Si desea actuar como las listas de enumeración, debe ceder cada elemento.

public static class EnumerableExtensions
{
    public static IEnumerable<T> ForEach<T>(this IEnumerable<T> enumeration, Action<T> action)
    {
        foreach (var item in enumeration)
        {
            action(item);
            yield return item;
        }
    }
}

Esto no es realmente ForEach cuando devuelve el artículo, es más como un Tap.
EluciusFTW

10

Hay una versión experimental de Microsoft de Interactive Extensions para LINQ (también en NuGet , consulte el perfil de RxTeams para obtener más enlaces). El video del Canal 9 lo explica bien.

Sus documentos solo se proporcionan en formato XML. He ejecutado esta documentación en Sandcastle para permitir que esté en un formato más legible. Descomprima el archivo de documentos y busque index.html .

Entre muchas otras cosas, proporciona la implementación esperada de ForEach. Te permite escribir código como este:

int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8 };

numbers.ForEach(x => Console.WriteLine(x*x));

2
El enlace de trabajo a las extensiones interactivas: microsoft.com/download/en/details.aspx?id=27203
Maciek Świszczowski

8

Según PLINQ (disponible desde .Net 4.0), puede hacer un

IEnumerable<T>.AsParallel().ForAll() 

hacer un bucle foreach paralelo en un IEnumerable.


Mientras su acción sea segura, todo está bien. pero ¿qué sucederá si tiene que realizar una acción no segura para subprocesos? que puede causar un caos ...
shirbr510

2
Cierto. No es seguro para subprocesos, se usarán diferentes subprocesos de algún grupo de subprocesos ... Y necesito revisar mi respuesta. No hay ForEach () cuando lo intento ahora. Debe haber sido mi código que contiene una extensión con ForEach cuando reflexioné sobre esto.
Wolf5

6

El propósito de ForEach es causar efectos secundarios. IEnumerable es para la enumeración diferida de un conjunto.

Esta diferencia conceptual es bastante visible cuando la consideras.

SomeEnumerable.ForEach(item=>DataStore.Synchronize(item));

Esto no se ejecutará hasta que haga un "conteo" o una "Lista ()" o algo así. Claramente no es lo que se expresa.

Debe usar las extensiones IEnumerable para configurar cadenas de iteración, definiendo el contenido por sus respectivas fuentes y condiciones. Los árboles de expresión son poderosos y eficientes, pero debes aprender a apreciar su naturaleza. Y no solo para programar a su alrededor para salvar algunos caracteres que anulan la evaluación perezosa.


5

Mucha gente lo mencionó, pero tuve que escribirlo. ¿No es esto más claro / más legible?

IEnumerable<Item> items = GetItems();
foreach (var item in items) item.DoStuff();

Corto y simple (st).


Puede soltar toda la primera línea y usar GetItems directamente en foreach
tymtam el

3
Por supuesto. Está escrito así para mayor claridad. Para que quede claro qué GetItems()método devuelve.
Nenad

44
Ahora, cuando leo mi comentario, veo que parece que te estoy diciendo algo que no sabes. No quise decir eso. Lo que quise decir es que foreach (var item in GetItems()) item.DoStuff();es solo una belleza.
tymtam el

4

Ahora tenemos la opción de ...

        ParallelOptions parallelOptions = new ParallelOptions();
        parallelOptions.MaxDegreeOfParallelism = 4;
#if DEBUG
        parallelOptions.MaxDegreeOfParallelism = 1;
#endif
        Parallel.ForEach(bookIdList, parallelOptions, bookID => UpdateStockCount(bookID));

Por supuesto, esto abre una nueva lata de gusanos de hilo.

ps (Perdón por las fuentes, es lo que decidió el sistema)


4

Como ya señalan numerosas respuestas, usted mismo puede agregar fácilmente dicho método de extensión. Sin embargo, si no desea hacer eso, aunque no estoy al tanto de algo así en el BCL, todavía hay una opción en el Systemespacio de nombres, si ya tiene una referencia a la Extensión reactiva (y si no , Deberías):

using System.Reactive.Linq;

items.ToObservable().Subscribe(i => i.DoStuff());

Aunque los nombres de los métodos son un poco diferentes, el resultado final es exactamente lo que está buscando.


Parece que ese paquete está en desuso ahora
reggaeguitar

3

ForEach también se puede encadenar , solo vuelve a ponerlo en la pila después de la acción. mantener fluidez


Employees.ForEach(e=>e.Act_A)
         .ForEach(e=>e.Act_B)
         .ForEach(e=>e.Act_C);

Orders  //just for demo
    .ForEach(o=> o.EmailBuyer() )
    .ForEach(o=> o.ProcessBilling() )
    .ForEach(o=> o.ProcessShipping());


//conditional
Employees
    .ForEach(e=> {  if(e.Salary<1000) e.Raise(0.10);})
    .ForEach(e=> {  if(e.Age   >70  ) e.Retire();});

Una versión ansiosa de implementación.

public static IEnumerable<T> ForEach<T>(this IEnumerable<T> enu, Action<T> action)
{
    foreach (T item in enu) action(item);
    return enu; // make action Chainable/Fluent
}

Editar: una versión perezosa está utilizando rendimiento de rendimiento, como este .

public static IEnumerable<T> ForEachLazy<T>(this IEnumerable<T> enu, Action<T> action)
{
    foreach (var item in enu)
    {
        action(item);
        yield return item;
    }
}

La versión Lazy NECESITA materializarse, ToList () por ejemplo, de lo contrario, no pasa nada. vea abajo los excelentes comentarios de ToolmakerSteve.

IQueryable<Product> query = Products.Where(...);
query.ForEachLazy(t => t.Price = t.Price + 1.00)
    .ToList(); //without this line, below SubmitChanges() does nothing.
SubmitChanges();

Mantengo ForEach () y ForEachLazy () en mi biblioteca.


2
¿Has probado esto? Hay varios contextos en los que este método se comporta de manera intuitiva. Mejor seríaforeach(T item in enu) {action(item); yield return item;}
Taemyr

2
Esto dará como resultado múltiples enumeraciones
regisbsb

Interesante. Reflexionando sobre cuándo me gustaría hacer lo anterior, para una secuencia de 3 acciones, en lugar de foreach (T item in enu) { action1(item); action2(item); action3(item); }? Un solo bucle explícito. Supongo que si era importante hacer todas las acciones1 ANTES de comenzar a hacer las acciones2.
ToolmakerSteve

@ Rm558: su enfoque mediante el "rendimiento de rendimiento". Pro: solo enumera la secuencia original una vez, por lo que funciona correctamente con un rango más amplio de enumeraciones. Con: si olvida ".ToArray ()" al final, no pasa nada. Aunque el fracaso será obvio, no se pasará por alto durante mucho tiempo, por lo que no es un costo importante. No me "siente limpio", pero es la compensación correcta, si uno va por este camino (un operador ForEach).
ToolmakerSteve

1
Este código no es seguro desde el punto de vista transaccional. ¿Qué pasa si falla ProcessShipping()? ¿Envía un correo electrónico al comprador, carga su tarjeta de crédito, pero nunca le envía sus cosas? Ciertamente preguntando por problemas.
zmechanic

2

Esta abstracción de "enfoque funcional" se escapa a lo grande. Nada en el nivel del idioma previene los efectos secundarios. Siempre que pueda hacer que llame a su lambda / delegado para cada elemento en el contenedor, obtendrá el comportamiento "ForEach".

Aquí, por ejemplo, una forma de fusionar srcDictionary en destDictionary (si la clave ya existe, se sobrescribe)

Este es un truco y no debe utilizarse en ningún código de producción.

var b = srcDictionary.Select(
                             x=>
                                {
                                  destDictionary[x.Key] = x.Value;
                                  return true;
                                }
                             ).Count();

66
Si tiene que agregar ese descargo de responsabilidad, probablemente no debería publicarlo. Solo es mi opinión.
Andrew Grothe

2
¿Cómo demonios es esto mejor que foreach(var tuple in srcDictionary) { destDictionary[tuple.Key] = tuple.Value; }? Si no está en el código de producción, ¿dónde y por qué debería usar esto?
Mark Sowul

3
Esto solo para demostrar que es posible en principio. También sucede lo que OP solicitó, y claramente indiqué que no debería usarse en la producción, aún es curioso y tiene valor educativo.
Zar Shardan

2

Inspirado por Jon Skeet, he ampliado su solución con lo siguiente:

Método de extensión:

public static void Execute<TSource, TKey>(this IEnumerable<TSource> source, Action<TKey> applyBehavior, Func<TSource, TKey> keySelector)
{
    foreach (var item in source)
    {
        var target = keySelector(item);
        applyBehavior(target);
    }
}

Cliente:

var jobs = new List<Job>() 
    { 
        new Job { Id = "XAML Developer" }, 
        new Job { Id = "Assassin" }, 
        new Job { Id = "Narco Trafficker" }
    };

jobs.Execute(ApplyFilter, j => j.Id);

. . .

    public void ApplyFilter(string filterId)
    {
        Debug.WriteLine(filterId);
    }


1

Estoy totalmente en desacuerdo con la idea de que los métodos de extensión de enlace deben estar libres de efectos secundarios (no solo porque no lo están, cualquier delegado puede realizar efectos secundarios).

Considera lo siguiente:

   public class Element {}

   public Enum ProcessType
   {
      This = 0, That = 1, SomethingElse = 2
   }

   public class Class1
   {
      private Dictionary<ProcessType, Action<Element>> actions = 
         new Dictionary<ProcessType,Action<Element>>();

      public Class1()
      {
         actions.Add( ProcessType.This, DoThis );
         actions.Add( ProcessType.That, DoThat );
         actions.Add( ProcessType.SomethingElse, DoSomethingElse );
      }

      // Element actions:

      // This example defines 3 distict actions
      // that can be applied to individual elements,
      // But for the sake of the argument, make
      // no assumption about how many distict
      // actions there may, and that there could
      // possibly be many more.

      public void DoThis( Element element )
      {
         // Do something to element
      }

      public void DoThat( Element element )
      {
         // Do something to element
      }

      public void DoSomethingElse( Element element )
      {
         // Do something to element
      }

      public void Apply( ProcessType processType, IEnumerable<Element> elements )
      {
         Action<Element> action = null;
         if( ! actions.TryGetValue( processType, out action ) )
            throw new ArgumentException("processType");
         foreach( element in elements ) 
            action(element);
      }
   }

Lo que muestra el ejemplo es realmente un tipo de enlace tardío que permite invocar una de las muchas acciones posibles que tienen efectos secundarios en una secuencia de elementos, sin tener que escribir una construcción de interruptor grande para decodificar el valor que define la acción y traducir en su método correspondiente.


99
Hay una diferencia entre si un método de extensión PUEDE tener efectos secundarios y si DEBERÍA tener. Solo está señalando que puede tener, aunque puede haber muy buenas razones para proponer que no deberían tener efectos secundarios. En la programación funcional, todas las funciones están libres de efectos secundarios, y cuando se utilizan construcciones de programación funcional, es posible que desee asumir que lo son.
Dave Van den Eynde

1

Para mantenerse fluido, uno puede usar un truco así:

GetItems()
    .Select(i => new Action(i.DoStuf)))
    .Aggregate((a, b) => a + b)
    .Invoke();


-1

Otro ForEachejemplo

public static IList<AddressEntry> MapToDomain(IList<AddressModel> addresses)
{
    var workingAddresses = new List<AddressEntry>();

    addresses.Select(a => a).ToList().ForEach(a => workingAddresses.Add(AddressModelMapper.MapToDomain(a)));

    return workingAddresses;
}

44
Qué desperdicio de memoria. Esto llama a ToList para agregar a una lista. ¿Por qué esto no solo devuelve direcciones. Seleccione (a => MapToDomain (a))?
Mark Sowul
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.