Creazione di un elenco separato da virgole da IList <string> o IEnumerable <string>


851

Qual è il modo più pulito per creare un elenco separato da virgole di valori stringa da un IList<string>o IEnumerable<string>?

String.Join(...)funziona in string[]modo da poter essere scomodo da utilizzare quando tipi come IList<string>o IEnumerable<string>non possono essere facilmente convertiti in un array di stringhe.


5
Oh ... whoops. Ho perso l'aggiunta del metodo di estensione ToArray in 3.5:public static TSource[] ToArray<TSource>(this IEnumerable<TSource> source)
Daniel Fortunov,

1
Se sei arrivato a questa domanda alla ricerca di un modo per scrivere CSV, vale la pena ricordare che l'inserimento di virgole tra gli elementi è insufficiente e causerà errori in caso di virgolette e virgole nei dati di origine.
spender

Risposte:


1448

.NET 4+

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

Detail & Pre .Net 4.0 Solutions

IEnumerable<string>può essere convertito in un array di stringhe molto facilmente con LINQ (.NET 3.5):

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

È abbastanza facile scrivere il metodo di supporto equivalente se è necessario:

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

Quindi chiamalo così:

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

È quindi possibile chiamare string.Join. Naturalmente, non è necessario utilizzare un metodo di supporto:

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

Quest'ultimo è un po 'un boccone però :)

Questo è probabilmente il modo più semplice per farlo, e anche abbastanza performante - ci sono altre domande su come sia esattamente la performance, incluso (ma non limitato a) questo .

A partire da .NET 4.0, ci sono più sovraccarichi disponibili in string.Join, quindi puoi semplicemente scrivere:

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

Molto più semplice :)


Il metodo helper prevede la creazione di due elenchi non necessari. È davvero il modo migliore per risolvere il problema? Perché non concatenarlo da soli in un ciclo foreach?
Eric,

4
Il metodo helper crea solo un elenco e un array. Il punto è che il risultato deve essere un array, non un elenco ... ed è necessario conoscere le dimensioni di un array prima di iniziare. Le migliori pratiche dicono che non dovresti enumerare una fonte più di una volta in LINQ a meno che tu non debba farlo - potrebbe fare tutti i tipi di cose cattive. Quindi, rimani con la lettura nei buffer e il ridimensionamento mentre procedi, il che è esattamente ciò che List<T>fa. Perché reinventare la ruota?
Jon Skeet,

9
No, il punto è che il risultato deve essere una stringa concatenata. Non è necessario creare un nuovo elenco o un nuovo array per raggiungere questo obiettivo. Questo tipo di mentalità .NET mi rende triste.
Eric,

38
Questo è tutto. Ogni risposta porta a Jon Skeet. Ho appena intenzione di variare PurchaseBooks = AmazonContainer.Where (p => p.Author == "Jon Skeet"). Select ();
Zachary Scott,

3
@ codeMonkey0110: Beh, non ha senso avere un'espressione di query lì o chiamare ToList. Va bene usare string myStr = string.Join(",", foo.Select(a => a.someInt.ToString()))però.
Jon Skeet,

179

Cordiali saluti, la versione .NET 4.0 di string.Join()ha alcuni sovraccarichi aggiuntivi , che funzionano IEnumerableinvece che solo array, incluso uno che può gestire qualsiasi tipo T:

public static string Join(string separator, IEnumerable<string> values)
public static string Join<T>(string separator, IEnumerable<T> values)

2
Questo chiamerà il metodo T.ToString ()?
Philippe Lavoie,

Stavo per commentare questo sulla risposta di Jon. Grazie per averlo menzionato.
Dan Bechard,

2
In ogni caso per farlo su una proprietà di un oggetto? (Es .: IEnumerable <Employee> e l'oggetto Employee ha una stringa .SSN proprietà su di esso, e ottiene un elenco separato da virgole di SSN.)
granadaCoder

1
Devi prima selezionare la stringa, anche se potresti creare un metodo di estensione che lo faccia. str = emps.Select(e => e.SSN).Join(",")
Xavier Poinas,

65

Il modo più semplice che posso vedere per fare questo è usando il Aggregatemetodo LINQ :

string commaSeparatedList = input.Aggregate((a, x) => a + ", " + x)

20
Questo non è solo più complicato (IMO) di ToArray + Join, è anche in qualche modo inefficiente - con una grande sequenza di input, le prestazioni inizieranno molto male.
Jon Skeet,

35
Tuttavia, è il più bello.
Merritt,

2
Puoi alimentare Aggregate un seme StringBuilder, quindi Agg Aggate diventa Func<StringBuilder,string,StringBuider>. Quindi chiama ToString()il StringBuilder restituito. Ovviamente non è altrettanto carino :)
Matt Greer,

3
Questo è il modo più chiaro di fare ciò che la domanda ha richiesto l'IMHO.
Derek Morrison,

8
Attenzione che input.Countdovrebbe essere più di 1.
Youngjae

31

Penso che il modo più pulito per creare un elenco separato da virgole di valori stringa sia semplicemente:

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

Ecco un esempio completo:

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

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

Non è necessario creare una funzione di supporto, che è integrata in .NET 4.0 e versioni successive.


4
Nota che questo è applicabile a partire da .NET 4 (come Xavier ha sottolineato nella sua risposta).
Derek Morrison,

Dal punto di vista del neofita di .NET 4 con meno di un mese di esperienza questa risposta è stata una bella combinazione di correttezza e
sintonia

13

Confrontando per prestazione il vincitore è "Ripeti, sb. Appendi, e fai un passo indietro". In realtà "enumerable and manual move next" è lo stesso buono (considera 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 |

Codice:

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 è stato utilizzato


11

Da quando ho raggiunto qui mentre cercavo di unire una proprietà specifica di un elenco di oggetti (e non il ToString ()) ecco un'aggiunta alla risposta accettata:

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

9

Ecco un altro metodo di estensione:

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

8

Arrivare un po 'tardi a questa discussione, ma questo è il mio contributo prima. Devo IList<Guid> OrderIdsessere convertito in una stringa CSV, ma di seguito è generico e funziona non modificato con altri tipi:

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

Breve e dolce, usa StringBuilder per costruire una nuova stringa, riduce la lunghezza di StringBuilder di una per rimuovere l'ultima virgola e restituisce una stringa CSV.

Ho aggiornato questo per usare multipli Append()per aggiungere stringa + virgola. Dal feedback di James ho usato Reflector per dare un'occhiata StringBuilder.AppendFormat(). Si scopre che AppendFormat()utilizza StringBuilder per costruire la stringa di formato che la rende meno efficiente in questo contesto rispetto al semplice utilizzo di più Appends().


Stupito, grazie Xavier non ero a conoscenza di quell'aggiornamento in .Net4. Il progetto a cui sto lavorando non ha ancora fatto il salto, quindi continuerò a usare il mio esempio pedonale nel frattempo.
David Clarke,

2
Ciò avrà esito negativo con un'origine IEnumerable a zero elementi. sb.Length-- ha bisogno di un controllo dei limiti.
James Dunne,

Bella cattura grazie James, nel contesto in cui sto usando questo sono "garantito" di avere almeno un OrderId. Ho aggiornato sia l'esempio che il mio codice per includere il controllo dei limiti (solo per essere sicuri).
David Clarke,

@James Penso che chiamare sb.Length-- un hack sia un po 'duro. In effetti sto solo evitando il test "if (notdone)" fino alla fine piuttosto che farlo in ogni iterazione.
David Clarke,

1
@James il mio punto è che spesso c'è più di una risposta corretta alle domande poste qui e riferirsi a una come un "hack" implica che è errato che contesterei. Per il piccolo numero di guide che sto concatenando la risposta di Daniel sopra sarebbe probabilmente perfettamente adeguata ed è certamente più concisa / leggibile della mia risposta. Sto usando questo in un solo posto nel mio codice e userò sempre e solo una virgola come delimitatore. YAGNI dice di non costruire qualcosa di cui non avrai bisogno. DRY è applicabile se avessi bisogno di farlo più di una volta a quel punto avrei creato un metodo di estensione. HTH.
David Clarke,

7

Qualcosa di un po 'fugace, ma funziona:

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

Ti dà un CSV da un Elenco dopo avergli dato il convertitore (in questo caso d => d.DivisionID.ToString ("b")).

Hacky ma funziona - forse potrebbe essere trasformato in un metodo di estensione?


7

Ecco come l'ho fatto, usando il modo in cui l'ho fatto in altre lingue:

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

7

Necessità specifica quando dovremmo circondarci di ", di 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()));

4

Abbiamo una funzione di utilità, qualcosa del genere:

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

Che può essere utilizzato per unire facilmente molte raccolte:

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

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

Nota che abbiamo il parametro collection prima del lambda perché intellisense quindi prende il tipo di raccolta.

Se hai già un elenco di stringhe tutto ciò che devi fare è ToArray:

string csv = string.Join( ",", myStrings.ToArray() );

2
Ho un metodo di estensione che fa quasi esattamente la stessa cosa, molto utile: stackoverflow.com/questions/696850/...
LukeH

Sì, potresti scriverlo come metodo di estensione .ToDelimitedString abbastanza facilmente. Vorrei andare con la mia stringa a riga singola. Unirmi a uno invece di usare StringBuilder per tagliare l'ultimo carattere.
Keith,

3

è possibile convertire IList in un array utilizzando ToArray e quindi eseguire un comando string.join sull'array.

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

3

Possono essere facilmente convertiti in un array usando le estensioni Linq in .NET 3.5.

   var stringArray = stringList.ToArray();

3

È inoltre possibile utilizzare qualcosa di simile al seguente dopo averlo convertito in un array utilizzando uno dei metodi elencati da altri:

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

Modifica: ecco un altro esempio


3

Ho appena risolto questo problema prima di imbattermi in questo articolo. La mia soluzione è simile al seguente:

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

Chiamato come:

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

Avrei anche potuto esprimermi altrettanto facilmente in quanto tale e sarei stato anche più efficiente:

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

3

La mia risposta è come sopra la soluzione aggregata ma dovrebbe essere meno pesante nello stack di chiamate poiché non ci sono chiamate delegate esplicite:

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

Naturalmente, si può estendere la firma per essere indipendente dal delimitatore. Non sono davvero un fan della chiamata sb.Remove () e mi piacerebbe riformattare in modo che sia un ciclo continuo su un IEnumerable e usare MoveNext () per determinare se scrivere o meno una virgola. Cercherò e pubblicherò quella soluzione se la trovo.


Ecco cosa volevo inizialmente:

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

Nessun array temporaneo o elenco di archiviazione richiesto e nessun StringBuilder Remove()o Length--hack richiesto.

Nella mia libreria di framework ho apportato alcune variazioni a questa firma del metodo, ogni combinazione di includere rispettivamente delimiteri converterparametri e con l'uso di ","e x.ToString()come valori predefiniti.


3

Spero che questo sia il modo più semplice

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

3

Mi sono imbattuto in questa discussione durante la ricerca di un buon metodo C # per unire le stringhe come è fatto con il metodo MySql CONCAT_WS(). Questo metodo differisce dal string.Join()metodo in quanto non aggiunge il segno di separazione se le stringhe sono NULL o vuote.

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

restituirà solo Lastnamese il nome è vuoto, mentre

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

tornerà strLastname + ", "nello stesso caso.

Volendo il primo comportamento, ho scritto i seguenti metodi:

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

2

Ho scritto alcuni metodi di estensione per farlo in modo efficiente:

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

Questo dipende da

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

        return s+delim+item;
    }

3
L'uso dell'operatore + per concatenare le stringhe non è eccezionale perché causerà ogni volta l'assegnazione di una nuova stringa. Inoltre, sebbene StringBuilder possa essere implicitamente lanciato su una stringa, farlo frequentemente (ogni iterazione del tuo ciclo) vanificherebbe ampiamente lo scopo di avere un generatore di stringhe.
Daniel Fortunov,

2

È possibile utilizzare .ToArray()su Listse IEnumerables, quindi utilizzare String.Join()come desiderato.


0

Se le stringhe che vuoi unire sono in Elenco di oggetti, puoi fare anche qualcosa del genere:

var studentNames = string.Join(", ", students.Select(x => x.name));
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.