过滤列表对象的通用方法

5
我正在尝试创建一个通用方法,需要三个参数: 1)List集合 2)String属性名称 3)String筛选字符串
这个想法是我们传递一个对象的集合、对象属性的名称和筛选条件,它会返回一个包含该属性包含筛选字符串的对象列表。
另外,PropertyName是可选的,如果没有提供,我希望返回所有包含FilterString的属性的对象。
对此有任何建议将非常有帮助。
我尝试使用以下方法签名: public static List FilterList(List collection, String FilterString, String Property = "")
这样,我可以从任何地方调用此方法并传递任何List,它将返回一个经过筛选的列表。

1
你需要使用反射和/或表达式树。 - SLaks
你需要使用一些反射。试着去做,当你遇到具体问题时再回来! - Jeroen Vannevel
2
为什么不直接使用Linq呢? - Brian Rogers
1
忘掉基于字符串的东西,使用LINQ。 - Federico Berasategui
4个回答

8

您可以使用LINQ来实现您想要做的事情,例如:

var collection = ...
var filteredCollection = 
    collection.Where(item => item.Property == "something").ToList();

否则,你可以尝试反射。
public List<T> Filter<T>(
    List<T> collection, 
    string property, 
    string filterValue)
{
    var filteredCollection = new List<T>();
    foreach (var item in collection)
    {
         // To check multiple properties use,
         // item.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)

         var propertyInfo = 
             item.GetType()
                 .GetProperty(property, BindingFlags.Public | BindingFlags.Instance);
         if (propertyInfo == null)
             throw new NotSupportedException("property given does not exists");             

         var propertyValue = propertyInfo.GetValue(item, null);
         if (propertyValue == filterValue)
             filteredCollection.Add(item);       
    }

    return filteredCollection;
}

这种解决方案的问题在于,属性名称更改或拼写错误会导致运行时错误,而不是使用实际属性表达式(其中名称是硬编码)会产生编译错误。
此外,请注意,基于绑定标志,这仅适用于公共、非静态属性。您可以通过传递不同的 标志 来修改此类行为。

谢谢您的回答,但我该如何将其变成一个可以接受任何List的方法,以便于List<SomeObject>不同,该方法看起来像这样; public static List<T> FilterList<T>(List<T> collection, String FilterString, String Property = "") - w2olves
你可以直接将 SomeObject 更改为 T,并使该方法成为泛型。我已经给你提供了一个提示。 - rae1
@user3067743,通过使用动态LINQ是可能的,我已经提供了答案。 - Konrad Kokosa
这是只允许 string 属性的正确方式吗?if (propertyInfo == null || propertyInfo.PropertyType != typeof(string)) throw new NotSupportedException("Unsupported property."); - Azimuth

5
您可以使用反射和动态表达式来实现此功能。我已经编写了一个示例,可能看起来有点长。但是,它符合您的要求,并通过以下方式解决了这些问题:
  • 使用反射查找类型为字符串并与属性名称匹配(如果提供)的属性。
  • 创建一个表达式,调用所有已识别属性上的string.Contains。如果已识别多个属性,则通过Or表达式组合对string.Contains的调用。将筛选器表达式编译并作为参数传递给Where扩展方法。使用表达式过滤所提供的列表。

请访问此链接以运行示例。

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using System.Linq.Expressions;

public class Test
{
    public static IEnumerable<T> SelectItems<T>(IEnumerable<T> items, string propName, string value)
    {
        IEnumerable<PropertyInfo> props;
        if (!string.IsNullOrEmpty(propName))
            props = new PropertyInfo[] { typeof(T).GetProperty(propName) };
        else
            props = typeof(T).GetProperties();
        props = props.Where(x => x != null && x.PropertyType == typeof(string));
        Expression lastExpr = null;
        ParameterExpression paramExpr = Expression.Parameter(typeof(T), "x");
        ConstantExpression valueExpr = Expression.Constant(value);
        foreach(var prop in props)
        {
            var propExpr = GetPropertyExpression(prop, paramExpr, valueExpr);
            if (lastExpr == null)
                lastExpr = propExpr;
            else
                lastExpr = Expression.MakeBinary(ExpressionType.Or, lastExpr, propExpr);
        }
        if (lastExpr == null)
            return new T[] {};
        var filterExpr = Expression.Lambda(lastExpr, paramExpr);
        return items.Where<T>((Func<T, bool>) filterExpr.Compile());
    }

    private static Expression GetPropertyExpression(PropertyInfo prop, ParameterExpression paramExpr, ConstantExpression valueExpr)
    {
        var memberAcc = Expression.MakeMemberAccess(paramExpr, prop);
        var containsMember = typeof(string).GetMethod("Contains");
        return Expression.Call(memberAcc, containsMember, valueExpr);
    }

    class TestClass
    {
        public string SomeProp { get; set; }
        public string SomeOtherProp { get; set; }
    }

    public static void Main()
    {
        var data = new TestClass[] {
            new TestClass() { SomeProp = "AAA", SomeOtherProp = "BBB" }, 
            new TestClass() { SomeProp = "BBB", SomeOtherProp = "CCC" }, 
            new TestClass() { SomeProp = "CCC", SomeOtherProp = "AAA" }, 
        };
        var result = SelectItems(data, "", "A");
        foreach(var item in result)
            Console.WriteLine(item.SomeProp);
    }
}

与完全基于反射的方法相比,这种方法仅组装并编译过滤表达式一次,因此我期望会有(小的)性能提升。

1
我必须说,这非常简洁明了。谢谢你。由于我的新手级别,我无法将其标记为有用,但我发现它很有用,我相信未来许多人也会这样认为。谢谢啊... - w2olves
哇,这太神奇了,我不知道 C# 竟然可以做到这一点。非常棒的编码,谢谢! - Zach Ioannou
@harris 不用谢。很高兴能帮到你 :-) - Markus

3

您应该使用动态LINQ,例如,给定SomeClass

public class SomeClass 
{
    public int SomeField { get; set; }
}
List<SomeClass> list = new List<SomeClass>() { new SomeClass() { SomeField = 2 } };

接下来:

var temp = list.AsQueryable().Where("SomeField == 1").Select("it");
var result= temp .Cast<SomeClass>().ToList();

所以,你的函数会更简单,名称和过滤器将合并为一个参数:
public List<T> Filter<T>(List<T> list, string filter)
{
    var temp = list.AsQueryable().Where(filter).Select("it");
    return temp.Cast<T>().ToList();
}

您可以提供不同的过滤器,例如"SomeField > 4 && SomeField < 10"等。


我也尝试了你的解决方案,它像广告中所说的一样有效。性能上没有任何区别。谢谢Konrad。 - w2olves

2
当使用Markus的解决方案时,只有在所有字符串属性都不为null时才能正常工作。 为确保这一点,您可以这样做:
class TestClass
{
    private string _someProp { get; set; }
    public string SomeProp { 
        get
        {
            if(string.IsNullOrEmpty(_someProp)
            {
                _someProp = "";
            }
            return _someProp;
        }
        set
        {
            _someProp = value;
        }
    }
    private string _someOtherProp { get; set; }
    public string SomeOtherProp { 
        get
        {
            if(string.IsNullOrEmpty(_someOtherProp)
            {
                _someOtherProp = "";
            }
            return _someOtherProp;
        }
        set
        {
            _someOtherProp = value;
        }
    }
}

由于我的声望不足50,我无法发表评论;)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接