Скажем, у меня есть набор массивов объектов одинаковой размерности, например:

var rows = new List<object[]>
{
    new object[] {1, "test1", "foo", 1},
    new object[] {1, "test1", "foo", 2},
    new object[] {2, "test1", "foo", 3},
    new object[] {2, "test2", "foo", 4},
};

И я хочу сгруппировать по одному или нескольким «столбцам» - какие из них следует определять динамически во время выполнения. Например, группировка по столбцам 1, 2 и 3 приведет к трем группам:

  • группа 1: [1, "test1", "foo"] (включает строки 1 и 2)
  • группа 2: [2, "test1", "foo"] (включает строку 3)
  • группа 3: [2, "test2", "foo"] (включает строку 4)

Конечно, я могу добиться этого с помощью своего рода настраиваемого группового класса, а также путем сортировки и повторения. Однако, похоже, я смогу сделать это намного чище с помощью группировки Linq. Но мой Linq-fu меня подводит. Любые идеи?

1
Tim Scott 31 Июл 2010 в 08:25

3 ответа

Лучший ответ

Решение @Matthew Whited хорошо, если вы заранее знаете столбцы группировки. Однако похоже, что вам нужно определить их во время выполнения. В этом случае вы можете создать компаратор равенства, который определяет равенство строк для GroupBy, используя настраиваемый набор столбцов:

rows.GroupBy(row => row, new ColumnComparer(0, 1, 2))

Компаратор проверяет равенство значений каждого указанного столбца. Он также объединяет хэш-коды каждого значения:

public class ColumnComparer : IEqualityComparer<object[]>
{
    private readonly IList<int> _comparedIndexes;

    public ColumnComparer(params int[] comparedIndexes)
    {
        _comparedIndexes = comparedIndexes.ToList();
    }

    #region IEqualityComparer

    public bool Equals(object[] x, object[] y)
    {
        return ReferenceEquals(x, y) || (x != null && y != null && ColumnsEqual(x, y));
    }

    public int GetHashCode(object[] obj)
    {
        return obj == null ? 0 : CombineColumnHashCodes(obj);
    }    
    #endregion

    private bool ColumnsEqual(object[] x, object[] y)
    {
        return _comparedIndexes.All(index => ColumnEqual(x, y, index));
    }

    private bool ColumnEqual(object[] x, object[] y, int index)
    {
        return Equals(x[index], y[index]);
    }

    private int CombineColumnHashCodes(object[] row)
    {
        return _comparedIndexes
            .Select(index => row[index])
            .Aggregate(0, (hashCode, value) => hashCode ^ (value == null ? 0 : value.GetHashCode()));
    }
}

Если это то, что вы будете делать часто, вы можете поместить это в метод расширения:

public static IGrouping<object[], object[]> GroupByIndexes(
    this IEnumerable<object[]> source,
    params int[] indexes)
{
    return source.GroupBy(row => row, new ColumnComparer(indexes));
}

// Usage

row.GroupByIndexes(0, 1, 2)

Расширение IEnumerable<object[]> будет работать только с .NET 4. Вам потребуется расширить List<object[]> непосредственно в .NET 3.5.

2
Tim Scott 3 Авг 2010 в 19:27
1
Вам не нужно просто xor хэш-коды. Если вы это сделаете, вы увеличите вероятность столкновения.
 – 
Matthew Whited
31 Июл 2010 в 15:27
Конечно! Красивое элегантное решение. В ColumnComparer было несколько небольших ошибок. Отредактировал ваш пост с поправками.
 – 
Tim Scott
31 Июл 2010 в 20:04
Уайтед: Вы правы, это неоптимальная реализация GetHashCode. Однако я хотел избежать этого беспорядочного обсуждения, поэтому выбрал подход с минимальным трением.
 – 
Bryan Watts
31 Июл 2010 в 20:19
Скотт: Спасибо за исправление ошибок, которые у меня были - было поздно :-) Я заметил, что вы удалили нулевую проверку в GetHashCode. Я включил это, потому что ColumnComparer является общедоступным типом. Если вы сделаете это private, где вы можете абсолютно гарантировать отсутствие нулей, то его можно безопасно удалить. Однако в будущем, пожалуйста, воздержитесь от стилистических изменений, таких как добавление локальной переменной в CombineColumnHashCodes. Для меня это лишнее, и я не хочу, чтобы его приняли за написанный мной код. Спасибо.
 – 
Bryan Watts
31 Июл 2010 в 20:23
@Bryan: Да, нулевая проверка должна быть там. Решарпер сказал мне, что это всегда будет ложью. Никогда раньше не видел, чтобы Решарпер ошибался в чем-то подобном. @Matthew Whited: Можете ли вы предложить более надежный способ реализации GetHashCode?
 – 
Tim Scott
31 Июл 2010 в 22:45

Если ваша коллекция содержит элементы с индексатором (например, ваш object[], вы можете сделать это так ...

var byColumn = 3;

var rows = new List<object[]> 
{ 
    new object[] {1, "test1", "foo", 1}, 
    new object[] {1, "test1", "foo", 2}, 
    new object[] {2, "test1", "foo", 3}, 
    new object[] {2, "test2", "foo", 4}, 
};

var grouped = rows.GroupBy(k => k[byColumn]);
var otherGrouped = rows.GroupBy(k => new { k1 = k[1], k2 = k[2] });

... Если вам не нравятся приведенные выше статические наборы, вы также можете сделать что-нибудь более интересное прямо в LINQ. Это предполагает, что ваши HashCodes будут работать для оценок Equals. Обратите внимание: вы можете просто написать IEqualityComparer<T>

var cols = new[] { 1, 2};

var grouped = rows.GroupBy(
    row => cols.Select(col => row[col])
               .Aggregate(
                    97654321, 
                    (a, v) => (v.GetHashCode() * 12356789) ^ a));

foreach (var keyed in grouped)
{
    Console.WriteLine(keyed.Key);
    foreach (var value in keyed)
        Console.WriteLine("{0}|{1}|{2}|{3}", value);
}
1
Matthew Whited 31 Июл 2010 в 15:49

Кратчайшее решение:

    int[] columns = { 0, 1 };

    var seed = new[] { rows.AsEnumerable() }.AsEnumerable();    // IEnumerable<object[]> = group, IEnumerable<group> = result

    var result = columns.Aggregate(seed, 
        (groups, nCol) => groups.SelectMany(g => g.GroupBy(row => row[nCol])));
0
Grozz 3 Авг 2010 в 13:47