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

LINQ To Google Image and Google Groups

starIconstarIconstarIconstarIconstarIcon

5.00/5 (15投票s)

2007 年 5 月 1 日

10分钟阅读

viewsIcon

97900

downloadIcon

415

Google 图片/群组搜索的 LINQ 实现

应用程序

这是一个我为了更好地理解 LINQ 内部工作原理而创建的“宠物项目”。它基本上为Google 图片搜索提供了一个 LINQ 查询接口。选择图片搜索而非更流行的网页搜索的原因是图片搜索更“结构化”。例如,您可以根据图片的大小、颜色或文件格式搜索图片。

由于目前关于“LINQ”的文档不多,大部分实现是基于使用“Reflector”检查 DLINQ 程序集。毋庸置疑,解决方案文件是使用 Visual C# Express (Orcas) 的 Beta 版创建的,最终产品发布时可能会有变化。

例如,该应用程序能够执行以下 LINQ 查询

var test = from img in ImageSearch.Instance
    where (img.RelatesTo("SQL")
    || img.RelatesTo("Windows"))
    && img.RelatesTo("Microsoft")
    && !img.RelatesTo("2005")
    && img.Size == ImageSize.Small
    && img.Format == ImageFormat.GIF
    && img.Domain == "www.microsoft.com"
    orderby img.Rank
    select img

将返回匹配搜索条件的一系列 `Image` 对象,然后可以通过 `foreach` 语句进行迭代。在这种情况下,搜索条件是:小尺寸 GIF 格式图片,必须与“Microsoft”相关,并且与“SQL”或“Windows”相关,但不与“2005”相关,且在“www.microsoft.com”域内。

以下是 `Image` 类的定义

namespace MChen.Linq.Google
{
    public enum ImageFormat 
    {
           Any, JPG, PNG, GIF
    }

    public enum ImageSize     
    {
           Any, Large, Medium, Small
    }

    public enum ImageColor 
    {
           Any, BlackWhite, Greyscale, Color
    }

    public class Image
    {
           public int Rank { get; internal set; }
           public Uri Url { get; internal set; }
           public string Domain { get; internal set; }
           public string Description { get; internal set; }
           public ImageFormat Format { get; internal set; }
           public ImageSize Size { get; internal set; }
           public ImageColor Color { get; internal set; }

           public int Width { get; internal set; }
           public int Height { get; internal set; }

           public int FileSize { get; internal set; }

           public string ThumbnailURL { get; internal set; }
           public int ThumbnailWidth { get; internal set; }
           public int ThumbnailHeight { get; internal set; }
    }
}

但是,`Image` 类的并非每个字段都可以用作查询条件。查询的功能受到 Google 上可用搜索容量的限制。例如,您不能搜索特定 `Width` 或 `Height` 的图像,也不能按 `FileSize` 对结果进行排序。如果您尝试任何此类查询,很可能会收到“不支持”异常。

Google 没有为其图片搜索公开任何 API(或者至少我没有找到任何),所以搜索功能是通过发送 HTTP 请求,然后用“正则表达式”解析结果来实现的。由于 LINQ 是本次练习的重点,我不会深入探讨请求和从 Google 提取结果的细节。如果感兴趣,可以查看源代码。

IQueryable 链和表达式树

LINQ 的核心是表达式树和 `IQueryable` 接口的概念。`IQueryable` 是编译器非常了解的特殊接口。当编译器看到一个实现了 `IQueryable` 的类实例在 LINQ 查询的 from 子句中使用时,它会自动将查询的其余部分的每个部分转换为表达式树(如果对象只实现了 `IEnumerable`,生成的代码会大不相同,实际上会简单得多)。对于那些学习过编译器理论的人来说,表达式树本质上是一个表示源代码片段的数据结构,在这种情况下就是 LINQ 查询。编译器或解释器可以遍历表达式树以生成实际的机器指令,或者只是解释和执行表达式。作为一个快速示例,语句“`x + 3`”可以转换为一个 `BinaryExpression`,其运算符是 `Add`,两个操作数是变量“`x`”和常量值“`3`”。

表达式树被用作中间数据结构有两个原因:首先,它将源代码的转换和底层机器代码的生成分离开来。在“`x + 3`”示例中,相同的 `BinaryExpression` 可以用于生成 Intel X86、MSIL 或 JVM 指令。第二个原因是许多优化技术使用表达式树更容易实现。例如,如果编译器在相同的代码片段中发现两个结构相同的表达式树,它将创建一个临时变量来保存表达式树的结果,并在两个地方重复使用它。

在上面的 LINQ 示例查询中,生成了两个表达式树来表示查询的 `Where` 和 `Order By` 部分。也应该生成另一个表达式树来表示 `Select`,但由于我们在 `Select` 中没有做太多事情,编译器足够聪明,将其优化掉了。如果我们在那里放置一个投影运算符,就会出现一个 `Select` 表达式树。

Screenshot - expression-tree.jpg

查询随后被翻译为

//Expression tree for Where clause
Expression<...> whereClause = ...;
//Expression tree for orderBy clause
Expression<...> orderByClause = ...;

IQueryable<Image> whereResult
       = ImageSearch.Instance.CreateQuery(whereClause);
IQueryable<Image> orderByResult
       = whereResult.CreateQuery(orderByClause);

正如我们所看到的,编译器通过 `CreateQuery` 函数调用将两个 `IQueryable` 链接在一起。对 `ImageSearch.Instance`(一个实现 `IQueryable` 的单例类)的第一次 `CreateQuery` 调用,带上 `whereClause` 表达式,又生成了另一个 `IQueryable`。然后 `orderByClause` 表达式被传递给返回的接口,返回第三个 `IQueryable`,并用作整个 LINQ 查询的最终结果。正如我们很快将看到的,这种链式 `IQueryable` 结构确保了在先前的查询子句 (`Where`) 中定义的查询条件能够传递给后面的子句。

IQueryable 的剖析

`IQueryable` 接口定义了三个方法(每个方法都有一个泛型版本和一个非泛型版本)。这些方法可以分为三个目的:

  • `IEnumerable.GetEnumerator`:`IQueryable` 继承自 `IEnumerable`。在上一节中,我们看到 `IQueryable`s 被链接在一起,链中的最后一个作为查询结果返回。客户端可能会打算使用 foreach 迭代查询结果。为了实现这一点,链中至少最后一个 `IQueryable` 对象必须实现 `GetEnumerator` 方法以返回一个有效的 `IEnumerator` 来迭代查询结果。另一方面,对于那些永远不会用作链中最后一个的 `IQueryable` 类,则不需要实现此方法。
  • `Execute` 方法:当查询结果是标量(例如,`Count`、`Sum` 等)时,会调用此方法。由于我们的 Google 图片搜索不支持任何标量查询,我们只是在此方法的实现中抛出异常。
  • `CreateQuery` 方法:对于返回结果列表的查询,这里就是所有神奇发生的地方。此方法遍历表达式树并生成最终可执行文件。对于 DLINQ,这将是 SQL 语句。对于我们的 Google Image LINQ,这是我们将发送请求搜索结果的实际 URL。然后它返回一个封装了这些“可执行”信息的 `IQueryable` 对象。如果这是链中的最后一个链接,当客户端遍历结果时(返回到 `GetEnumerator`),“可执行文件”将实际执行;如果链中还有更多链接,这个 `IQueryable` 将把自己的表达式树信息合并到“可执行文件”中并传递下去。

`IQueryable` 的另一个值得注意的成员是 `Expression` 属性。这个属性基本上要求 `IQueryable` 将自身封装为一个表达式,然后 LINQ 框架可以将这个表达式放入表达式树并沿着链传递。一个简单而标准的属性实现是将自身封装到一个 `ConstantExpression` 中。

public System.Linq.Expressions.Expression Expression
{
       get { return Expression.Constant(this); }
}

如果您检查表达式树的打印输出,此 `ConstantExpression` 位于根节点的第一个参数中。

实现 IQueryable.CreateQuery

Google 图片搜索请求可以通过以下结构进行概括,该结构直接映射 Google 上的高级图片搜索网页:

internal class ImageQueryInfo
{
         public List<string> AllWords = new List<string>();
         public List<string> OrWords = new List<string>();
         public List<string> NotWords = new List<string>();

         public ImageSize Size { get; set; }
         public ImageFormat Format { get; set; }
         public ImageColor Color { get; set; }

         public string Domain { get; set; }
}

`CreateQuery` 方法的任务仅仅是将传入的表达式树转换为等效的 `ImageQueryInfo` 对象。这是通过递归遍历表达式树,逐个处理节点并将相关信息填充到 `ImageQueryInfo` 对象中来实现的。

//entry point
public ImageQueryInfo BuildQuery(Expression exp)
{
    ImageQueryInfo qinfo = new ImageQueryInfo();
    return Visit(exp, qinfo);
}

private ImageQueryInfo Visit(Expression node, ImageQueryInfo qinfo)
{
    switch (node.NodeType)
    {
        case ExpressionType.AndAlso:
             return VisitAnd((BinaryExpression)node, qinfo);

        case ExpressionType.OrElse:
             return VisitOr((BinaryExpression)node, qinfo);

        case ExpressionType.Equal:
             return VisitEquals((BinaryExpression)node, qinfo);

        case ExpressionType.Call:
             return VisitMethodCall(
               (MethodCallExpression)node, qinfo, false, false
             );

        case ExpressionType.Lambda:
             return VisitLambda((LambdaExpression)node, qinfo);

        case ExpressionType.Not:
             return VisitNot((UnaryExpression)node, qinfo);

        //...
    }
}

//process And expression
private ImageQueryInfo VisitAnd(BinaryExpression node, ImageQueryInfo qinfo)
{
    if (node.NodeType != ExpressionType.AndAlso)
        throw new ArgumentException("Argument is not AND.", "node");

    //simply visit left and right
    qinfo = Visit(node.Left, qinfo);
    qinfo = Visit(node.Right, qinfo);
    return qinfo;
}

//...

在处理表达式树的叶节点时,我们可以非常具体地处理细节。例如,由于我们只处理一个方法调用“`RelatesTo`”,我们可以检查方法名并在无法处理时生成异常。

Type declaringType = node.Method.DeclaringType;
if (declaringType == typeof(Image))
{
    if (node.Method.Name == "RelatesTo")
    {
        //parse the parameter
        if (node.Arguments.Count != 1 ||
            node.Arguments[0].NodeType != ExpressionType.Constant)
            throw new ArgumentException
        ( "Only constant search terms are supported.");

        ConstantExpression cont =
            node.Arguments[0] as ConstantExpression;
        string term = cont.Value.ToString();
        if (forceNot) qinfo.NotWords.Add(term);
        else if (forceOr) qinfo.OrWords.Add(term);
        else qinfo.AllWords.Add(term);
    } 
    else 
    {
        throw new ArgumentException(
            string.Format(
                "Method {0} is not supported.", node.Method.Name));
    }
} 
else 
{
    throw new ArgumentException(
        string.Format("Method {0} is not supported.", node.Method.Name));
}
return qinfo;

实现 IQueryable.GetEnumerator

一旦我们有了包含所有查询条件的 `ImageQueryInfo` 对象,我们所需要做的就是发送正确的请求,获取响应并提取图片搜索结果。此逻辑在 `IQueryable.GetEnumerator` 方法中实现。借助 `yield` 语句,实现非常简单(`RequestForPage` 方法构造请求 URL,获取响应,然后用正则表达式解析它)。

public IEnumerator<T> GetEnumerator()
{
    int cnt = 1;
    //implementation of enumerator
    while (true)
    {
        IList<T> batch = RequestForPage(cnt);
        foreach (var img in batch)
        {
            cnt++;
            yield return img;
        }

        //stop condition
        if (batch.Count == 0) break;
    }
}

LambdaExpression.Compile

使 `LambdaExpression` 在其他表达式类型中脱颖而出的一个特点是它的 `Compile` 方法。顾名思义,调用 `Compile` 将表达式树的数据表示转换为实际可执行代码,表示为委托。例如,在以下代码中:

Expression<Func<int, int>> expr = a => a + 3;

Func<int, int> func = expr.Compile();
Console.WriteLine(func(5));

表达式 `(a+3)` 实际上是在运行时编译的,一个指向编译方法的委托作为 `Func` 返回。然后我们可以调用该委托来“执行表达式”。

但这与我们的 LINQ 实现有什么关系呢?在下一节中,我们将看到 `LambdaExpression.Compile` 为我们提供了一种快速简便的方法来支持某些查询结构,否则我们将需要不眠之夜才能实现。

查询参数化和投影

我们的 Google 图片查询简单而实用。但它并没有让我们走得足够远。例如,它无法处理参数化查询,例如:

string param = ...;
var test = from img in Google.Images
           where img.RelatesTo("microsoft")
              && img.Format == (ImageFormat)
                    Enum.Parse(typeof(ImageFormat), param)
              && img.Domain == "www.microsoft.com"
           orderby img.Rank
           select img;

请注意,查询结果的 `Format` 部分作为变量传递,并且必须使用 `Enum.Parse` 进行评估。通常这不会是一个大问题,因为它不过是一个普通的运行时方法调用。但在 LINQ 领域,编译器不会为方法调用生成 MSIL,相反,方法调用会作为表达式树的一部分进行转换,就像查询的其余部分一样。

这给我们的 LINQ 实现带来了严重的问题,因为现在我们被迫处理几乎所有 .NET 语言构造:你可能会在表达式树中看到算术运算、方法调用甚至对象创建,你必须处理所有这些才能使查询语言完整。我们几乎在编写 C# 编译器的后半部分!(前半部分,语法解析,已经由真正的 C# 编译器完成了,因为我们手上有一个表达式树)。

`LambdaExpression.Compile` 应运而生。当我们不打算在查询中处理表达式时,我们总是可以将其转换为 `LambdaExpression`,并要求 .NET 代表我们编译并执行它。当我们在 LINQ 实现中处理 `equals` 运算符时,我们总是期望运算符的一侧是对 `Image` 类的直接成员访问,另一侧是常量表达式,或者现在,是可以在查询执行之前求值为常量表达式的东西。这可以通过以下方法实现:

internal static ConstantExpression
ProduceConstantExpression<X>(Expression exp)
{
    try
    {
        return Expression.Constant(
        Expression.Lambda<Func<X>>(exp, null).Compile().Invoke());
    }
    catch (Exception ex)
    {
        return null;
    }
}

类型参数 `X` 是 `LambdaExpression` 期望的返回类型。对于 `ImageFormat` 等枚举类型,它将是底层值类型 `Int32`。这非常酷:只需一个函数调用,我们现在就可以处理任何表达式,只要它能在查询执行之前在运行时求值为常量。

实现 `Projection` 运算符的思想非常相似。不同之处在于 `Projection` 运算符只能在查询执行之后,当 `Image` 对象可用时才能进行评估。

var test = from img in Google.Images
           where img.RelatesTo("microsoft")
           select new { img.Rank, img.Url };

匿名类型 `{img.Rank, img.Url}` 是在编译时生成的,不属于表达式树。查询的 `Select` 部分实际上被翻译成一个 `MemberInit` 表达式,它将一个 `Image` 对象作为其参数并创建匿名类型的实例。

为了支持这一点,我们可以创建一个实现 `IQueryable` 的适配器类,将其包装在我们的 `ImageSearcher` 周围,并通过 `LambdaExpression.Compile` 对结果 `Image` 对象执行 `MemberInit` 表达式。

internal sealed class Projector<T, S> :
IQueryable<T>, IOrderedQueryable<T>
{
    private IQueryable<S> _chain = null;
    private Func<S, T> _delegate = null;

    public Projector(MethodCallExpression expr)
    {
        Diagnostic.DebugExpressionTree(expr);
        if (expr.Method.Name == "Select" &&
            expr.Arguments.Count == 2 &&
            ExpressionUtil.IsLambda(expr.Arguments[1]))
        {
            //get last IQueryable in the chain
            ConstantExpression cont = (ConstantExpression)expr.Arguments[0];
            _chain = (IQueryable<S>)cont.Value;

            //create delegate from LambdaExpression (wraps MemberInit inside)
            LambdaExpression lambda = 
        ExpressionUtil.GetLambda(expr.Arguments[1]);
            if (lambda.Parameters.Count == 1 &&
                lambda.Parameters[0].Type == typeof(S))
            {
                _delegate = (Func<S, T>)lambda.Compile();
                return;
            }
        }

        //not support
        throw new NotSupportedException(
              string.Format("Only projection base on type {0} is supported.",
              typeof(S)));
    }

    public IEnumerator<T> GetEnumerator()
    {
        foreach (S s in _chain)
        {
            //call the member init delegate and return the result
            yield return _delegate(s);
        }
    }

    //implement other IQueryable members as usual...
}

就是这样!现在您可以通过简单直观的 LINQ 查询在应用程序中执行 Google 图片搜索。而且它还支持投影等酷炫功能!相当不错,不是吗?

历史

  • 2007 年 5 月 8 日更新
    • 增加了 `LambdaExpression` 的使用以及参数化查询和投影运算符的实现。
    • 源代码已重构,现在也支持 Google 群组查询。
LINQ To Google 图片和 Google 群组 - CodeProject - 代码之家
© . All rights reserved.