65.9K
CodeProject 正在变化。 阅读更多。
Home

等待表达式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (19投票s)

2014年10月28日

CPOL

5分钟阅读

viewsIcon

18812

downloadIcon

1

将 await 关键字与 Expression-Trees 一起使用

C# 5.0 中最酷的新特性可能就是 awaitasync 关键字了。
通常,await 用于编写涉及多个时间点的代码。从我们的角度来看,当 async 方法遇到 await 时会暂停执行,并在 await 操作的结果可用后恢复执行。这种异步行为与任务(tasks)和并发代码密切相关。

你可能不知道的是,即使没有涉及任何任务或线程,你也可以利用 await
我今天的目标是实现对自定义表达式(如 x == 5 * y)的等待。

幕后

你需要了解的第一件事是,await 特性只是基于模式的语法糖的另一种应用。C# 编译器会将 await 转换为一系列预定义的​​方法调用和属性查找。只要底层方法存在,代码就能编译通过。
这是 C# 中的一个常见主题;例如,考虑 Foreach 语句:只要对象能够提供 GetEnumeratorMoveNextCurrentDispose 方法,你就可以在该对象上使用 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 中,甚至 可以在 catchfinally 块内使用。
然而,只要你提供了 GetAwaiterIsCompletedGetResultOnCompleted,编译器就会将 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 接口。顾名思义,它旨在在对象属性更改时通知外部观察者。

因此,这为我们提供了以下算法:

  1. 扫描表达式中实现 INotifyPropertyChanged 的对象。
  2. 订阅这些对象上的 PropertyChanged 事件。
  3. 每次事件触发时,重新评估原始表达式。
  4. 如果值为 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 来执行异步操作。

你可以在 这里 下载源代码。

© . All rights reserved.