C# Notizen 12 查询表达式

应用程序还需要操作存储在其他数据源(如SQL数据库或XML文件)中的数据,甚至通过Web服务访问它们。传统上,查询这些数据源时需要使用不同的语法,且在编译期间不进行类型检查。

一、LINQ
在.NET Framework中,查询表达式是一组统称为语言集成查询(LINQ)的技术的一部分,将查询功能直接集成到了C#语言中。LINQ是.NET Framework 3.5新增的,它提供了适用于所有数据源(SQL数据库、XML文档、Web服务、ADO.NET数据库以及任何支持接口IEnumberable或IEnumerable<T>的集合)的查询语言,从而避免了操作数据和对象时经常出现的语言不匹配问题。
LINQ让查询变成了基本语言构造,就像算术运算和流程控制语句是C#基本概念一样。LINQ 查询将重点放在常用的操作而不是数据结构上,能够以一致的方式从任何支持的数据源检索数据,并对其进行转换。
与SQL(Structured Query Language,结构化查询语言)查询相比,LINQ查询的语法相同,使用的一些关键字相同,提供的很多优点也相同。您可随便修改被查询的底层数据的结构,而不会导致需要修改查询。SQL只能用于操作关系型数据,而LINQ支持的数据结构要多得多。

如下代码是一个用于Contact对象集合的查询

class Contact
{    
    public int Id { get; set; }    
    public string Company { get; set; }    
    public string LastName { get; set; }    
    public string FirstName { get; set; }    
    public string Address { get; set; }    
    public string City { get; set; }    
    public string StateProvince { get; set; }
}
IEnumerable<Contact> contacts = GetContacts();
var result = from contact in contacts select contact.FirstName;

foreach(var name in result)
{    
    Console.WriteLine(name);
}

这个简单的查询演示了 C#语言支持的声明性语法(declartive syntax,也叫 query comprehension syntax)。这种语法让您能够使用类似于SQL查询的语法编写查询,灵活性和表达能力都极强。虽然查询表达式中所有的变量都是强类型的,但是在大多数情况下,不需要显式地指定类型,因为编译器能够推断出来。

ps:LINQ查询语法

如果您熟悉SQL,就不会对LINQ使用的查询语法感到陌生。最明显的差别是,from运算符位于select运算符的前面,而不像SQL中那样位于后面。

1.1 选择数据

虽然上述所示的代码看起来可能很简单,但是实际上涉及的内容很多。首先应注意到的是,使用了一个类型将隐式确定的变量result,其类型实际上是IEnumerable<string>。查询表达式(赋值运算符右边的代码)的结果为查询,而不是查询的结果。select 子句返回一个对象,该对象表示对一个序列( contacts 列表)执行投影操作的结果(一系列contact.FirstName值)。由于结果是一系列字符串,因此result必然是由字符串组成的可枚举集合。当前,并不会检索数据,而只是返回一个可枚举集合,以后再将数据取回。

这个查询相当于说,从contacts指定的数据源中,选择每个元素(contact)的FirstName字段。from子句中指定的变量contact类似于foreach语句中的迭代变量,它是一个只读局部变量,作用域为查询表达式。in子句指定了要从中查询元素的数据源,而select子句指定在迭代期间只选择每个元素的contact.FirstName字段。

选择单个字段时,这种语法的效果很好,但通常需要选择多个字段,甚至以某种方式对数据进行变换,如合并字段。所幸的是,LINQ通过类似的语法提供了这样的支持。实际上,有多种方法执行这些类型的选择。

第一种方法是在select子句中拼接这些字段,这样将只返回一个字段,如下代码所示:

var result = from contact in contacts select contact.FirstName + " " + contact.LastName;

foreach(var name in result)
{    
    Console.WriteLine(name);
}

显然,这种选择方式只适用于有限的情形。一种更灵活的方法是返回多个字段,即返回一个数据子集,如下所示:

var result = from contact in contacts             
                      select new             
                      {                 
                          Name = contact.LastName + ", " + contact.FirstName;                 
                          DateOfBirth = contact.DateOfBirth             
                      };

foreach(var contact in result)
{    
    Console.WriteLine("{0} born on {1}", contact.Name, contact.DateOfBirth);
}

这里返回的仍是IEnumberable,但其类型是什么呢?如果查看程序中的select子句,就会发现它返回了一种新类型,其中包含字段contact.FirstName和contact.LastName的值。这实际上是一个匿名类型(anonymous type),它包含属性Name和DateOfBirth。这种类型之所以是匿名的,是因为它没有名称。无需显式地声明与返回值对应的新类型,编译器会自动生成。

ps:匿名类型
以这种方式创建匿名类型是LINQ工作方式的核心,如果没有var提供的引用类型,这根本不可能。

1.2 筛选数据
选择数据很重要,但以这种方式选择数据时,无法指定要返回哪些数据。SQL 提供了where子句,同样,LINQ也提供了where子句,它返回一个可枚举的集合,其中包含符合指定条件的元素。如下代码再上一个示例的查询中添加了一个where子句,将结果限定为StateProvince字段为FL的联系人。

var result = from contact in contacts             
                        whrere contact.StateProvince == "FL"             
                        select new {customer.FirstName, customer.LastName};

foreach(var name in result)
{    
    Console.WriteLine(name.FirstName + " " + name.LastName);
}

首先执行where子句,再对得到的可枚举集合执行select子句,其结果为一个包含属性FirstName和LastName的匿名类型。

1.3 对数据进行分组和排序
为支持更复杂的情形,如对返回的数据进行排序或分组,LINQ 提供了 orderby 子句和group 子句。可将数据按升序(从最小到最大)或降序(从最大到最小)排列,由于升序是默认设置,因此不需要指定升序。如下程序将结果按字段LastName排序。

var result = from contact in contacts             
                        oderby contact.LastName             
                        select contact.FirstName;

foreach(var name in result)
{     
    Console.WriteLine(name);
}

可根据多个字段进行排序,并混合使用升序和降序,这将创建出非常复杂的 orderby 语句,如下代码所示:

var result = from contact in contacts             
                    oderby                 
                        contact.LastName ascending,                
                        contact.FirstName descending             
                    select customer.FirstName;

foreach(var name in result)
{    
    Console.WriteLine(name);
}

将数据分组的方法与此类似,但将使用group子句替换select子句。对数据分组时的不同之处在于,返回的结果为由 IGrouping<TKey, TElement>对象组成的 IEnumerable,可将其视为由列表组成的列表。这要求使用两条嵌套的foreach语句来访问结果。
如下代码是一个使用group子句的LINQ查询

var result = from contact in contacts             
                   group contact by contact.LastName[0];

foreach(var group in result){    
    Console.WriteLine("Last names starting width {0}", group.key);    
    foreach(var name in result)    
    {        
        Console.WriteLine(name);    
    }    
    Console.WriteLine();
}

如果需要引用分组操作的结果,就可创建一个标识符,使用关键字into将查询结果存储到该标识符中,并对其做进一步查询。这种组合方式称为查询延续(continuation)。
如下代码演示了一个使用group和into的LINQ查询:

var result = from contact in contacts             
                    group contact by contact.LastName[0] into namesGroup             
                    where namesGroup.Count() > 2             
                    select namesGroup;

foreach(var group in result)
{    
    Console.WriteLine("Last names starting width {0}", group.key);    
        foreach(var name in result)    
        {        
            Console.WriteLine(name);    
        }    
    Console.WriteLine();
}

1.4 联接数据

LINQ 还能够合并多个数据源,这是通过利用一个或多个都有的字段将它们联接起来实现的。查询多个没有直接关系的数据源时,联接数据很重要。SQL支持使用很多运算符进行联接,但LINQ联接基于相等性。
前面的示例只使用了Contact类,要执行联接操作,至少需要两个类。程序清单12.9再次列出了Contact类,还列出了新增的JournalEntry类。继续假设通过调用GetContacts填充了contacts列表,并通过调用GetJournalEntries填充了journal列表。
如下代码演示了Contact和JournalEntry类

class Contact
{    
    public int Id { get; set; }    
    public string Company { get; set; }    
    public string LastName { get; set; }    
    public string FirstName { get; set; }    
    public string Address { get; set; }    
    public string City { get; set; }    
    public string StateProvince { get; set; }
}

class JournalEntry
{    
    public int Id { get; set; }    
    public int ContactId { get; set; }    
    public string Description { get; set; }    
    public string EntryType { get; set; }    
    public DateTime Date { get; set; }
}

IEnumberable<Contact> contacts = GetContacts();
IEnumberable<JournalEntry> journal = GetJournalEntries();

在LINQ中,最简单的联接查询与SQL内联接等效,这种查询使用join子句。SQL联接可使用很多不同的运算符,而LINQ联接只能使用相等运算符,称之为相等联接(equijoin)。
如下示例的查询使用Contact.ID和JournalEntry.ContactId作为联接键,将一个由Contact对象组成的列表和一个由JournalEntry对象组成的列表联接起来:

var result =    
        from contact in contacts    
        join journalEntry in journal    
        on contact.Id equals journalEntry.ContactId    
        select new    
        {        
            contact.FirstName,        
            contact.LastName,        
            journalEntry.Date,        
            journalEntry.EntryType,        
            journalEntry.Description    
        };

如上所示的 join子句创建了一个名为 journalEntry的范围变量(range ariable),其类型为JournalEntry;然后使用equals运算符将两个数据源联接起来。
LINQ还支持分组联接概念,而SQL没有与之对应的查询。分组联接使用关键字into,其结果为层次结构。就像使用group子句时那样,需要使用嵌套foreach语句来访问结果。
ps:顺序很重要
使用LINQ联接时,顺序很重要。被联接的数据源必须位于equals运算符左边,而联接数据源必须位于右边。在这个示例中,contacts是被联接的数据源,而journal是联接数据源。
所幸的是,顺序不正确时,编译器能够捕获并生成编译错误。如果交换join子句中的参数,将出现下面的编译错误:
名称“Journalentry”不在“equals”左侧的范围内。请考虑交换“equals”两侧的表达式。
需要注意的另一个重点是,join子句使用运算符equals,它与相等运算符(==)不完全相同。
如下代码的查询联接contacts和journal,并将结果按联系人姓名分组。在返回的结果集中,每个元素都包含一个由 JournalEntry 组成的可枚举集合,该集合由返回的匿名类型的JournalEntries属性表示。

var result =    
        from contact in contacts    
        join journalEntry in journal    
        on contact.Id equals journalEntry.ContactId              
        into journalGroups    
        select new    
        {        
            Name = contact.LastName + "," +contact.FirstName,        
            JournalEntries = journalGroups    
        };

1.5 数据平坦化
虽然选择和联接数据时,返回的数据是合适的,但层次型数据使用起来比较繁琐。LINQ能够创建返回平坦化数据的查询,就像查询SQL数据源一样。
假设对Contact和JournalEntry类进行了修改:在Contact类中添加了一个Journal字段,其类型为List<JournalEntries>,并删除了JournalEntry类的属性ContactId,如下所示

class Contact
{    
    public int Id { get; set; }    
    public string Company { get; set; }    
    public string LastName { get; set; }    
    public string FirstName { get; set; }    
    public string Address { get; set; }    
    public string City { get; set; }    
    public string StateProvince { get; set; }    
    public List<JournalEntries> Journal;
}

class JournalEntry
{    
    public int Id { get; set; }    
    public string Description { get; set; }    
    public string EntryType { get; set; }    
    public DateTime Date { get; set; }
}

IEnumerable<Contact> contacts = GetContacts();

在这种情况下,可使用查询检索特定联系人的JournalEntry列表,如下所示:

var result =    
        from contact in contacts    
        where contact.id == 1    
        select contact.Journal;

foreach(var item in result)
{    
    foreach(var journalEntry in item)    
    {        
        Console.WriteLine(journalEntry);    
    }
}

虽然这样可行,也返回了所需的结果,但是仍需使用嵌套 foreach 语句来访问结果。所幸的是,LINQ 支持从多个数据源选择数据,从而提供了一种返回平坦化数据的查询语法。如下程序演示了这种查询语法,它使用多个from子句,使得访问数据时只需一条foreach语句。

var result =    
        from contact in contacts    
        from journalEntry in contact.Journal    
        where contact.id == 1    
        select contact.Journal;

foreach(var item in result)
{    
    Console.WriteLine(journalEntry);
}

二、标准查询运算符方法

前面介绍的所有查询都使用声明性查询语法,但也可使用标准查询运算符方法来编写这些查询。标准查询运算符方法实际上是命名空间System.Linq中定义的Enumerable类的扩展方法。对于使用声明性语法的查询表达式,编译器将其转换为等价的查询运算符方法调用。
使用using语句包含命名空间System.Linq后,对于任何实现了接口IEnumberable<T>的类,智能感知列表都可包含标准查询运算符方法。
虽然声明性查询语法几乎支持所有的查询操作,但是也有一些操作(如Count和Max)没有对应的查询语法,必须使用方法调用来表示。由于每个方法调用都返回IEnumerable,因此通过串接方法调用,可编写出复杂的查询。编译声明性查询表达式时,编译器就是这样做的。

ps:使用声明性语法还是方法语法
使用声明性语法还是方法语法因人而异,这取决于个人认为哪种语法更容易理解。无论使用哪种语法,执行查询得到的结果都相同。

如下示例演示了使用方法语法的LINQ查询

var result = contacts.     
        Where(contact => contact.StateProvince == "FL").     
        Select(contact => new { contact.FirstName, contact.LastName });

foreach(var name in result)
{    
    Console.WriteLine(name.FirstName + " " + name.LastName);
}

三、Lambda

上述示例中,传递给方法Where和Select的参数看起来与以前使用过的参数不同。这些参数实际上包含的是代码,而不是数据。之前介绍过委托和匿名方法,委托能够将一个方法作为参数传递给另一个方法,而匿名方法能够编写未命名的内联语句块,这些语句块将在调用委托时执行。
Lambda 结合使用了这两个概念,它是可包含表达式和语句的匿名函数。通过使用Lambda,可以更方便、更简洁的方式编写这样的代码,即正常情况下需要使用匿名方法或泛型委托进行编写。
ps:Lambda和委托
由于Lambda是编写委托的更简洁方式,因此可在通常需要使用委托的任何地方使用它们。所以,Lambda的形参类型必须与相应的委托类型完全相同,返回类型也必须隐式地转换为委托的返回类型。
虽然 Lambda 没有类型,但是它们可隐式地转换为任何兼容的委托类型。正是这种隐式转换让您无需显式赋值就能够传递它们。
在C#中,Lambda使用Lambda运算符(=>)。在方法调用中,该运算符左边指定了形参列表,而该运算符右边为方法体。匿名方法的所有限制也适用于Lambda。

在上述示例中,实参 contact => contact.StateProvince == "FL"的意思为,这是一个以 contact为参数的函数,其返回值为表达式 contact.StateProvince == "FL"的结果。
ps:捕获的变量和定义的变量
Lambda还能捕获变量,这可以是Lambda所属方法的局部变量或参数。这使得可在Lambda体内通过名称访问捕获的变量。如果捕获是局部变量,那么必须赋值后才能在Lambda中使用它。ref或out参数无法捕获。
然而,需要注意的是,对于Lambda捕获的变量,在引用它的委托超出作用域前,垃圾收集器将不会收集它们。
Lambda中声明的变量在Lambda所属方法内不可见,输入参数也如此,因此可在多个Lambda中使用同一个标识符。

表达式Lambda
在Lambda中,如果运算符右边为表达式,该Lambda就为表达式Lambda,它返回该表达式的结果。表达式Lambda的基本格式如下:

(input parameters) => expressions

如果只有一个输入参数,那么括号是可选择的;否则(包括没有参数时),括号将是必不可少的。
就像泛型方法可推断其类型参数的类型一样,Lambda 也能推断其输入参数的类型。如果编译器无法推断出类型,您就可以显式地指定类型。

如果将表达式 Lambda 的表达式部分视为方法体,那么表达式 Lambda 包含一条隐式的return语句,它返回表达式的结果。
ps:包含方法调用的表达式Lambda
大部分示例都在右边使用了方法,但是如果创建的Lambda将用于其他域,如SQL Server,就不应使用方法调用,因为它们在.NET Framework公共语言运行时外面没有意义。

语句Lambda
在 Lambda 的右边,可使用一条或多条用大括号括起的语句,这种 Lambda 称为语句Lambda。语句Lambda的基本形式如下:
(input parameters) => { statement; }
与表达式 Lambda 一样,如果只有一个输入参数,那么括号是可选的;否则,括号就必不可少。语句Lambda也遵循同样的类型推断规则。
虽然表达式Lambda包含一条隐式的return语句,但是语句Lambda没有,您必须在语句Lambda中显式地指定return语句。return语句只导致从Lambda表示的隐式方法返回,而不会导致从Lambda所属的方法返回。
语句Lambda不能包含这样的goto、break和continue语句,即其跳转目标在Lambda外。同样,作用域规则禁止从嵌套Lambda分支到外部Lambda。
预定义的委托
虽然 Lambda 是 LINQ 的有机组成部分,但是可将其用于任何可使用委托的地方。因此,.NET Framework提供了很多预定义的委托,可将其作为方法参数进行传递,而无需首先声明显式的委托类型。
由于返回Boolean值的委托很常见,因此.NET Framework定义了一个Predicate<in T>委托,Array和List<T>类的很多方法都使用它。
Predicate<T>定义了一个总是返回Boolean值的委托,而Func系列委托封装了有指定返回值,且接受0~16个输入参数的方法。
Predicate<T>和 Func 系列委托都有返回值,但是 Action 系列委托表示返回类型为 void的方法。就像Func系列委托一样,Action系列委托也接受0~16个输入参数。
四、延迟执行
不同于众多传统的数据查询技术,LINQ 查询要等到实际迭代其结果时才执行,称之为延迟执行(lazy evaluation)。其优点之一是,在指定查询和检索查询指定的数据之间,可修改原始集合中的数据。这意味着您获得的数据总是最新的。
虽然LINQ首选延迟执行,但是使用了任何聚合函数的查询都必须先迭代所有元素。这些函数(如Count、Max、Average和First)都返回一个值,并且无需使用显式foreach语句就能执行。
ps:延迟执行和串接查询
延迟执行的另一个优点是,让您能够串接查询,从而提供编码效率。由于查询对象表示的是查询,而不是查询的结果,因此可轻松地串接或重用它们,而不会导致开销高昂的数据取回操作。
也可强制查询立刻执行,这有时称为贪婪执行(greedy evaluation)。为此,可在查询表达式后面,紧接着放置一条foreach语句,也可调用方法ToList 或ToArray。方法ToList 和ToArray还可用于将数据缓存到一个集合对象中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容