LINQ按名称选择属性

12
我正在尝试在LINQ选择语句中使用变量。
以下是我目前的示例。
using System;
using System.Collections.Generic;
using System.Linq;
using Faker;

namespace ConsoleTesting
{
internal class Program
{
    private static void Main(string[] args)
    {
        List<Person> listOfPersons = new List<Person>
        {
            new Person(),
            new Person(),
            new Person(),
            new Person(),
            new Person(),
            new Person(),
            new Person(),
            new Person(),
            new Person(),
            new Person(),
            new Person()
        };

        var firstNames = Person.GetListOfAFirstNames(listOfPersons);

        foreach (var item in listOfPersons)
        {
            Console.WriteLine(item);
        }

        Console.WriteLine();
        Console.ReadKey();
    }


    public class Person
    {
        public string City { get; set; }
        public string CountryName { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public Person()
        {
            FirstName = NameFaker.Name();
            LastName = NameFaker.LastName();
            City = LocationFaker.City();
            CountryName = LocationFaker.Country();
        }

        public static List<string> GetListOfAFirstNames(IEnumerable<Person> listOfPersons)
        {
            return listOfPersons.Select(x => x.FirstName).Distinct().OrderBy(x => x).ToList();
        }

        public static List<string> GetListOfCities(IEnumerable<Person> listOfPersons)
        {
            return listOfPersons.Select(x => x.FirstName).Distinct().OrderBy(x => x).ToList();
        }

        public static List<string> GetListOfCountries(IEnumerable<Person> listOfPersons)
        {
            return listOfPersons.Select(x => x.FirstName).Distinct().OrderBy(x => x).ToList();
        }

        public static List<string> GetListOfLastNames(IEnumerable<Person> listOfPersons)
        {
            return listOfPersons.Select(x => x.FirstName).Distinct().OrderBy(x => x).ToList();
        }
    }
}
}

我是一位有用的助手,可以为您翻译文本。

我有一些不太DRY的代码,其中包含GetListOf...方法

我觉得我应该能做到这样

public static List<string> GetListOfProperty(
IEnumerable<Person> listOfPersons, string property)
        {
            return listOfPersons.Select(x =>x.property).Distinct().OrderBy(x=> x).ToList();
        }

但那不是有效的代码。我认为关键可能与创建函数有关。如果这是答案,我该怎么做?这里是第二次尝试使用反射,但这也行不通。
        public static List<string> GetListOfProperty(IEnumerable<Person> 
listOfPersons, string property)
        {
            Person person = new Person();
            Type t = person.GetType();
            PropertyInfo prop = t.GetProperty(property);
            return listOfPersons.Select(prop).Distinct().OrderBy(x => 
x).ToList();
}

我认为反射可能是一个死胡同/误导,但我想无论如何都要展示我的工作。

请注意,示例代码在现实中是简化的,它用于通过AJAX填充datalist以创建自动完成体验。该对象有20多个属性,我可以编写20多个方法来完成,但我觉得应该有一种DRY的方法来完成这个任务。此外,将其作为一个方法也可以大大清理我的控制器操作。

问题:

鉴于第一段代码,是否有一种方法可以通过将某些对象传递到选择语句中来将这些类似的方法抽象成单个方法?

感谢您的时间。


你能否用文字陈述你的问题,而不仅仅展示代码? - Crowcoder
在调用GetProperty()并在没有阅读MSDN文档的情况下寻求帮助之前,请先查看文档。你所需要的答案已经在文档中了。 - 15ee8f99-57ff-4f92-890c-b56153
2
@Crowcoder 这只是另一个想要根据属性名称(作为字符串)选择属性值的人。 - 15ee8f99-57ff-4f92-890c-b56153
@Crowcoder 我添加了空格以更轻松地查看问题。 - Luke Hammer
6个回答

18

你需要构建选择器。

.Select(x =>x.property).

手动操作。幸运的是,这不是一个棘手的问题,因为您希望它始终是相同类型(string),因此:

var x = Expression.Parameter(typeof(Person), "x");
var body = Expression.PropertyOrField(x, property);
var lambda = Expression.Lambda<Func<Person,string>>(body, x);

那么上面的 Select 变成了:

.Select(lambda).

(适用于基于IQueryable<T>的LINQ)或
.Select(lambda.Compile()).

(基于 IEnumerable<T> 的 LINQ)。

请注意,您可以通过 property 对最终形式进行缓存,这将是很好的做法。


那么,我对反射的理解还算正确吗? - Luke Hammer
@WizardHammer 可能可以 - 如果您喜欢,也可以通过反射来完成 - JamesFaix的答案展示了如何使用GetValue - 您还可以执行类似以下的操作(我现在没有IDE):var func = (Func<Person,string>)Delegate.CreateDelegate(typeof(Func<Person,string>), null, prop.GetGetMethod()); 并将 func 传递给 Select - Marc Gravell
谢谢Marc,我现在采用你的代码。我很好奇你和JamesFaix的答案之间的性能差异。 - Luke Hammer
@WizardHammer 如果你很少使用它:那没关系;如果你经常使用它 - 无论是从 Delegate.CreateDelegate 还是 Expression<T>.Compile 中创建委托,只要你缓存并重复使用委托实例 - 不要每次都生成它,这样会更快。 - Marc Gravell
这段代码我觉得比JamesFaix的例子难读,但是我还是成功实现了它,并且它能正常运行。 - Luke Hammer

6

根据您的示例,我认为您想要的是这个:

public static List<string> GetListOfProperty(IEnumerable<Person> 
    listOfPersons, string property)
{
    Type t = typeof(Person);         
    PropertyInfo prop = t.GetProperty(property);
    return listOfPersons
        .Select(person => (string)prop.GetValue(person))
        .Distinct()
        .OrderBy(x => x)
        .ToList();

typeof 是 C# 中的内置运算符,您可以将类型名称“传递”给它,它将返回相应的 Type 实例。它在编译时起作用,而不是在运行时,所以它不像普通函数那样工作。

PropertyInfo 有一个 GetValue 方法,它接受一个对象参数。该对象是要从中获取属性值的类型实例。如果您要针对静态属性进行操作,请使用 null 作为该参数。

GetValue 返回一个 object,您必须将其强制转换为实际类型。

person => (string)prop.GetValue(person) 是一个类似于此签名的 lambda 表达式:

string Foo(Person person) { ... }

如果您想让它适用于任何类型的属性,请将其定义为泛型,而不是硬编码为 string

public static List<T> GetListOfProperty<T>(IEnumerable<Person> 
    listOfPersons, string property)
{
    Type t = typeof(Person);         
    PropertyInfo prop = t.GetProperty(property);
    return listOfPersons
        .Select(person => (T)prop.GetValue(person))
        .Distinct()
        .OrderBy(x => x)
        .ToList();
}

我比@Marc Gravell更好地理解这段代码。在我的情况下,它将始终是一个字符串。 - Luke Hammer

4

在可能的情况下,我建议避免使用反射和硬编码字符串...

你可以定义一个扩展方法来接受 T 类型的函数选择器,这样你就可以处理除字符串属性之外的其他类型了。

public static List<T> Query<T>(this IEnumerable<Person> instance, Func<Person, T> selector)
{
    return instance
        .Select(selector)
        .Distinct()
        .OrderBy(x => x)
        .ToList();
}

假设你有一个人类,除了已经公开的属性外,还有一个类型为整数的id属性。

public class Person
{
    public int Id { get; set; }
    public string City { get; set; }
    public string CountryName { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

您只需使用类型安全的 Lambda 选择器获取结果即可。

var ids = listOfPersons.Query(p => p.Id);
var firstNames = listOfPersons.Query(p => p.FirstName);
var lastNames = listOfPersons.Query(p => p.LastName);
var cityNames = listOfPersons.Query(p => p.City);
var countryNames = listOfPersons.Query(p => p.CountryName);

编辑

看起来您确实需要将硬编码字符串作为属性输入,那么怎么样放弃一些动态性,使用一些确定性呢?

public static List<string> Query(this IEnumerable<Person> instance, string property)
{
    switch (property)
    {
        case "ids": return instance.Query(p => p.Id.ToString());
        case "firstName": return instance.Query(p => p.FirstName);
        case "lastName": return instance.Query(p => p.LastName);
        case "countryName": return instance.Query(p => p.CountryName);
        case "cityName": return instance.Query(p => p.City);
        default: throw new Exception($"{property} is not supported");
    }
}

并且可以通过这种方式访问所需的结果。
var cityNames = listOfPersons.Query("cityName");

+1 绝对不需要使用反射或 Expression,除非这些名称来自外部来源(例如数据库)。使用 Func,Luke。 - Kenneth K.
我同意这一点,因为它与我的示例(经过简化)相关。但实际上,这是由一个接收所需列表类型的Ajax调用驱动的,并期望返回一个Json对象。以下是基于@Marc Gravell示例的内容。 - Luke Hammer
@marcgravell的示例在理论上很好地完成了工作,但是想象一下,在开发过程的后期,您需要将LastName重命名为FamillyName。这意味着您还需要更新客户端,因为客户端会将LastName作为属性选择器发送,而该属性已不再存在... - Dan Dohotaru
似乎你使用了反射并处理了它的问题,或者你需要额外的维护开销。 - Luke Hammer
这确实是我在自己的代码中要做的事情。反射有时很慢,也存在维护问题。 - JamesFaix
显示剩余3条评论

2

您可以使用反射来完成此操作。我使用类似的方法。

只需更改您的反射尝试即可:

public static List<string> GetListOfValues(IEnumerable<Person> listOfPersons, string propertyName)
{
    var ret = new List<string>();

    PropertyInfo prop = typeof(Person).GetProperty(propertyName);
    if (prop != null)
        ret = listOfPersons.Select(p => prop.GetValue(p).ToString()).Distinct().OrderBy(x => x).ToList();

    return ret;
}

希望这有所帮助。

它基于C# 6


1
你也可以使用这个。对我有效。
public static class ObjectReflectionExtensions
{
    public static  object GetValueByName<T>(this T thisObject,  string propertyName)
    {
        PropertyInfo prop = typeof(T).GetProperty(propertyName);
        return prop.GetValue(thisObject);

    }
}

就像这样调用。
public static List<string> GetListOfProperty(IEnumerable<Person> listOfPersons, string propertyName)
    {
        return listOfPersons.Select(x =>(string)x.GetValueByName(propertyName)).Distinct().OrderBy(x=> x).ToList();
    }

-1

如果您想选择所有的值:

object[] foos = objects.Select(o => o.GetType().GetProperty("PropertyName").GetValue(o)).ToArray();

1
在我看来,这个答案并没有回答原问题,虽然提供了好的信息,但我认为它不适合作为这个问题的答案。 - Luke Hammer

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