Создание ILookups

У меня есть ILookup, сгенерированный каким-то сложным выражением. Допустим, это поиск людей по фамилии. (В нашей упрощенной модели мира фамилии уникальны по фамилии)

ILookup<string, Person> families;

Теперь у меня есть два вопроса, которые меня интересуют, как построить.

Во-первых, как бы я фильтровал по фамилии?

var germanFamilies = families.Where(family => IsNameGerman(family.Key));

Но здесь, germanFamilies является IEnumerable<IGrouping<string, Person>>; если я позвоню ToLookup() на это, я бы лучше поспорил бы получить IGrouping<string, IGrouping<string, Person>>, Если я попытаюсь быть умным и позвонить SelectMany Во-первых, в конечном итоге компьютер делал много ненужной работы. Как бы вы легко преобразовали это перечисление в поиск?

Во-вторых, я хотел бы получить только для взрослых.

var adults = families.Select(family =>
         new Grouping(family.Key, family.Select(person =>
               person.IsAdult())));

Здесь я столкнулся с двумя проблемами: Grouping Тип не существует (кроме как внутренний внутренний класс Lookup), и даже если бы это было так, у нас была бы проблема, о которой говорилось выше.

Таким образом, кроме полной реализации интерфейсов ILookup и IGrouping или заставления компьютера выполнять глупые объемы работы (перегруппировать то, что уже было сгруппировано), есть ли способ изменить существующие ILookups для создания новых, которые я пропустил?

3 ответа

Решение

(Я предполагаю, что вы действительно хотите отфильтровать по фамилии, учитывая ваш запрос.)

Вы не можете изменить любую реализацию ILookup<T> что я в курсе. Это, безусловно, возможно реализовать ToLookup с неизменным поиском, как вы четко знаете:)

Что вы можете сделать, однако, это изменить, чтобы использовать Dictionary<string, List<Person>>:

var germanFamilies = families.Where(family => IsNameGerman(family.Key))
                             .ToDictionary(family => family.Key,
                                           family.ToList());

Этот подход также работает для вашего второго запроса:

var adults = families.ToDictionary(family => family.Key,
                                   family.Where(person => persion.IsAdult)
                                         .ToList());

Хотя все еще выполняется немного больше работы, чем мы могли бы подумать, это не так уж плохо.

РЕДАКТИРОВАТЬ: обсуждение с Ани в комментариях стоит прочитать. По сути, мы уже собираемся перебирать каждого человека в любом случае - поэтому, если мы предполагаем поиск и вставку словаря O(1), мы на самом деле не лучше с точки зрения сложности времени, используя существующий поиск, чем сглаживание:

var adults = families.SelectMany(x => x)
                     .Where(person => person.IsAdult)
                     .ToLookup(x => x.LastName);

В первом случае мы могли бы потенциально использовать существующую группировку, например так:

// We'll have an IDictionary<string, IGrouping<string, Person>>
var germanFamilies = families.Where(family => IsNameGerman(family.Key))
                             .ToDictionary(family => family.Key);

Это потенциально намного более эффективно (если в каждой семье много людей), но означает, что мы используем группировки "вне контекста". Я считаю, что на самом деле все в порядке, но почему-то у меня во рту немного странный вкус. Как ToLookup материализует запрос, трудно понять, как он может на самом деле пойти не так...

Для вашего первого запроса, как насчет реализации вашего собственного FilteredLookup возможность воспользоваться пришествием от другого ILookup?
(спасибо Джону Скиту за подсказку)

public static ILookup<TKey, TElement> ToFilteredLookup<TKey, TElement>(this ILookup<TKey, TElement> lookup, Func<IGrouping<TKey, TElement>, bool> filter)
{
    return new FilteredLookup<TKey, TElement>(lookup, filter);
}

С FilteredLookup класс существо:

internal sealed class FilteredLookup<TKey, TElement> : ILookup<TKey, TElement>
{
    int count = -1;
    Func<IGrouping<TKey, TElement>, bool> filter;
    ILookup<TKey, TElement> lookup;

    public FilteredLookup(ILookup<TKey, TElement> lookup, Func<IGrouping<TKey, TElement>, bool> filter)
    {
        this.filter = filter;
        this.lookup = lookup;
    }

    public bool Contains(TKey key)
    {
        if (this.lookup.Contains(key))
            return this.filter(this.GetGrouping(key));
        return false;
    }

    public int Count
    {
        get
        {
            if (count >= 0)
                return count;
            count = this.lookup.Where(filter).Count();
            return count;
        }
    }

    public IEnumerable<TElement> this[TKey key]
    {
        get
        {
            var grp = this.GetGrouping(key);
            if (!filter(grp))
                throw new KeyNotFoundException();
            return grp;
        }
    }

    public IEnumerator<IGrouping<TKey, TElement>> GetEnumerator()
    {
        return this.lookup.Where(filter).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    private IGrouping<TKey, TElement> GetGrouping(TKey key)
    {
        return new Grouping<TKey, TElement>(key, this.lookup[key]);
    }
}

и группировка:

internal sealed class Grouping<TKey, TElement> : IGrouping<TKey, TElement>
{
    private readonly TKey key;
    private readonly IEnumerable<TElement> elements;

    internal Grouping(TKey key, IEnumerable<TElement> elements)
    {
        this.key = key;
        this.elements = elements;
    }

    public TKey Key { get { return key; } }

    public IEnumerator<TElement> GetEnumerator()
    {
        return elements.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

Итак, в основном ваш первый запрос будет:

var germanFamilies = families.ToFilteredLookup(family => IsNameGerman(family.Key));

Это позволяет вам избежать повторного сглаживания-фильтрации-ToLookup или создания нового словаря (и, таким образом, снова хэшировать ключи).

Для второго запроса идея будет похожей, вам просто нужно создать похожий класс, не фильтруя для всего IGrouping но для элементов IGrouping,

Просто идея, может быть, это не может быть быстрее, чем другие методы:)

Поиск создает индекс с типом ключа и универсальным индексатором типа значения. Вы можете добавлять в поиск и удалять из поиска, используя concat для добавления и итерации, а также удаляя ключевые элементы во временном списке, а затем перестраивая поиск. Затем поиск работает как словарь, извлекая тип значения по ключу.

      public async Task TestILookup()
{
    // Lookup<TKey,TElement>
    List<Product> products = new List<Product>
        {
            new Product { ProductID = 1, Name = "Kayak", Category = "Watersports", Price = 275m },
            new Product { ProductID = 2, Name = "Lifejacket", Category = "Watersports", Price = 48.95m },
            new Product { ProductID = 3, Name = "Soccer Ball", Category = "Soccer", Price = 19.50m },
            new Product { ProductID = 4, Name = "Corner Flag", Category = "Soccer", Price = 34.95m }
         };

    // create an indexer
    ILookup<int, Product> lookup = (Lookup<int,Product>) products.ToLookup(p => p.ProductID, p => p);

    Product newProduct = new Product { ProductID = 5, Name = "Basketball", Category = "Basketball", Price = 120.15m };

    lookup = lookup.SelectMany(l => l)
                   .Concat(new[] { newProduct })
                   .ToLookup(l => l.ProductID, l=>l);

    foreach (IGrouping<int, Product> packageGroup in lookup)
    {
        // Print the key value of the IGrouping.
        output.WriteLine("ProductID Key {0}",packageGroup.Key);

        // Iterate over each value in the IGrouping and print its value.
        foreach (Product product in packageGroup)
            output.WriteLine("Name {0}", product.Name);
    }

    Assert.Equal(lookup.Count(), 5);
}

public class Product
{
    public int ProductID { get; set; }
    public string Name { get; set; }
    public string Category { get; set; }
    public decimal Price { get; set; }
}

Выход:

      ProductID Key 1
Name Kayak
ProductID Key 2
Name Lifejacket
ProductID Key 3
Name Soccer Ball
ProductID Key 4
Name Corner Flag
ProductID Key 5
Name Basketball
Другие вопросы по тегам