最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

第 113 講:C# 3 之查詢表達(dá)式(十七):表達(dá)式樹和動(dòng)態(tài)查詢

2022-05-29 10:20 作者:SunnieShine  | 我要投稿

今天我們來(lái)看一個(gè)新的知識(shí)點(diǎn):表達(dá)式樹(Expression Tree)。表達(dá)式樹對(duì)于一些高性能的場(chǎng)景下會(huì)比較實(shí)用;不過(guò),因?yàn)樗闹R(shí)點(diǎn)會(huì)用到和結(jié)合到 LINQ,所以本特性我們歸納在查詢表達(dá)式里介紹,這也是 C# 3 里的最后一個(gè)新語(yǔ)法特性。

本講內(nèi)容難度較大,雖然還沒有達(dá)到完全理論的、Lambda 和匿名函數(shù)遞歸的難度,但是也比較困難,理解上來(lái)說(shuō)是有一些復(fù)雜的。好在平時(shí)寫代碼用得不是特別多,因此可以跳過(guò)這一講的內(nèi)容。如果你需要用到它們,可以再回來(lái)看。

Part 1 IQueryable<> 接口簡(jiǎn)介

在之前的內(nèi)容里,我們介紹了 IEnumerable<>、IOrderedEnumerable<>IGrouping<,> 三種接口的用法,以及都對(duì)應(yīng)什么意思。它們表示的是一個(gè)待迭代的序列。這個(gè)序列可以是有限的,也可以是無(wú)限的。只要運(yùn)用靈活和嚴(yán)謹(jǐn),無(wú)限的迭代序列也是可以的。

IOrderedEnumerable<> 接口則對(duì)應(yīng)的是 orderby 從句(或 OrderByOrderByDescending、ThenByThenByDescending 四個(gè)方法)。這些方法因?yàn)閳?zhí)行起來(lái)是為了優(yōu)化性能,所以所有的排序依據(jù)會(huì)因?yàn)檫@些方法逐個(gè)追加、添加到底層的排序依據(jù)列表里去,等待合適的時(shí)候才會(huì)一口氣調(diào)用和執(zhí)行。正是因?yàn)檫@些原因,所以只用 IEnumerable<> 接口因?yàn)槭嵌ㄋ懒说臄?shù)據(jù)類型,所以做不到這一點(diǎn)。因此,排序操作有了一個(gè)單獨(dú)的接口。你可以認(rèn)為,它包含排序依據(jù),但也支持迭代,所以 IOrderedEnumerable<> 接口從 IEnumerable<> 派生,是這么一個(gè)關(guān)系。

IGrouping<,> 接口則對(duì)應(yīng)的是 group-by 從句(或 GroupBy 方法)。該方法動(dòng)用了分組依據(jù),但它和前文的排序依據(jù)不同,分組依據(jù)是唯一的,所以只能定義一個(gè)分組依據(jù),所以它不具備前面還需要累計(jì)排序依據(jù)的效果。不過(guò),因?yàn)榉纸M依據(jù)會(huì)帶有分組的結(jié)果,因此 IEnumerable<> 接口也無(wú)法做到,它沒有支持這個(gè)分組依據(jù)的屬性或別的成員,因此只能單獨(dú)創(chuàng)建出一個(gè)類型。而 IGrouping<,> 屬于是一個(gè)一對(duì)多的關(guān)系列表,因此它仍然也支持迭代,所以它也從 IEnumerable<> 接口派生。

今天我們要說(shuō)一種新接口類型:IQueryable<>。該接口和前面的 IOrderedEnumerable<>IGrouping<,> 不同的地方在于,IQueryable<> 接口不是一個(gè)單獨(dú)的某種查詢從句導(dǎo)致和產(chǎn)生的結(jié)果。它是什么呢?我們將稍后說(shuō)到,下面我們賣個(gè)關(guān)子,先說(shuō)說(shuō)一個(gè)神奇的概念:表達(dá)式樹(Expression Tree)。

Part 2 表達(dá)式樹:米飯是菜代碼是數(shù)據(jù)

有沒有想過(guò)一個(gè)問題。我們之前 C# 的靈活程度允許了我們將整數(shù)小數(shù)等數(shù)值(包括字符串啊字符這些)存儲(chǔ)起來(lái),而學(xué)習(xí)到了委托之后,我們發(fā)現(xiàn),方法也能存儲(chǔ)起來(lái)了。

那么,有沒有更高級(jí)的存儲(chǔ)模式呢?有的,存儲(chǔ)代碼。這是個(gè)什么概念呢?你想想,我如果連代碼的邏輯也能抽象出來(lái)存儲(chǔ)到變量里去,這是不是有些科幻了?表達(dá)式樹就是這么一種思維,它允許你甚至把你想要的操作模型拆解成一個(gè)一個(gè)的小部分,然后組裝起來(lái)存儲(chǔ)起來(lái),也可以只存儲(chǔ)這些小部分。這就是表達(dá)式樹的基本思想。

2-1 引例 1:表達(dá)式樹求 1 加 2 的和

先來(lái)看最基礎(chǔ)的例子,求 1 + 2 的結(jié)果。當(dāng)然,大家都知道是 3,不過(guò)我們這里不是教各位計(jì)算的,這里是給大家說(shuō)明,如何去使用表達(dá)式樹。

我們先思考,1 + 2 是什么。1 + 2 是一個(gè)表達(dá)式,它利用到了加法運(yùn)算。加法運(yùn)算是一個(gè)二元運(yùn)算(Binary Operation),換句話說(shuō)就是,加號(hào)運(yùn)算符是雙目運(yùn)算符,就這個(gè)意思。加法計(jì)算既然需要兩個(gè)數(shù)進(jìn)行計(jì)算,按照數(shù)學(xué)的思維,我們是在加號(hào)的左右兩側(cè)匹配上一個(gè)數(shù)字信息,然后寫到加號(hào)的兩側(cè),最終表達(dá)出來(lái)的結(jié)果就是兩個(gè)數(shù)的和了。

是的,我們可以抽取出來(lái)這種抽象的邏輯。我們知道,C# 編程語(yǔ)言怎么搞,語(yǔ)法再?gòu)?fù)雜,底層也是進(jìn)行的各種操作和運(yùn)算。我們只需要把這些運(yùn)算抽象成上面這樣,就可以得到我們應(yīng)該有的結(jié)果。當(dāng)然,單目運(yùn)算(一元運(yùn)算)也就是把下面的 1 和 2 兩個(gè)部件刪掉一個(gè),三目運(yùn)算(三元運(yùn)算)則是把下面的部分再追加一個(gè)部件上去。更加復(fù)雜的話,就是往這個(gè) 1 和 2 上遞歸地套用這些運(yùn)算過(guò)程。這就很像是你學(xué)的英語(yǔ)語(yǔ)法,主句加從句,從句里可能還會(huì)有別的主從句關(guān)系,就是這么個(gè)意思。

表達(dá)式樹的邏輯就是,我們把這些運(yùn)算符按照上述圖片給出的模型來(lái)存儲(chǔ)起來(lái)即可。那么,怎么寫代碼來(lái)完成呢?這就需要我們先用到 Expression 類型了。該類型是一個(gè)抽象類,而且還是一個(gè)工廠類型。這個(gè)類型可以搭配里面的方法來(lái)構(gòu)建出一個(gè)表達(dá)式的抽象表達(dá)出來(lái),然后按變量的形式存儲(chǔ)起來(lái)。

當(dāng)然,你熟練了的話,因?yàn)樗鼈兌际亲兞康年P(guān)系,你完全可以內(nèi)聯(lián)放進(jìn) addOperation 變量的執(zhí)行方法的參數(shù)里去。

我們仔細(xì)來(lái)看。Add 方法是一個(gè)方法,它帶有兩個(gè)參數(shù),對(duì)應(yīng)了我們上方給出的這個(gè)加號(hào)下面引用的兩個(gè)數(shù)值 1 和 2;而 1 和 2 是一個(gè)數(shù),而我們這里是把 1 和 2 當(dāng)成了存儲(chǔ)的東西,因此不能直接寫 1 和 2,而是把這個(gè) 1 和 2 呢,用 Expression.Constant 方法給包裹起來(lái)。這個(gè) Expression.Constant 方法就是包裹一個(gè)常量進(jìn)去。它傳入一個(gè)參數(shù),將常量數(shù)值轉(zhuǎn)換為一個(gè)可存儲(chǔ)的表達(dá)式。這里的 1 和 2 會(huì)被編程視為是“常量表達(dá)式”,它也是表達(dá)式的一種,只不過(guò)它沒有別的分支,只有一個(gè)數(shù)字的節(jié)點(diǎn)而已。

接著,我們有了兩個(gè)常量表達(dá)式之后,我們就需要完成加法操作,把它們倆給關(guān)聯(lián)起來(lái)。關(guān)聯(lián)的方法則是調(diào)用 Add 方法。這個(gè)方法專門用來(lái)計(jì)算兩個(gè)表達(dá)式的加法。于是,我們用法就是,把剛才的兩個(gè) Constant 方法調(diào)用的結(jié)果給傳入進(jìn)去,然后作為這個(gè) Add 方法的參數(shù)。于是乎,該 Add 方法得到的結(jié)果就是,加法運(yùn)算的結(jié)果表達(dá)式。是的,它得到的結(jié)果仍然是一個(gè)表達(dá)式,而不是結(jié)果數(shù)值。因?yàn)槲覀冊(cè)诒疚慕榻B的內(nèi)容上來(lái)說(shuō),它存儲(chǔ)的就是一個(gè)一個(gè)的表達(dá)式,而并不是結(jié)果。

那么,我們使用 Console.WriteLine 方法會(huì)得到什么結(jié)果呢?我們來(lái)試試看:

我們看到了一個(gè)括號(hào)包裹的表達(dá)式:1 + 2,這就是整個(gè)表達(dá)式樹做基本的樣貌和用法。

至于怎么去獲取其中的結(jié)果(即這個(gè)表達(dá)式的結(jié)果 3),我們稍后來(lái)說(shuō)。

2-2 如果我記不住這些方法名字怎么辦?

呃,這個(gè)只需要你去查資料和看元數(shù)據(jù)就可以了。你完全不必去記住它們。我再三強(qiáng)調(diào),我們是學(xué)習(xí)知識(shí),不是應(yīng)付考試。這些東西不需要你全都記住,到時(shí)候查找就可以了。

另外,如果你稍微會(huì)一丟丟的英語(yǔ)也不至于猜不到結(jié)果。比如說(shuō),我如果要完成減法,那么用的方法名就可以是 minus、subtract 這類型的詞語(yǔ)表示減法。實(shí)際上,表達(dá)式樹里用到的是 Expression.Subtract 方法,用的是 subtract 這個(gè)單詞。因此,你猜都猜得到這些結(jié)果,甚至不必自己去背。

因此,不要隨時(shí)隨地都死記硬背。

2-3 如果表達(dá)式再?gòu)?fù)雜一些,怎么做?

我剛才說(shuō)到了,表達(dá)式用的是嵌套的思維。如果我有個(gè)比如說(shuō) 3 + 2 - 5 的表達(dá)式的話,要去存儲(chǔ)的話,我們會(huì)按照數(shù)學(xué)運(yùn)算的優(yōu)先級(jí),先確定是 3 + 2 作為第一步運(yùn)算結(jié)果,然后再把結(jié)果再減 5,得到最終的結(jié)果,于是:

是的,這里有一點(diǎn)奇怪的是,如果你第一次接觸語(yǔ)義分析的話,可能很難去理解為啥一個(gè)減號(hào)直接和一個(gè)加號(hào)連起來(lái)了。實(shí)際上,這里的箭頭并不是我們真正意義上的傳遞的過(guò)程,它只是表示我這里有兩個(gè)分支。其中,下面的 3 和 2 同時(shí)由一個(gè)加號(hào)的節(jié)點(diǎn)連下來(lái),因此下面的這個(gè)模塊是一個(gè)表達(dá)式。

所以,整個(gè)這種分支關(guān)系,我們能夠看成整體的就把它看成整體,整體是表達(dá)式之后,你再來(lái)看這個(gè)完整的圖就很好理解了:它是在對(duì)兩個(gè)表達(dá)式進(jìn)行減法運(yùn)算,左側(cè)的表達(dá)式是我們的被減數(shù),只不過(guò)此時(shí)被減數(shù)也是一個(gè)表達(dá)式,是這個(gè)表達(dá)式的結(jié)果才能作為被減數(shù)的結(jié)果來(lái)用;而右邊的 5 是充當(dāng)減數(shù)的角色。

那么,寫成表達(dá)式樹的代碼的話,是這樣的:

來(lái)看下運(yùn)行結(jié)果吧:

打括號(hào)也就是為了你能夠看清楚分清楚調(diào)用邏輯和表達(dá)式的關(guān)系。

如果還有更復(fù)雜的處理的話,這個(gè)就需要你自己想了,大概就是反復(fù)進(jìn)行嵌套調(diào)用,這種思路。

2-4 引例 2:表達(dá)式樹賦值無(wú)參 lambda 表達(dá)式

表達(dá)式樹不僅可以只操作一個(gè)數(shù)學(xué)表達(dá)式,它也可以操作一個(gè) lambda 表達(dá)式。Lambda 表達(dá)式我們已經(jīng)再熟悉不過(guò)了,它的出現(xiàn)是為了接替匿名函數(shù),稱為最好用的、內(nèi)聯(lián)使用的、無(wú)方法名的方法。

那么,如果要完成一個(gè)方法改造成表達(dá)式的邏輯的話,我們的辦法就是采用一個(gè)叫 Expression.Lambda 方法來(lái)完成。

假設(shè),我們想要將返回 3 + 2 的表達(dá)式一個(gè) lambda 表達(dá)式給作為表達(dá)式樹的賦值,那么我們?cè)趺醋瞿??首先我們要了解,lambda 和普通表達(dá)式的區(qū)別。

Lambda 表達(dá)式多了一坨參數(shù)。Lambda 右邊賦值的是一個(gè)表達(dá)式結(jié)果(當(dāng)然也可以是一個(gè)大括號(hào)包裹的多個(gè)語(yǔ)句)。由于表達(dá)式樹的限制,我們只可以操作單個(gè)返回表達(dá)式的 lambda 表達(dá)式。因?yàn)楸磉_(dá)式樹的發(fā)明是為了輔佐查詢表達(dá)式的,而查詢表達(dá)式的逐個(gè)執(zhí)行步驟(你回憶一下,查詢表達(dá)式的每一個(gè)從句),是不是都是只帶一個(gè)基本的、簡(jiǎn)單的表達(dá)式處理過(guò)程?

而由于這種限制,貌似我們對(duì)表達(dá)式樹寫起來(lái)更加方便一些了,因?yàn)槲覀兛梢园凑找?guī)則進(jìn)行一步一步地拆分得到這些過(guò)程,完全不必去考慮如果多個(gè)語(yǔ)句怎么去寫代碼的問題。

回到這里。我們要想把 () => 3 + 2 翻譯成表達(dá)式樹,怎么做呢?首先,因?yàn)樵?lambda 表達(dá)式?jīng)]有參數(shù),只有一個(gè)返回值,因此我們可以試著把表達(dá)式 3 + 2 構(gòu)造起來(lái),這個(gè)前面的內(nèi)容里已經(jīng)給出了;而這個(gè) () => 的部件怎么去體現(xiàn)呢?

實(shí)際上,前文說(shuō)到的 Expression.Lambda 已經(jīng)給出了答案。你只需要把表達(dá)式賦值到這個(gè)方法里當(dāng)參數(shù),然后給出對(duì)應(yīng)的 lambda 表達(dá)式的兼容委托類型即可:

內(nèi)層(第 2 到第 5 行代碼)構(gòu)造的是 3 + 2 的表達(dá)式,而我們只需要直接將結(jié)果傳遞到里面去即可。

我們來(lái)看這個(gè)表達(dá)式,然后直接使用輸出語(yǔ)句得到的結(jié)果吧:

可以看到,這個(gè)表達(dá)式還是很清晰的。這就是這個(gè)處理方式。

2-5 引例 3:表達(dá)式樹賦值帶參數(shù)的 lambda 表達(dá)式

那么,如果 lambda 帶有參數(shù)的話,怎么辦呢?比如說(shuō) (x, y) => x + y 的表達(dá)式,我們要怎么去做呢?這次,我們需要用 Expression.Lambda 的另外一個(gè)重載版本:這次我們需要傳入兩個(gè)參數(shù)。

首先,我們要定義兩個(gè)參數(shù)的表達(dá)式樹的實(shí)體類型,這里我們用到的方法是 Expression.Parameter。該方法要求傳入兩個(gè)參數(shù),一個(gè)是參數(shù)類型,另外一個(gè)是參數(shù)名稱:

很好理解,對(duì)吧。接著,我們將它們傳入到 Expression.Lambda 里去當(dāng)參數(shù)。這一次,由于我們帶有新鮮額外的參數(shù),因此我們這次傳入的這個(gè) Expression.Lambda 也有所不同。此時(shí)我們需要為這個(gè)重載版本傳入兩個(gè)參數(shù)。第一個(gè)參數(shù)是 lambda 表達(dá)式本來(lái)應(yīng)該執(zhí)行返回的表達(dá)式,那么,由于本例子里是“參數(shù) + 參數(shù)”,也就是這里所說(shuō)的 xy 的和,因此我們直接使用如此操作給它們加起來(lái):

接著,我們將該結(jié)果當(dāng)成第一個(gè)參數(shù)傳入到 Expression.Lambda 里去;另外,第二個(gè)參數(shù)呢?第二個(gè)參數(shù)這里我們傳入的就是這些參數(shù)本身了。如果沒有參數(shù)直接加起來(lái)肯定是不合理的操作,于是我們要帶入兩個(gè)參數(shù)進(jìn)去。

最后,我們將它們一并傳入進(jìn)方法之中:

請(qǐng)一定注意這一次我們的 lambda 有兩個(gè)參數(shù)和一個(gè)返回值,因此我們寫的時(shí)候,Expression.Lambda 的泛型參數(shù)上兼容的類型就應(yīng)該是 Func<int, int, int>。

來(lái)看下結(jié)果吧:

帶參數(shù)的 lambda 表達(dá)式就是如此搞定的。

2-6 引例 4:lambda 可以直接賦值給表達(dá)式樹的部件

實(shí)際上,上述的 lambda 表達(dá)式有些復(fù)雜,因此 C# 允許我們直接將 lambda 傳入進(jìn)去。于是,原本很長(zhǎng)的賦值過(guò)程,可以簡(jiǎn)化成一個(gè) lambda 直接賦值的操作。

先別急。為了能夠吃透賦值的原理,我們這里需要先看看這個(gè) finalOperation 的類型才行。把鼠標(biāo)放到這個(gè)最終表達(dá)式的變量上去:

我們可以看到,它的類型其實(shí)是 Expression<Func<int, int, int>>。我們之前教過(guò)大家如何看長(zhǎng)泛型名稱。Expression<Func<int, int, int>> 要從內(nèi)往外理解。先看到內(nèi)層,我們可以看到 Func<int, int, int> 的結(jié)果,它表示兩個(gè) int 參數(shù)和一個(gè) int 類型返回值的委托類型;而外側(cè)就是套了一個(gè) Expression<該委托類型> 的殼子罷了。所以這個(gè)長(zhǎng)泛型名稱的意思是,表示如此委托類型的表達(dá)式樹的部件。而這里的 Expression<> 類型的名字 Expression,實(shí)際上是代表了可以包裹任何一個(gè) lambda 表達(dá)式的表達(dá)式樹的部件。而如果把類型名稱取名叫 LambdaExpression<> 就顯得有些啰嗦了,所以就省去了前面的 lambda 這個(gè)單詞前綴。

那么,此時(shí)我們刪除掉多余的部件,只保留一個(gè) lambda 表達(dá)式,并且顯式給出變量的返回值類型:

這樣一句話就可以了。是不是很方便?C# 甚至連這樣的 lambda 的賦值過(guò)程都可以幫我們自動(dòng)處理,是不是很秀~

我們?cè)俅未蜷_程序運(yùn)行起來(lái),可以看到結(jié)果和原來(lái)寫了一大堆的賦值是完全一樣的:

2-7 引例 5:帶 .NET 庫(kù)里的 API 的表達(dá)式樹的寫法

假設(shè),我們將前文的簡(jiǎn)易操作改成方法調(diào)用的話,這怎么做呢?比如說(shuō),我們要比較兩個(gè)字符串,其中第一個(gè)字符串是 x,第二個(gè)字符串是 y,那么它們的比較操作就是 x == y 或者是 x.Equals(y) 了。

假如我們這次用一下 Equals 方法的調(diào)用過(guò)程的話,那么這個(gè)操作應(yīng)該如何去做到呢?

我們照舊使用前面一節(jié)的辦法——lambda 的 C# 語(yǔ)法糖,直接把 lambda 賦值給一個(gè)。

我的天,這也太方便了點(diǎn)吧!C# 的編譯器甚至連這樣的操作都可以直接幫我們?nèi)ヌ幚淼?,C# 的編譯器 yyds!

這個(gè)方法的原本操作就比較麻煩了,因此本小節(jié)內(nèi)容就不給大家介紹它的完整版實(shí)現(xiàn)方式了。

Part 3 AsQueryable 方法

前文我們說(shuō)了這么多的處理機(jī)制,下面我們也應(yīng)該說(shuō)一些契合 LINQ 的內(nèi)容了。

先來(lái)說(shuō)說(shuō) IQueryable<> 接口。我們查看該接口的元數(shù)據(jù)可以發(fā)現(xiàn),它里面啥玩意兒沒有,但走了 IEnumerable 接口派生,并且也走了一個(gè)沒有帶泛型參數(shù)的 IQueryable 接口派生。

而我們?cè)俅未蜷_ IQueryable 無(wú)泛型參數(shù)的接口的元數(shù)據(jù)可以發(fā)現(xiàn):

它里面就只帶有三個(gè)屬性:ElementType、Expression 以及 Provider。這三個(gè)屬性大概給大家說(shuō)說(shuō)是什么。

第一個(gè)屬性 ElementType 的意思是,我們用于最終迭代的序列,它的元素的類型是什么。

第二個(gè)屬性 Expression 就是當(dāng)前查詢的表達(dá)式了。比如這個(gè)查詢機(jī)制里面就代指這個(gè)完整的 LINQ 查詢表達(dá)式。雖然我們知道這個(gè)查詢表達(dá)式特別復(fù)雜,帶有 where 從句和 select 從句,但實(shí)際上在底層也會(huì)被處理為一個(gè)完整的表達(dá)式樹的實(shí)體。

第三個(gè)屬性 Provider 為其提供基本的轉(zhuǎn)換,即如何把一個(gè)查詢過(guò)程轉(zhuǎn)換為一個(gè)迭代對(duì)象,或者反過(guò)來(lái),即如何將一個(gè)迭代對(duì)象搞成一個(gè)查詢過(guò)程存儲(chǔ)起來(lái)。它里面帶有四個(gè)方法,分別是兩個(gè) CreateQuery 的方法重載,和兩個(gè) Execute 的方法重載。其中 CreateQuery 就是把序列轉(zhuǎn)換轉(zhuǎn)為查詢存儲(chǔ)過(guò)程的表達(dá)式樹的實(shí)體的對(duì)象;而 Execute 就是專門執(zhí)行查詢過(guò)程。

從設(shè)計(jì)來(lái)說(shuō),第三個(gè)屬性已經(jīng)被完整實(shí)現(xiàn)好了,而它也在底層使用,因此我們并不需要去管它。我們需要做到的,就只是去迭代序列而已。做法其實(shí)很簡(jiǎn)單,直接 foreach 即可。

結(jié)果也是完全可以得到的,只不過(guò)你可能會(huì)在運(yùn)行期間看到它的速度會(huì)比普通的迭代過(guò)程要慢上一些:

為什么運(yùn)行的時(shí)候,迭代過(guò)程是逐步而且緩慢的呢?因?yàn)樗牡讓邮菍⑽覀兊拿恳粋€(gè)迭代過(guò)程都存儲(chǔ)起來(lái)了,因此操作是逐步進(jìn)行的,所以才會(huì)這樣。

Part 4 動(dòng)態(tài)查詢

下面我們來(lái)說(shuō)說(shuō)表達(dá)式樹和動(dòng)態(tài)查詢的關(guān)聯(lián)和用法。先來(lái)說(shuō)說(shuō)動(dòng)態(tài)查詢的概念吧。

4-1 動(dòng)態(tài)查詢的概念

我們來(lái)看看 B 站的番劇查詢頁(yè)面。

在右側(cè)我們可以發(fā)現(xiàn)各種各樣的條件設(shè)置,提供給用戶自己查詢。右側(cè)可以查詢番劇的類型、配音是國(guó)配還是日配、地區(qū)是國(guó)漫還是日漫還是別的國(guó)家的漫畫、番劇的發(fā)布時(shí)間是什么時(shí)候,以及番劇的具體類型。

這么多條件不可能人人全都會(huì)選擇,我們只會(huì)逐步選擇其中一些,或者我們只會(huì)選擇其中的一部分作為搜索選項(xiàng)。我們就把條件并不是固定的,而是通過(guò)用戶控制而逐步添加的查詢規(guī)則稱為動(dòng)態(tài)查詢(Dynamic Query)。

我們不來(lái)做這么復(fù)雜的,為了大家初步了解動(dòng)態(tài)查詢,我們簡(jiǎn)單給大家介紹一些基本的查詢規(guī)則的疊加替換就足夠了。

4-2 理解代碼

假設(shè)我們?nèi)匀皇遣檎曳瑒〉木唧w情況,那么我們可能會(huì)用到里面的一些條件,比如查詢番劇的名字帶有什么詞語(yǔ)之類的。會(huì)編程的你自然會(huì)想到,用戶輸入一個(gè)名字的部件,然后程序可以使用字符串的 Contains 方法來(lái)完成匹配,看看哪些番劇都包含了這個(gè)輸入的內(nèi)容。另外,我們還會(huì)有番劇是哪個(gè)國(guó)家的,這些條件。

那么,做法是這樣的。假如我們給一個(gè)叫做番劇創(chuàng)建一個(gè)對(duì)象類型:

分別的四個(gè)屬性是番劇名字、番劇的國(guó)家、番劇的發(fā)布日期以及番劇是哪個(gè)季節(jié)的番劇。

這里簡(jiǎn)單做一個(gè)科普。日漫番劇只分 1、4、7、10 月份的四種日期的新番,因?yàn)槿毡镜姆瑒∫话闶且粋€(gè)季度一個(gè)季度的發(fā)布進(jìn)度。正是因?yàn)檫@個(gè)原因,一周一集的番劇在一個(gè)季度里最多可以發(fā)布到 13 集,最少也有 11 集,所以這就是為什么日漫的一個(gè)番劇有些只有這么多的集數(shù)。

這里我們規(guī)劃的 Season 枚舉類型匹配上對(duì)應(yīng)的 1、4、7、10 月番,不過(guò)我們用的是春、夏、秋、冬四個(gè)季節(jié)來(lái)代替它們各自的英文名 January、April、July 和 October,寫這些的話,不好記也比較長(zhǎng)。

我們直接來(lái)看代碼吧。

4-3 擴(kuò)展方法 And

因?yàn)閮?nèi)容太多了,還是寫中文注釋吧。寫英語(yǔ)注釋有點(diǎn)看著費(fèi)力。先來(lái)說(shuō)說(shuō)最下面的這段代碼。最后面這個(gè)代碼是用來(lái)拼接兩個(gè)表達(dá)式樹的條件部分的。Update 方法用來(lái)更新查詢期間的 lambda 表達(dá)式的返回部分代碼。因?yàn)槲覀兪窍雸?zhí)行取出過(guò)程,因此我們每一個(gè) lambda 表達(dá)式的參數(shù)并沒有改變,但改變了的是返回結(jié)果。比如說(shuō),我有條件 A,那么新加入的條件 B 就和 A 應(yīng)該是邏輯與的關(guān)系,那么兩個(gè) lambda 表達(dá)式本來(lái)分別為

  • (bangumi) => a

  • (bangumi) => b

現(xiàn)在我們更新后,需要改成

  • (bangumi) => a && b

這個(gè)道理,即參數(shù)仍然不變動(dòng),只是需要我們把兩個(gè)條件給邏輯與起來(lái),因此寫的時(shí)候,用到的是 Update 方法,并且使用到 AndAlso 來(lái)完成邏輯與的拼接過(guò)程。注意是 AndAlso,因?yàn)樵诿Q取名的時(shí)候,And 被視為位與運(yùn)算了,因此這里用別的名字代替了一下。

接著,我們回到上面來(lái)看逐個(gè)代碼。我們說(shuō)一個(gè)就行了,剩下的倆可以自己看懂的。

來(lái)說(shuō)說(shuō)第一個(gè)吧。

4-4 第一個(gè)判斷邏輯:判斷番劇名稱

首先,程序會(huì)打出一行文字提示用戶輸入你要檢索的番劇的名稱。名稱可以是全名,也可以是名字的一部分。如果不想按這個(gè)條件搜索的話,可以輸入下劃線 _ 跳過(guò)。

那么,下一行代碼就會(huì)開始執(zhí)行讀取。讀取后是一個(gè)字符串,正好是我們需要的內(nèi)容。接著,我們拿出這個(gè)字符串來(lái)判斷看它是不是下劃線。如果是的話,就說(shuō)明用戶不想判斷這個(gè)條件,于是我們就跳過(guò) if 的條件;否則就執(zhí)行里面的語(yǔ)句。

里面的語(yǔ)句實(shí)際上是一個(gè)更新過(guò)程。做法就是把原始的 predicate 條件和新條件給拼起來(lái)。拼起來(lái)的操作是用的我們最后面實(shí)現(xiàn)的這個(gè)擴(kuò)展方法 And 來(lái)做的。我們把 predicate 放在實(shí)例的位置上,然后傳入一個(gè) lambda 表達(dá)式。這里的 lambda 表達(dá)式想必我不再多說(shuō)了吧,因?yàn)槲覀冋f(shuō)的 Expression<> 類型是可以直接接收 lambda 表達(dá)式的結(jié)果的,它自己就可以直接拿來(lái)被編譯器自動(dòng)處理為表達(dá)式樹的構(gòu)造。這里我們參照規(guī)則,把 Bangumi 類型的實(shí)例當(dāng)成參數(shù)的部分要保留起來(lái),然后后面的執(zhí)行判斷的表達(dá)式是 bangumi.Name.Contains(bangumiName)。這里的 bangumiName 實(shí)際上就是 lambda 表達(dá)式捕獲而來(lái)的外部變量 bangumiName。網(wǎng)上看就會(huì)發(fā)現(xiàn),它就是 Console.ReadLine 得到的這個(gè)字符串。所以,它剛好匹配我們需要檢索的規(guī)則,即判斷這個(gè)番劇的番劇名稱是不是包含這個(gè)我們輸入的字符串信息。如果是的話,就說(shuō)明名字這部分我們檢索成功了。

后面兩個(gè)判斷過(guò)程分別是判斷國(guó)家名稱以及判斷番劇的日期和季節(jié)。這個(gè)就不多說(shuō)了,雖說(shuō)判斷日期和季節(jié)相對(duì)看起來(lái)復(fù)雜不少,但實(shí)際上難度也不大,就只是一個(gè)類型的轉(zhuǎn)換,從我們熟悉的字符串 string 類型轉(zhuǎn)換為日期類型 DateOnly 的過(guò)程。轉(zhuǎn)換的目的是為了簡(jiǎn)化使用這個(gè)日期的年份和月份的數(shù)據(jù)。

4-5 查詢表達(dá)式構(gòu)造完成,最后使用查詢過(guò)程

最后我們稍微說(shuō)一下查詢末尾的過(guò)程。

實(shí)際上也不是特別復(fù)雜。第 2 行代碼我們用到了 AsQueryable 方法,它的目的是為了起到一個(gè)轉(zhuǎn)換為 IQueryable<> 的對(duì)象。這樣的轉(zhuǎn)換是為了讓我們可以使用剛才的拼接的條件。接著,我們使用前文各種拼接之后得到的 predicate 的變量,直接傳到 Where 方法里。因?yàn)檫@里的 Where 方法和我們的 IEnumerable<> 里學(xué)到的查詢表達(dá)式不同,這里的 Where 需要我們傳入一個(gè)表達(dá)式樹的 lambda 表達(dá)式的部件進(jìn)去,所以我們要把我們前文構(gòu)造的 predicate 這個(gè)變量傳進(jìn)去。這個(gè) predicate 變量仍然保持著 lambda 的樣貌,只是期間可能變動(dòng)過(guò)幾次判斷代碼,畢竟代碼都用 &&(即調(diào)用的 AndAlso 方法)給連起來(lái)了。

然后,我們就會(huì)得到一個(gè)新篩選的結(jié)果,接著使用 foreach 循環(huán)迭代該查詢集合就完事了。

順帶一說(shuō)。實(shí)際上排序也可以做的,不過(guò)在這里已經(jīng)很復(fù)雜了,所以我沒有考慮把它放進(jìn)來(lái)說(shuō)。實(shí)際上,.NET 的查詢架構(gòu)里,IEnumerable<>IQueryable<> 是一樣的,Enumerable 靜態(tài)類型控制和包含了眾多的 IEnumerable<> 的查詢方法,而 IQueryable<> 也有和 Enumerable 靜態(tài)類型包裹的這些查詢方法,只不過(guò)傳參不同罷了。IQueryable<> 的對(duì)應(yīng)類型是 Queryable。你可以通過(guò)這個(gè)類型里的擴(kuò)展方法來(lái)完成對(duì) IQueryable<> 接口實(shí)例的查詢過(guò)程。而你可以打開元數(shù)據(jù)看看 Queryable 類型,它包含的眾多方法,雖然傳參不同,但名字卻和 Enumerable 里的取名如出一轍。比如剛才的條件判斷,原來(lái)用的就是 Where,現(xiàn)在轉(zhuǎn)到這個(gè)類型里仍然是 Where;同理地,如果要排序,我們也只需要多一次調(diào)用 OrderBy 即可。是的,Queryable 靜態(tài)類型也包含 OrderBy 方法來(lái)表示查詢規(guī)則。所以,這并不需要你再學(xué)一遍 Queryable 的查詢的方法。

那么,至此我們就把動(dòng)態(tài)查詢的代碼全部給大家講解了一遍。而且 C# 3 的語(yǔ)法就全部完結(jié)了。下面我們要進(jìn)入 C# 4 的學(xué)習(xí)。C# 4 比較奇怪的是,它和 C# 5 差不多,新特性相當(dāng)少。因此 C# 4 可能只會(huì)有四五講的內(nèi)容就可以完全介紹完畢;不過(guò) C# 5 的特性是我們以后學(xué)習(xí)更復(fù)雜的語(yǔ)法的奠基石,因此它的內(nèi)容會(huì)相對(duì)較多,就跟泛型一樣,已經(jīng)是 C# 里一個(gè)眾人皆知的標(biāo)志了。


第 113 講:C# 3 之查詢表達(dá)式(十七):表達(dá)式樹和動(dòng)態(tài)查詢的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
烟台市| 康马县| 南靖县| 新安县| 石景山区| 沾化县| 汝州市| 石楼县| 吴桥县| 濮阳市| 如皋市| 祥云县| 京山县| 双鸭山市| 襄樊市| 上高县| 江陵县| 益阳市| 景德镇市| 安国市| 绿春县| 安义县| 黄山市| 祁阳县| 林西县| 长宁县| 榆树市| 云龙县| 睢宁县| 浮梁县| 高密市| 安徽省| 阿鲁科尔沁旗| 四会市| 阜平县| 淮阳县| 共和县| 大丰市| 股票| 确山县| 丹巴县|