Tworzenie listy oddzielonej przecinkami z IList <ciąg> lub IEnumerable <ciąg>


848

Jaki jest najczystszy sposób utworzenia rozdzielonej przecinkami listy wartości ciągów z IList<string>lub lub IEnumerable<string>?

String.Join(...)działa na string[]tak, więc może być uciążliwy w pracy, gdy typy takie jak IList<string>lub IEnumerable<string>nie mogą być łatwo przekonwertowane na tablicę ciągów.


5
Och ... ups. Brakowało mi dodania metody rozszerzenia ToArray w 3.5:public static TSource[] ToArray<TSource>(this IEnumerable<TSource> source)
Daniel Fortunov

1
Jeśli przyszedłeś do tego pytania, szukając sposobu na zapisanie CSV, warto pamiętać, że zwykłe wstawianie przecinków między elementami jest niewystarczające i spowoduje niepowodzenie w przypadku cudzysłowów i przecinków w danych źródłowych.
spędzić

Odpowiedzi:


1446

.NET 4+

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

Szczegóły i rozwiązania .Net 4.0

IEnumerable<string>można bardzo łatwo przekonwertować na tablicę ciągów za pomocą LINQ (.NET 3.5):

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

Wystarczy napisać równoważną metodę pomocnika, jeśli potrzebujesz:

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

Następnie nazwij to tak:

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

Możesz wtedy zadzwonić string.Join. Oczywiście nie musisz używać metody pomocniczej:

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

Ten ostatni jest jednak trochę kęs :)

Jest to prawdopodobnie najprostszy sposób, aby to zrobić, a także dość wydajny - istnieją inne pytania dotyczące dokładnie tego, jak wygląda wydajność, w tym (ale nie wyłącznie) ten .

Począwszy od .NET 4.0, dostępnych jest więcej przeciążeń w string.Join , więc możesz po prostu napisać:

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

O wiele prostsze :)


Metoda pomocnicza polega na utworzeniu dwóch niepotrzebnych list. Czy to naprawdę najlepszy sposób na rozwiązanie problemu? Dlaczego nie połączyć go samodzielnie w pętli foreach?
Eric

4
Metoda pomocnicza tworzy tylko jedną listę i jedną tablicę. Chodzi o to, że wynikiem musi być tablica, a nie lista ... i musisz znać rozmiar tablicy przed rozpoczęciem. Najlepsza praktyka mówi, że nie powinieneś wymieniać źródła więcej niż raz w LINQ, chyba że musisz - może to robić różnego rodzaju paskudne rzeczy. Pozostaje Ci zatem czytanie buforów i zmienianie rozmiaru w miarę przemieszczania się - właśnie to List<T>robi. Po co wymyślać koło ponownie?
Jon Skeet

9
Nie, chodzi o to, że wynik musi być połączonym łańcuchem. Nie ma potrzeby tworzenia nowej listy ani nowej tablicy, aby osiągnąć ten cel. Ten rodzaj mentalności .NET sprawia mi przykrość.
Eric

38
Otóż ​​to. Każda odpowiedź prowadzi do Jona Skeeta. Właśnie zamierzam zmienić opcję PurchaseBooks = AmazonContainer.Where (p => p.Author == "Jon Skeet"). Select ();
Zachary Scott

3
@ codeMonkey0110: Cóż, nie ma sensu mieć wyrażenia zapytania ani wywoływać ToList. Można go string myStr = string.Join(",", foo.Select(a => a.someInt.ToString()))jednak używać .
Jon Skeet

179

Do Twojej wiadomości, wersja .NET 4.0 string.Join()ma pewne dodatkowe przeciążenia , które działają IEnumerablezamiast tylko tablic, w tym takie, które mogą poradzić sobie z dowolnym rodzajem T:

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

2
To wywoła metodę T.ToString ()?
Philippe Lavoie,

Właśnie miałem skomentować to w odpowiedzi Jona. Dzięki za wzmiankę.
Dan Bechard

2
W każdym razie, aby to zrobić na właściwości obiektu? (Np .: IEnumerable <Pracownik> i obiekt Employee ma na sobie ciąg .SSN, a także listę rozdzielonych przecinkami
numerów

1
Najpierw musisz wybrać ciąg, ale możesz utworzyć metodę rozszerzenia, która to robi. str = emps.Select(e => e.SSN).Join(",")
Xavier Poinas,

65

Najłatwiejszy sposób to zrobić, używając Aggregatemetody LINQ :

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

20
Jest to nie tylko bardziej skomplikowane (IMO) niż ToArray + Join, ale także nieco nieefektywne - przy dużej sekwencji wejściowej zacznie działać bardzo źle.
Jon Skeet

35
Mimo to jest najładniejszy.
Merritt,

2
Możesz karmić Aggregate ziarnem StringBuilder, a następnie twoja Fungregacja staje się Func<StringBuilder,string,StringBuider>. Następnie po prostu wywołaj ToString()zwrócony StringBuilder. Oczywiście nie jest tak ładna :)
Matt Greer

3
Jest to najwyraźniejszy sposób robienia tego, o co pytano w IMHO.
Derek Morrison

8
Uważaj, input.Countpowinno być więcej niż 1.
Youngjae

31

Myślę, że najczystszym sposobem utworzenia listy wartości łańcuchowych oddzielonych przecinkami jest po prostu:

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

Oto pełny przykład:

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

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

Nie ma potrzeby wykonywania funkcji pomocniczej, jest ona wbudowana w .NET 4.0 i nowsze wersje.


4
Zauważ, że ma to zastosowanie od .NET 4 (jak Xavier wskazał w swojej odpowiedzi).
Derek Morrison

Z punktu widzenia początkującego .NET 4 z mniej niż miesięcznym doświadczeniem, ta odpowiedź była miłym połączeniem poprawności i zwięzłości
Dexygen

13

Porównując wydajność, zwycięzcą jest „Zapętlić, sb. Dołącz go i cofnij się”. W rzeczywistości „ruchome, wyliczalne i ręczne przejście dalej” jest tym samym dobrem (rozważ 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 |

Kod:

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

Wykorzystano https://github.com/dotnet/BenchmarkDotNet


11

Ponieważ doszedłem tutaj, szukając dołączyć do określonej właściwości listy obiektów (a nie jej ToString ()), oto dodatek do zaakceptowanej odpowiedzi:

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

9

Oto kolejna metoda rozszerzenia:

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

8

Przybyłem trochę za późno na tę dyskusję, ale to mój wkład fwiw. Mam IList<Guid> OrderIdsdo przekonwertowania na ciąg CSV, ale poniższy jest ogólny i działa niezmodyfikowany z innymi typami:

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

Krótki i słodki, używa StringBuilder do konstruowania nowego ciągu, zmniejsza długość StringBuilder o jeden, aby usunąć ostatni przecinek i zwraca ciąg CSV.

Zaktualizowałem to, aby użyć wielu Append(), aby dodać ciąg + przecinek. Na podstawie opinii Jamesa użyłem Reflectora, żeby się przyjrzeć StringBuilder.AppendFormat(). Okazuje się, że AppendFormat()używa StringBuilder do skonstruowania ciągu formatu, co czyni go mniej wydajnym w tym kontekście niż tylko używanie wielu Appends().


Gazumped, dzięki Xavier Nie wiedziałem o tej aktualizacji w .Net4. Projekt, nad którym pracuję, jeszcze się nie skoczył, dlatego w międzyczasie będę nadal korzystać z mojego przykładu dla pieszych.
David Clarke

2
Nie powiedzie się to ze źródłem IEnumerable o zerowej pozycji. sb.Length-- wymaga sprawdzenia granic.
James Dunne

Niezły chwyt dzięki Jamesowi, w kontekście, w którym go używam, mam „gwarancję” posiadania co najmniej jednego OrderId. Zaktualizowałem zarówno przykład, jak i własny kod, aby uwzględnić sprawdzanie granic (dla pewności).
David Clarke

@James Myślę, że nazywając sb.Length - hack jest trochę trudny. W rzeczywistości po prostu unikam twojego testu „jeśli (nie zrobione)” do końca, zamiast robić to w każdej iteracji.
David Clarke

1
@James mam na myśli to, że często zadaje się więcej niż jedną poprawną odpowiedź na zadane tutaj pytania, a określenie jednego z nich jako „hack” oznacza, że ​​jest to nieprawda, którą bym kwestionował. Dla niewielkiej liczby przewodników konkatenujących powyższą odpowiedź Daniela prawdopodobnie byłaby całkowicie adekwatna i na pewno jest bardziej zwięzła / czytelna niż moja odpowiedź. Używam tego tylko w jednym miejscu w kodzie i zawsze używam przecinka jako separatora. YAGNI mówi, nie buduj czegoś, czego nie będziesz potrzebować. DRY ma zastosowanie, gdybym musiał to zrobić więcej niż jeden raz, w którym to momencie stworzyłbym metodę wyjątkową. HTH.
David Clarke

7

Coś trochę nieprzyzwoicie, ale działa:

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

Daje plik CSV z listy po przekazaniu mu konwertera (w tym przypadku d => d.DivisionID.ToString („b”)).

Hacky, ale działa - czy można go przekształcić w metodę rozszerzenia?


7

Oto jak to zrobiłem, korzystając ze sposobu, w jaki to zrobiłem w innych językach:

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

Szczególna potrzeba, kiedy powinniśmy otaczać „, np .:

        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

Mamy funkcję narzędzia, coś takiego:

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

Które można wykorzystać do łatwego łączenia wielu kolekcji:

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

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

Zauważ, że mamy parametr kolekcji przed lambda, ponieważ intellisense następnie wybiera typ kolekcji.

Jeśli masz już wyliczenie ciągów, wszystko, co musisz zrobić, to ToArray:

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

2
Mam metodę rozszerzenia, która robi prawie dokładnie to samo, bardzo przydatne: stackoverflow.com/questions/696850/…
LukeH

Tak, możesz napisać to jako metodę rozszerzenia .ToDelimitedString dość łatwo. Wybrałbym ciąg z jednym wierszem. Dołącz do jednego zamiast używania StringBuilder i przycinania ostatniego znaku.
Keith,

3

można przekonwertować IList na tablicę za pomocą ToArray, a następnie uruchomić komendę string.join na tablicy.

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

3

Można je łatwo przekonwertować na tablicę za pomocą rozszerzeń Linq w .NET 3.5.

   var stringArray = stringList.ToArray();

3

Możesz również użyć czegoś takiego jak następujące po przekonwertowaniu go na tablicę przy użyciu jednej z metod wymienionych przez innych:

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

Edycja: Oto inny przykład


3

Właśnie rozwiązałem ten problem, zanim przejrzałem ten artykuł. Moje rozwiązanie wygląda mniej więcej tak:

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

Nazywany jak:

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

Mógłbym równie łatwo wyrazić jako taki i byłbym również bardziej wydajny:

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

3

Moja odpowiedź jest jak powyższe rozwiązanie agregujące, ale powinno być mniej obciążające stos wywołań, ponieważ nie ma wyraźnych wywołań delegowania:

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

Oczywiście można rozszerzyć podpis, aby był niezależny od separatora. Naprawdę nie jestem fanem wywołania sb.Remove () i chciałbym przekształcić go w pętlę while over-up nad IEnumerable i użyć MoveNext (), aby ustalić, czy napisać przecinek. Będę majstrować i publikować to rozwiązanie, jeśli na niego wpadnę.


Oto, czego początkowo chciałem:

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

Nie jest wymagane tymczasowe przechowywanie tablic lub list, StringBuilder Remove()ani Length--hack.

W mojej bibliotece frameworków dokonałem kilku zmian w sygnaturze tej metody, każda kombinacja włączenia delimiteri converterparametrów odpowiednio z użyciem ","i x.ToString()jako domyślnych.


3

Mam nadzieję, że jest to najprostszy sposób

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

3

Przeszedłem tę dyskusję, szukając dobrej metody C # do łączenia ciągów, tak jak w przypadku metody MySql CONCAT_WS(). Ta metoda różni się od string.Join()metody tym, że nie dodaje znaku separatora, jeśli łańcuchy są NULL lub puste.

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

zwróci tylko Lastnamewtedy, gdy imię jest puste, natomiast

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

wróci strLastname + ", " w tym samym przypadku.

Chcąc pierwszego zachowania, napisałem następujące metody:

    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

Napisałem kilka metod rozszerzenia, aby zrobić to w sposób, który jest wydajny:

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

To zależy od

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

        return s+delim+item;
    }

3
Używanie operatora + do łączenia łańcuchów nie jest świetne, ponieważ spowoduje to przydzielenie nowego ciągu za każdym razem. Co więcej, chociaż StringBuilder można niejawnie rzutować na ciąg, wykonywanie tak często (każda iteracja pętli) w dużej mierze pokonałoby cel posiadania konstruktora ciągów.
Daniel Fortunov

2

Można używać .ToArray()na Listsi IEnumerables, a następnie użyj String.Join()jak chciałeś.


0

Jeśli ciągi, które chcesz połączyć, znajdują się na liście obiektów, możesz również zrobić coś takiego:

var studentNames = string.Join(", ", students.Select(x => x.name));
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.