Crear una lista separada por comas de IList <string> o IEnumerable <string>

851

¿Cuál es la forma más limpia de crear una lista de valores de cadena separados por comas a partir de un IList<string>o IEnumerable<string>?

String.Join(...)funciona de string[]manera que puede ser engorroso trabajar cuando tipos como IList<string>o IEnumerable<string>no se pueden convertir fácilmente en una matriz de cadenas.

Daniel Fortunov
fuente
55
Oh ... whoops Me perdí la adición del método de extensión ToArray en 3.5:public static TSource[] ToArray<TSource>(this IEnumerable<TSource> source)
Daniel Fortunov
1
Si ha llegado a esta pregunta buscando un medio para escribir CSV, vale la pena recordar que simplemente insertar comas entre los elementos es insuficiente y causará fallas en el caso de comillas y comas en los datos de origen.
gastador

Respuestas:

1448

.NET 4+

IList<string> strings = new List<string>{"1","2","testing"};
string joined = string.Join(",", strings);

Detalle y soluciones Pre .Net 4.0

IEnumerable<string>se puede convertir en una matriz de cadenas muy fácilmente con LINQ (.NET 3.5):

IEnumerable<string> strings = ...;
string[] array = strings.ToArray();

Es bastante fácil escribir el método auxiliar equivalente si necesita:

public static T[] ToArray(IEnumerable<T> source)
{
    return new List<T>(source).ToArray();
}

Entonces llámalo así:

IEnumerable<string> strings = ...;
string[] array = Helpers.ToArray(strings);

Entonces puedes llamar string.Join. Por supuesto, no tiene que usar un método auxiliar:

// C# 3 and .NET 3.5 way:
string joined = string.Join(",", strings.ToArray());
// C# 2 and .NET 2.0 way:
string joined = string.Join(",", new List<string>(strings).ToArray());

Sin embargo, este último es un poco bocado :)

Es probable que esta sea la forma más sencilla de hacerlo, y también bastante eficaz: hay otras preguntas sobre exactamente cómo es el rendimiento, incluido (pero no limitado a) este .

A partir de .NET 4.0, hay más sobrecargas disponibles string.Join, por lo que puede escribir:

string joined = string.Join(",", strings);

Mucho más simple :)

Jon Skeet
fuente
El método auxiliar implica crear dos listas innecesarias. ¿Es esta realmente la mejor manera de resolver el problema? ¿Por qué no concatenarlo usted mismo en un bucle foreach?
Eric
44
El método auxiliar solo crea una lista y una matriz. El punto es que el resultado debe ser una matriz, no una lista ... y necesita saber el tamaño de una matriz antes de comenzar. La mejor práctica dice que no debe enumerar una fuente más de una vez en LINQ a menos que tenga que hacerlo, ya que podría estar haciendo todo tipo de cosas desagradables. Entonces, te queda leer en los buffers y cambiar el tamaño a medida que avanzas, que es exactamente lo que List<T>hace. ¿Por qué reinventar la rueda?
Jon Skeet el
99
No, el punto es que el resultado debe ser una cadena concatenada. No es necesario crear una nueva lista o una nueva matriz para lograr este objetivo. Este tipo de mentalidad .NET me pone triste.
Eric
38
Eso es. Cada respuesta lleva a Jon Skeet. Solo voy a var PurchaseBooks = AmazonContainer.Where (p => p.Author == "Jon Skeet"). Seleccione ();
Zachary Scott
3
@ codeMonkey0110: Bueno, no tiene sentido tener una expresión de consulta allí o llamar ToList. Sin string myStr = string.Join(",", foo.Select(a => a.someInt.ToString()))embargo, está bien usarlo .
Jon Skeet
179

Para su información, la versión .NET 4.0 string.Join()tiene algunas sobrecargas adicionales , que funcionan en IEnumerablelugar de solo matrices, incluida una que puede manejar cualquier tipo T:

public static string Join(string separator, IEnumerable<string> values)
public static string Join<T>(string separator, IEnumerable<T> values)
Xavier Poinas
fuente
2
Esto llamará al método T.ToString ()?
Philippe Lavoie
Estaba a punto de comentar esto sobre la respuesta de Jon. Gracias por mencionar
Dan Bechard el
2
De todos modos para hacer esto en una propiedad de un objeto? (Ej: IEnumerable <Empleado> y el objeto empleado tiene una propiedad de cadena .SSN en él, y obtener una lista separada por comas de SSN de separarse.)
granadaCoder
1
Primero debe seleccionar la cadena, aunque podría crear un método de extensión que lo haga. str = emps.Select(e => e.SSN).Join(",")
Xavier Poinas
65

La forma más fácil que puedo ver para hacer esto es usando el Aggregatemétodo LINQ :

string commaSeparatedList = input.Aggregate((a, x) => a + ", " + x)
Daniel Fortunov
fuente
20
Eso no solo es más complicado (IMO) que ToArray + Join, también es algo ineficiente: con una secuencia de entrada grande, comenzará a funcionar muy mal.
Jon Skeet
35
Aún así, es la más bonita.
Merritt el
2
Puede alimentar a Aggregate una semilla de StringBuilder, luego su Func de Aggregate se convierte Func<StringBuilder,string,StringBuider>. Luego solo llame ToString()al StringBuilder devuelto. Por supuesto, no es tan bonito :)
Matt Greer
3
Esta es la forma más clara de hacer lo que la pregunta pedía en mi humilde opinión.
Derek Morrison
8
Tenga en cuenta que input.Countdebería ser más de 1.
Youngjae
31

Creo que la forma más limpia de crear una lista de valores de cadena separados por comas es simplemente:

string.Join<string>(",", stringEnumerable);

Aquí hay un ejemplo completo:

IEnumerable<string> stringEnumerable= new List<string>();
stringList.Add("Comma");
stringList.Add("Separated");

string.Join<string>(",", stringEnumerable);

No hay necesidad de hacer una función auxiliar, esto está integrado en .NET 4.0 y superior.

Dan VanWinkle
fuente
44
Tenga en cuenta que esto es aplicable a partir de .NET 4 (como señaló Xavier en su respuesta).
Derek Morrison
Desde el punto de vista de .NET 4 novato con menos de un mes de experiencia, esta respuesta fue una buena combinación de corrección y concisión
Dexygen
13

Comparando por rendimiento, el ganador es "Loop, sb. Añádelo y retrocede". En realidad, "mover enumerable y manual a continuación" es lo mismo (considere stddev).

BenchmarkDotNet=v0.10.5, OS=Windows 10.0.14393
Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4
Frequency=3233539 Hz, Resolution=309.2587 ns, Timer=TSC
  [Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1637.0
  Clr    : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1637.0
  Core   : .NET Core 4.6.25009.03, 64bit RyuJIT


                Method |  Job | Runtime |     Mean |     Error |    StdDev |      Min |      Max |   Median | Rank |  Gen 0 | Allocated |
---------------------- |----- |-------- |---------:|----------:|----------:|---------:|---------:|---------:|-----:|-------:|----------:|
            StringJoin |  Clr |     Clr | 28.24 us | 0.4381 us | 0.3659 us | 27.68 us | 29.10 us | 28.21 us |    8 | 4.9969 |   16.3 kB |
 SeparatorSubstitution |  Clr |     Clr | 17.90 us | 0.2900 us | 0.2712 us | 17.55 us | 18.37 us | 17.80 us |    6 | 4.9296 |  16.27 kB |
     SeparatorStepBack |  Clr |     Clr | 16.81 us | 0.1289 us | 0.1206 us | 16.64 us | 17.05 us | 16.81 us |    2 | 4.9459 |  16.27 kB |
            Enumerable |  Clr |     Clr | 17.27 us | 0.0736 us | 0.0615 us | 17.17 us | 17.36 us | 17.29 us |    4 | 4.9377 |  16.27 kB |
            StringJoin | Core |    Core | 27.51 us | 0.5340 us | 0.4995 us | 26.80 us | 28.25 us | 27.51 us |    7 | 5.0296 |  16.26 kB |
 SeparatorSubstitution | Core |    Core | 17.37 us | 0.1664 us | 0.1557 us | 17.15 us | 17.68 us | 17.39 us |    5 | 4.9622 |  16.22 kB |
     SeparatorStepBack | Core |    Core | 15.65 us | 0.1545 us | 0.1290 us | 15.45 us | 15.82 us | 15.66 us |    1 | 4.9622 |  16.22 kB |
            Enumerable | Core |    Core | 17.00 us | 0.0905 us | 0.0654 us | 16.93 us | 17.12 us | 16.98 us |    3 | 4.9622 |  16.22 kB |

Código:

public class BenchmarkStringUnion
{
    List<string> testData = new List<string>();
    public BenchmarkStringUnion()
    {
        for(int i=0;i<1000;i++)
        {
            testData.Add(i.ToString());
        }
    }
    [Benchmark]
    public string StringJoin()
    {
        var text = string.Join<string>(",", testData);
        return text;
    }
    [Benchmark]
    public string SeparatorSubstitution()
    {
        var sb = new StringBuilder();
        var separator = String.Empty;
        foreach (var value in testData)
        {
            sb.Append(separator).Append(value);
            separator = ",";
        }
        return sb.ToString();
    }

    [Benchmark]
    public string SeparatorStepBack()
    {
        var sb = new StringBuilder();
        foreach (var item in testData)
            sb.Append(item).Append(',');
        if (sb.Length>=1) 
            sb.Length--;
        return sb.ToString();
    }

    [Benchmark]
    public string Enumerable()
    {
        var sb = new StringBuilder();
        var e = testData.GetEnumerator();
        bool  moveNext = e.MoveNext();
        while (moveNext)
        {
            sb.Append(e.Current);
            moveNext = e.MoveNext();
            if (moveNext) 
                sb.Append(",");
        }
        return sb.ToString();
    }
}

https://github.com/dotnet/BenchmarkDotNet fue utilizado

Roman Pokrovskij
fuente
11

Como llegué aquí mientras buscaba unirme en una propiedad específica de una lista de objetos (y no en ToString ()), aquí hay una adición a la respuesta aceptada:

var commaDelimited = string.Join(",", students.Where(i => i.Category == studentCategory)
                                 .Select(i => i.FirstName));
sam
fuente
9

Aquí hay otro método de extensión:

    public static string Join(this IEnumerable<string> source, string separator)
    {
        return string.Join(separator, source);
    }
Chris McKenzie
fuente
8

Llegué un poco tarde a esta discusión, pero esta es mi contribución. Tengo que IList<Guid> OrderIdsconvertirlo en una cadena CSV, pero el siguiente es genérico y funciona sin modificaciones con otros tipos:

string csv = OrderIds.Aggregate(new StringBuilder(),
             (sb, v) => sb.Append(v).Append(","),
             sb => {if (0 < sb.Length) sb.Length--; return sb.ToString();});

Corto y dulce, utiliza StringBuilder para construir una nueva cadena, reduce la longitud de StringBuilder en uno para eliminar la última coma y devuelve la cadena CSV.

He actualizado esto para usar múltiples Append()'s para agregar cadena + coma. De los comentarios de James utilicé Reflector para echar un vistazo StringBuilder.AppendFormat(). Resulta que AppendFormat()utiliza un StringBuilder para construir la cadena de formato que lo hace menos eficiente en este contexto que simplemente usar múltiples Appends()'s.

David Clarke
fuente
Gazumped, gracias Xavier No estaba al tanto de esa actualización en .Net4. El proyecto en el que estoy trabajando aún no ha dado el salto, por lo que seguiré usando mi ejemplo peatonal mientras tanto.
David Clarke
2
Esto fallará con una fuente IEnumerable de cero elementos. sb.Length-- necesita una verificación de límites.
James Dunne
Buena captura gracias James, en el contexto en el que estoy usando esto, estoy "garantizado" de tener al menos un OrderId. He actualizado tanto el ejemplo como mi propio código para incluir la verificación de límites (solo para estar seguro).
David Clarke
@ James Creo que llamar a sb.Length-- un hack es un poco duro. Efectivamente, estoy evitando su prueba "if (notdone)" hasta el final en lugar de hacerlo en cada iteración.
David Clarke
1
@James, mi punto es que a menudo hay más de una respuesta correcta a las preguntas que se hacen aquí y referirme a una como "pirateo" implica que es incorrecto, lo que discutiría. Para la pequeña cantidad de guías que estoy concatenando, la respuesta anterior de Daniel probablemente sería perfectamente adecuada y ciertamente es más sucinta / legible que mi respuesta. Estoy usando esto en un solo lugar en mi código y solo usaré una coma como delimitador. YAGNI dice que no construyas algo que no vas a necesitar. DRY es aplicable si tuviera que hacerlo más de una vez, en ese momento crearía un método de exención. HTH.
David Clarke
7

Algo un poco feo, pero funciona:

string divisionsCSV = String.Join(",", ((List<IDivisionView>)divisions).ConvertAll<string>(d => d.DivisionID.ToString("b")).ToArray());

Le proporciona un CSV de una Lista después de asignarle el convertidor (en este caso d => d.DivisionID.ToString ("b")).

Hacky pero funciona, ¿podría convertirse en un método de extensión tal vez?

Mike Kingscott
fuente
7

Así es como lo hice, usando la forma en que lo hice en otros idiomas:

private string ToStringList<T>(IEnumerable<T> list, string delimiter)
{
  var sb = new StringBuilder();
  string separator = String.Empty;
  foreach (T value in list)
  {
    sb.Append(separator).Append(value);
    separator = delimiter;
  }
  return sb.ToString();
}
Dale King
fuente
7

Necesidad específica cuando debemos rodearnos de ', por ej:

        string[] arr = { "jj", "laa", "123" };
        List<string> myList = arr.ToList();

        // 'jj', 'laa', '123'
        Console.WriteLine(string.Join(", ",
            myList.ConvertAll(m =>
                string.Format("'{0}'", m)).ToArray()));
serhio
fuente
4

Tenemos una función de utilidad, algo como esto:

public static string Join<T>( string delimiter, 
    IEnumerable<T> collection, Func<T, string> convert )
{
    return string.Join( delimiter, 
        collection.Select( convert ).ToArray() );
}

Que se puede usar para unir fácilmente muchas colecciones:

int[] ids = {1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233};

string csv = StringUtility.Join(",", ids, i => i.ToString() );

Tenga en cuenta que tenemos el parámetro de colección antes de lambda porque intellisense luego recoge el tipo de colección.

Si ya tiene una enumeración de cadenas, todo lo que necesita hacer es ToArray:

string csv = string.Join( ",", myStrings.ToArray() );
Keith
fuente
2
Tengo un método de extensión que hace casi exactamente lo mismo, muy útil: stackoverflow.com/questions/696850/…
LukeH
Sí, podrías escribir esto como un método de extensión .ToDelimitedString con bastante facilidad. Me gustaría ir con mi cadena de línea única. Unirme a uno en lugar de usar un StringBuilder y recortar el último carácter.
Keith
3

puede convertir el IList en una matriz usando ToArray y luego ejecutar un comando string.join en la matriz.

Dim strs As New List(Of String)
Dim arr As Array
arr = strs.ToArray
Vikram
fuente
3

Se pueden convertir fácilmente a una matriz utilizando las extensiones de Linq en .NET 3.5.

   var stringArray = stringList.ToArray();
Ricardo
fuente
3

También puede usar algo como lo siguiente después de haberlo convertido a una matriz utilizando uno de los métodos enumerados por otros:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Net;
using System.Configuration;

namespace ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            CommaDelimitedStringCollection commaStr = new CommaDelimitedStringCollection();
            string[] itemList = { "Test1", "Test2", "Test3" };
            commaStr.AddRange(itemList);
            Console.WriteLine(commaStr.ToString()); //Outputs Test1,Test2,Test3
            Console.ReadLine();
        }
    }
}

Editar: Aquí hay otro ejemplo

Puntilla
fuente
3

Acabo de resolver este problema antes de pasar por este artículo. Mi solución es algo como a continuación:

   private static string GetSeparator<T>(IList<T> list, T item)
   {
       return (list.IndexOf(item) == list.Count - 1) ? "" : ", ";
   }

Llamado como:

List<thing> myThings;
string tidyString;

foreach (var thing in myThings)
{
     tidyString += string.format("Thing {0} is a {1}", thing.id, thing.name) + GetSeparator(myThings, thing);
}

También podría haberme expresado tan fácilmente como tal y también habría sido más eficiente:

string.Join(“,”, myThings.Select(t => string.format(“Thing {0} is a {1}”, t.id, t.name)); 
SpaceKat
fuente
3

Mi respuesta es como la solución agregada anterior, pero debería tener menos pila de llamadas, ya que no hay llamadas de delegado explícitas:

public static string ToCommaDelimitedString<T>(this IEnumerable<T> items)
{
    StringBuilder sb = new StringBuilder();
    foreach (var item in items)
    {
        sb.Append(item.ToString());
        sb.Append(',');
    }
    if (sb.Length >= 1) sb.Length--;
    return sb.ToString();
}

Por supuesto, uno puede extender la firma para que sea independiente del delimitador. Realmente no soy fanático de la llamada sb.Remove () y me gustaría refactorizarlo para que sea un bucle while directo sobre un IEnumerable y use MoveNext () para determinar si escribir o no una coma. Voy a jugar y publicar esa solución si me encuentro con ella.


Esto es lo que quería inicialmente:

public static string ToDelimitedString<T>(this IEnumerable<T> source, string delimiter, Func<T, string> converter)
{
    StringBuilder sb = new StringBuilder();
    var en = source.GetEnumerator();
    bool notdone = en.MoveNext();
    while (notdone)
    {
        sb.Append(converter(en.Current));
        notdone = en.MoveNext();
        if (notdone) sb.Append(delimiter);
    }
    return sb.ToString();
}

No se requiere una matriz temporal o almacenamiento de lista y no se requiere StringBuilder Remove()o Length--piratear.

En mi biblioteca de framework hice algunas variaciones en la firma de este método, cada combinación de incluir delimiterlos converterparámetros y con el uso de ","y x.ToString()como valores predeterminados, respectivamente.

James Dunne
fuente
3

Esperemos que esta sea la forma más simple

 string Commaseplist;
 string[] itemList = { "Test1", "Test2", "Test3" };
 Commaseplist = string.join(",",itemList);
 Console.WriteLine(Commaseplist); //Outputs Test1,Test2,Test3
Thangamani Palanisamy
fuente
3

Llegué a esta discusión mientras buscaba un buen método de C # para unir cadenas como se hace con el método MySql CONCAT_WS(). Este método difiere del string.Join()método en que no agrega el signo separador si las cadenas son NULL o están vacías.

CONCAT_WS (',', tbl.Lastname, tbl.Firstname)

solo volverá Lastnamesi el nombre está vacío, mientras que

string.Join (",", strLastname, strFirstname)

volverá strLastname + ", "en el mismo caso.

Deseando el primer comportamiento, escribí los siguientes métodos:

    public static string JoinStringsIfNotNullOrEmpty(string strSeparator, string strA, string strB, string strC = "")
    {
        return JoinStringsIfNotNullOrEmpty(strSeparator, new[] {strA, strB, strC});
    }

    public static string JoinStringsIfNotNullOrEmpty(string strSeparator, string[] arrayStrings)
    {
        if (strSeparator == null)
            strSeparator = "";
        if (arrayStrings == null)
            return "";
        string strRetVal = arrayStrings.Where(str => !string.IsNullOrEmpty(str)).Aggregate("", (current, str) => current + (str + strSeparator));
        int trimEndStartIndex = strRetVal.Length - strSeparator.Length;
        if (trimEndStartIndex>0)
            strRetVal = strRetVal.Remove(trimEndStartIndex);
        return strRetVal;
    }
Håkon Seljåsen
fuente
2

Escribí algunos métodos de extensión para hacerlo de manera eficiente:

    public static string JoinWithDelimiter(this IEnumerable<String> that, string delim) {
        var sb = new StringBuilder();
        foreach (var s in that) {
            sb.AppendToList(s,delim);
        }

        return sb.ToString();
    }

Esto depende de

    public static string AppendToList(this String s, string item, string delim) {
        if (s.Length == 0) {
            return item;
        }

        return s+delim+item;
    }
Paul Houle
fuente
3
Usar el operador + para concatenar cadenas no es excelente porque hará que se asigne una nueva cadena cada vez. Además, aunque StringBuilder se puede convertir implícitamente en una cadena, hacerlo con frecuencia (cada iteración de su ciclo) anularía en gran medida el propósito de tener un generador de cadenas.
Daniel Fortunov el
2

Puede usar .ToArray()en Listsy IEnumerables, y luego usar String.Join()como quisiera.

JoshJordan
fuente
0

Si las cadenas que desea unir están en la Lista de objetos, entonces también puede hacer algo como esto:

var studentNames = string.Join(", ", students.Select(x => x.name));
Saksham Chaudhary
fuente