【问题标题】:Dynamic lambda for IEnumerable.SelectIEnumerable.Select 的动态 lambda
【发布时间】:2021-01-13 03:04:46
【问题描述】:

我需要将一个大表分解为一系列 2 列的表,以便为配置器引擎动态创建表规则。这段代码演示了这个问题:

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

namespace Spike
{
    class Program
    {
        static void Main(string[] args)
        {
            // The actual data I need to break down has ~20 properties of type string and decimal, over 18,000 rows
            var data = new List<MyData>()
            {
                new MyData("one", "two", 3m, "four"),
                new MyData("five", "six", 7m, "eight"),
                new MyData("nine", "ten", 11m, "twelve"),
                new MyData("thirteen", "fourteen", 15m, "sixteen"),
                new MyData("one", "five", 9m, "thirteen"),
                new MyData("two", "six", 10m, "fourteen"),
                new MyData("three", "seven", 11m, "fifteen"),
                new MyData("four", "eight", 12m, "sixteen")
            };

            // This shows the desired combinations of properties
            // The actual data will have ~230 combinations
            var properties = typeof(MyData).GetProperties(BindingFlags.Instance | BindingFlags.Public);
            for (var i = 0; i < properties.Length - 1; i++)
            {
                for (var j = i + 1; j < properties.Length; j++)
                {
                    Console.WriteLine($"{properties[i].Name} <=> {properties[j].Name}");
                }
            }
            /* output:
                P1 <=> P2
                P1 <=> P3
                P1 <=> P4
                P2 <=> P3
                P2 <=> P4
                P3 <=> P4
            */

            // This shows how I want one combination to appear
            // The challenge seems to be the creation of a dynamic lambda in the Select method.
            var items = data.Select(x => new { x.P2, x.P3 }).Distinct().ToList();
            Console.WriteLine();
            items.ForEach(x => Console.WriteLine($"{x.P2}, {x.P3}"));
            /* output:
                two, 3
                six, 7
                ten, 11
                fourteen, 15
                five, 9
                six, 10
                seven, 11
                eight, 12
            */

            Console.ReadKey();
        }
    }

    public class MyData
    {
        public string P1 { get; set; }
        public string P2 { get; set; }
        public decimal P3 { get; set; }
        public string P4 { get; set; }

        public MyData(string p1, string p2, decimal p3, string p4)
        {
            P1 = p1;
            P2 = p2;
            P3 = p3;
            P4 = p4;
        }
    }
}

我研究过 Linq、反射和表达式树,但似乎无法克服动态构建此表达式的障碍:

var items = data.Select(x => new { x.P2, x.P3 }).Distinct().ToList();

其中 x.P2 和 x.P3 是动态的。

This post 似乎正朝着正确的方向前进,但我没有得到结果。

建议?提前致谢!

【问题讨论】:

  • 您的预期输出是什么?我不明白你在做什么。
  • 不确定你真的需要Expression,你可以用属性getter的CreateDelegate来做。但是你想做什么?获得不同的属性组合?你考虑过{p1,p2} - {p2,p1}吗?
  • 嗨@Charlieface 和@Sweeper 是的,我所做的似乎没有意义,但在第三方配置引擎中是必需的。这就像尝试一次过滤两列的 Excel 电子表格 - 分别建立每列与所有其他列的关系。在这种情况下,{p1, p2} 意味着 {p2, p1}。我会看看CreateDelegate。谢谢!
  • 如果你愿意,很高兴为你写点东西

标签: c# linq reflection expression-trees


【解决方案1】:

希望我能正确理解您的问题。这是枚举所需对的简单扩展:

var items = data.EnumeratePropPairs().Distinct().ToList();
items.ForEach(x => Console.WriteLine($"{x.Item1}, {x.Item2}"));

及实施

public static class EnumerableExtensions
{
    public static IEnumerable<Tuple<string, string>> EnumeratePropPairs<T>(this IEnumerable<T> items)
    {
        var properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
        var param = Expression.Parameter(typeof(T));
        var accessors = properties.ToDictionary(p => p, p =>
        {
            var body = (Expression)Expression.MakeMemberAccess(param, p);
            if (body.Type != typeof(string))
            {
                body = Expression.Call(body, "ToString", Type.EmptyTypes);
            }

            var lambda = Expression.Lambda<Func<T, string>>(body, param);
            return lambda.Compile();
        });

        var pairs = new List<Tuple<Func<T, string>, Func<T, string>>>();

        for (var i = 0; i < properties.Length - 1; i++)
        {
            var prop1 = properties[i];
            var prop1Accessor = accessors[prop1];

            for (var j = i + 1; j < properties.Length; j++)
            {
                var prop2 = properties[j];
                var prop2Accessor = accessors[prop2];

                pairs.Add(Tuple.Create(prop1Accessor, prop2Accessor));
            }
        }

        return items.SelectMany(item => pairs.Select(p => Tuple.Create(p.Item1(item), p.Item2(item))));
    }
}

【讨论】:

  • 我喜欢在匿名类型上使用元组,但这个答案要求所有源属性都是字符串。我的问题混合了字符串和小数,最终可能有其他简单类型,如 bool、float 等。我会看看我是否能适应我的需求,如果成功,我会标记为答案。谢谢!
  • 其实我已经把其他类型转成字符串了。如果没问题,我们可以返回object ;)
  • @Tim 您可以将此处的 lambda 替换为 CreateDelegate,它应该具有更快的启动速度。 p =&gt; p.GetMethod.CreateDelegate(typeof(Func&lt;T, string&gt;), null) 您也可以将 Tuple&lt;string, string&gt; 替换为 ValueTuple (string, string)
【解决方案2】:

幸运的是,我偶然发现了这个Fiddle 的答案,它使用了 NuGet 包 LatticeUtils.Core。这个 sn-p 说明了结果:

            var properties = typeof(MyData).GetProperties(BindingFlags.Instance | BindingFlags.Public);
            for (var i = 0; i < properties.Length - 1; i++)
            {
                for (var j = i + 1; j < properties.Length; j++)
                {
                    var subTable = data.SelectDynamic(new[] { properties[i].Name, properties[j].Name }).Distinct();
                    Console.WriteLine($"{properties[i].Name} <=> {properties[j].Name}: {subTable.Count()}");
                }
            }

使用 18,753 行和 21 列的源数据集,输出为

P1 <=> P2: 26
...
P2 <=> P3: 18
... and so forth

这允许我以编程方式创建目标系统将接受的 210 个两列表,这对于人类使用供应商的 UI 输入是不切实际的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-04-09
    • 1970-01-01
    • 2013-03-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-31
    相关资源
    最近更新 更多