表达式树如何允许消费者评估变量?

时间:2017-05-23 06:30:23

标签: c# linq

这个问题试图理解LINQ如何工作,用另一种语言实现类似的东西。

考虑以下LINQ查询,将其转换为表达式树:

var my_variable = "abc";
var qry = from x in source.Foo
          where x.SomeProp == my_variable
          select x.Bar;

由编译器映射到代码中:

var qry = source.Foo
           .Where(x => x.SomeProp == my_variable)
           .Select(x => x.Bar);

当它转换为表达式树时,此表达式树的使用者如何访问" my_variable"的值。

注意:我现在看到我的困惑是将表情树看作是AST。他们不是。它们是(被动)AST和实时程序数据指针的混合体。这样,Expression树的使用者可以决定查看符号AST源信息,还是将其评估为实时程序值。

这个答案最有助于我理解:

How to get the value of a ConstantExpression which uses a local variable?

1 个答案:

答案 0 :(得分:0)

LINQ有两个版本。一个用于IEnumerable序列,一个用于IQueryable序列。

IEnumerable版本是最简单的。只需使用正确的参数调用正确的扩展函数即可。 The reference source code can be found here.

由于您正在讨论查询,我假设您想知道IQueryable如何做到这一点。

实现IEnumerable的Queryable对象和实现相同接口的序列类之间的区别在于Queryable对象包含表达式和可以计算表达式的提供程序。像Where和Select这样的IQueryable的扩展函数知道如何更改表达式,以使其成为一个表达式,用于选择LINQ语句指定的项目。

由于它们是IQueryable类的扩展函数,因此实现IQueryable的人不必为Where / Select / GroupBy等提供功能。因此可以断言实现者已经表达了正确的表达式。

困难的部分不是为您的类实现IQueryable,而是实现IQueryable.Provider返回的Provider对象。

但在进入提供者的详细信息之前,让我们创建实现IQueryable的类:

对于这个解释,我使用了以下来源

假设我有一个类Word(正如您所期望的那样,就像一串没有空格的字符)和一个实现WordCollection的类IQueryable<Word>。自WordCollection实施IQueryable<Word>以来,它还实施了IQueryableIEnumerable<Word>IEnumerable

class MyWordCollection : IQueryable<Word>, IQueryable,
    IEnumerable<Word>, IEnumerable
{
    public Type ElementType { get { return typeof(Word); } }
    public IQueryProvider Provider { get; private set; }
    public Expression Expression { get; private set; }

    #region enumerators
    public IEnumerator<Word> GetEnumerator()
    {
        return (Provider.Execute<IEnumerable<Word>>
            (Expression)).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
    #endregion enumerators
}

请注意IQueryableProvider之间的区别。

  • IQueryable是一个可以查询元素的对象。 IQueryable不必持有这些元素,但它知道谁可以提供这些元素。
  • 如果您提供Expression来选择要提供的对象,则提供者可以提供查询的对象。

提供者可以自己保存这些元素,也可以将这些元素放在其他地方,例如从数据库中获取,或者形成文件,从互联网下载数据,或者只是在其中保存一系列这些元素。 / p>

因为您隐藏了提供者获取其数据的位置,您可以通过提供相同类型元素但从其他来源获取它们的其他提供者进行交换。

在课程中,我们看到了实现IQueryable<Word>IQueryable的三个属性:

  • ElementType返回查询类型:查询时,对象将返回Word
  • 的序列
  • Expression保存由LINQ扩展函数
  • 计算的表达式
  • Provider包含可以解释表达式的对象,并返回ElementType的对象序列,MyWordCollection承诺返回

后两个函数实现IEnumerable<Word>IEnumerable。他们只是命令Provider来计算表达式。结果是内存中的Enumerable序列。它们返回此序列的GetEnumerator()

ElementType已实施。所以我们要做的就是填写ExpressionProvider。我们可以在构造函数中执行此操作:

public WordCollection(IQueryProvider provider, Expression expression)
{
    this.Provider = provider;
    this.Expression = expression;
}

此构造函数为WordCollection的用户提供了任何Provider,例如Provider从文件中获取Words,或Provider 1}}从互联网上得到它的话等。

让我们看一下ProviderProvider必须实现IQueryProvider,其中包含四个函数:IQueryable<Word>的两个通用函数和IQueryable的两个非泛型函数。

其中一个函数是,给定一个表达式,返回一个可以执行该表达式的IQueryable。另一个函数最终将解释表达式并返回所请求的项

作为一个例子,我有一个单词提供者,代表William ShakeSpeare十四行诗中所有单词的集合

internal class ShakespeareDownloader
{
    private const string sonnetsShakespeare = "http://www.gutenberg.org/cache/epub/1041/pg1041.txt";

    public string DownloadSonnets()
    {
        string bookShakespeareSonnets = null;
        using (var downloader = new WebClient())
        {
            bookShakespeareSonnets = downloader.DownloadString(sonnetsShakespeare);
        }
        return bookShakespeareSonnets;
    }
}

所以现在我们有了所有单词的来源,我可以创建一个单词提供者:

class WordProvider : IQueryyProvider
{
    private IQueryable<Word> allSonnetWords = null;

    private IQueryable<Word> GetAllSonnetWords()
    {
        if (allSonnetWords == null)
        {
            string sonnets = shakespeareDownLoader.DownloadSonnets();
            // split on whitespace:
            string[] sonnetWords = sonnets.Split((char[])null,
                StringSplitOptions.RemoveEmptyEntries);

            this.allSonnetWords = sonnetWords
                .Select(word => new Word() { Text = word })
                .AsQueryable<Word>();
        }
    }

好的,现在我们有一些可以为我们提供文字的东西。我们可以这样写:

IQueryable<Word> allWords = new WordCollection();

如上所示,这将构造一个带有表达式的WordProvider以获取所有Words。一旦执行查询,就会调用WordProvider CreateQuery。它所要做的就是为这个WordCollection和给定的表达式创建一个新的WordProvider

public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
    return new WordCollection(this, expression) as IQueryable<TElement>;
}

一段时间后,该集合是枚举器,并调用WordCollection.GetEnumerator()。如上所示,这将要求提供者使用表达式枚举,导致WordProvider.Execute<TResult>(Expression)被调用

在WordCollection和WordProvider之间进行所有这些ping-pong之后,提供者最终可以开始执行表达式。

public TResult Execute<TResult>(Expression expression)
{
}

虽然我的提供者只能提供单词序列,但这个函数是一个通用函数。执行的结果是一个单词序列或一个单词(例如使用First()或Max()时)。如果它要求其他东西我无法提供它,并且允许例外。

该函数被声明为通用,以防我本来是一个提供者,除了单词之外还可以提供其他东西,比如句子。

因此Execute函数应该解释Expression并返回一个TResult,它应该是一个单词序列或一个Word。

我们要做的第一件事就是获取sonnetWords的完整集合,并确定是否请求序列或单个Word:

public TResult Execute<TResult>(Expression expression)
{
    IQueryable<Word> allSonnetWords = this.GetAllSonnetWords();
    var sequenceRequested = typeof(TResult).Name == "IEnumerable`1";

到目前为止,所有代码都相当简单。但现在困难的部分是解释表达式以找出要返回的内容。

幸运的是,在这个例子中,allSonnetWords的集合也是IQueryable。我们必须稍微更改表达式,以便它可以在IQueryable上工作,而不是在WordProvider上工作。

这可以通过ExpressionVisitor的派生类来完成:

class WordExpressionModifier : System.Linq.Expression.ExpressionVisitor
{
    public ExpressionTreeModifier(IQueryable<Word> allWords)
    {
        this.allWords = allWords;
    }

    private readonly IQueryable<Word> allWords;

    protected override Expression VisitConstant(ConstantExpression c)
    {
        // if the type of the constant expression is a WordCollection
        // return the same expression for allWords
        if (c.Type == typeof(WordCollection))
            return Expression.Constant(allWords);
        else
            return c;
    }

现在我们可以完成执行功能

public TResult Execute<TResult>(Expression expression)
{
    IQueryable<Word> allSonnetWords = this.GetAllSonnetWords();
    var sequenceRequested = typeof(TResult).Name == "IEnumerable`1";

    // replace the expression for a WordCollection to an expression for IQueryable<Word>
    var expressionModifier = new ExpressionTreeModifier(allSonnetWords);
    Expression modifiedExpression = expressionModifier.Visit(expression);

    TResult result;
    if (isEnumerable)
        // A sequence is requested. Return a query on allSonnetWords provider
        result = (TResult)allSonnetWords.Provider.CreateQuery(modifiedExpression);
    else
        // a single element is requested. Execute the query on the allSonnetWords.provider
            result = (TResult)allSonnetWords.Provider.Execute(modifiedExpression);
    return result;
}

现在所有的工作都已完成,我们可以尝试一下:

static void Main(string[] args)
{
    IQueryable<Word> allWords = new WordCollection();
    IQueryable<Word> Bwords = allWords
        .Where(word => word.Text.FirstOrDefault() == 'b')
        .Take(40);

    foreach (var word in Bwords)
    {
        Console.WriteLine(word.Text);
    }
}