周克的技术Blog

在.Net的海洋里面寻找真我

Linq本质论(1)

本篇blog是我在深圳.Net俱乐部12月活动的讲座的内容,现在整理出来,以飨读者。

本文的目的是帮助大家搞清楚C#3.0里面的Linq查询表达式的来龙去脉,搞清楚以后对C#3.0的新特性基本上也就了然于胸了。建议大家看完Linq查询后再看各个语法新特性对Linq的意义,这样会更加有帮助一些。

1、自动属性。

class Class
{
    //C#2.0 属性
    //private int _id;
    //public int ID
    //{
    //    get {
    //        return _id;
    //    }

    //    set
    //    {
    //        _id = value;
    //    }
    //}

    //C#3.0 属性 可以给get set加访问修饰符
    public int ID { get; private set; }
    public string Name { get; set; }
    public Class(int id)
    {
        //加了private之后的属性只能在类的内部访问
        this.ID = id;
    }
}

本质:和原来的属性没啥两样 ,简化了语法而已。

对Linq的意义:无。


2、初始化器。

private static void Initializer()
{
    //C#2.0 对象初始化
    //Class c = new Class(1);
    //c.Name = "终极一班";
    //C#3.0 对象初始化器
    Class c = new Class(1) { Name = "终极一班" };
    //C#2.0 集合初始化
    //ClassCollection list = new ClassCollection();
    //list.Add(c);
    //C#3.0 集合初始化器
    ClassCollection list = new ClassCollection
    {
        new Class(1) { Name="终极一班"},
        new Class(2){Name="终极二班"}
    };
    foreach (Class item in list)
    {
        Console.WriteLine(item.ID + " " + item.Name);

    }
}

相关的班级集合类代码:

class ClassCollection : List<Class>
{ }

本质:和原来的构造函数初始化或构造后通过属性初始化没啥两样 ,简化了语法而已。

对Linq的意义:和匿名类型结合起来构造查询结果集合里面的新元素类型。


4、具有隐式类型的局部变量

private static void Var()
{
    var i = 1;// 编译过后的结果实际是 int i=1; var并不是动态变量,它的类型实际上是c#编译器通过上下文推断是int
    //var i = DateTime.Now; //编译不过,和JavaScript不一样
    var d = DateTime.Now;//=后面支持各种类型
    var a = new int[] { 1, 2, 3 };//var也支持数组
    foreach (var item in a)//item的类型通过C#编译器推断得知是int
    {
        Console.WriteLine(i);
    }

//var x;                 // 错误,没有用来推断类型的初始化器
//var y = { 1, 2, 3 };  // 错误,不允许使用集合初始化器
//var z = null;        // 错误,不允许出现空类型
}

本质:var并非动态类型 ,C#仍然是静态语言,引入var方便我们写代码了,可以不管"="后面的赋值表达式类型了,由编译器自己去推断生成对应类型了。

对Linq的意义:可以自动推断出Linq查询返回的集合类型。

   

5、匿名类型。

private static void AnonymousType()
{
    var v = new { Name = "张三", Sex = true };//无须显示声明一个类,而且在初始化器里面可以获取上下文的变量——闭包
    Console.WriteLine(v.Name);
}

本质:有了匿名类型后我们不需要显示的声明一个类型了,这个类型由C#编译器自动生成,而且利用了初始化器和var的新特性

对Linq的意义:和初始化器结合起来构造查询结果集合里面的新元素类型。


6、扩展方法。

比如我们现在想给int类型增加(扩展)一个方法,判断一个整数自身是否偶数,我们期望的语法是这样的:

private static void ExtendMethod()
{
    int i = 2;
    Console.WriteLine(i.IsEven());
}

注意原来int原来是没有IsEven()这个方法的,要实现这个方法,必须写一个静态类和一个静态方法。

static class MyExtention
   {
       public static bool IsEven(this int num)//this 表示针对int的实例和索引器的this的含义是一样的,int表示给int这种类型进行扩展
       {
           return num % 2 == 0;
       }

}

本质:编译i.IsEven()的本质是C#编译器生成了了MyExtention.IsEven(i)的代码,实际上仍然没有破坏类型的结构,并不是真的象语法看上去那样平白无故给int增加了一个IsEven()方法,和设计模式里面的Visitor模式动态注入方法,或者JavaScript语言里面的动态给对象扩展方法还是有很大区别的。

对Linq的意义:用来对集合类型扩展不同的查询方法。


7、Lambda表达式和Linq查询。

接下来我们通过一个例子来看一下Lambda表达式和Linq查询的关系:我们现在想给ClassCollection增加一个过滤方法,方法的目的是能够过滤返回班级名称为"终极一班"的集合来。

0)首先给MyExtention增加这么一个静态方法:

public static ClassCollection Filter(this ClassCollection classes)
{
    var newlist = new ClassCollection();
    foreach (var item in classes)
    {
        if (item.Name=="终极一班")

        {
            newlist.Add(item);
        }
    }
    return newlist;
}

private static void LambdaLinq()
       {
           var classes = GetClasses();
           //var students = GetStudents();
           //0 原始版本
           var result = classes.Filter();

          foreach (var item in result)
         {
            Console.WriteLine(item.ID+ " " + item.Name);
         }

}

相关的工厂方法:

static ClassCollection GetClasses()
{
    return new ClassCollection{
    new Class(1){ Name = "终极一班"},
    new Class(2){ Name = "终极二班"},
    };
}

1)现在需求发生了变化,需要上面的红色部分需要发生变化,也就是说我们希望这个查询条件可以在我们调用Filter方法的时候动态的指定,这时候我们可以把这个变化封装成一个接口,当然还可以封装成一个委托,这是.net的非常好用的独有特性,委托的最直接的作用可以把一个具体的方法引用封装成一个变量传递。好,开始变形!

delegate bool FilterHandler(Class c); //注意这个要放到namespace下面,不要放到Program类里面

public static ClassCollection Filter(this ClassCollection classes,FilterHandler f)
{
    var newlist = new ClassCollection();
    foreach (var item in classes)
    {
        if (f(item))

        {
            newlist.Add(item);
        }
    }
    return newlist;
}

static bool F(Class c)
{
    return c.Name == "终极一班";
}

private static void LambdaLinq()
       {
           var classes = GetClasses();
           // C#1.0 使用委托封装过滤条件
           FilterHandler f=new FilterHandler(F);
          var result = classes.Filter(f);
          foreach (var item in result)
         {
            Console.WriteLine(item.ID+ " " + item.Name);
         }

}

我们声明了一个委托FilterHandler,只要满足这个委托的方法我们都可以传递给Filter方法,这样就实现了动态的改变查询条件的目的,F方法内部可以是任意的查询条件比如return c.Name != "终极一班";同时我们不需要改变Filter方法内部稳定的部分。

2)c#2.0里面也支持直接把一个方法传给一个委托,但本质上也是编译器把方法转换成了一个委托,例如上面:

private static void LambdaLinq()
       {
           var classes = GetClasses();
           // C#2.0 直接传递方法
          var result = classes.Filter(F);
          foreach (var item in result)
         {
            Console.WriteLine(item.ID+ " " + item.Name);
         }

}

3)C#2.0里面有个新特性,叫匿名方法,我们可以直接传递匿名方法:

private static void LambdaLinq()
       {
           var classes = GetClasses();
           // C#2.0 传递匿名方法
          var result = classes.Filter(delegate(Class c) { return c.Name == "终极一班"; });
          foreach (var item in result)
         {
            Console.WriteLine(item.ID+ " " + item.Name);
         }

}

好,变形到这里,我们发现这个匿名其实不仅仅可以给我们带来不用给方法命名的好处,在这个方法内部我们还可以使用外部上下文环境的变量成员,这个特性也叫"闭包(Closure)",JavaScript也支持这个特性,比如:

private static void LambdaLinq()
       {
           var classes = GetClasses();         

           string className = "终极一班";
           // C#2.0 传递匿名方法

           var result = classes.Filter(delegate(Class c) { return c.Name == className; });
         foreach (var item in result)
         {
            Console.WriteLine(item.ID+ " " + item.Name);
         }

}

4)大家发现没有,上面的语法还是有点拖沓,伟大的Microsoft又给我们提供了一种更简洁的写法(我怎么说了个"又"呢?^_^),这也就是我们所说的Lambda表达式了:

private static void LambdaLinq()
       {
           var classes = GetClasses();

          string className = "终极一班";

          //4 C#3.0 Lambda表达式
          var result = classes.Filter(c=> c.Name == className);
          foreach (var item in result)
         {
            Console.WriteLine(item.ID+ " " + item.Name);
         }

}

"=>"左边的就是我们上面匿名方法的参数列表,右边的是方法体,实际上lambda表达式也可以写成如下形式:

Class c => c.Name == className

(Class c) => c.Name == className

(Class c) => {return  c.Name == className;}

(x,y)=>x+y;//多参数

等等,函数的返回类型也是由编译器根据"=>"右边的表达式自动推断出来的。

而且需要提到的是由于Filter是ClassCollection的扩展方法的缘故,而且Filter方法返回类型是ClassCollection,所以可以无限扩展下去,例如

var result = classes.Filter( c => c.Name == className).Filter(c=>c.ID>1);

这就是扩展方法的魅力所在!

5)实际上不知不觉,我们已经实现了Linq里面的一个Where功能了

我们现在导入命名空间

using System.Linq;

然后会发现classes这个实例会增加了很多扩展方法例如Where,OrderBy,Union,GroupBy这些方法实际上就是一些给实现了IEnumerable接口的类型的扩展方法,说白了就是针对集合类型的一些相关方法,比如过滤、排序、合并、分组等方法,这些方法的返回类型依然是IEnumerable(大家可以把光标移动到Where方法上,然后调用"转到定义"去看看这些方法的定义就明白了)当然这些方法都离不开我们的Lambda表达式做参数。

private static void LambdaLinq()
       {
           var classes = GetClasses();
           string className = "终极一班";

          //5 C#3.0里面的Where扩展方法(专门给实现了IEnumerable接口的类做扩展)
           var result = classes.Where(c => c.Name == className);
          foreach (var item in result)
         {
            Console.WriteLine(item.ID+ " " + item.Name);
         }

}

 我们还可以这样可以无限扩展下去:var result = classes.Where(c => c.Name == className).OrderBy(c=>c.ID);

6)这样写针对IEnumarable类型的查询其实已经不错了,微软觉得还不过瘾,又提供了我们传说中的Linq查询表达式(又是"又"?!)

private static void LambdaLinq()
       {
           var classes = GetClasses();
           string className = "终极一班";

          //6 Linq查询表达式

         var result = from c in classes where c.Name==className orderby c.ID select c;
            foreach (var item in result)
            {
                Console.WriteLine(item.ID+" "+item.Name);
            }

}

到这时候你还认识原来的扩展方法吗?所以说语言的抽象确实很好用,和人的思维很接近,但是我们还是要看到它的本质,其实它的本质都是面向对象的一些东西,并没有创造出一些什么新的东西来,这样我们才可以真正理解语言。

7)最后一个稍微复杂一些的Linq查询,就是班级和学生结合的一个连接,连接的条件是班级的id和学生的所属班级id,然后生成一个新的集合,这个集合里面的元素成员包括班级名称和学生名称。涉及到的相关类如下:

class Student
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public int ClassID { get; set; }
    }
    class StudentCollection : List<Student>
    {
    }

获取学生集合的工厂方法:

static StudentCollection GetStudents()
{
    return new StudentCollection()
    {
        new Student { ID=1,Name="大东", ClassID=1},
        new Student{ID=2,Name="亚瑟",ClassID=1},
        new Student { ID=3,Name="小雨", ClassID=1},
        new Student{ID=4,Name="雷克斯",ClassID=1},
        new Student{ID=2,Name="张三",ClassID=2},
        new Student { ID=3,Name="李四", ClassID=2},
        new Student{ID=4,Name="王二麻子",ClassID=2}
    };
}

   

private static void LambdaLinq()
       {
           var classes = GetClasses();

           var students=GetStudents(); 

          //7 Linq查询表达式 Join

var result = from c in classes
             join s in students on c.ID equals s.ClassID
             select new { ClassName = c.Name, StudentName = s.Name };//匿名类型和初始化器新特性的使用
//var result = classes.Join(students,c=>c.ID,s=>s.ClassID,(c,s)=>new {ClassName=c.Name,StudentName=s.Name}

foreach (var item in result)
{
    Console.WriteLine(item.ClassName + " " + item.StudentName);//注意元素的属性成员已经改变
}

}

相关代码下载

总结一下,Linq的本质就是使用扩展方法扩展了对集合类型的各种查询方法,最后返回的还是一个集合类型。Linq的来龙去脉基本清楚了,Linq to Object、Linq to  Sql 、Linq to DataSet 、Linq to Xml等等也就是Linq针对来自不同的数据源(Object、Sql、DataSet、Xml)的集合类型进行查询的具体实现,大家剩下的事情就是在工作中根据需要用不同的Linq实现写各自的Linq查询了,以后有时间再写一些这方面的文章,这里提供一些资源以供我们大家一起学习Linq这个强大的工具:

Visual Studio 2008 Training Kit
Visual Studio 2008 Samples Page

最后感谢Thin李建忠,还有深圳俱乐部的朱兴林主席黄耀辉善友,对本人的帮助,顺便再感谢一下派对、还有cctv和广电总局吧^_^!

ps:

1、解释一下c#语言的版本问题:

 .Net Framework 的版本

对应C#的版本

1.1

1.1

2.0,3.0

2.0

3.5

3.0

.Net Framework 3.5 和3.0实际上仅仅是.NetFramework 2.0的一个扩展而已,3.0增加了WPF,WF,WCF,WCS等类库,3.5增加了Linq、Asp.Net3.5等类库。

2、另外关于C#3.0里面唯一没有谈到的新特性——表达式树和Linq的关系我还不是很清楚,有没有哪位达人可以解释一下也好让我对这篇blog做进一步补充,先谢谢了!这里仅仅简单介绍一下:

表达式树允许将拉姆达表达式表现为数据结构而不是可执行代码。一个可以转换为委托类型D的拉姆达表达式也可以转换为一个类型为System.Query.Expression<D>的表达式树。

private static void ExpressionTree()
{
    string className = "终极二班";
    Expression<FilterHandler> e = c => c.Name == className;//Expression<T>构造函数的可访问修饰符不是public的
    Class c1 = new Class(1);
    c1.Name = className;
    FilterHandler f = e.Compile();//表达式树经过编译之后又成了委托
    Console.WriteLine(f(c1));
}

一蓑烟雨任平生

posted on 2007-12-16 14:04 周克 阅读(2669) 评论(28)  编辑 收藏 所属分类: C#

Feedback

#1楼  2007-12-16 17:28 SW515      

谢谢奔驰,受教了!呵呵~~~   回复  引用  查看    

#2楼  2007-12-16 17:30 周银辉      

好文~   回复  引用  查看    

#3楼  2007-12-16 17:33 hnxy [未注册用户]

受益匪浅呀,谢谢周老师,让我大开眼界!
2008一旦拥有,别无所求!   回复  引用    

#4楼  2007-12-16 17:56 xlzhu      

好,从入门到提高啊,期望继续深入   回复  引用  查看    

#5楼  2007-12-16 18:02 黄耀辉      

老周,Well done! 继续写个连载,将你顶到太空去   回复  引用  查看    

#6楼  2007-12-16 18:13 蛙蛙池塘      

李建忠的webcast有讲。   回复  引用  查看    

#7楼  2007-12-16 19:46 pk的眼泪      

不错   回复  引用  查看    

#8楼 [楼主] 2007-12-16 20:24 周克      

@蛙蛙池塘
好像唯独没有讲ExpressionTree   回复  引用  查看    

#9楼  2007-12-16 20:34 武广敬      

感谢周老师的分享!!   回复  引用  查看    

#10楼  2007-12-16 20:58 e旋风      

我觉得新的语法让复杂程序的可读性会下降~   回复  引用  查看    

#11楼 [楼主] 2007-12-16 22:05 周克      

@e旋风
所以说才要搞懂语言的本质,才能灵活运用   回复  引用  查看    

#12楼  2007-12-16 22:14 hwl [未注册用户]

谢谢!很精彩,但自己还有些看不明白,要下载msn看下,谢谢老师!   回复  引用    

#13楼  2007-12-16 22:40 Boler Guo      

简单说,L2O 指数据源实现IEnumerable接口,编译的时候把Lambda翻译成匿名方法;L2S 指数据源实现IQueryable接口,编译的时候把Lambda翻译成表达式树。   回复  引用  查看    

#14楼 [楼主] 2007-12-16 22:47 周克      

@Boler Guo
那这个表达式树在L2S里面有什么用呢   回复  引用  查看    

#15楼  2007-12-17 07:57 lirenqing [未注册用户]

看到了一些JAVA中匿名类的影子.   回复  引用    

#16楼  2007-12-17 11:13 xingd      

lamda表达式编译时转换为Expression Tree,通过Vistor方式将Expression Tree转换为SQL查询,这就是L2S的实现方式。   回复  引用  查看    

#17楼  2007-12-17 11:57 装配脑袋      

在此文章中搜索了一下yield关键字,没搜到……   回复  引用  查看    

#18楼  2007-12-17 13:40 txdlf      

一直不明白Ling to DataSet是个啥东西啊~
我看就Linq to Object和Linq to Sql,顶多再加个Linq to xml   回复  引用  查看    

#19楼  2007-12-17 15:50 THIN      

好哇   回复  引用  查看    

#20楼 [楼主] 2007-12-17 18:18 周克      

@THIN
谢谢,接触到Linq还是从你开始的,呵呵   回复  引用  查看    

#21楼  2007-12-18 10:04 pp1982 [未注册用户]

还应该有partial的方法吧? 在LINQ里通过partial的方法在LINQ Data Entity里加入直接的数据验证规则   回复  引用    

#22楼  2007-12-18 10:05 pp1982 [未注册用户]

直接的数据验证规则 ---> 自己的数据验证规则   回复  引用    

#23楼  2007-12-18 13:20 要有好的心情      

to txdlf
Ling to DataSet:正式版中并没有提供将数据库中的数据加载到DataSet的功能,还是用DataAdapter.Fill()来加载数据。Ling to DataSet目前好像指在DataSet上执行查询、汇总等。   回复  引用  查看    

#24楼  2007-12-18 16:22 Davy~~~ [未注册用户]

不知道C#的版本是啥意思
2.0~3.5的CLR都是2.0的
  回复  引用    

#25楼  2007-12-19 10:04 txdlf      

@要有好的心情
即使提供将数据库中的数据加载到DataSet的功能也是Linq to Sql的子功能,并不能称为Linq to DataSet,我觉得Linq to DataSet说白了就是Linq to Object   回复  引用  查看    

#26楼  2007-12-20 01:28 张旋      

@Davy~~~
framework 2.0不支持linq和本文写的各种新特性,这就是区别。语法和clr版本无关,仔细看看本文就明白了。linq的本质是语法的简化,并不是编译器效率或clr的进化   回复  引用  查看    

#27楼  2008-01-10 17:51 Annie      

已经全抄下来了,可以回家看了,谢谢你!   回复  引用  查看    

#28楼  2008-04-24 22:52 opoll [未注册用户]

好文章。
才发现c#3.0的很多特性和概念在js中都已经有了。   回复  引用    



标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-01-01 12:28 编辑过
 
历史上的今天: