追根溯源之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,記得勾選「包括預發行版」~

NuGet Badge
Github://github.com/HekunX/EazyPageQuery