第 112 講:C# 3 之查詢表達式(十六):LINQ 階段性練習(二)
今天咱們來完成階段練習。階段練習在之前已經(jīng)做過一次了,不過用的是關(guān)鍵字的查詢表達式寫法。
這次我們確實也沒啥好說和好驗證測試的,不過我們還額外學習了一些方法,比如聚合函數(shù)以及關(guān)系代數(shù)的相關(guān)運算。這次我們就來做一些題目,當然,可能一些題目不會考察這些知識點,只是想看你靈活使用 LINQ 的程度。
上題。
題目 1
給定一個序列 int[] array
,由一些可能相同也可以不同的整數(shù)構(gòu)成。請寫出查詢表達式或 LINQ 的相關(guān)方法查詢獲取這個數(shù)字所有數(shù)字出現(xiàn)的頻次。所謂的頻次就是出現(xiàn)的次數(shù)的一個總結(jié)表,比如數(shù)組里有兩個 1,那么查詢結(jié)果里必須包含這個數(shù)字 1 有兩次的信息。
答案:
題目 2
給定一個字符串 string s
,請刪除該字符串里的所有 'a'
字符。注意,因為查詢表達式的結(jié)果是返回新對象,因此我們無需從字符串里改變結(jié)果。
答案:
題目 3
統(tǒng)計一個數(shù)組 int[] array
里每一個元素的和。
答案:
題目 4
拼接一系列字符串構(gòu)成的字符串數(shù)組 string[] strings
。不能使用 string
類型里給出的方法來完成。
答案:
題目 5
迭代所有 7 和 7 的倍數(shù)。
答案 1:
答案 2:
題目 6
假設(shè)一個類型 Student
里只包含學生名(Name
屬性,string
類型)、語數(shù)外三科成績(Chinese
、English
、Math
屬性,且均為 int
類型),請找出三科都有成績的學生的名字。比如說,有學生作弊或者沒有參加考試導致某學科甚至三個學科都為 0 分。我們需要找的是沒有曠考并且沒有作弊得到有成績結(jié)果的學生,而且還得是三科全都有成績的學生。
答案 1:
答案 2:
答案 3:
題目 7
作者將整個 C# 教程的文檔全部備份放在了本地路徑在一個叫 Tutorial
的文件夾里。假設(shè)它位于 P:\Tutorial
。

名字的格式我都固定為“三位數(shù)字(表示講數(shù))+講解的內(nèi)容的名字.md”?,F(xiàn)在我想讓你找到所有關(guān)于 LINQ 的文件的對應講數(shù)(就前面那個三位數(shù)就是對應的講數(shù))。
答案:
本題目需要你至少熟悉文件操作,以及正則表達式。本教程對 API 的內(nèi)容并不會作過多介紹,因為是語法教程。不過,如果我有空的話,我會出單獨的教程視頻或者文章給大家專門介紹這些專門的 API 的用法和操作。
如果你會這些 API 的話,你可以試著去了解一下這個題目的具體的查詢表達式的書寫,每一個從句都是什么意思。大概思路就是,先獲取文件夾的信息,然后迭代這個文件夾的所有文件。接著,由于迭代的變量是 FileInfo
類型,里面有一個 Name
是這個文件的文件名。我們需要取出該文件名,然后參與比較和判別。正則表達式的作用就是為了去契合判斷和排除掉不滿足命名規(guī)則的文件。首先我們要求文件必須是 .md
的后綴名,所以不是的可以排除掉(第 11 行代碼);然后去看文件的文件名是否按規(guī)范取名(規(guī)范是三個數(shù)字,外帶“C# 3 之查詢表達式”,然后后面跟著編號表示整個板塊的第幾講,然后是冒號,最后是一些額外的說明文字)。我們只需要找到規(guī)范標準的部分(三個數(shù)字,然后知道 LINQ 的命名文件一定是“C# 3 之查詢表達式”,然后后面隨意,所以我們按照這個規(guī)則去寫正則表達式進行匹配就可以了。
如果文件匹配失敗,那么我們就不去管這個文件了,因為它不是我們需要的文件(第 14 行)。注意第 14 行還有一個額外的判斷:match.Groups.Count >= 2
。這是正則表達式匹配期間需要的額外規(guī)則:因為我們想要取出這個三位數(shù),因此正則表達式允許我們?nèi)〕鲎远x的信息,做法是給需要取出的信息用小括號括起來,所以這里的正則表達式的最開頭的 \d{3}
要括起來。
括起來之后,我們在取值得到的結(jié)果里(Groups
屬性里)就有我們需要的結(jié)果了。注意,Groups
默認匹配成功會至少帶有一個元素,就是這個匹配成功的字符串本身;如果要取出自定義的括號的內(nèi)容,是從索引為 1 開始的部分來取值的,所以我們這里要求至少 Groups
在匹配成功之后包含兩項元素,因為 [1]
才是我們需要的內(nèi)容。
最后,我們?nèi)〕鲞@個三位數(shù),嘗試去將其解析為 int
類型。早就說過 string
轉(zhuǎn) int
是沒辦法直接轉(zhuǎn)的,需要我們使用解析的相關(guān)方法來完成。這里使用 TryParse
方法可以保證和驗證我們是否可以將結(jié)果正常解析為 int
數(shù)值。該方法帶有一個 out
參數(shù)。該參數(shù)我們是無法在查詢表達式里使用的。因為查詢表達式最終會被翻譯成一個個的方法調(diào)用,以及 lambda。而 lambda 里的變量的可訪問范圍只在 lambda 環(huán)境里才能用。我這里定義的 out
參數(shù)實際上也只能在當前的這個 lambda 范疇里使用。因此,我們無法在后續(xù)的比如 let
從句甚至是 select
從句里使用這個從句里定義的 out
參數(shù)結(jié)果。
于是,我們在查詢表達式的外部定義一個完全無意義的臨時變量 discard
來接收該不使用的 out
參數(shù),保證語法的正確性。這種操作很騷,但是也很嚴謹正確,希望你能夠?qū)W會。
最后,如果 TryParse
方法返回 true
,那么就說明這個三位數(shù)可以成功解析為 int
結(jié)果,那么我們就調(diào)用 Parse
方法將其真正意義上轉(zhuǎn)為整數(shù)結(jié)果,然后得到的 integer
結(jié)果直接返回出去就可以了。至此,這個查詢表達式就結(jié)束了。
呃,稍微啰嗦一下。
注意,lambda 里定義的變量不能在別的地方用,但是反之不然。lambda 可以捕獲外部變量,這意味著你可以“單向”從外往里傳輸數(shù)值和數(shù)據(jù);但你不能反過來。這是 lambda 和匿名函數(shù)的基本規(guī)約。剛才我們使用了 discard
變量來接收這個不需要的結(jié)果,實際上在后續(xù)我們?nèi)钥梢允褂?discard
變量,因為這個變量此時是寫在兩個 lambda 的外面的,從語法上說,你是可以在后面的 select
和 let
從句里使用它的,但是,這不是真正意義上的正確的用法。因為我們再三說明,查詢表達式等價于方法調(diào)用的模型,所以里面?zhèn)魅氲膮?shù)是一個一個的 lambda 表達式。這些 lambda 表達式的執(zhí)行先后順序并不是真正意義上的先誰后誰,因為 lambda 本身就有延遲推遲被調(diào)用的效果,所以我們拿不準該結(jié)果的真正意義上的正確,所以這種用法并不是很合理。
不過你非要這么用的話,你會收到一個編譯器錯誤,告訴你 discard
變量并沒有得到賦值。不過,你可以試著給它一個沒有意義的初始值,然后試著運行一下程序來看結(jié)果。
我們來試試一個稍微簡單一點的例子吧:
注意此時的 Select
方法里,我們的參數(shù)是沒有用到的,而是直接把這里的 discard
給拿出去了。
運行程序,實際上是可以得到正確的結(jié)果的。原因是,在整個 lambda 使用環(huán)境里,因為 discard
為捕獲變量,因此它會被放進閉包里,就是我們之前說的生成的額外的嵌套類類型里當字段。所以,所有使用到的這個捕獲變量,全都會被看成是在使用這個嵌套類里的這個字段。所以,上下使用的 discard
其實也還是同一個 discard
變量,不會有錯。
但是,我們從來也強烈不建議各位這么用。因為它不是很好理解清楚,而且理解錯誤會造成很可怕的后果,所以我們不建議你這么寫代碼。
至此,我們就把階段練習給結(jié)束了。之后我們再給大家安排兩講關(guān)于 LINQ 和表達式樹的內(nèi)容,那么就可以完結(jié) LINQ 了。