Criando uma lista separada por vírgula a partir de IList <string> ou IEnumerable <string>

851

Qual é a maneira mais limpa de criar uma lista separada por vírgula de valores de seqüência de caracteres de um IList<string>ou IEnumerable<string>?

String.Join(...)opera de string[]maneira que pode ser complicado trabalhar quando tipos como IList<string>ou IEnumerable<string>não podem ser facilmente convertidos em uma matriz de cadeias.

Daniel Fortunov
fonte
5
Oh ... gritos. Eu perdi a adição do método de extensão ToArray em 3.5: #public static TSource[] ToArray<TSource>(this IEnumerable<TSource> source)
Daniel Fortunov 28/04/09
1
Se você chegou a essa pergunta procurando um meio de escrever CSV, vale lembrar que simplesmente inserir vírgulas entre os itens é insuficiente e causará falhas no caso de aspas e vírgulas nos dados de origem.
spender

Respostas:

1448

.NET 4+

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

Detalhe e soluções pré .Net 4.0

IEnumerable<string>pode ser convertido em uma matriz de cadeia muito facilmente com LINQ (.NET 3.5):

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

É fácil escrever o método auxiliar equivalente se você precisar:

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

Então chame assim:

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

Você pode ligar string.Join. Obviamente, você não precisa usar um 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());

O último é um pouco de um bocado embora :)

É provável que seja a maneira mais simples de fazê-lo e também tenha um bom desempenho - há outras perguntas sobre exatamente como é o desempenho, incluindo (mas não limitado a) este .

No .NET 4.0, há mais sobrecargas disponíveis no string.Join, então você pode simplesmente escrever:

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

Muito mais simples :)

Jon Skeet
fonte
O método auxiliar envolve a criação de duas listas desnecessárias. Essa é realmente a melhor maneira de resolver o problema? Por que não concatenar você mesmo em um loop foreach?
Eric
4
O método auxiliar cria apenas uma lista e uma matriz. O ponto é que o resultado precisa ser uma matriz, não uma lista ... e você precisa saber o tamanho de uma matriz antes de iniciar. A prática recomendada diz que você não deve enumerar uma fonte mais de uma vez no LINQ, a menos que seja necessário - pode estar fazendo todos os tipos de coisas desagradáveis. Então, você fica lendo os buffers e redimensionando à medida que avança - o que é exatamente o que List<T>faz. Por que reinventar a roda?
23909 Jon Skeet
9
Não, o ponto é que o resultado precisa ser uma sequência concatenada. Não há necessidade de criar uma nova lista ou uma nova matriz para atingir esse objetivo. Esse tipo de mentalidade .NET me deixa triste.
Eric
38
É isso aí. Toda resposta leva a Jon Skeet. Eu apenas vou var PurchaseBooks = AmazonContainer.Where (p => p.Author == "Jon Skeet"). Select ();
Zachary Scott
3
@ codeMonkey0110: Bem, não faz sentido ter uma expressão de consulta ou chamada ToList. É bom usar string myStr = string.Join(",", foo.Select(a => a.someInt.ToString()))embora.
22814 Jon Skeet
179

FYI, a versão .NET 4.0 do string.Join()possui algumas sobrecargas extras , que funcionam em IEnumerablevez de apenas matrizes, incluindo uma que pode lidar com qualquer tipo T:

public static string Join(string separator, IEnumerable<string> values)
public static string Join<T>(string separator, IEnumerable<T> values)
Xavier Poinas
fonte
2
Isso chamará o método T.ToString ()?
Philippe Lavoie
Estava prestes a comentar isso na resposta de Jon. Obrigado por mencionar.
precisa
2
Enfim, para fazer isso na propriedade de um objeto? (Ex: IEnumerable <Employee> eo objeto Employee tem uma propriedade string .SSN sobre ele, e se uma vírgula lista de SSN de separados.)
granadaCoder
1
Você deve selecionar a sequência primeiro, embora possa criar um método de extensão que faça isso. str = emps.Select(e => e.SSN).Join(",")
Xavier Poinas
65

A maneira mais fácil de ver isso é usando o Aggregatemétodo LINQ :

string commaSeparatedList = input.Aggregate((a, x) => a + ", " + x)
Daniel Fortunov
fonte
20
Isso não é apenas mais complicado (IMO) que o ToArray + Join, também é um pouco ineficiente - com uma grande sequência de entradas, que começará a apresentar um desempenho muito ruim.
31909 Jon Skeet
35
Ainda assim, é a mais bonita.
Merritt
2
Você pode alimentar Agregar uma semente StringBuilder e, em seguida, tornar-se seu Agregado Func Func<StringBuilder,string,StringBuider>. Em seguida, basta chamar ToString()o StringBuilder retornado. É claro que não tão bonito embora :)
Matt Greer
3
Esta é a maneira mais clara de fazer o que a pergunta pediu ao IMHO.
Derek Morrison
8
Tenha em atenção que input.Countdeve ser superior a 1.
Youngjae
31

Eu acho que a maneira mais limpa de criar uma lista separada por vírgula de valores de string é simplesmente:

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

Aqui está um exemplo completo:

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

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

Não há necessidade de criar uma função auxiliar, isso é incorporado ao .NET 4.0 e superior.

Dan VanWinkle
fonte
4
Observe que isso é aplicável a partir do .NET 4 (como Xavier apontou em sua resposta).
Derek Morrison
Do ponto de vista .NET 4 novato com a experiência de menos de um mês esta resposta foi uma boa combinação de correção e concisão
Dexygen
13

Comparando pelo desempenho, o vencedor é "Faça um loop, sb. Anexe-o e faça o passo anterior". Na verdade, "próximo passo enumerável e manual" é o mesmo bem (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 foi usado

Roman Pokrovskij
fonte
11

Desde que cheguei aqui enquanto procurava ingressar em uma propriedade específica de uma lista de objetos (e não o ToString ()), aqui está uma adição à resposta aceita:

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

Aqui está outro método de extensão:

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

Chegando um pouco tarde para esta discussão, mas esta é a minha contribuição fwiw. Eu tenho IList<Guid> OrderIdsque ser convertido em uma string CSV, mas o seguinte é genérico e funciona sem modificações com outros tipos:

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

Curto e agradável, usa StringBuilder para construir uma nova string, reduz o comprimento de StringBuilder em um para remover a última vírgula e retorna a string CSV.

Eu atualizei isso para usar vários Append()para adicionar string + vírgula. Pelo feedback de James, usei o Reflector para dar uma olhada StringBuilder.AppendFormat(). Acontece que AppendFormat()usa um StringBuilder para construir a string de formato, o que o torna menos eficiente nesse contexto do que apenas o uso de múltiplos Appends().

David Clarke
fonte
Gazumped, obrigado Xavier Eu não estava ciente dessa atualização no .Net4. O projeto no qual estou trabalhando ainda não deu o salto, então continuarei usando meu exemplo agora de pedestre.
David Clarke
2
Isso falhará com uma fonte IEnumerable de item zero. sb.Length-- precisa de uma verificação de limites.
James Dunne
Boa captura, obrigado James, no contexto em que estou usando isso, estou "garantido" de ter pelo menos um OrderId. Atualizei o exemplo e meu próprio código para incluir a verificação de limites (apenas para ter certeza).
David Clarke
@ James Eu acho que chamar sb.Length-- um hack é um pouco duro. Efetivamente, estou apenas evitando o teste "if (notdone)" até o final, em vez de fazê-lo em cada iteração.
David Clarke
1
@ James, meu ponto de vista é que geralmente há mais de uma resposta correta para as perguntas feitas aqui e a referência a uma delas como "hack" implica que está incorreto o que eu contestaria. Pelo pequeno número de guias que estou concatenando, a resposta de Daniel acima provavelmente seria perfeitamente adequada e é certamente mais sucinta / legível do que a minha resposta. Estou usando isso em apenas um lugar no meu código e sempre utilizarei uma vírgula como delimitador. YAGNI diz que não construa algo que você não precisará. DRY é aplicável se eu precisasse fazê-lo mais de uma vez, altura em que criaria um método de extensão. HTH.
David Clarke
7

Algo um pouco fugaz, mas funciona:

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

Fornece a você um CSV de uma lista depois de atribuir o conversor (neste caso, d => d.DivisionID.ToString ("b")).

Hacky, mas funciona - poderia ser transformado em um método de extensão, talvez?

Mike Kingscott
fonte
7

Aqui está o jeito que eu fiz, usando o que eu fiz em outros 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
fonte
7

Necessidade específica quando devemos cercar por ', por ex:

        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
fonte
4

Temos uma função utilitária, algo como isto:

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

Que pode ser usado para juntar muitas coleções facilmente:

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

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

Observe que temos o parâmetro de coleção antes do lambda porque o intellisense seleciona o tipo de coleção.

Se você já possui uma enumeração de strings, tudo o que você precisa fazer é o ToArray:

string csv = string.Join( ",", myStrings.ToArray() );
Keith
fonte
2
Eu tenho um método de extensão que faz quase exatamente a mesma coisa, muito útil: stackoverflow.com/questions/696850/…
#
Sim, você pode escrever isso como um método de extensão .ToDelimitedString com bastante facilidade. Eu iria com a minha única linha string.Join um em vez de usar um StringBuilder e aparar o último char.
291 Keith
3

você pode converter o IList em uma matriz usando o ToArray e, em seguida, executar um comando string.join na matriz.

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

Eles podem ser facilmente convertidos em uma matriz usando as extensões Linq no .NET 3.5.

   var stringArray = stringList.ToArray();
Richard
fonte
3

Você também pode usar algo como o seguinte depois de convertê-lo em uma matriz usando um dos métodos listados por outros:

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();
        }
    }
}

Edit: Aqui está outro exemplo

Brad
fonte
3

Acabei de resolver esse problema antes de passar por este artigo. Minha solução é algo como abaixo:

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

Chamado 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);
}

Eu também poderia ter sido tão facilmente expresso como tal e também teria sido mais eficiente:

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

Minha resposta é como a solução agregada acima, mas deve ter menos pilha de chamadas, pois não há chamadas delegadas 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();
}

Obviamente, pode-se estender a assinatura para ser independente do delimitador. Eu realmente não sou fã da chamada sb.Remove () e gostaria de refatorá-la para ser um loop while direto sobre um IEnumerable e usar MoveNext () para determinar se você deve ou não escrever uma vírgula. Vou mexer e postar essa solução, se eu encontrar.


Aqui está o que eu queria 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();
}

Não é necessário armazenamento temporário de matriz ou lista e não é necessário StringBuilder Remove()ou Length--hackear.

Na minha biblioteca de framework, fiz algumas variações nessa assinatura de método, todas as combinações de incluindo delimiteros converterparâmetros e com o uso de ","e x.ToString()como padrões, respectivamente.

James Dunne
fonte
3

Espero que esta seja a maneira mais simples

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

Eu discuti essa discussão enquanto procurava por um bom método C # para juntar strings, como é feito com o método MySql CONCAT_WS(). Esse método difere do string.Join()método, pois ele não adiciona o sinal separador se as strings forem NULL ou vazias.

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

retornará apenas Lastnamese o primeiro nome estiver vazio, enquanto

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

retornará strLastname + ", "no mesmo caso.

Desejando o primeiro comportamento, escrevi os seguintes 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
fonte
2

Eu escrevi alguns métodos de extensão para fazer isso de uma maneira 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();
    }

Isso 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
fonte
3
Usar o operador + para concatenar seqüências de caracteres não é bom, pois fará com que uma nova sequência seja alocada a cada vez. Além disso, embora o StringBuilder possa ser implicitamente convertido em uma string, fazê-lo com frequência (toda iteração do loop) anularia amplamente o objetivo de ter um construtor de strings.
22330 Daniel Fortunov
2

Você pode usar .ToArray()on Listse IEnumerables, e depois usar String.Join()como quiser.

JoshJordan
fonte
0

Se as seqüências de caracteres que você deseja ingressar estiverem na Lista de Objetos, também poderá fazer algo assim:

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