今天我们通一个简单的示例代码的演进过程,来学习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前需要准备的知识已经介绍完成。希望能给你来帮助,或是温习那曾经熟悉却又不小心忘记的知识。如有什么不恰当的地方,恳请指正!如果你喜欢或是期待后面的介绍,请点推荐支持。再次谢谢。