.NET進階篇05-Linq、Lambda表示式
知識需要不斷積累、總結和沉澱,思考和寫作是成長的催化劑
內容目錄
一、Lambda表示式1、匿名方法2、Lambda表示式二、Linq概述三、查詢操作符1、linq初見2、常用查詢操作符篩選排序分組連接合並分頁聚合轉換四、並行Linq五、表示式樹1、認識表示式目錄樹2、拼裝表示式樹3、應用六、小結
一、Lambda表示式
1、匿名方法
使用delegate的時候很多時候沒必要使用一個普通方法,因為這個方法只有delegate會用,並且只用一次
,這時候使用匿名方法最合適。
匿名方法就是沒有名字的方法
。示例中就是用一個匿名方法建立委託例項,我們無需在寫一個具名方法給委託,使程式碼更簡潔和可讀。匿名方法也是在呼叫時執行,在myDele(1,"test")處呼叫。(用反編譯器看一下還是會生成一個具名方法的,只不過在編譯器內部使用)。
delegate bool MyDelegate(int i, string s);
MyDelegate myDele = delegate (int i, string s)
{
Console.WriteLine($"我是匿名方法,引數值{i},{s}");
return true;
};
bool b = myDele(1, "test");
2、Lambda表示式
函數語言程式設計,在C#3.0開始,我們有了Lambda表示式代替匿名方法,它比匿名方法更加簡單。Lambda運算子“=>”(發音goesto)
的左邊列出了需要的引數,右邊是利用該引數方法的實現程式碼。
Action<string> a1 = delegate (string s) { Console.WriteLine(s); };
a1("匿名方法");
Action<string> a2 = (string s)=> { Console.WriteLine(s); };
a1("Lambda表示式");
Action<string> a3 = s => { Console.WriteLine(s); };
a3("Lambda表示式,有一個引數的可以簡寫不要小括號,引數型別會自動推斷");
Action<string> a4 = s => Console.WriteLine(s);
a4("Lambda表示式,方法體只有一行,連花括號也可以省略");
另一點,通過Lambda表示式可以訪問Lambda表示式塊外部的變數。這是一個非常好的功能,但如果未正確使用,也會非常危險。
int sommVal = 5;
Func<int, int> f = x => x + sommVal;
sommVal = 7;
Console.WriteLine(f(3));
如果外部修改了sommVol值就會影響Lambda表示式的輸出,特別是在多執行緒中,可能無法確定當前的sommVal值。
Lambad表示式內部是如何使用外部的變數呢?首先編譯器會建立一個匿名類,然後將使用到的外部變數當做匿名類的建構函式的引數,當呼叫時候,就建立匿名類的一個例項,並傳遞呼叫該方法時外部變數的值。
二、Linq概述
Linq(language integrated query)語言整合查詢集成了C#程式語言中的查詢語法,使之可以使用相同的語法訪問不同的資料來源
。
根據資料來源的不同,Linq可分為linq to object,linq to sql,linq to xml,你也可以擴充套件linq to excel,to everything。為不同的資料來源提供相同的查詢介面即可。
三、查詢操作符
1、linq初見
現在我們有如下實體的集合
public class Student
{
public int Id { get; set; }
public int ClassId { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
List<Student> studentLst = new List<Student>();
假設studentLst裡已經有一些資料,然後需要查詢出年紀小於25的學生。有很多方法,可以迴圈列表挑出age<25的學生,可以使用List的FindAll,Where等方法,看起來像下面這樣(注意只有在訪問list中資料時,才會去執行過濾條件查詢,延遲查詢)
var list = studentLst.Where<Student>(s => s.Age < 25);
foreach (var item in list)
{
Console.WriteLine("Name={0} Age={1}", item.Name, item.Age);
}
where擴充套件方法的內部邏輯大概像這樣,foreach迴圈呼叫過濾的委託方法,yield
關鍵字語法糖包裝了一些複雜行為,包括會初始化一個IEnumerable類,然後給新增內容。
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> func)
{
if (source == null)
{
throw new Exception("source is null");
}
if (func == null)
{
throw new Exception("func is null");
}
foreach (var item in source)
{
if (func.Invoke(item))
{
yield return item;
}
}
}
那麼用Linq如何查詢呢?
var list = from s in studentList
where s.Age < 25
select s;
foreach (var item in list)
{
Console.WriteLine("Name={0} Age={1}", item.Name, item.Age);
}
From、where、select
都是預定義的關鍵字,查詢表示式必須以from開頭,以select或group子句結束,中間可以使用where、orderby、join等
。from子句引入資料來源studentList和範圍變數s,s就像foreach迴圈中的迭代變數。
同樣的,在執行期間定義查詢表示式時,查詢不會立即執行,在迭代資料項時執行。
2、常用查詢操作符
篩選
最常見的查詢操作就是以布林表示式的形式應用篩選器。通過where
子句篩選表示式為true的結果
var list = from s in studentList
where s.Age < 25 && s.ClassId==1
select s;
排序
Orderby
子句根據要排序型別的預設比較器,對返回序列中的元素進行排序
var list = from s in studentList
where s.Age < 25
orderby s.Name ascending
select s;
和list的以下方法類似,就是把關鍵字解析為方法,隨著查詢越來越複雜,linq這種類似sql語句就表現的更加簡潔直觀
var list = studentList.Where(s => s.Age < 25).OrderByDescending(s => s.Name).Select(s => s);
分組
group
子句用於對根據您指定的鍵所獲得的結果進行分組。示例中into關鍵字建立進一步查詢的標識,用select子句建立了一個帶key和maxAge屬性的匿名型別,返回每個班級中年齡小於25歲的最大年齡。
var list = from s in studentList
where s.Age < 25
group s by s.ClassId into sg
select new
{
key = sg.Key,
maxAge = sg.Max(t => t.Age)
};
foreach (var item in list)
{
Console.WriteLine($"key={item.key} maxAge={item.maxAge}");
}
連線
使用join
子句可以根據特性的條件合併兩個資料來源。例如通過連線查詢選擇相同課程的學生
List<Class> classList = new List<Class>(){
new Class()
{
Id=1,
ClassName="高數"
},
new Class()
{
Id=2,
ClassName="毛概"
}
};
var list = from s in studentList
join c in classList on s.ClassId equals c.Id
select new
{
Name = s.Name,
CalssName = c.ClassName
};
foreach (var item in list)
{
Console.WriteLine($"Name={item.Name},CalssName={item.CalssName}");
}
合併
zip
方法是.NET4新增的,允許用一個函式把兩個序列合併為一個。第一個集合中的第一項會與第二個集合中的第一項合併,第一個集合中的第二項與第二個集合中的第二項合併,以此類推。如果兩個集合的專案不同,zip方法就在到達較小集合的末尾時停止
。
var list = from s in studentList
where s.Age < 25
select s;
var list2 = from s in studentList
where s.Age < 25
select s;
var lst = list.Zip(list2, (first, second) => first.Name + "," + second.Name);
分頁
擴充套件方法Take()和Skip()
等的分割槽操作用於分頁。使用時把擴充套件方法take、skip新增到查詢的最後,skip方法會忽略根據頁面大小和實際頁數計算出的項數,再使用take方法根據頁面大小提取一定數量的項
int pageSize = 5;
int pageIdx = 0;
var list = (from s in studentList
where s.Age < 25
select s).Skip(pageIdx * pageSize).Take(pageSize);
聚合
聚合操作符Count(),Sum(),Min(),Average()
等不返回一個序列,而返回一個值。
轉換
Linq不只是檢索資料。 它也是用於轉換資料的強大工具。 通過使用 LINQ 查詢,可以使用源序列作為輸入,並通過多種方式對其進行修改,以建立新的輸出序列。 通過排序和分組,你可以修改序列本身,而無需修改這些元素本身。 但也許 LINQ 查詢最強大的功能是建立新型別
。
以下示例將記憶體中資料結構中的物件轉換為 XML 元素。
var studentsToXML = new XElement("Root",
from student in studentList
select new
XElement("student",
new XElement("name", student.Name),
new XElement("age", student.Age)
)
);
Console.WriteLine(studentsToXML);
四、並行Linq
.NET4在System.Linq名稱空間中包含了一個新類ParallelEnumerable
,可以分解查詢的工作使其分佈在多個執行緒上。集合序列會分成多個部分,不同的執行緒處理,完成後合併。這對大集合,又是多核CPU的可以提高效率
var list = (from s in studentList.AsParallel()
where s.Age < 25
select s.Age).Sum();
var list2 = (from s in Partitioner.Create(studentList,true).AsParallel().WithDegreeOfParallelism(8)
where s.Age < 25
select s.Age).Sum();
可以使用Partitioner類建立分割槽器,WithDegreeOfParallelism指定最大並行任務數
。
並行linq往往需要較多耗時使用,那應該也有取消長時間執行的任務需求。給查詢新增一個WithCancellation方法
,並傳遞一個CancellationToken令牌
作為引數。該查詢在單獨執行緒中使用,主執行緒中觸發取消命令。
var cts = new CancellationTokenSource();
new Thread(()=>
{
try
{
var sum= (from s in studentList.AsParallel().WithCancellation(cts.Token)
where s.Age < 25
select s.Age).Sum();
}
catch (OperationCanceledException ex)
{
Console.WriteLine(ex.Message);
}
}
).Start();
//外部動作觸發取消
cts.Cancel();
五、表示式樹
1、認識表示式目錄樹
出現在System.Linq.Expression
中,就是為Linq to sql服務的。表示式樹以樹形資料結構表示程式碼,其中每一個節點都是一種表示式。它可以將我們原來直接由程式碼編寫的邏輯儲存在一個樹狀的結構
裡,然後執行的時候就去動態解析這個樹。lambda表示式宣告表示式目錄樹可以像下面這樣。
Func<int, int, int> func = (m, n) => m * n + 2;// new Func<int, int, int>((m, n) => m * n + 2);
Expression<Func<int, int, int>> exp = (m, n) => m * n + 2;//lambda表示式宣告表示式目錄樹
//Expression<Func<int, int, int>> exp1 = (m, n) =>//只能一行 不能有大括號
// {
// return m * n + 2;
// };
//Queryable //a=>a.Id>3
//表示式目錄樹:語法樹,或者說是一種資料結構;可以被我們解析
int iResult1 = func.Invoke(12, 23);
int iResult2 = exp.Compile().Invoke(12, 23);
2、拼裝表示式樹
如果使用Expression
類介面宣告看起來會像下面這樣,注意比較Lambda表示式宣告和Expression類自己拼裝宣告的區別,最後都是需要Compile()
編譯後執行。
Expression<Func<int, int, int>> exp = (m, n) => m * n + 2;
ParameterExpression parameterExpression = Expression.Parameter(typeof(int), "m");
ParameterExpression parameterExpression2 = Expression.Parameter(typeof(int), "n");
var multiply = Expression.Multiply(parameterExpression, parameterExpression2);
var constant = Expression.Constant(2, typeof(int));
var add = Expression.Add(multiply, constant);
Expression<Func<int, int, int>> expression =
Expression.Lambda<Func<int, int, int>>(
add,
new ParameterExpression[]
{
parameterExpression,
parameterExpression2
});
int iResult1 = exp.Compile().Invoke(11, 12);
int iResult2 = expression.Compile().Invoke(11, 12);
表示式樹是由表示式的主體body、表示式的引數parameters、表示式型別Type、返回型別NodeType組成
。一個樹可能有很多葉子節點,複雜一點的例子像下面這樣
//i*j+w*x
ParameterExpression a = Expression.Parameter(typeof(int), "i"); //建立一個表示式樹中的引數,作為一個節點,這裡是最下層的節點
ParameterExpression b = Expression.Parameter(typeof(int), "j");
BinaryExpression r1 = Expression.Multiply(a, b); //這裡i*j,生成表示式樹中的一個節點,比上面節點高一級
ParameterExpression c = Expression.Parameter(typeof(int), "w");
ParameterExpression d = Expression.Parameter(typeof(int), "x");
BinaryExpression r2 = Expression.Multiply(c, d);
BinaryExpression result = Expression.Add(r1, r2); //運算兩個中級節點,產生終結點
Expression<Func<int, int, int, int, int>> lambda = Expression.Lambda<Func<int, int, int, int, int>>(result, a, b, c, d);
Console.WriteLine(lambda + ""); //輸出‘(i,j,w,x)=>((i*j)+(w*x))’,z對應引數b,p對應引數a
Func<int, int, int, int, int> f = lambda.Compile(); //將表示式樹描述的lambda表示式,編譯為可執行程式碼,並生成該lambda表示式的委託;
Console.WriteLine(f(1, 1, 1, 1) + ""); //輸出結果2
上面例子形成的表示式樹就像下面這樣。
3、應用
最常用的地方還是查詢資料
時。以往我們做一個查詢,根據使用者輸入,去資料庫中查詢匹配的資訊,可能去想到去拼一條帶where條件的sql語句,然後去執行這條sql即可。
如果無法確定需要查詢的欄位,當每換一個查詢條件或組合多個查詢條件,我們可以用表示式目錄樹動態的拼裝起來。
還可以用來代替反射
,我們知道反射有效能問題,硬編碼是最快的,但不夠靈活。像泛型一樣,表示式樹可以動態生成硬編碼,快取後以後訪問呼叫就相當於硬編碼效能。比如示例中,我們如果需要對一個型別物件轉換成另一個物件。這裡可以有很多方法,硬編碼、反射、序列化等都可以實現,現在我們用表示式樹試一下。
People people = new People()
{
Id = 11,
Name = "Wang",
Age = 31
};
PeopleCopy peopleCopy = new PeopleCopy()
{
Id = people.Id,
Name = people.Name,
Age = people.Age
};
硬編碼像上面這樣,我們用表示式目錄樹就是為了能夠生成這種硬編碼的委託
。
public class ExpressionMapper
{
private static Dictionary<string, object> _Dic = new Dictionary<string, object>();
/// <summary>
/// 字典快取表示式樹
/// </summary>
/// <typeparam name="TIn"></typeparam>
/// <typeparam name="TOut"></typeparam>
/// <param name="tIn"></param>
/// <returns></returns>
public static TOut Trans<TIn, TOut>(TIn tIn)
{
string key = string.Format("funckey_{0}_{1}", typeof(TIn).FullName, typeof(TOut).FullName);
if (!_Dic.ContainsKey(key))
{
ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "p");
List<MemberBinding> memberBindingList = new List<MemberBinding>();
foreach (var item in typeof(TOut).GetProperties())
{
MemberExpression property = Expression.Property(parameterExpression, typeof(TIn).GetProperty(item.Name));
MemberBinding memberBinding = Expression.Bind(item, property);
memberBindingList.Add(memberBinding);
}
foreach (var item in typeof(TOut).GetFields())
{
MemberExpression property = Expression.Field(parameterExpression, typeof(TIn).GetField(item.Name));
MemberBinding memberBinding = Expression.Bind(item, property);
memberBindingList.Add(memberBinding);
}
MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TOut)), memberBindingList.ToArray());
Expression<Func<TIn, TOut>> lambda = Expression.Lambda<Func<TIn, TOut>>(memberInitExpression, new ParameterExpression[]
{
parameterExpression
});
Func<TIn, TOut> func = lambda.Compile();//拼裝是一次性的
_Dic[key] = func;
}
return ((Func<TIn, TOut>)_Dic[key]).Invoke(tIn);
}
}
var result = ExpressionMapper.Trans<People, PeopleCopy>(people);
表示式和表示式樹什麼關係呢?首先表示式是匿名方法生成委託例項,而表示式樹是一種資料結構,本身不能執行的,需要編譯成sql,然後解釋執行表示式樹中每個節點的表示式。
六、小結
本章認識了Lambda表示式,linq查詢以及相關的常用操作符,它們不僅用於篩選資料來源,給資料來源排序,還用於執行分割槽,分組,轉換,連線等操作,使用並行linq可以提高大型資料集的查詢效率。另一個重要概念就是表示式目錄樹。表示式目錄樹允許在執行期間構建對資料來源的查詢,儲存在程式集中,主要用在linq to sql中,後面學習EntityFramework框架時會用到大量的表示式目錄樹。
debug everything
願一覺醒來,陽光正好
而不是,一覺醒來,天都黑了
如果手機在手邊,也可以關注下vx:xishaobb,互動或獲取更多訊息。當然這裡也一直更新de,下期見,拜了個拜拜。