1. 程式人生 > 實用技巧 >追根溯源之Linq與表示式樹

追根溯源之Linq與表示式樹

一、什麼是表示式樹?

  首先來看下官方定義(以下摘錄自巨硬官方文件)

  表示式樹表示樹狀資料結構中的程式碼,其中每個節點都是表示式,例如,方法呼叫或諸如的二進位制操作x < y。
  您可以編譯和執行由表示式樹表示的程式碼。這樣就可以對可執行程式碼進行動態修改,在各種資料庫中執行LINQ查詢以及建立動態查詢。有關LINQ中的表示式樹的更多資訊,請參見如何使用表示式樹構建動態查詢(C#)。
  在動態語言執行時(DLR)中還使用了表示式樹,以提供動態語言和.NET之間的互操作性,並使編譯器編寫程式可以發出表示式樹而不是Microsoft中間語言(MSIL)。有關DLR的更多資訊,請參見《動態語言執行時概述》。
  您可以讓C#或Visual Basic編譯器根據匿名lambda表示式為您建立一個表示式樹,或者您可以使用System.Linq.Expressions名稱空間手動建立表示式樹。

  從上面我們可以提取一些關鍵資訊——它是一種樹型結構、表示式樹可以被編譯成可執行程式碼然後執行、DLR使用了表示式樹、可以用表示式樹來達到和直接寫MSIL一樣的效果、C#編譯器能夠根據匿名Lambda表示式靜態生成構建表示式樹的程式碼、你可以手動編寫構建表示式樹的程式碼。
  其實第一個關鍵資訊就是表示式樹的全部,後面的所有功能都是在這之上衍生出來的,所以用我的話來回答,什麼是表示式樹?表示式樹就是一種樹形資料結構,在這個結構上包含了程式碼邏輯所必須的資訊,用這些資訊我們可以用來做很多事,例如,生成MSIL程式碼,生成SQL語句等等,這也是Linq To Anything的基礎。

二、Linq

  Linq(語言整合查詢),在.Neter中經常用到的技術,你雖然在開發中經常用到,但你有沒有了解過到它到底是怎麼運作的呢?我們來扒一扒。

1.Linq To Entity

  首先,Linq的鏈式呼叫,是靠擴充套件方法實現的,Linq主要擴充套件了IEnumerable<T>IQueryable<T>兩大介面。我們看下針對IEnumerable<T>的擴充套件。

public static class Enumerable
{
    //所有針對IEnumerable<TSource>的擴充套件方法
    public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, int, bool> predicate)
    //省略......
}

  觀察可以發現,針對IEnumerable的擴充套件方法,貌似跟Expression沒有半毛錢關係。是的,半分錢關係都沒有。這樣做其實是為了效能考慮,因為這些查詢實際上是從MSIL翻譯成機器程式碼本地執行,我何必要先解析表示式樹,然後翻譯成MSIL,再到機器程式碼呢?這也是所謂的Linq To Entity

2.Linq To Other

  對IQueryable<T>的擴充套件如下:

public static class Queryable
{
    //所有針對IEnumerable<TSource>的擴充套件方法
    public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)
    //省略......
}

  觀察可以發現,在Where擴充套件方法中有一個Expression<Func<TSource, bool>>型別的引數。這就是一個表示式樹,確切的說是一個Lambda表示式樹,這個Lamdbda表示式樹包含了必要的資訊,在對source上呼叫了這個方法,並傳入一個Lambda表示式樹之後,source內部會被把傳入的表示式樹新增到之前的表示式樹節點上,然後返回一個新的IQueryable<TSource>例項,其中內部的表示式樹已經包含了你剛傳入的表示式節點,然後你可以在此之上繼續呼叫擴充套件方法,當在呼叫諸如First()ToList()Count()等之類的方法之後,將會導致內部的表示式樹被一個解析器解析,然後根據解析出來的結果,去查資料庫、去檢索JSON檔案、去檢索XML檔案或是呼叫外部服務等,最後生成資料到記憶體,構造成一個List例項給你。至於內部的細節到底是什麼,有時間再寫。

3.問題

  細心的朋友可能注意到,上節提到的一個Expression<Func<TSource, bool>>型別的引數,這個是怎麼構造出來的呢?我們平時開發的時候好像從沒有構造過啊。其實文章開頭就有提到,

  您可以讓C#或Visual Basic編譯器根據匿名lambda表示式為您建立一個表示式樹,或者您可以使用System.Linq.Expressions名稱空間手動建立表示式樹。

  發現沒,這個髒活其實是由編譯器幫我們幹了,我們來驗證一下。新建.Net Core控制檯程式如下:

    static void Main(string[] args)
    {
        List<int> datas = new List<int> { 1, 2, 3, 4, 5, 6 };
        var res = datas.AsQueryable().Where(x => x > 3).ToList();
    }

  使用Debug模式編譯,然後用一個你喜歡的反編譯工具(PS:反編譯一般指把中間語言程式碼變成高階語言程式碼,而反彙編一般指把機器程式碼變成組合語言程式碼)反編譯生成的程式集,這裡我使用的是DNSPY。
如果使用的是DNSPY,記得把“反編譯表示式樹”選項關掉。
  內容如下:

// Token: 0x02000002 RID: 2
internal class Program
{
    // Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
    private static void Main(string[] args)
    {
        List<int> datas = new List<int>
        {
            1,
            2,
            3,
            4,
            5,
            6
        };
        IQueryable<int> source = datas.AsQueryable<int>();
        ParameterExpression parameterExpression = Expression.Parameter(typeof(int), "x");
        List<int> res = source.Where(Expression.Lambda<Func<int, bool>>(Expression.GreaterThan(parameterExpression, Expression.Constant(3, typeof(int))), new ParameterExpression[]
        {
            parameterExpression
        })).ToList<int>();
    }
}

  可以發現,編譯器幫我們把Lambda表示式編譯成了表示式樹。

三、總結

  總的來說,表示式樹是Linq中不可或缺的一環,為了方便人們使用表示式樹,編譯器也做了許多工作,從而避免使用者手動構造表示式樹,因此選用了Lambda表示式這種使用者熟悉的形式給使用者使用,但同時,也提高了理解門檻。

四、題外話

  為了減少重複勞動,我編寫了一個動態構建查詢的類庫,基於.NetStandard,支援靜態排序,動態排序,多重排序,模糊查詢,分頁查詢,能適用大多數的後臺管理應用開發場景。原理其實就是動態構建表示式樹。GitHub上有文件,Nuget上搜索EazyPageQuery,記得勾選“包括預發行版”~


Github:https://github.com/HekunX/EazyPageQuery