【问题标题】:How do Expression trees allow consumers to evaluate variables? [duplicate]表达式树如何允许消费者评估变量? [复制]
【发布时间】:2017-05-23 06:30:22
【问题描述】:

这个问题试图了解 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);

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


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

这个答案对帮助我理解最有帮助:

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

【问题讨论】:

  • 不要再问同样的问题。如果您认为它不是重复的,请编辑第一个并尝试重新打开它。
  • Linq 是一种类似于 c# 的计算机语言。语言必须遵循一些规则。所以任何计算机语言都会有变量、预定义的词和运算符。
  • 事实证明我的问题是误解了表达式树。我认为它们是“被动”AST,但事实并非如此。表达式节点(如 MemberExpression)连接到实时程序数据,这允许表达式的使用者通过表达式树检索实时值。 - stackoverflow.com/questions/6998523/…
  • @ManfredRadlwimmer 仅供参考 - 我发布了一个新问题,因为网站说明明确指出“编辑问题或发布新问题”,而旧问题被标记为与不存在的内容重复回答我的问题。
  • @DavidJeske 我非常怀疑它在本网站的任何地方都说,当您的问题被搁置时,只需将其复制并粘贴为新问题。

标签: c# linq


【解决方案1】:

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

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

既然您在谈论查询,我假设您想知道 IQueryable 是如何做到这一点的。

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

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

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

但在详细介绍提供者之前,让我们创建实现 IQueryable 的类:

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

假设我有一个类Word(正如你所期望的一个Word,就像一个没有空格的字符串)和一个实现IQueryable<Word>的类WordCollection。由于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 以选择要提供的对象。

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

由于您隐藏了 Provider 获取数据的位置,您可以将其与提供相同类型元素的其他 Provider 交换,但从其他来源获取它们。

在类中我们看到实现IQueryable&lt;Word&gt;IQueryable的三个属性:

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

后两个函数实现IEnumerable&lt;Word&gt;IEnumerable。他们只是命令提供者计算表达式。结果是内存中的 Enumerable 序列。他们返回这个序列的 GetEnumerator()

ElementType 已实现。所以我们所要做的就是填写ExpressionProvider。我们可以在构造函数中这样做:

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

此构造函数允许WordCollection 的用户将任何Provider 放入其中,例如从文件中获取WordsProvider,或从互联网获取其文字的Provider等。

让我们看看ProviderProvider 必须实现IQueryProvider,它有四个函数:IQueryable&lt;Word&gt; 的两个通用函数和IQueryable 的两个非通用函数。

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

作为一个例子,我有一个单词提供者,它代表了威廉莎士比亚十四行诗中所有单词的集合

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。它所要做的就是为这个WordProvider 和给定的表达式创建一个新的WordCollection

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

一段时间后,集合成为枚举器,并调用WordCollection.GetEnumerator()。如上所示,这将要求提供者使用表达式进行枚举,从而调用WordProvider.Execute&lt;TResult&gt;(Expression)

在 WordCollection 和 WordProvider 之间的所有这些反复之后,提供者终于可以开始执行表达式了。

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

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

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

所以 Execute 函数应该解释 Expression 并返回一个 TResult,它应该是一个 Word 序列或一个 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;
    }

所以现在我们可以完成 Execute 函数了

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);
    }
}

【讨论】:

  • 这是一个非常详细的答案,但它没有回答我的问题。我的问题是......当 LINQ 将 lambda `word => word.Text.FirstOrDefault() == my_local_var` 引用到运行时可解析表达式中时,它如何知道捕获“my_local_var”而不是简单地将符号放入表达?它只是 LINQ 编译器中的一组硬编码规则吗?
猜你喜欢
  • 1970-01-01
  • 2017-01-16
  • 1970-01-01
  • 2013-05-13
  • 1970-01-01
  • 2016-05-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多