白话LINQ系列2---以代码演进方式学习LINQ必备条件

今天我们通一个简单的示例代码的演进过程,来学习LINQ必备条件:隐式类型局部变量;对象集合初始化器;委托;匿名函数;lambda表达式;扩展方法;匿名类型。废话不多说,我们直接进入主题。

一、实现要求

    1、获取全部女生;

    2、对满足要求的结果按年龄排序;

    3、获取结果的前两名;

    4、对获取结果计算平均年龄;

    5、输出结果信息,包含姓名、性别、年龄;

    说明:学生类为Student(包含学生完整信息),输出结果类为:StudentInfo(包含我们关心的信息,后面将演示它是如何消失的)。在此我们不讨论示例的实用性,使用它,仅是方便引出我们今天的学习内容。

二、代码演进

1、传统方法

    现在我们实现第一个要求,找出全部女生。我们平常实现的逻辑大致是:循环学生对象集合,在循环体内逐一判断每一个学生对象的性别是否为要求的性别,如果是则放进结果集合。循环结束后输出结果集合中学生信息对象中的数据(这里我们使用ObjectDumper类来输出信息,它是微软提供的LINQ示例中的一个类)代码出下:

 1    /// <summary>
 2         /// 获取女生信息并输出
 3         /// </summary>
 4         public static void GetSutdents() {
 5             //由于Student可能会比较多的字段,而我们只输出关心的内容,
 6             //因此使用StudentInfo类来存在我们关心的信息
 7             List<StudentInfo> studentents = new List<StudentInfo>();
 8             foreach (Student student in CreateStudents()) {
 9                 if (student.Sex == SexType.Woman) {
10                     StudentInfo info = new StudentInfo();
11                     info.Age = student.Age;
12                     info.Sex = student.Sex;              info.Name = student.Name;
13                     studentents.Add(info);
14                 }
15             }
16             ObjectDumper.Write(studentents);
17         }    

2、隐式类型局部变量
  上面的方法是我们经常使用的,平常忙于为老板赚钱的我们可能没有时间去考虑上面的代码是否可以精简,项目中到处充斥着类似的逻辑。《重构》告诉我们要尽量消灭重复的代码,以写出优美的、可维护的高质量代码。我们一起来看看上面的代码,它有两个地方出现了重复(studentents、info声名、studentents、info初始化地方)。这里可以精简吗?让我们少敲几下键盘吗?答案当然可以。C#3.0提供了一个新的、名为var的关键词,允许我们无须显示给定类型即可定义一个局部变量。它就是隐式类型局部变量【在使用VAR关键字声明变量,编译器会通过该变量的初始化代码来推断其真正的类型】使用隐式类型局部变量重构上面的代码,如下所示:

 1       /// <summary>
 2         /// 获取女生信息并输出
 3         /// </summary>
 4         public static void GetSutdents2() {
 5             //由于Student可能会比较多的字段,而我们只输出关心的内容,
 6             //因此使用StudentInfo类来存在我们关心的信息
 7             var studentents = new List<StudentInfo>();
 8             foreach (var student in CreateStudents()) {
 9                 if (student.Sex == SexType.Woman) {
10                     var info = new StudentInfo();
11                     info.Age = student.Age;
12                     info.Sex = student.Sex;              info.Name = student.Name;
13                     studentents.Add(info);
14                 }
15             }
16             ObjectDumper.Write(studentents);
17         }

  上面的代码声明变量通过var关键字进行,例如: var studentents = new List<StudentInfo>(); 变量studentents的类型是通过后面的初始化表达式new List<StudentInfo>();来推断出变量studentents的类型为List<StudentInfo>。虽然这项改进没有为我们省下多少代码。但如果在整上项目中来看,能为我们节省不少时间,提高效率。需要说明的是:这里不用担心性能问题,因为他和显示声明的写法其实是一样的,只是因为编译器编译器帮我们做了点事,可以通过查看中间语言来证明,因此不要被使用var影响性能的观点所误导,请放心使用。

3、对象初始化器

  接下来我们来看一下循环体里对象info的赋值语句,不知道大家有没有对这样的语句感到不舒服,对此我是感觉很不舒服的,但赋值又必须进行,有什么方法能改进吗?增加相应的构造函数看起来是一项不错的选择,至少在能帮我们少写几句代码。但真是这样吗?如果赋值的字段发生变化,怎么办?修改构造函数?这不是有违初衷,本来想少写几行代码,结果不但没有,反而增加了维护的复杂度,因此增加相应的构造函数不是可取的方法。C#3.0引用了对象初始化器【对象初始化器充许我们在单一语句中为对象指定一个或多个字段/属性的值】,这样我们就可以以声明的方式初始化任意类型的对象。在上面的代码中使用对象初始化器改造后如下所示:

 1          /// <summary>
 2         /// 获取女生信息并输出
 3         /// </summary>
 4         public static void GetSutdents3() {
 5             //由于Student可能会比较多的字段,而我们只输出关心的内容,
 6             //因此使用StudentInfo类来存在我们关心的信息
 7             var studentents = new List<StudentInfo>();
 8             foreach (var student in CreateStudents()) {
 9                 if (student.Sex == SexType.Woman) {
10                     studentents.Add(new StudentInfo() { Age = student.Age, Sex = student.Sex,Name=student.Name });
11                 }
12             }
13             ObjectDumper.Write(studentents);
14         }    

  赋值部份已经由5行变成1行了,这样的改进是不是越来越让我得到了实实在在的好处?

4、委托

至此,上面的代码是不是已经到了无法重构或者非常完善的地步了呢?  如果现在需求发生改变,用户要求查询20岁以下的女生。 这时我们去修改if语句的条件判断?虽然能完成任务,但这种方法不具备可扩展性,因为需求可能又一次发现变化,且更加复杂,此进我们可能需要一单独的方法来做为条件判断,可能是更好的选择。为了更好的通用性,我们不应该把条件写死在方法内部,而应该通过外面传进来。C#2.0提供的委托【委托可以认为是一种对象,用来保存指向函数的指针,类似C++中的函数指针】正好能远成这个任务。现在过滤方法应该是这样的:接受一个Student对象作为参数,返回一个布尔值(代表该对象是否满足特定条件)。我们可以自定义一个指向这类过滤方法(相同的返回类型、相同的参数个数且类型相同(注意:严格来说,这里的表述是不对的,因为C#4.0引入的协变使方法返回的类型可以不相同,逆变使方法的参数类型可以不相同))的委托,但C#2.0提供了能满足我们需求的内置委托类型(delegate Boolean Predicate<T>(T obj);),通过委托来完成新的需要(查询20岁以下的女生)的代码如下:

 1        /// <summary>
 2         /// 获取女生信息并输出(通过委托实现)
 3         /// </summary>
 4         public static void GetSutdents4(Predicate<Student> match ) {
 5             //由于Student可能会比较多的字段,而我们只输出关心的内容,
 6             //因此使用StudentInfo类来存在我们关心的信息
 7             var studentents = new List<StudentInfo>();
 8             foreach (var student in CreateStudents()) {
 9                 if (student.Sex == SexType.Woman) {
10                     studentents.Add(new StudentInfo() { Age = student.Age, Sex = student.Sex, Name = student.Name });
11                 }
12             }
13             ObjectDumper.Write(studentents);
14         }
15
16         /// <summary>
17         /// 条件过滤方法
18         /// </summary>
19         /// <param name="student"></param>
20         /// <returns></returns>
21         private static bool Filter(Student student)
22         {
23             return student.Sex == SexType.Woman && student.Age < 20;
24         }
25
26         /// <summary>
27         /// 主程序
28         /// </summary>
29         /// <param name="args"></param>
30         static void Main(string[] args)
31         {
32             //调用通过委托实现过滤的GetSutdents
33             GetSutdents4(Filter);
34         }

5、匿名函数

虽然我们通过委托解决了通用问题,但增加了一个函数。《重构》中提到的一种重构手法--内联函数(一个函数本体与名称同样清楚易懂时,在函数调用点插入函数本体,然后移除该方法),我们刚提取出来,又内联回去,这不是在做无用功吗?看来我们得在提取和内联之间找到平衡点,C#2.0中的匿名函数【无需声明一个类似Filter的方法,而只需要将这部分逻辑直接传递给GetSutdents4方法即可】正是我们要找的这个平衡点。虽然我们没有声明方法,但编译器会为我们生成,匿名函数增强了委托,降低了代码量。 使用匿名方法调用代码如下:

 1          /// <summary>
 2         /// 主程序
 3         /// </summary>
 4         /// <param name="args"></param>
 5         static void Main(string[] args) {
 6             //使用匿名方法调用GetSutdents4
 7             GetSutdents4(delegate(Student student) {
 8                 return student.Sex == SexType.Woman && student.Age <= 20;
 9             }
10                 );
11         }    

 6、Lambda表达式

  虽然使用匿名方法已经给我们降低了代码,但可读性确降低了。为此,C#3.0引入了更为简洁的Lambda表达式。它直接将函数编程的精彩表达能力引入到了代码中。它与匿名方法相比提供了如下的一些额外功能(下面4点引用至LINQ IN ACTION):

a、Lambda表达式能够推导出参数的类型,因此程序中无需显式声明;

b、Lambda表达式支持用语句块或表达式作为方法体,语法上比匿名方法更加灵活(匿名方法的方法体只能用语句块);

c、在以参数形式传递时,Lambda表达式能够参与到参数类型推断及对重载方法的选择中。

d、带有表达式体的Lambda表达式能够转化为表达式树;

Lambda表达工的写法如下图所示,它由三个部分组成1、参数;2、Lambda操作符(=>读作:goes to (导出));3、表达式或语句块;

使用Lambda表达式修改后的代码如下:

1      /// <summary>
2         /// 主程序
3         /// </summary>
4         /// <param name="args"></param>
5         static void Main(string[] args) {
6             //使用Lambda表达式调用GetSutdents4
7             GetSutdents4((student=>student.Sex==SexType.Woman && student.Age<20) );
8         }

通过引入Lambda表达式后,代码更加清晰自然了,同时也满足了简明、通用的要求。至此,第一个要求就算完成了。接下来我们将完成排序、获取结果集中前两名女生及计算其平均年龄的功能。

7、扩展方法

  如果我们要对上面获取的结果集合排序、计算平均值等操作,使用传统的方法的话,不用说大家也明白要写多少代码吧,这里我们就不再去讲述传统方法了,直接进入主题。我们将使用扩展方法来实现上面的要求,同时也展现使用LINQ带的扩展方法的魔力。扩展方法【用来在类型定义完成后,由于某些原因不能修改源类型的情况下,继续为基添加新方法】C#中定义扩展方法必须在非泛型的静态类中定义一个静态方法,此方法能够接受任意多个参数,但是第一个参数的类型必须和所扩展的类型一致,且用this关键修饰。LINQ为我们带来了一系列的扩展方法(不管是否用到了LINQ,我们都可以根据实际需要使用他们)。使用扩展方法修改后代码如下所示:

 1        /// <summary>
 2         /// 获取女生信息并输出
 3         /// </summary>
 4         public static void GetSutdents5(Predicate<Student> match) {
 5             //由于Student可能会比较多的字段,而我们只输出关心的内容,
 6             //因此使用StudentInfo类来存在我们关心的信息
 7             var studentents = new List<StudentInfo>();
 8             foreach (var student in CreateStudents()) {
 9                 if (student.Sex == SexType.Woman) {
10                     studentents.Add(new StudentInfo() { Age = student.Age, Sex = student.Sex, Name = student.Name });
11                 }
12             }
13
14         //按年龄排序后获取前两个女学生并求年龄的平均值
15             var average = studentents.OrderBy(s => s.Age)
16                 .Take(2)
17                 .Average(s => s.Age);
18             ObjectDumper.Write(average);
19         }

看到这些扩展方法带来的魔力了吧,后面几个要求,被这么简单的一句链式调用【通过“.”对所需方法进行连续调用,就像串起来的链一样】就完成了。其中OrderBy为定义于System.Linq.Enumerable类中的扩展方法。它们的用途,这里就不赘述了。

8、匿名类型

这是开始LINQ之前的最后一个重量级的的C#语言特性了,匿名类型【能像对象初化器一样构建事先没有定义的类型,编译器会帮我定义(又是编译器,真是一位强大的助手,也正是因为编译器在后面帮我们做了幕后工作,虽然这些在C#3.0中定义的语言特性编译后能运行在.NET2.0上,而无需引用那庞大的.NET3.0或.NET3.5,当然上面用到的扩展方法需要System.Runtime.CompilerServices.ExtensionAttribute属性的支持。但我们可以自行引用入或将System.Core.dll和.NET2.0一起分发) 上面我们为了返回关心的学生信息而定义一个StudentInfo类,其实有了匿名类型后,像这种简单的类,可以不用特意去定义,从而节约时间。这能减少系统中的一些无行为的(只是一个数据容器)的杂乱的类。使用匿名类型修改后的代码如下:

 1     /// <summary>
 2         /// 获取女生信息并输出(使用匿名类型获取关心的信息)
 3         /// </summary>
 4         public static void GetSutdents6() {
 5             var studentents = new List<object>();
 6             foreach (var student in CreateStudents()) {
 7                 if (student.Sex == SexType.Woman) {
 8                     studentents.Add(new { Age = student.Age, Sex = student.Sex, Name = student.Name });
 9                 }
10             }
11             ObjectDumper.Write(studentents);
12         }

从上面的代码,我们可以看到,StudentInfo类型不见了,它已经被匿名类型 new { Age = student.Age, Sex = student.Sex, Name = student.Name } 所代替。

三、结束语

至此,学习LINQ前需要准备的知识已经介绍完成。希望能给你来帮助,或是温习那曾经熟悉却又不小心忘记的知识。如有什么不恰当的地方,恳请指正!如果你喜欢或是期待后面的介绍,请点推荐支持。再次谢谢。

时间: 2024-10-13 06:06:31

白话LINQ系列2---以代码演进方式学习LINQ必备条件的相关文章

C#复习笔记(4)--C#3:革新写代码的方式(查询表达式和LINQ to object(下))

查询表达式和LINQ to object(下) 接下来我们要研究的大部分都会涉及到透明标识符 let子句和透明标识符 let子句不过是引入了一个新的范围变量.他的值是基于其他范围变量的.let 标识符=表达式; 首先展示一个不适用let操作符来使用的按用户名称长度来排序: ... var queryWithoutLet = from user in SampleData.AllUsers orderby user.Name.Length select user; foreach (User us

一步一步学LINQ系列1---什么是LINQ?

一.本系列目标 1.理解LINQ: 2.能写得复杂的LINQ语句(比如:动态查询): 3.理解表达式树及相关概念: 4.熟练运用LINQ写出优美的代码(希望一起努力,最终达到): 二.LINQ为何物?   LINQ之争的销烟已经退去,如今,LINQ已经成为C#开发人必备技术之一.很多人用它写出了优美的代码,它已经成为处理数据的一种全新开发方式,这也许是你选择.NET作为开发平台的福利之一.越来越多的开源库.框架都大量地使用LINQ.不管是出于提高自身技能还是读懂别人的代码,它都得是被你我拿下的一

Linq to Sql : 三种事务处理方式

原文:Linq to Sql : 三种事务处理方式 Linq to SQL支持三种事务处理模型:显式本地事务.显式可分发事务.隐式事务.(from  MSDN: 事务 (LINQ to SQL)).MSDN中描述得相对比较粗狂,下面就结合实例来对此进行阐述. 0. 测试环境 OS Windows Server 2008 Enterprise + sp1 IDE Visual Studio 2008, .net framework 3.5 + SP1 DB SQL Server 2000 + sp

通过代码的方式完成WCF服务的寄宿工作

使用纯代码的方式进行服务寄宿 服务寄宿的目的是为了开启一个进程,为WCF服务提供一个运行的环境.通过为服务添加一个或者多个终结点,使之暴露给潜在的服务消费,服务消费者通过匹配的终结点对该服务进行调用,除去上面的两种寄宿方式,还可以以纯代码的方式实现服务的寄宿工作. 新建立一个控制台应用程序,添加System.ServiceModel库文件的引用. 添加WCF服务接口:ISchool使用ServiceContract进行接口约束,OperationContract进行行为约束 1 using Sy

32位汇编第三讲,RadAsm,IDE的配置和使用,以及汇编代码注入方式

32位汇编第三讲,RadAsm,IDE的配置和使用,以及汇编代码注入方式 一丶RadAsm的配置和使用 用了怎么长时间的命令行方式,我们发现了几个问题 1.没有代码提醒功能 2.编写代码很慢,记不住各种声明 那么现在有大神,已经帮我们做了一个IDE环境,就是RadAsm,首先简单介绍一下界面 (对于这个IDE(最新版是3.0)我已经打包好了,有中文版本,和英文版本) 我们需要配置一下环境 1.配置编译环境,配置lib文件库,配置Debug调试器 打开后会弹出 首先这里我们注意下面的几个选项 1.

ReactiveSwift源码解析(九) SignalProducerProtocol延展中的Start、Lift系列方法的代码实现

上篇博客我们聊完SignalProducer结构体的基本实现后,我们接下来就聊一下SignalProducerProtocol延展中的start和lift系列方法.SignalProducer结构体的方法扩展与Signal的扩展相同,都是面向协议的扩展.首先创建了一个SignalProducerProtocol协议,使SignalProducer在延展中遵循SignalProducerProtocol协议.然后我们再对SignalProducerProtocol进行扩展.这样一来,SignalP

最新的JavaScript核心语言标准&mdash;&mdash;ES6,彻底改变你编写JS代码的方式!【转载+整理】

原文地址 本文内容 ECMAScript 发生了什么变化? 新标准 版本号6 兑现承诺 迭代器和for-of循环 生成器 Generators 模板字符串 不定参数和默认参数 解构 Destructuring 箭头函数 Arrow Functions Symbols 集合 学习Babel和Broccoli,马上就用ES6 代理 Proxies ES6 说自己的宗旨是"凡是新加入的特性,势必已在其它语言中得到强有力的实用性证明."--TRUE!如果你大概浏览下 ES6 的新特性,事实上它

C#复习笔记(4)--C#3:革新写代码的方式(Lambda表达式和表达式树)

Lambda表达式和表达式树 先放一张委托转换的进化图 看一看到lambda简化了委托的使用. lambda可以隐式的转换成委托或者表达式树.转换成委托的话如下面的代码: Func<string, int> getLength = s => s.Length; 转换成表达式树的话是下面的代码: Expression<Func<string, int>> getLength = s => s.Length; 委托方面的东西前面都做了详细的介绍.我们主要学习表达

格局中@null的代码实现方式

布局中通常会用到@null.如RadioButton常用的技巧通过RadioGroup实现Tab,需要设置android:button="@null".如果要在代码中动态创建控件,android中并不能找到相关的属性或方法.搜索均无解决办法,最后想到一个变通的方法:通过透明色获取drawable. setButtonDrawable(getResources().getDrawable(android.R.color.transparent)) 实际还是可以通过布局的方法来动态创建控件