文章目录
一、前言
一次我问同事,“你们在开发项目时SQL语句怎么处理?”没等我把问题问清楚,他就回答我:“LINQ+lambda。”
其实我本意是项目中存在很多SQL语句,统一管理起来麻烦,而且有时候语句中的参数也会发生变化,要用字符串拼接的形式来组成,有没有办法可以把这些散落且多样的SQL语句更好地管理起来呢。虽然最后我用了一个公共类,将语句在公共类中制成静态格式化字符串了,一定程度上缓解了这个问题,但LINQ+lambda的事被我丢在了脑边。
ok,言归正传,其实话题没继续下去的原因是我不清楚LINQ是啥,所以我决定花点时间学习一下,万一它带给我惊喜了呢。
二、认识
作为一个接触.NET时长两个月的实习生来说,微软官方文档应该还是比较适合我的。
当然,我对LINQ也不是一无所知。在学习.NET概述时,有一个词条就是描述LINQ的:
Language-integrated query (LINQ) lets you write declarative code for operating on data. The data can be in many forms (such as
in-memory objects, a SQL database, or an XML document), but the LINQ code you write typically doesn't differ by data source.
从描述中可知,LINQ的中文名应该叫语言集成查询,从名字中可以知一二,方便查询是它的特点,简介中也有提到它的代码无关数据源。
进入词条内,概述描述到,LINQ提供了语言级别的查询功能,并且有着相比C#和VB更高阶的函数API,能让你写出极具表现力的声明性代码。这句话初学者读起来挺难懂的,我试着解释一下其中加粗的几个关键词:
- 语言级别,它的语法本身就支持一些查询功能。这个你先可以不用硬理解,因为马上就有例子。先想想一般语言(C、C++、Java等)要实现一个查询,比如说查找一个列表中值为xxx的元素该怎么写。我想,通常是写一个循环,遍历该列表,加if语句判断。但LINQ中可以简化这些语句,浓缩到一个简洁的式子中。
- 高阶,我的理解是可以以C#和VB的函数作为参数,或者说底层是由它们构成,所以语法上无关这两种语言,看似通用。(好吧,这个我也不是特别理解)
- 声明性代码,这是相对于命令性代码来说的。举个也许不那么恰当的现实例子,比如说你渴了,你想让人帮你倒杯水,如果是用命令性代码写,你就得将详细步骤写出来,你得告诉他用什么打水,去哪里打水等等;如果用声明性代码实现,那你只需要说我渴了,我想喝水,至于他怎么搞到水你不用关心。简言之就是更符合人直接思维的代码。
将这几个关键词结合接下来的内容来看,我相信会有更深的体会。
2.1. 语言级别的查询语法
直接上例子,
下面是一条语言级别的查询语法(就是上面说的LINQ提供的):
varlinqExperts = from p in programers where p.IsNewToLINQ select new LINQExpert(p);
// 语言级别的意思这下很明显了吧,from/in/where/select这些关键字本身就是查询的组成部分
// 什么!你问我这条语句什么意思?
// 我想你可以直接读出来,从一群程序员中筛选出LINQ新手,然后选出一个LINQ专家。
下面是一条使用了IEnumerable<T>API的语句,效果和上面一样(也是LINQ的功能,用了API):
var linqExperts = programers.Where(p = >p.IsNewToLINQ).Select(p => new LINQExpert(p));
// 或许你觉得和上面语句复杂度差不多,其实我也这么觉得
// 但是某种程度上来讲,是不是更容易直接读出来了呢
// 程序员中找LINQ新手,选出LINQ专家
// 连语序都不需要调整了
不过这节也不是要比较它俩谁好谁不好,而是让你自行感受一下LINQ的语法。
2.2. LINQ是富有表现力的
这节的标题,怎么感觉不像在介绍编程呢!
其实,这就是LINQ给人的感觉。也如上面的例子那样,它语句本身的含义就很明显了,能表达出你想干什么。
想象一下,你有一个宠物列表(List),但是想要将它转换成一个宠物词典(Dictionary),然后你就可以使用宠物对应的RFID(电子标签?)值来直接访问该宠物了。
下面是传统的命令性代码:
var petLookup = new Dictionary<int, Pet>();
foreach(var pet in pets)
{
petLookup.Add(pet.RFID, pet);
}
结合上面口渴喝水的例子,这段代码是不是很“喝水”呢?我得先建一个词典,然后遍历列表,将列表中每个宠物的标签和宠物本身作为参数,添加到词典中。一步步怎么做,一条条命令都得说清楚。
下面是一条等价的LINQ表达式:
var petLookup = pets.ToDictionary(pet => pet.RFID);
显然,这段LINQ代码是有意义的。如果我告诉你pets是一个宠物列表,那么当你看到这条语句应该就能很快“猜”出它的意思。它能减少程序员没必要的推理,平衡代码本身与代码意图之间的关系。另一个好处是它非常简洁,这能将代码量大大减少,而你只需要学习LINQ,这笔交易不错,不是吗?
2.3. LINQ供应者简化了数据访问
LINQ供应者,英文是LINQ Providers(也称LINQ供应程序)。
因为LINQ的数据源是各种类型的数据,对于常见类型的数据源查询访问,微软都提供了对应的模块。
而这个查询模块就是LINQ供应者(它能访问原始数据源,然后将数据提供给LINQ,这不是LINQ供应者是什么?)。
通常大部分软件是围绕着数据(数据库、JSON、XML等等)进行运转的。当数据源发生改变,你往往需要学习新的API,这很烦人。而LINQ通过抽象数据访问的公共元素为查询语法来简化这一点,无论你选择哪种数据源,查询语法看起来都是相同的。
下面是找到所有指定属性值的XML元素的一段代码:
public static IEnumerable<XElement> FindAllElementsWithAttribute(XElement documentRoot, string attributeName, string value)
{
return from el in documentRoot.Elements(elementName)
where (string)el.Element(attributeName) == value
select el;
}
看起来似乎也不简单。但你若自己编写代码来遍历XML文档完成这项任务,那将非常具有挑战性。
与XML交互并不是LINQ供应者唯一能做的事。Linq to SQL是一个用于MSSQL服务器数据库的非常简单粗暴的ORM(Object-Relational Mapper,对象关系映射器)。Json.NET库也通过LINQ提供了高效的JSON文档遍历。而且,如果没有你需要的库,你也可以实现你自己的LINQ供应者(某种程度上,LINQ供应者有点像ODBC驱动)。
2.4. 使用查询语法的原因
那为什么要用查询语法呢?经常有人问这个问题。毕竟,以下的代码(API语法):
var filteredItems = myItems.Where(item => item.Foo);
比下面代码要简洁许多(查询语法):
var filteredItems = from item in myItems
where item.Foo
select item;
所以API语法是比查询语法更简洁的方式?
当然不是。查询语法允许你使用let子句,该子句允许你在表达式的范围内引入并绑定变量(从名字也可以猜出一二,let子句就是让某个变量等于某个查询结果,用于暂存结果,下面出现的例子中会有演示),并在表达式的后续部分中使用它。当然你也可以只用API语法重新生成相同的代码,但这很可能导致代码难以阅读。
所以又引出了一个问题,应该只使用查询语法吗?
这个问题的回答得分情况,如果是下列情况,你可以只用查询语法:
- 你已存在的代码库已经使用了查询语法(为了保持一致性)
- 由于复杂性,你需要在你的查询中确定变量范围(这个我没懂)
- 你个人更倾向于查询语法,并且它不会给你的代码带来麻烦
如果是下列情况,那答案就是否:
- 你已存在的代码是API语法
- 在查询中没有确定变量范围的需求
- 你更倾向于API语法,且它不会给代码带来麻烦
2.5. LINQ核心
如果你想要看完整的LINQ示例,请访问101 LINQ Samples。
以下的例子是用于快速演示LINQ的核心功能的。它并不全面,LINQ真正的功能比所展示的多许多。
2.5.1. 面包与黄油-Where,Select 和 Aggregate
面包与黄油大概是想说,这三个关键词是LINQ中必不可少的,或许对于国人来讲是米饭与馒头?
// 过滤一个列表
var germanShephreds = dogs.Where(dog => dog.Breed == DogBreed.GermanShepherd);
// 使用查询语法实现
var queryGermanShepherds = from dog in dogs
where dog.Breed == DogBreed.GermanShepherd
select dog;
// 将一个列表从类型A映射到类型B
var cats = dogs.Select(dog => dog.TurnIntoACat());
// 使用查询语法实现
var queryCats = from dog in dogs
select dog.TurnIntoACat();
// 求几个字符串的长度总和
int seed = 0;
int sumOfStrings = strings.Aggregate(seed, (s1, s2) => s1.Length + s2.Length);
2.5.2. 扁平化列表中的列表
// 将狗舍的列表 转换 其中所有狗的列表
var allDogsFromKennels = kennels.SelectMany(kennel => kennel.Dogs);
// 所以扁平化列表是说将列表中的部分列表元素取出重新生成一张列表?
2.5.3. 求两个集合的并集(使用自定义的比较器)
public class DogHairLengthComparer : IEqualityComparer<Dog>
{
public bool Equals(Dog a, Dog b)
{
if (a == null && b == null)
{
return true;
}
else if ((a == null && b != null) ||
(a != null && b == null))
{
return false;
}
else
{
return a.HairLengthType == b.HairLengthType;
}
}
public int GetHashCode(Dog d)
{
// Default hashcode is enough here, as these are simple objects.
// 默认哈希码是足够的,因为它们都是简单的对象
return d.GetHashCode();
}
}
...
// Gets all the short-haired dogs between two different kennels.
var allShortHairedDogs = kennel1.Dogs.Union(kennel2.Dogs, new DogHairLengthComparer());
2.5.4. 求两个集合的交集
// 从两个humane society中找寻志愿者
var volunteers = humaneSociety1.Volunteers.Intersect(humaneSociety2.Volunteers,
new VolunteerTimeComparer());
2.5.5. 排序
略
2.5.6. 实例属性相等
最后,是一个更高级的例子:确定同样类型的两个实例的属性值是否相等:
public static bool PublicInstancePropertiesEqual<T>(this T self, T to, params string[] ignore) where T : class
{
if (self == null || to == null)
{
return self == to;
}
// Selects the properties which have unequal values into a sequence of those properties.
var unequalProperties = from property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
where !ignore.Contains(property.Name)
let selfValue = property.GetValue(self, null)
let toValue = property.GetValue(to, null)
where !Equals(selfValue, toValue)
select property;
return !unequalProperties.Any();
}
// 例子或许有点长,但是没事,长咱就不看了
// 至少很明显的可以看出let的用法
2.6. PLINQ
PLINQ,或者叫Parallel LINQ(并行LINQ),它是一种LINQ表达式的并行执行引擎。换句话说,就是一个常见的LINQ表达式可以在多个线程上并行化执行。通过在表达式前调用AsParallel来实行并行化。
考虑以下代码:
public static string GetAllFacebookUserLikesMessage(IEnumerable<FacebookUser> facebookUsers)
{
var seed = default(UInt64);
Func<UInt64, UInt64, UInt64> threadAccumulator = (t1, t2) => t1 + t2;
Func<UInt64, UInt64, UInt64> threadResultAccumulator = (t1, t2) => t1 + t2;
Func<UInt64, string> resultSelector = total => $"Facebook has {total} likes!";
returnfacebookUsers.AsParallel().Aggregate(seed, threadAccumulator, threadResultAccumulator, resultSelector);
}
这段代码会根据需要通过系统将facebookUsers划分,然后汇总每个线程的like总数,汇总每个线程运算的结果,并将结果投射到一个字符串中。
以图表来描述如下:
通过LINQ可以轻松实现并行的CPU绑定作业(也就是说,这是纯函数操作且没有副作用),这是PLINQ的典型应用。对于并行操作可能有副作用的作业,你也可以考虑使用Task Parallel Library(TPL)(.NET中的一种多线程操作库)。
2.7. 更多资源
三、结尾
LINQ是种语法,是实操类的知识点。Blog和文档只能带来基本的认识。想要进一步掌握还是得实践,在项目中多用。