Получение имени свойства и различных значений типа T из списка‹T› с отражением

У меня есть класс Product с набором свойств:

public class Product
{
     public string Id { get; set; }
     public string Name { get; set; }
     public string Categories { get; set; }
}

Из компонента я получаю List<Product>, и по нескольким причинам мне нужно использовать Reflection, чтобы получить свойства Product, а затем получить значения Distinct и их Count() для каждого свойства.

Можно ли достичь своей цели посредством размышления? Если нет, есть ли другой способ сделать это? Спасибо!

ОБНОВЛЕНИЕ

Проблема в том, что я не знаю заранее, какие свойства мне нужно использовать и какие свойства относятся к классу Product. Вот почему я думаю, что отражение - лучший вариант.

Я могу добиться того же результата, используя конструкцию Switch - Case, в которой переключатель сравнивает Имя свойства, извлеченное из класса, и каждый Case соответствует определенному Имени свойства. Но гибкости этого решения недостаточно для моей задачи


person CiccioMiami    schedule 02.05.2012    source источник
comment
С какой стати вы добавили бы ограничение на принуждение себя к использованию отражения, особенно для проблемы, которая естественно не требует этого или не работает с ним?   -  person Servy    schedule 02.05.2012
comment
Можете ли вы объяснить нам, почему вам нужно использовать Reflection? Может быть, есть лучшее решение (например, кастинг).   -  person Olivier Jacot-Descombes    schedule 02.05.2012
comment
Спасибо, ребята, я знаю, как это сделать с LINQ, это довольно тривиально. Проблема в том, что я не знаю заранее, какие свойства мне нужно использовать, а какие свойства находятся в классе Product. Вот почему я думаю, что отражение - лучший вариант.   -  person CiccioMiami    schedule 02.05.2012
comment
Я понимаю, что вы хотите выбирать свойства динамически; однако как может быть, что вы не знаете, какие свойства находятся в классе Product? Они должны быть известны во время компиляции.   -  person Olivier Jacot-Descombes    schedule 02.05.2012
comment
Я пытаюсь реализовать узкий поиск в веб-приложении с флажками, и мне нужно динамически создавать их (идентификатор флажка - это имя свойства), и аналогичным образом, когда я получаю запрос POST от пользователя, я должен взять идентификатор списка флажков и сравните с именем свойства продукта   -  person CiccioMiami    schedule 02.05.2012


Ответы (6)


Так что, похоже, вы просите о чем-то несколько ином, чем мы думали ранее. Вы не ищете количество отдельных значений или ищете количество дубликатов каждого отдельного значения, что по сути является группировкой с подсчетом каждой группы.

private static Dictionary<string, Dictionary<object, int>> 
    getDistinctValues<T>(List<T> list)
{
    var properties = typeof(T).GetProperties();

    var result = properties
        //The key of the first dictionary is the property name
        .ToDictionary(prop => prop.Name,
            //the value is another dictionary
            prop => list.GroupBy(item => prop.GetValue(item, null))
                //The key of the inner dictionary is the unique property value
                //the value if the inner dictionary is the count of that group.
                .ToDictionary(group => group.Key, group => group.Count()));

    return result;
}

В какой-то момент я разбил это на два метода, но я немного сократил их до такой степени, что я не думаю, что это необходимо. Если у вас возникли проблемы с пониманием всех уровней вложенности этого запроса, не стесняйтесь обращаться за дополнительными разъяснениями.

person Servy    schedule 02.05.2012
comment
как это помогает, когда ОП заявляет, что это должно быть сделано путем отражения? - person Andras Zoltan; 02.05.2012
comment
@AndrasZoltan Если нет, то есть ли другой способ сделать это? Это хороший другой способ. Если он не может или не хочет его использовать, это нормально, но его все же стоит предоставить. - person Servy; 02.05.2012
comment
Спасибо, ребята, я знаю, как это сделать с LINQ, это довольно тривиально. Проблема в том, что я не знаю заранее, какие свойства мне нужно использовать, а какие свойства находятся в классе Product. Вот почему я думаю, что отражение - лучший вариант. - person CiccioMiami; 02.05.2012
comment
Добавлено решение на основе отражения - person Servy; 02.05.2012
comment
спасибо, ваше решение действительно хорошее. Можете ли вы объяснить, почему вы используете HashSet? В случае, если какое-то свойство является коллекцией? - person CiccioMiami; 03.05.2012
comment
@CiccioMiami HashSet - это то, как значения различаются. HashSet не позволяет добавлять повторяющиеся значения; он сохранит только один из дубликатов. Это также эффективный способ хранения данных при одновременном удалении дубликатов. Метод Linq distinct делает почти то же самое, просто он не раскрывает HashSet, который он использовал, когда это было сделано, что на самом деле полезно в вашем контексте. (Это позволяет легко получить счет). - person Servy; 03.05.2012
comment
Хорошо понял. Но таким образом я не могу подсчитать, сколько вхождений одного и того же значения для определенного свойства в моем наборе - person CiccioMiami; 03.05.2012
comment
@CiccioMiami Похоже, что большинство из нас неправильно поняли проблему. Я полностью переписал свой ответ. - person Servy; 03.05.2012
comment
Да извините, возможно мой вопрос был не правильно сформулирован! Большое спасибо! - person CiccioMiami; 04.05.2012

private static int DistinctCount<T>(IEnumerable<T> items, string property)
{
    var propertyInfo = typeof(T).GetProperty(property);

    return items.Select(x => propertyInfo.GetValue(x, null)).Distinct().Count();
}

Применение:

List<Product> prods = GetProductsFromSomeplace();

int distinctCountById = DistinctCount(prods, "Id");
int distinctCountByName = DistinctCount(prods, "Name");
int distinctCountByCategories = DistinctCount(prods, "Categories");

Если вы хотите разрешить настраиваемый компаратор IEqualityComparer для свойств, у вас может быть перегрузка:

private static int DistinctCount<TItems, TProperty>(IEnumerable<TItems> items,
                                                    string property,
                                                    IEqualityComparer<TProperty> propertyComparer)
{
    var propertyInfo = typeof(TItems).GetProperty(property);

    return items.Select(x => (TProperty)propertyInfo.GetValue(x, null))
                                 .Distinct(propertyComparer).Count();
}

И используйте так:

List<Product> prods = GetProductsFromSomeplace();

int distinctCountById = DistinctCount(prods, "Id", new MyCustomIdComparer());

Где MyCustomIdComparer реализует IEqualityComparer<TProperty> (в данном случае IEC<int>)

person SimpleVar    schedule 02.05.2012

Я представляю решение ниже, но в идеале вам следует попытаться сломать абстракцию для этой проблемы, которая позволяет объекту, возвращающему IEnumerable<T>, предоставить список «фильтруемых» свойств T вместе со значениями, которые должны быть использовал. Таким образом, все, что возвращает данные из источника данных, может делать это с полным знанием. Это переносит большую часть работы обратно в ваш источник данных/службу/что-то еще, но делает ваш пользовательский интерфейс намного проще.

Поскольку вы не знаете свойства, вы можете сделать это (я предполагаю, что IEnumerable, потому что я предполагаю, что общего решения нет, поскольку вы говорите, что вам нужно отражение). Если у вас есть типизированное выражение (т. е. у вас действительно есть List<Product>), то общее решение будет лучше, поскольку оно избавит вас от необходимости получать первый элемент:

public Dictionary<string, IEnumerable<object>> 
  GetAllPropertyDistincts(IEnumerable unknownValues)
{
  //need the first item for the type:
  var first = unknownValues.Cast<object>().First(); //obviously must NOT be empty :)
  var allDistinct = first.GetType()
    .GetProperties(BindingFlags.Public | BindingFlags.Instance)
    .Select(p => new 
      { 
        PropName = p.Name,
        Distinct = unknownValues.Cast<object>().Select(
          o => property.GetValue(o, null)
        ).Distinct() 
      }).ToDictionary(v => v.PropName, v => v.Distinct);
}

Теперь у вас есть словарь, содержащий имя свойства каждого отдельного значения для каждого свойства каждого объекта в вашем нетипизированном перечислимом (ну, если предположить, что все они одного типа или базы). Примечание. Могут быть некоторые проблемы со свойствами определенных типов и значением по умолчанию IEqualityComparer, которое использует метод расширения Distinct, потому что это общий метод, и на данный момент он будет использовать EqualityComparer<object>.Default, что не обязательно будет работать для некоторых типов.

Чтобы превратить это в общее решение, вы можете просто изменить первые четыре строки на:

public Dictionary<string, IEnumerable<object>> 
  GetAllPropertyDistincts<T>(IEnumerable<T> unknownValues)
{
  var allDistinct = typeof(T)

С следующей строкой .GetProperties(BindingFlags.Public | BindingFlags.Instance), а затем измените внутренний вызов unknownValues.Cast<object>().Select( на просто unknownValues.Select(.

person Andras Zoltan    schedule 02.05.2012

Если список не типизирован с Product, а действительно с открытым общим параметром T и этот параметр не имеет ограничений (where T : Product), то приведение может помочь

int count = list
    .Cast<Product>()
    .Select(p => p.Id)
    .Distinct()
    .Count();  
person Olivier Jacot-Descombes    schedule 02.05.2012

Итак, я немного запутался со своим ответом, но вот он... полноценный счетчик уникальных значений. Это не полностью отвечает на ваш вопрос, но должно стать хорошим началом для подсчета свойства данного объекта. Использование этого в сочетании с циклическим просмотром всех свойств объекта должно помочь: p

/// <summary>
/// A distinct value counter, using reflection
/// </summary>
public class DistinctValueCounter<TListItem>
{
    /// <summary>
    /// Gets or sets the associated list items
    /// </summary>
    private IEnumerable<TListItem> ListItems { get; set; }

    /// <summary>
    /// Constructs a new distinct value counter
    /// </summary>
    /// <param name="listItems">The list items to check</param>
    public DistinctValueCounter(IEnumerable<TListItem> listItems)
    {
        this.ListItems = listItems;
    }

    /// <summary>
    /// Gets the distinct values, and their counts
    /// </summary>
    /// <typeparam name="TProperty">The type of the property expected</typeparam>
    /// <param name="propertyName">The property name</param>
    /// <returns>A dictionary containing the distinct counts, and their count</returns>
    public Dictionary<TProperty, int> GetDistinctCounts<TProperty>(string propertyName)
    {
        var result = new Dictionary<TProperty, int>();

        // check if there are any list items
        if (this.ListItems.Count() == 0)
        {
            return result;
        }

        // get the property info, and check it exists
        var propertyInfo = this.GetPropertyInfo<TProperty>(this.ListItems.FirstOrDefault(), propertyName);
        if (propertyInfo == null)
        {
            return result;
        }

        // get the values for the property, from the list of items
        return ListItems.Select(item => (TProperty)propertyInfo.GetValue(item, null))
            .GroupBy(value => value)
            .ToDictionary(value => value.Key, value => value.Count());
    }

    /// <summary>
    /// Gets the property information, for a list item, by its property name
    /// </summary>
    /// <typeparam name="TProperty">The expected property type</typeparam>
    /// <param name="listItem">The list item</param>
    /// <param name="propertyName">The property name</param>
    /// <returns>The property information</returns>
    private PropertyInfo GetPropertyInfo<TProperty>(TListItem listItem, string propertyName)
    {
        // if the list item is null, return null
        if (listItem == null)
        {
            return null;
        }

        // get the property information, and check it exits
        var propertyInfo = listItem.GetType().GetProperty(propertyName);
        if (propertyInfo == null)
        {
            return null;
        }

        // return the property info, if it is a match
        return propertyInfo.PropertyType == typeof(TProperty) ? propertyInfo : null;
    }
}

Применение:

var counter = new DistinctValueCounter<Person>(people);
var resultOne = counter.GetDistinctCounts<string>("Name");
person Richard    schedule 02.05.2012

Если я понимаю цель, вы сможете просто использовать LINQ:

List<Product> products = /* whatever */
var distinctIds = products.Select(p=>p.Id).Distinct();
var idCount = distinctIds.Count();
...
person goric    schedule 02.05.2012
comment
Обратите внимание, что для этого вы повторяете список дважды; вы можете ToList различать значения, чтобы запрашивать список только один раз. - person Servy; 02.05.2012