等待表达式
将 await 关键字与 Expression-Trees 一起使用
C# 5.0 中最酷的新特性可能就是 await
和 async
关键字了。
通常,await
用于编写涉及多个时间点的代码。从我们的角度来看,当 async
方法遇到 await
时会暂停执行,并在 await
操作的结果可用后恢复执行。这种异步行为与任务(tasks)和并发代码密切相关。
你可能不知道的是,即使没有涉及任何任务或线程,你也可以利用 await
。
我今天的目标是实现对自定义表达式(如 x == 5 * y
)的等待。
幕后
你需要了解的第一件事是,await
特性只是基于模式的语法糖的另一种应用。C# 编译器会将 await
转换为一系列预定义的方法调用和属性查找。只要底层方法存在,代码就能编译通过。
这是 C# 中的一个常见主题;例如,考虑 Foreach
语句:只要对象能够提供 GetEnumerator
、MoveNext
、Current
和 Dispose
方法,你就可以在该对象上使用 Foreach
语句。同样的情况也适用于 LINQ 查询语法。
那么,支持 await
语句的底层模式是什么呢?考虑以下代码
private async void Test()
{
Part1();
var x = await Something();
Part2(x);
}
这将转换为类似这样的内容
private async void Test()
{
Part1(); // Do the first part
var awaiter = Something().GetAwaiter();
if (awaiter.IsCompleted) // If the result is ready
{
Part2(awaiter.GetResult()); // Just continue with the result.
}
else
{
// Register a callback to get the result and continue the calculation upon completion:
awaiter.OnCompleted(() => Part2(awaiter.GetResult()));
// At this point Test will return to it's caller.
// Part2 will be continued somewhere in the future.
}
}
这当然是一个过度简化的示例,因为 await
可以在循环和分支内使用,作为表达式求值的一部分,并且在 C# 6 中,甚至 可以在 catch
和 finally
块内使用。
然而,只要你提供了 GetAwaiter
、IsCompleted
、GetResult
和 OnCompleted
,编译器就会将 await
语句转换为对你的实现的方法调用。这四个成员共同构成了 Awaiter 模式。
作战计划
考虑到这一点,要对自定义表达式(如 x == 5 * y
)使用 await
需要什么?
首先,我们需要为自定义表达式实现 **Awaiter 模式**。
接下来,我们需要一些逻辑来分析表达式,并告诉我们表达式的值何时发生变化。
最后,一旦表达式的值变为 true,我们将触发 awaiter 的完成。
实现 Awaiter 模式
理想情况下,我们希望在 表达式树 数据结构上使用 await
。
表达式树具有一些有用的属性
- 它们可以从 lambda 表达式自动生成。
- 它们可以在运行时进行分析和重写。
- 表达式树的任何部分都可以在运行时编译并转换为委托。
通常,扩展现有类型以支持 await
的方法是使用扩展方法。
public static class ExpressionExtensions
{
public static ExpressionAwaiter GetAwaiter(this Expression<Func<bool>> expression)
{
throw new NotImplementedException();
}
}
在表达式树的情况下,这种方法将不起作用。代码会编译,但你将无法编写 await x == 2
。
问题在于 C# 中的 lambda 表达式没有自己的类型(就像 null
值一样)。如果你曾尝试编译类似 var f = () => 5
的内容,你可能已经意识到这个问题。
据我所知,编译器实际上**可以**将 () => 5
的类型推断为 Func<int>
,但它无法确定 f
是引用一段可执行代码(匿名函数)还是引用一段数据(表达式树)。这就是为什么 lambda 表达式的最终类型总是从其周围环境中推断出来的。
例如,你可以写
public Expression<Func<T>> Exp<T>(Expression<Func<T>> exp)
{
return exp; // Do nothing.
}
public Func<T> Func<T>(Func<T> func)
{
return func; // Do nothing.
}
//..
var a = Exp(() => 5); // Type of a is Expression<Func<int>>.
var b = Func(() => 5); // Type of a is Func<int>.
在此示例中,每个变量的类型在编译时就已确定,但这些方法本身没有任何有价值的操作,并且可能会被优化掉。这意味着我们必须将代码包装在一个显式函数中。
public static class Until
{
public static ExpressionAwaitable BecomesTrue(Expression<Func<bool>> expression)
{
return new ExpressionAwaitable(expression);
}
}
// Our awaitable wrapper to the original expression.
public class ExpressionAwaitable
{
private readonly ExpressionAwaiter _awaiter = new ExpressionAwaiter();
public ExpressionAwaitable(Expression<Func<bool>> expression)
{
// TODO: Put logic here.
}
public ExpressionAwaiter GetAwaiter()
{
return _awaiter;
}
}
// Very simple awaiter that can be marked complete using the Complete method.
public class ExpressionAwaiter : INotifyCompletion
{
private Action _continuation;
public ExpressionAwaiter()
{
IsCompleted = false;
}
public bool IsCompleted { get; private set; }
public void GetResult()
{
// Nothing to return.
}
public void Complete()
{
if (_continuation != null)
{
_continuation();
IsCompleted = true;
}
}
public void OnCompleted(Action continuation)
{
_continuation += continuation;
}
}
// Usage:
await Until.BecomesTrue(() => txtBox.Text == Title);
分析表达式
好了,我们现在可以**编译**一个表达式的 await
了。但是如何将其转换为有用的运行时行为仍然不清楚。我们需要一种方法来注意到表达式的值从 false
变为 true
。
我们可以启动一个线程并不断检查表达式的值,但这种方法有很多缺点。首先,值可能变为 true
然后又变回 false
,我们很容易错过。
在合理假设下,表达式的值仅在表达式中涉及的对象发生变化时才会改变。在 .NET Framework 中,有许多标准方法可以知道对象何时发生变化。其中一个标准是 INotifyPropertyChanged
接口。顾名思义,它旨在在对象属性更改时通知外部观察者。
因此,这为我们提供了以下算法:
- 扫描表达式中实现
INotifyPropertyChanged
的对象。 - 订阅这些对象上的
PropertyChanged
事件。 - 每次事件触发时,重新评估原始表达式。
- 如果值为 true,则取消订阅所有事件并触发 awaiter 的完成。
分析表达式树的标准方法是使用 访问者模式。要实现新的表达式访问者,你只需继承 System.Linq.Expressions
命名空间中的 ExpressionVisitor
类,并重写要检查的表达式类型的访问方法。
我们将实现一个自定义访问者,它提取所有派生自或实现某个泛型类型 T
的对象。
public class TypeExtractor<T> : ExpressionVisitor
{
// Here's where the results will be stored:
public IReadOnlyCollection<T> ExtractedItems { get { return _extractedItems; } }
private readonly List<T> _extractedItems = new List<T>();
private TypeExtractor() {}
// Factory method.
public static TypeExtractor<T> Extract<S>(Expression<Func<S>> expression)
{
var visitor = new TypeExtractor<T>();
visitor.Visit(expression);
return visitor;
}
private void ExtractFromNode(Type nodeReturnType, Expression node)
{
// Is the return type of the expression implements / derives from T?
if (typeof(T).IsAssignableFrom(nodeReturnType))
{
// Cast node to an expression of form Func<T>
var typedExpression = Expression.Lambda<Func<T>>(node);
// Compile the expression (this will produce Func<T>)
var compiledExpression = typedExpression.Compile();
// Evaluate the expression (this will produce T)
var expressionResult = compiledExpression();
// Add the result to our collection of T's.
_extractedItems.Add(expressionResult);
}
}
// If the expression is a constant, then:
protected override Expression VisitConstant(ConstantExpression node)
{
ExtractFromNode(node.Value.GetType(), node);
return node;
}
}
// Usage:
[TestMethod]
public void TypeExtractOnConst_ReturnsConst()
{
var visitor = TypeExtractor<int>.Extract(() => 1);
visitor.ExtractedItems.Should().Contain(1);
}
当然,并非我们表达式中的所有对象都是常量。让我们添加几个更多的情况。
// Expression is of type x.Property or x._field
protected override Expression VisitMember(MemberExpression node)
{
Visit(node.Expression); // For chained properties, like X.Y.Z
node.Member.If()
.Is<FieldInfo>(_ => ExtractFromNode(_.FieldType, node))
.Is<PropertyInfo>(_ => ExtractFromNode(_.PropertyType, node));
return node;
}
protected override Expression VisitParameter(ParameterExpression node)
{
ExtractFromNode(node.Type, node);
return node;
}
将所有内容整合
使用 TypeExtractor
,我们可以识别表达式中实现 INotifyPropertyChanged
的所有对象。以下是如何将其组装成一个可等待对象:
public class ExpressionAwaitable
{
private readonly Func<bool> _predicate;
private readonly List<INotifyPropertyChanged> _iNotifyPropChangedItems;
private readonly ExpressionAwaiter _awaiter = new ExpressionAwaiter();
public ExpressionAwaitable(Expression<Func<bool>> expression)
{
_predicate = expression.Compile(); // Generate a function that when invoked would evaluate our expression.
if (_predicate()) // If the value is already true, complete the awaiter.
{
_awaiter.Complete();
}
else
{
// Find all objects implementing INotifyPropertyChanged.
_iNotifyPropChangedItems = TypeExtractor<INotifyPropertyChanged>
.Extract(expression).ExtractedItems.ToList();
HookEvents(); // Register for notifications about changes.
}
}
private void HookEvents()
{
foreach (var item in _iNotifyPropChangedItems)
{
item.PropertyChanged += NotifyPropChanged;
}
}
private void UnhookEvents()
{
foreach (var item in _iNotifyPropChangedItems)
{
item.PropertyChanged -= NotifyPropChanged;
}
}
private void NotifyPropChanged(object sender, PropertyChangedEventArgs agrs)
{
ExpressionChanged();
}
private void ExpressionChanged()
{
if (_predicate()) // Did the value became true?
{
UnhookEvents(); // Don't forget to unsubscribe.
_awaiter.Complete(); // Signal completion.
}
}
public ExpressionAwaiter GetAwaiter()
{
return _awaiter;
}
}
同样,你可以扩展对 INotifyCollectionChanged
的支持,以便从 可观察集合 获取通知。
另一种可观察对象是 WPF 中的 DependencyObject。正确处理它会更棘手一些,但它对我们的目标很重要,因为许多有用的属性实际上是依赖属性。首先,我们需要从表达式中提取所有依赖属性。为此,我们将实现另一个表达式访问者。依赖属性始终是属性,因此我们可以将精力集中在 VisitMember
函数上。
protected override Expression VisitMember(MemberExpression node)
{
Visit(node.Expression); // For chained properties.
var member = node.Member;
var declaringType = member.DeclaringType;
// Does the property live inside a DependencyObject?
if (declaringType != null && typeof(DependencyObject).IsAssignableFrom(declaringType))
{
// Located the corresponding static field.
var propField = declaringType.GetField(member.Name + "Property", BindingFlags.Public | BindingFlags.Static);
if (propField != null)
{
// Extract the value like before.
var typedExpression = Expression.Lambda<Func<DependencyObject>>(node.Expression);
var compiledExpression = typedExpression.Compile();
var expressionResult = compiledExpression();
_extractedItems.Add(new DependencyPropertyInstance
{
Owner = expressionResult,
// Get the corresponding dependency property:
Property = propField.GetValue(expressionResult) as DependencyProperty
});
}
}
return node;
}
为了从依赖属性获取通知,我们需要向 HookEvents
添加代码。
private void HookEvents()
{
...
foreach (var item in _iDPItems)
{
var descriptor = DependencyPropertyDescriptor.FromProperty(item.Property, item.Owner.GetType());
descriptor.AddValueChanged(item.Owner, DependencyPropertyChanged);
}
}
用法
private async void Window_OnLoaded(object sender, RoutedEventArgs e)
{
// Wait until text box content is equal to forms title.
// Note that the completion could be triggered both by changes
// to the text box content and changes to the forms title.
await Until.BecomesTrue(() => txtBox.Text == Title);
MessageBox.Show("Well done!");
}
你可能会问,这比绑定好在哪里?
在 WPF 中,使用 MultiBinding
和多值转换器 可以完成我们讨论的大部分内容。然而,一般来说,这需要更多的代码和精力。你可以将我们的方法视为快速而粗糙的单次绑定。
此外,我希望这篇帖子能消除围绕 await
关键字的一些困惑。
在这里,你有一个完全单线程的应用程序,使用 await
来执行异步操作。
你可以在 这里 下载源代码。