Использование метода Enumerable.Aggregate(...) над пустой последовательностью

Я хотел бы использовать метод Enumerable.Aggregate(...), чтобы объединить список строк, разделенных точкой с запятой. Скорее легко, не так ли?

Учитывая следующее:

  • private const string LISTSEPARATOR = "; ";
  • . OrderedTracks - List<TrackDetails>
  • В TrackDetails есть DiscNumber Int16? Свойство

Следующий оператор вызовет исключение, если последовательность, возвращаемая Distinct(), пуста (поскольку метод Aggregate() не применяется в пустой последовательности):

    txtDiscNumber.Text = album.OrderedTracks
        .Where(a => a.DiscNumber.HasValue)
        .Select(a => a.DiscNumber.Value.ToString())
        .Distinct()
        .Aggregate((i, j) => i + LISTSEPARATOR + j);

Обходной путь, который я использую:

    List<string> DiscNumbers = 
        album.OrderedTracks
            .Where(a => a.DiscNumber.HasValue)
            .Select(a => a.DiscNumber.Value.ToString())
            .Distinct()
            .ToList();

    if (!DiscNumbers.Any())
        txtDiscNumber.Text = null;
    else
        txtDiscNumber.Text = 
            DiscNumbers.Aggregate((i, j) => i + LISTSEPARATOR + j);

Есть ли лучшее решение? Возможно ли это сделать в одном заявлении LINQ?

Спасибо заранее.

Ответ 1

Чтобы объединить список строк, используйте метод string.Join.

Функция Aggregate не работает с пустыми коллекциями. Для этого требуется двоичная функция накопления, и в коллекции нужно, чтобы элемент в коллекции передавался двоичной функции в качестве начального значения.

Однако существует перегрузка Aggregate:

public static TResult Aggregate<TSource, TAccumulate, TResult>(
    this IEnumerable<TSource> source,
    TAccumulate seed,
    Func<TAccumulate, TSource, TAccumulate> func,
    Func<TAccumulate, TResult> resultSelector
)

Эта перегрузка позволяет указать начальное значение. Если задано начальное значение, оно также будет использоваться в качестве результата, если коллекция пуста.

EDIT: Если вы действительно хотите использовать Aggregate, вы можете сделать это следующим образом:

sequence.Aggregate(string.Empty, (x, y) => x == string.Empty ? y : x + Separator + y)

Или с помощью StringBuilder:

sequence.Aggregate(new StringBuilder(), (sb, x) => (sb.Length == 0 ? sb : sb.Append(Separator)).Append(x)).ToString()

Ответ 2

Я думаю, вы могли бы найти следующий вспомогательный метод расширения.

public static TOut Pipe<TIn, TOut>(this TIn _this, Func<TIn, TOut> func)
{
    return func(_this);
}

Он позволяет вам выразить свой запрос следующим образом.

txtDiscNumber.Text = album.OrderedTracks
    .Where(a => a.DiscNumber.HasValue)
    .Select(a => a.DiscNumber.Value.ToString())
    .Distinct()
    .Pipe(items => string.Join(LISTSEPARATOR, items));

Это все еще читается "сверху вниз", что значительно облегчает читаемость.

Ответ 3

Используйте String.Join следующим образом:

 txtDiscNumber.Text = String.Join(LISTSEPARATOR,
      album.OrderedTracks
                  .Where(a => a.DiscNumber.HasValue)
                  .Select(a => a.DiscNumber.Value.ToString())
                  .Distinct());

Ответ 4

Использованные методы, подобные этому, для целей отладки, придумали два метода расширения:

public static string Concatenate<T, U>(this IEnumerable<T> source, Func<T, U> selector, string separator = ", ")
{
    if (source == null)
    {
        return string.Empty;
    }

    return source
        .Select(selector)
        .Concatenate(separator);
}

public static string Concatenate<T>(this IEnumerable<T> source, string separator = ", ")
{
    if (source == null)
    {
        return string.Empty;
    }

    StringBuilder sb = new StringBuilder();
    bool firstPass = true;
    foreach (string item in source.Distinct().Select(x => x.ToString()))
    {
        if (firstPass)
        {
            firstPass = false;
        }
        else
        {
            sb.Append(separator);
        }

        sb.Append(item);
    }

    return sb.ToString();
}

Используйте это:

string myLine = myCol.Concatenate(x => x.TheProperty);