LINQ的预备知识

来源:互联网 发布:网络公会是什么意思 编辑:程序博客网 时间:2024/05/15 13:05

LINQ的预备知识

 

不管进行任何类型的项目开发,都免不了和数据打交道.数据的来源多种多样,可以是内存中的一个集合,比如数组,集合,列表;可以是数据库,比如SQL Server;可以是一个XML文件;可以是一张Excel表格.无论何种数据源,都免不了对其数据进行操作,比如筛选,排序,链接等.这些操作的实现方式,因为数据源的不同而各不相同,这多少给开发者带来了不便.前面说的关于对象的筛选和排序就需要编写不少的代码.

 

所以.NET框架的3.5版本引入了一项新技术--LINQ.为了支持LINQ,C#也添加了许多新功能,比如扩展方法,匿名类型,隐式类型局部变量,Lan.bda表达式.

 

LINQ的全面是语言集成查询(languaga Integrated Query),

它为查询各种不同的数据源提供了一个统一的接口.通过这个接口,查询各种数据源可以使用几乎一致的方式和语法.既然是一个接口,就有接口的调用者和接口的实现者(或者叫做提供者Provider).接口的调用者通常是各种各样的应用程序,对他们来说,接口的使用方式是一致的;对于接口的实现者,则根据各种不同的数据源有着各自不同的实现.根据数据源的不同,LINQ也产生了不同的分支,比如LINQ to Objects,LINQ to SQL,LINQ to XML,LINQ to Entities,LINQ to Excel.

 

LINQ提供了一种很灵活的架构,使得开发者可以编写自己的LINQ实现.现在已经有了很多这样的LINQ实现,比如LINQ to Amazon,LINQ to Google,LINQ to Oracle.从名称上已经看出它们是用来查询何种数据源的.而开发者最常用的,无非是LINQ to Objects,LINQ to SQL,LINQ to XML.

 

 

LINQ预备知识

 

LINQ涉及了很多相关的其他知识,因此有必要对它们进行一下学习和了解.首先需要回顾一下泛型和委托.

 

泛型定义了类型的模板,委托定义了方法的模板.当泛型遇到委托会发生啥呢?

不着急,咱们先定义一个非泛型委托:

public delegate bool Predicate(string name);

根据前面说的知识可以知道:这个委托定义了所有以string类型作为输入参数,并且以bool参数作为输出参数的方法.例如下面这个方法就符合Predict委托的定义:

        public bool IsCorrectName(string name)        {            return true;        }

这个委托有一个问题,就是哪天有了一个这样的方法,由于入口参数的类型不符合,上面的Predicate委托就没法使用了(例如这里有一个Product的自定义类型):

        public bool IsCorrectProduct(Product item)        {            return true;        }

这样的话,我们就不得不再定义一个委托来代表这个方法:

public delegate bool Predicate(Product obj);

由于总是可以使用不同的类型,因此类型的数目是无限多的,使用上面的方法显然是行不通的.既然已经学习过了泛型,这就是泛型该出场的时候了,现在可以将入口参数的类型替换为类型参数,将委托的定义修改为下面这样:

public delegate bool Predicate<T>(T obj);

这样的话,这个委托就可以兼容上面的两种方法.

            Predicate<string> del1 = IsCorrectName;            Predicate<Product> del2 = IServiceProvider;

这个Predicate<T>委托就是上一章”对象的筛选和排序”中遇到的委托了.

Predicate<T>还有一个问题:如果一个方法的返回值不是bool,那么不是还得再写一个委托?干脆把返回值类型也用泛型来定义,于是上面你的委托就变成了下面的模样:

    public delegate TResult GeneralMethod<in T, out TResult>(T arg);


可以注意到,由于返回值类型已经不一定是bool,此时的方法也就不再是一个谓词了,关于啥叫谓词自己去百度,所以委托的名字也改了,这样的名字更加贴切一点.

 

现在总算是可以放松一下了,但是别急,想想看现在这个委托可以定义哪些方法?答案是:该委托定义了所有拥有一个入口参数和一个返回值的方法,无论入口参数或返回值是何种类型.

 

下面的代码演示了对这个委托的赋值:

        static void Main(string[] args)        {            GeneralMethod<string,bool> a = IsCorrectName;            GeneralMethod<int, string> b = GetDay;        }        public static bool IsCorrectName(string name)        {            return true;        }        public static string GetDay(int dayOfWeek)        {            string[] day = {"1","2","3","4","5","6","7" };            return day[dayOfWeek];        }


依次类推,咱们可以批量定义这样的已租委托:

    public delegate TResult GeneralMethod<in T, out TResult>(T arg);    public delegate TResult GeneralMethod<in T,in T2, out TResult>(T arg);    public delegate TResult GeneralMethod<in T, in T2, in T3,out TResult>(T arg);    public delegate TResult GeneralMethod<in T, in T2, in T3, in T4,out TResult>(T arg);


这组委托定义了所有包含1-4个参数,并且拥有一个返回值的方法(当然,如果觉得4个参数不够的话,可以再加).可以说,有了这样一组泛型委托以后,以后就再也不用定义任何委托了,因为它们实在是太通用了.LINQ,大量用到了上面定义的委托,不过不叫GeneralMethod,而叫Func.

 

大家可以看一下mscorlib.dll中的Func委托定义.它定义了含有最多16个参数的委托.

 

Func委托中,类型参数的名称用了通用的T1,T2,T3.LINQ,大量使用了Func委托组,同时,为了提高代码的可读性,对泛型参数进行了重命名,使得他们具有一定得含义:

TSource:输入参数的类型.

TResult:输出参数的类型.

TKey:用于排序,分组,或者链接等操作的主键类型.

 

例如:

Func<TSource,TKey>keySelector,    Func<TSource,TResult>selector.由于泛型参数的名称是无关紧要的,因此Func<TSource,TKey>Func<T1,TRseult>实际上是一致的.

 

 

隐式类型的局部变量

 

C#中使用var关键字对隐式类型的局部变量进行声明,在声明时无须明确指定变量的类型,而交由编译器去推断.var关键字的用法不多说了.

需要注意var关键字在使用的时候有两个限制:

1.在声明一个隐式类型时,必须对它赋值,否则就会出现上面代码的编译错误:隐式类型的局部变量必须已经初始化.

2.var只能用于局部变量,不能将字段,属性,或者方法的返回值声明为隐式类型.

 

将变量声明为var和声明为object是不同的.object是所有类型的基类,当为object赋值字符串后,无法继续使用stirng类型的方法,因为字符串已经隐式转换为其基类型object;但是再为var赋值字符串时,该变量实际上还有string类型.

 

 

匿名类型

 

那么使用隐式类型有啥好处呢?好像除了头来以外没啥好处了...而且还会降低清晰度和可读性.所以,咱们以后只在LINQ的查询表达式和创建匿名类型时使用隐式类型,其他情况使用显示类型声明.但是,对于匿名类型类型来说,就是另外一回事了.

 

匿名类型允许开发者不必预先定义类型,就创建类型的实例.例如:

        static void Main(string[] args)        {            var developer = new            {                name = "Jimmy",                age = 30,                favorites = new[] { "C#", ".NET", "JAVA" }            };            Console.WriteLine(developer.name);            Console.WriteLine(developer.age);            Console.WriteLine(string.Join(",",developer.favorites));        } 


这里有2点需要注意:

1.上面的代码并没有定义developer变量的类型,他的类型由编译器进行推断和定义.

2.匿名类型的属性的类型,例如name,age的类型,也是由编译器推断和定义的.

 

可以编写一个帮助方法showTypeInfo(),用来查看developer的类型信息:

  

      static void Main(string[] args)        {            var developer = new            {                name = "Jimmy",                age = 30,                favorites = new[] { "C#", ".NET", "JAVA" }            };            Console.WriteLine(developer.name);            Console.WriteLine(developer.age);            Console.WriteLine(string.Join(",",developer.favorites));            showTypeInfo(developer);        }        static void showTypeInfo(object obj)        {            Console.WriteLine("type: {0}",obj.GetType().Name);            Console.WriteLine("location: {0}\r\n",obj.GetType().Assembly.GetName().Name);        }

看一下输出,什么玩意,没见过,其实这里用到了一种强大的技术,叫做反射.慢慢来,以后说,等楼主明白的差不多了在给大家说下.这里只需要知道他输出了对象的实际类型名称和所在的程序集名称注意虽然方法的入口参数是object,但输出的是实际类型的信息,而不是object基类的信息.

 

可以看到,type: <>f__AnonymousType0`3是编译器为匿名类型命名的类型名称.

 

 

扩展方法

 

LINQ使用到的另一个关键技术就是扩展方法(Extension Method),他也是从C#3.0才开始添加的功能.本次就对扩展方法做一个简单的介绍,依然从一个示例开始.

在很多与数据库打交道的项目中,都会有这样一个常见的任务:将数据返回的DataTable转换为结合对象,代码通常是这样的,你没见过也不要紧:

 

 

            DataTable table = getData();//这里是个方法,具体不写了,这里不重要            List<Developer> list = new List<Developer>();            foreach (DataRow row in table.Rows)            {                Developer item = new Developer();                if (row["Name"]==DBNull.Value)                {                    item.Name = null;                }                else                {                    item.Name = row["Name"].ToString();                }                 if (row["WorkYears"]==DBNull.Value)                {                    item.WorkYears = Int32.MinValue;                }                else                {                    item.WorkYears = Convert.ToInt32(row["WorkYears"]);                }                if (row["DateTime"]==DBNull.Value)                {                    item.Birthday = null;                }                else                {                    item.Birthday = Convert.ToDateTime(row["DateTime"]);                }                list.Add(item);

很显然,上面你的if语句很烦人,我写的时候也是够够的了,如果你的代码出现这样的语句,你就需要好好考虑一下是不是要继续干下去了.

那么你最好是编写一个帮助类:

        #region 帮助类        public class DataRowHelper        {            public static string GetString(DataRow row, string columnName)            {                if (row[columnName]==DBNull.Value)                {                    return null;                }                else                {                    return row[columnName].ToString();                }            }             public static int GetInt32(DataRow row, string columnName)            {                if (row[columnName]==DBNull.Value)                {                    return Int32.MinValue;                }                else                {                    return Convert.ToInt32(row[columnName]);                }            }             public static DateTime? GetDataTimeNullable(DataRow row, string columnName)            {                if (row[columnName]==DBNull.Value)                {                    return null;                }                else                {                    return Convert.ToDateTime(row[columnName]);                }            }        }        #endregion


这个类的好处就是不必每次处理DataRow数据都要重复的编写if-else.现在,上面你的DataRow数据获取的代码可以简化一下了:

                Developer item = new Developer();                item.Name = DataRowHelper.GetString(row,"Name");                item.WorkYears = DataRowHelper.GetInt32(row,"WorkYears");                item.Birthday = DataRowHelper.GetDataTimeNullable(row,"Birthday");


现在DataRow数据使用起来很简单了,再思考一下就会想到:如果DataRow类型本身就提供一组GetString(),GetInt32,GetDateTimeNullable()方法就好了,这样上面你的调用可以进一步简化成这样:

item.Name=row.GetString(“name”);


值得庆幸的是,C#3.0开始,引入了一个新的功能--扩展方法(Extension Method).这项机制使上面的想法得以实现.他的语法很简单,只需要将DataRowHelper稍微做一点修改就行了:

        public static class DataRowExtension        {            public static string GetString(this DataRow row, string columnName)            {                if (row[columnName]==DBNull.Value)                {                    return null;                }                else                {                    return row[columnName].ToString();                }            }            //其余的就省略了        } 


注意一下:

1.扩展方法所在的类型要为静态的,因此需要将DataRowExtension类型声明为static.

2.被扩展的类型要为方法列表的第一个参数的类型,并且前面要加上this关键字

 

这样的话,就可以实现item.Name=rowGetString(“Name”);这样简单的调用了.

 

关于扩展方法其他的东西不多了,也几乎不太常用,就不说了.

 

 

匿名方法和Lambda表达式

 

LINQ预备知识中的最后一个就是匿名方法和Lambda表达式(拉姆达表达式).

 

Lambda表达式与委托和匿名方法有着很深的联系,来一个示例程序说明一下:

创建一个Windows窗体应用程序,然后在默认创建的Form1上添加两个按钮...(这个应该会吧.)分别命名为btnClick1btnClick2

实现的功能很简单,在单击这两个按钮的时候弹出一个对话框,向用户显示一个提示,例如”你好”.在以前,大家会毫不犹豫的在按钮上双击,然后编写按钮的事件处理代码,现在已经学习过了委托,可以换一种方式了,想下面这样修改Form1.cs文件:

    public partial class Form1 : Form    {        public Form1()        {            InitializeComponent();            btnClick1.Click += new EventHandler(btnClick);            btnClick2.Click += new EventHandler(btnClick);        }        void btnClick(object sender, EventArgs e)        {            MessageBox.Show("你好");        }    }


上面这段代码比起双击按钮来编写事件处理代码的一个好处就是进行了代码重用,因为两个Click事件共享了同一个事件处理方法.如果直接在按钮上双击,VS来创建事件响应的方法,那么会分别产生两个方法:

        private void btnClick1_Click(object sender, EventArgs e)        {         }         private void btnClick2_Click(object sender, EventArgs e)        {         }


接下来再来思考一下,像这样两个按钮共享一个Click事件处理方法的情况并不多见.现在删除按钮2,仅保留按钮1.这样大家看出问题了么?btnClick()方法仅仅为btnClick1这一个对象的Click事件服务,除了btnClick1以外,再也没有任何其他对象需要用到btnClick().但是btnClick1btnClick()却离得如此远,以至于在窗体代码很多的情况下不得不花些时间来寻找他(可以想象一下:窗体代码有好几百行,btnClick()隐藏在一个小角落里).

所以说,这种用法不好!

 

如果有一种语法,能够直接将方法体赋给btnClick1的的Click时间就好了,比如这样:

btnClick1.Click +={ MessageBox.Show("你好"); };//这段代码仅仅是为了演示,不能运行


现在大家应该能明白接下来要干啥了吧,匿名方法!不过上面你的代码可能是我自己YY出来的,现在需要修改一下:

            btnClick1.Click +=delegate(object sender,EventArgs e)            { MessageBox.Show("你好"); };

这样似乎可以运行了!

 

匿名方法的语法很简单:使用了delegate关键字进行声明,后面紧跟方法的参数列表,然后在花括号中编写方法体.

 

现在这个程序可以正常运行了.仔细看这段代码,还有没有改进之处呢?应该注意到:这个方法体既没有使用参数sender,也没有使用参数e,但是为了让编译器通过,却将它们添加了进来,这样是不是有点多余?为了解决这个问题,匿名方法提供了一种更简洁的语法:当不需要方法的参数时,可以将参数列表省略掉.所以上面你的代码可以是这样:

btnClick1.Click +=delegate{ MessageBox.Show("你好"); };


这里连小括号也一起删除了!

除了上面这些之外,匿名方法还有一种有意思的使用方式,就是将匿名方法需要用到的变量定义在匿名方法之外,此时的变量叫做外部变量.例如:

        public Form1()        {            InitializeComponent();            string greeting = "你好";            btnClick1.Click +=delegate{ MessageBox.Show(greeting); };        }


上例中,greeting便是外部变量.匿名方法使得使用委托更加方便.下次编写代码的时候,可以试着试用一下你的方法,这样对它的印象会深一点.

 

 

 

Lambda表达式

 

Lambda表达式可以看错是匿名方式的等价物,但是语法更简洁,使得匿名方法的编写变得更加方便.

 

接下来看看Lambda表达式的使用:

btnClick1.Click += (x, y) => MessageBox.Show("你好");


相比匿名方式,Lambda表达式省略了delegate关键字,也省略了参数类型,xy分别代表传递的参数类型实例,(x,y)就相当于delegate(object x,EventArgs y),=>的右边则为方法体.

 

如果方法体不止一行:

            btnClick1.Click += (x, y) =>            {                MessageBox.Show("你好");                MessageBox.Show("hello");            };


运行这个案例就会发现出现了两个对话框.

这个例子也说明了x确实是EventHandler委托定义中的object类型的sender参数.

 

EventHandler的定义是public delegate void EventHandler(object sender EventArgs e);它是不包含返回值的.下面再用Predicate<T>委托来实现一个包含返回值的例子:

    public delegate bool Predicate<in T>(T obj);    class Program    {        static void Main(string[] args)        {            Predicate<int> pre = x => x==3;            Predicate<int> pre2 = x => { return x==3; };        }    }


上面的两个Lambda表达式是等价的,需要注意的是,在不使用花括号时,默认会将方法体的执行结果作为返回值;而使用了花括号,尽管方法体的语句只有一条,仍然需要使用return 明确的返回执行结果.LINQ,将大量的使用Lambda表达式.

 

 

小结

关于LINQ的知识还没开始说,但是已经说了很多关于LINQ的预备知识,这些知识都是前面说过的,在这里再说一遍只是为了勾起大家的回忆,关于LINQ的知识还是很重要的,而关于LINQ的预备知识也不少.

0 0