【问题标题】:Is it possible to know what actions have been called on an IEnumerable?是否有可能知道在 IEnumerable 上调用了哪些操作?
【发布时间】:2020-10-16 18:00:35
【问题描述】:

我有几个条件可能会影响列表中使用的过滤器 (.Where(...))。在某些时候会引发异常,我想知道在此之前列表上调用了哪些操作。

这样的事情可能吗?

var myList = new List<SomeClass>();

myList = myList.Where(item => item.property == value);
.
.
.
myList = myList.Where(item => item.otherProperty < otherValue);

Console.WriteLine(myList.ToActionsString());

它可能会打印如下内容:

list.Where(i => i.property == <the actual value>)
    .Where(i => i.otherProperty < <the actual otherValue>)

只是在列表中调用toString() 并不能准确地提供任何相关信息,并且仅列出列表中的项目是不感兴趣的。

【问题讨论】:

  • 在 VS-->Debug-->Window-->Exception Settings 中启用“抛出时中断”。向您准确显示异常发生的位置。
  • 这似乎是您试图通过为自己创造更多的工作来节省自己的工作。为什么不直接将输入记录到条件中,并显示它是针对什么条件的消息
  • 到目前为止,列表中已经调用了哪些操作 LINQ 并不是这样工作的。代表逐项应用于列表。因此,您的第一个 Where 子句应用于第一项,然后是第二个 Where 子句……然后两个 Where 子句都应用于第二项……等等。这称为延迟执行。您无法记录任何描述到目前为止如何过滤列表的内容,因为该列表没有按照您的代码出现的顺序逐步过滤。除非你在每个子句之间调用ToList()
  • 最重要的是,新列表不记得它的来源——这里询问的内容本质上是某种审计线索。好主意。在 99.9999% 的情况下完全没用(即在大多数用例中没有人用过),但在所有这些情况下都会花费大量资金。

标签: c# list ienumerable


【解决方案1】:

警告:这是有开销的,因为编译器必须创建所有 Expression 对象(然后必须在运行时分配和编译)。 谨慎使用

您可以使用AsQueryable 并依靠内置EnumerableQueryExpression 类的ToString 逻辑来执行此操作。以下扩展方法会将您的查询转换为它的文本表示:

public static string GetText<T>(this IQueryable<T> query) {
   retury query.Expression.ToString();
}

可以这样使用:

var list = new List<int>();
var query = list.AsQueryable()
                .Select((c, i) => c * (i + 1))
                .Where(c => c > 5)
                .Where(c => c < 10 && c != 7)
                .Take(2)
                .OrderBy(x => 1);   
var text = query.GetText();

这会导致以下结果:

System.Collections.Generic.List`1[System.Int32].Select((c, i) => (c * (i + 1))).Where(c => (c > 5)).Where(c => ((c < 10) AndAlso (c != 7))).Take(2).OrderBy(x => 1)

我们可以在混合中添加一个匿名类型来看看它的外观:

var query = list.AsQueryable()
                .Select((c, i) => c * (i + 1))
                .Select(x => new { Value = x, ValueSquared = x * x });
var result = query.GetText();

将打印的内容:

System.Collections.Generic.List`1[System.Int32].Select((c, i) => (c * (i + 1))).Select(x => new <>f__AnonymousType0`2(Value = x, ValueSquared = (x * x)))

通过使用Expression 操作,我们可以让这个方法更加健壮一点。我们可以在方法调用之间添加换行符,并且可以选择去掉列表类型的名称。

public static string GetText<T>(this IQueryable<T> query, bool lineBreaks, bool noClassName)
{
    var text = query.Expression.ToString();

    if (!lineBreaks && !noClassName) 
        return text;

    var expression = StripQuotes(query.Expression);
    if (!(expression is MethodCallExpression mce)) 
        return text;

    if (lineBreaks)
    {
        var strings = new Stack<string>();
        strings.Push(mce.ToString());

        while (mce.Arguments.Count > 0 && mce.Arguments[0] is MethodCallExpression me)
        {
            strings.Push(me.ToString());
            mce = me;
        }

        var sb = new StringBuilder(strings.Pop());

        var len = sb.Length;
        while (strings.TryPop(out var item))
        {
            sb.AppendLine().Append(item.Substring(len));
            len = item.Length;
        }    
        text = sb.ToString();
    }

    if (mce.Arguments.Count > 0 && mce.Arguments[0] is ConstantExpression ce)
    {
        var root = ce.Value.ToString();
        if (root != null && text.StartsWith(root))
        {
            text = noClassName
                    ? text.Substring(root.Length + 1)
                    : text.Insert(root.Length, Environment.NewLine);
        }        
    }

    return text;
}

// helper in case we get an actual Queryable in there
private static Expression StripQuotes(Expression e) 
{
    while (e.NodeType == ExpressionType.Quote)
        e = ((UnaryExpression)e).Operand;
    return e;
}

我们可以这样调用这个方法:

var list = new List<int>();
var query = list.AsQueryable()
                .Select((c, i) => c * (i + 1))
                .Where(c => c > 5)
                .Where(c => c < 10 && c != 7)
                .Take(2)
                .OrderBy(x => 1);   
var text = query.GetText(true, true);

这将产生以下内容:

Select((c, i) => (c * (i + 1)))
.Where(c => (c > 5))
.Where(c => ((c < 10) AndAlso (c != 7)))
.Take(2)
.OrderBy(x => 1)

请注意,这是非常基本的。它不会涵盖闭包(传入变量)的情况,您将在查询中写入&lt;&gt;DisplayClass 对象。我们可以使用 ExpressionVisitor 来解决这个问题,它遍历表达式并计算代表闭包的 ConstantExpressions。

(很遗憾,我目前没有时间提供 ExpressionVisitor 解决方案,但请继续关注更新)

【讨论】:

  • 我今天会尝试发布更新,提供一个非常强大的解决方案。
【解决方案2】:

不,这是不可能的,至少不是以您问题中描述的直接方式。

List 不是逐步过滤的(即,对所有元素应用Where(expr1),然后对所有剩余元素应用Where(expr2),...),而是以一种延迟的方式:

  1. 如果您请求生成的 IEnumerable 的第一项,LINQ 会为每个项目评估 expr1 子句,直到有一项匹配。
  2. 然后它检查这个列表项是否也匹配expr2
    • 如果有,请将其返回(或传递到进一步的Where 阶段)。
    • 如果不匹配,请返回步骤 1 并继续查找与 expr1 匹配的项目。

所以在这里通过简单地调用ToActionsString() 来记录是很困难的。正如 cmets 中已经指出的那样,在添加 Where-clauses 时只记录可能要容易得多,因为无论如何您都处于已知状态:

if(condition1)
{
    myList = myList.Where(item => item.Property == value);
    Log($"Adding expression 1 with value '{value}'");
}

如果您担心value 可能在IEnumerable 实际评估(捕获的变量)之前发生变化,并且您无法充分重组您的控制流,一种解决方法可能是创建@987654335 @ 输出捕获变量的对象,并在迭代列表之前立即评估这些变量:

List<Func<int>> values = new List<Func<int>>();
if(condition1)
{
    myList = myList.Where(item => item.Property == value);
    values.Add(() => value);
}
...
foreach(var v in values)
    Log($"List will be filtered by {v()}");
var filteredList = myList.ToList();

最后,您可以在表达式中调用一些日志记录函数,该函数记录条件和/或在评估这些条件时捕获异常:

myList.Where(item => 
{
    Log(item, value);
    return item.Property == value;
});

【讨论】:

  • 感谢您的回复。我担心这是不可能的,但你的回答让我清楚地了解了 LINQ 评估项目的方式。
  • 您可以使用 AsQueryable 然后在可查询对象上构建 Where 子句,这将创建一个表达式树。之后,您可以使用表达式访问者打印出对查询的调用。最后 ToList (或任何你用来实现的东西)本身将使用底层表达式来获得结果。内置 linq to objects 查询提供程序将转换为正确的结果。虽然这有一些开销
  • @pinkfloydx33 不关心 ExpressionVisitor,但是关于使用 AsQueryable 创建表达式树的部分可能刚刚解决了这个问题,因为当只是打印 Queryable.Expression 时,我或多或少地得到了我的输出想要(除了我们正在检查的实际值)。创建了一个小提琴dotnetfiddle.net/Mdt2Ti 来展示这个。
猜你喜欢
  • 2010-10-20
  • 1970-01-01
  • 2021-02-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-12
  • 2018-12-27
相关资源
最近更新 更多