是否有可能知道在 IEnumerable 上调用了哪些操作?
Is it possible to know what actions have been called on an IEnumerable?
我有几个条件可能会影响在列表中使用的过滤器 (.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()
并不能准确地给出任何相关信息,只是列出列表中的项目也不是我们感兴趣的。
不,这是不可能的,至少不能像你的问题中描述的那样直接。
List
不是一步一步过滤的(即,对所有元素应用 Where(expr1)
,然后对所有剩余元素应用 Where(expr2)
,...),而是在延迟方式:
- 如果您请求结果
IEnumerable
的第一个项目,LINQ 会计算每个项目的 expr1
子句,直到有一个项目匹配。
- 然后检查这个列表项是否也匹配
expr2
.
-
- 如果是,return 它(或传递到进一步的
Where
阶段)。
- 如果不匹配,返回步骤 1 并继续查找匹配
expr1
的项目。
所以在这里通过简单地调用一些 ToActionsString()
来记录是很困难的。正如评论中已经指出的那样,添加 Where
子句时只记录可能更容易,因为无论如何你都处于已知状态:
if(condition1)
{
myList = myList.Where(item => item.Property == value);
Log($"Adding expression 1 with value '{value}'");
}
如果您担心 value
可能会在 IEnumerable
实际 求值 (捕获的变量)之前发生变化,并且您无法充分重组控制流,解决方法可能是创建 Func<T>
输出捕获变量的对象,并在迭代列表之前立即评估这些变量:
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();
最后,您可以在表达式中调用一些日志记录函数,记录条件 and/or 在评估这些条件时捕获异常:
myList.Where(item =>
{
Log(item, value);
return item.Property == value;
});
警告:这会产生开销,因为编译器必须创建所有 Expression
对象(然后必须在运行时分配和编译)。 谨慎使用。
您可以使用 AsQueryable
并依靠内置 EnumerableQuery
和 Expression
类 的 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)
请注意,这是非常基本的。它不会涵盖闭包(传入变量)的情况,您将获得 <>DisplayClass
对象写入您的查询。我们可以用 ExpressionVisitor
来解决这个问题,它遍历表达式并计算代表闭包的 ConstantExpression
s。
(遗憾的是,我目前没有时间提供 ExpressionVisitor
解决方案,但请继续关注更新)
我有几个条件可能会影响在列表中使用的过滤器 (.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()
并不能准确地给出任何相关信息,只是列出列表中的项目也不是我们感兴趣的。
不,这是不可能的,至少不能像你的问题中描述的那样直接。
List
不是一步一步过滤的(即,对所有元素应用 Where(expr1)
,然后对所有剩余元素应用 Where(expr2)
,...),而是在延迟方式:
- 如果您请求结果
IEnumerable
的第一个项目,LINQ 会计算每个项目的expr1
子句,直到有一个项目匹配。 - 然后检查这个列表项是否也匹配
expr2
. -
- 如果是,return 它(或传递到进一步的
Where
阶段)。 - 如果不匹配,返回步骤 1 并继续查找匹配
expr1
的项目。
- 如果是,return 它(或传递到进一步的
所以在这里通过简单地调用一些 ToActionsString()
来记录是很困难的。正如评论中已经指出的那样,添加 Where
子句时只记录可能更容易,因为无论如何你都处于已知状态:
if(condition1)
{
myList = myList.Where(item => item.Property == value);
Log($"Adding expression 1 with value '{value}'");
}
如果您担心 value
可能会在 IEnumerable
实际 求值 (捕获的变量)之前发生变化,并且您无法充分重组控制流,解决方法可能是创建 Func<T>
输出捕获变量的对象,并在迭代列表之前立即评估这些变量:
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();
最后,您可以在表达式中调用一些日志记录函数,记录条件 and/or 在评估这些条件时捕获异常:
myList.Where(item =>
{
Log(item, value);
return item.Property == value;
});
警告:这会产生开销,因为编译器必须创建所有 Expression
对象(然后必须在运行时分配和编译)。 谨慎使用。
您可以使用 AsQueryable
并依靠内置 EnumerableQuery
和 Expression
类 的 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)
请注意,这是非常基本的。它不会涵盖闭包(传入变量)的情况,您将获得 <>DisplayClass
对象写入您的查询。我们可以用 ExpressionVisitor
来解决这个问题,它遍历表达式并计算代表闭包的 ConstantExpression
s。
(遗憾的是,我目前没有时间提供 ExpressionVisitor
解决方案,但请继续关注更新)