Linq的初步了解

Posted kit-l

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linq的初步了解相关的知识,希望对你有一定的参考价值。

参考资料:
《C# 7.0 核心技术指南第七版》第8章

Linq查询表达式

示例:

var names = new string[] { "Tom", "Dick", "Harry", "Mary", "Jay" };

var query = 
    from n in names
    where n.Contains("a")
    orderby n.Length
    select n.ToUpper();

    foreach (string name in query)
    {
        Console.WriteLine(name);
    }

看起来与SQL语句非常像,但实际上是完全不同的。查询表达式的语法结构:

技术图片

在写Linq查询表达式的时候,从上图左边开始依次进行处理。在必选的from子句之后,可以添加orderby、where、let和join子句。在它们之后,还可以添加select子句或group子句,或者使用另一租from、orderby、where、let或者join子句开始下一轮查询。

编辑器在处理查询表达式前会将其翻译为流式语法形式。

流式查询语法

上面的查询表达式的语法形式在一开始就会被编译器翻译为以下形式。

示例:

var query = names.Where(n => n.Contains("a"))
                    .OrderBy(n => n.Length)
                    .Select(n => n.ToUpper());

对比查询语法和流式语法

它们各有优势。查询语法在以下方面显得更加简洁:

  • 在查询语句中使用let子句在现有范围变量的基础上引入新变量
  • 在SelectMany、Join或者GroupJoin中引用外部范围变量

若查询只包含简单的Where、OrderBy和Select,则这两种查询方式都很好用,可根据个人的偏好进行选择。若查询是由单个运算符构成的,那么流式语法更简短,结构也更清晰。

若查询含有以下运算符之外的其他运算符,则需要选用流式语法:

Where, Select, SelectMany
OrderBy, ThenBy, OrderByDecending, ThenByDescending
GroupBy, Join, GroupJoin

混合查询语法

示例:

var names = new string[] { "Tom", "Dick", "Harry", "Mary", "Jay" };

int matches = (from n in names where n.Contains("a") select n).Count();
Console.WriteLine(matches);

以及:

string first = (from n in names orderby n select n).First();
Console.WriteLine(first);

在复杂查询中,使用混合语法查询有时会有奇效。而上述两个简单的例子则可以直接私用流式语法编写。

延迟执行

大部分查询运算符并非在构造时执行,而是在枚举时执行(在枚举器上调用MoveNext时执行)。

例如:

var numbers = new List<int> { 1 };

var query = numbers.Select(n => n*10);

numbers.Add(2);

foreach (int n in query)
{
    Console.WriteLine(n);
}

上述查询语句输出的结果为10 20。可见在查询语句创建之后,向列表中新添加的数字也出现在了查询结果中。

因为这些筛选和排序逻辑只会在foreach语句执行时才会生效,这成为延迟加载懒惰执行,它和委托效果类似。

以下运算符没有延迟执行能力:

  • 返回单个元素或标量值的运算符,如First或Count
  • 转换运算符,如ToArray、ToList、ToDictionary、ToLookup

比如Count方法返回一个整数,显然这个结果无法再被美剧,所以立即执行。

延迟执行将查询的创建和查询的执行进行了解耦,使得查询可以分多个步骤进行创建。

子查询中的任何语句都会延迟执行,包括集合和转换方法。

重复执行

延迟执行会导致,当重复枚举时,延迟执行的查询会重复执行。例如:

var numbers = new List<int> { 1, 2 };

var query = numbers.Select(n => n*10);

foreach (int n in query)
{
    Console.WriteLine(n);
}

Console.WriteLine();

numbers.Clear();

foreach (int n in query)
{
    Console.WriteLine(n);
}

上述代码输出结果如下:

10
20
    // 空

重复执行无法缓存某一个时刻的查询结果,而且对于一些计算密集型查询或依赖远程数据库的查询,会带来不必要的浪费。

可以使用转换运算符ToArray或ToList来避免重复执行。ToArray会将查询的输出结果复制到一个数组中,ToList会将结果复制到一个泛型的List对象中。如下:

var timesTen = numbers.Select( n => n*10).ToList();

numbers.Clear();

Console.WriteLine(timesTen.Count);

输出结果如下:

2

捕获外部变量

如果lambda表达式捕获了外部变量,那么该变量的值将在表达式执行时决定。例如:

var numbers = new int[] { 1, 2 };

int factor = 10;

IEnumerable<int> query = numbers.Select(n => n * factor);

factor = 20;

foreach (int n in query)
{
    Console.WriteLine(n);
}

输出结果为20 40。

当在for循环中构造查询时,很容易掉进坑里。如下:

IEnumerable<char> query = "Not what you might expect";
string vowels = "aeiou";

for (int i=0; i < vowels.Length; i++)
{
    query = query.Where(c => c != vowels[i]);
}

foreach (char c in query)
{
    Console.WriteLine(c);
}

编译器会将for循环中的迭代变量看成循环作用域之外的变量,因此每一个闭包都不活了相同的变量i,且在枚举查询结果时其值为5。

可以在for中再声明一个变量,将循环变量赋值给语句块内声明的这个变量:

for (int i=0; i < vowels.Length; i++)
{
    char vowel = vowels[i];
    query = query.Where(c => c != vowel);
}

这样可以保证每次循环迭代捕获到的都是全新的局部变量。现在也可以使用foreach替代for循环来解决这个问题:

foreach (char vowel in vowels)
{
    query = query.Where(c => c != vowel);
}

子查询

子查询就是包含在另一个查询的Lambda表达式中的查询语句。例如:

var musos = new string[] { "David Gilmour", "Roger Waters", "Rick Wright", "Nick Mason" };

IEnumerable<string> query = musos.OrderBy(m => m.Split().Last());

m.Split将每一个string转换为一组单词,而在每一组上都调用了Last查询运算符。m.Split().Last()就是子查询,而query则引用了外部查询。再看一个更加明显的例子:

var names = new string[] { "Tom", "Dick", "Harry", "Mary", "Jay" };

IEnumerable<string> outerQuery = names.Where(n => n.Length == names.OrderBy(n2 => n2.Length).Select(n2 => n2.Length).First());

foreach (string n in outerQuery)
{
    Console.WriteLine(n);
}

上述代码中子查询的names.OrderBy(n2 => n2.Length)对names根据字符串长度进行了排序,names.OrderBy(n2 => n2.Length).Select(n2 => n2.Length).First()这块代码的总体功能显然就是找出了排序后位于首位的字符串的长度,也就是3。names.Where(n => n.Length == names.OrderBy(n2 => n2.Length).Select(n2 => n2.Length).First())也就是长度为3的所有字符串。

可以使用查询表达式语法实现:

IEnumerable<string> outerQuery = 
                from n in names
                where n.Length == (from n2 in names
                                    orderby n2.Length
                                    select n2.Length).First()
                select n;

还可以写成这样:

IEnumerable<string> query = 
                from n in names
                where n.Length == names.OrderBy(n2 => n2.Length).First().Length
                select n;

也可以直接使用聚合函数Min进一步简化:

IEnumerable<string> query = 
                from n in names
                where n.Length == names.Min(n2 => n2.Length)
                select n;

子查询与延迟执行

子查询中的元素相关运算符和聚合运算符,如First、Count,不会导致外部查询立即执行。

构造复杂查询的方式

1 渐进式查询构造

例如:

var names = new string[] { "Tom", "Dick", "Harry", "Mary", "Jay" };

var query = 
    from n in names
    select n.Replace("a", "").Replace("e", "").Replace("i", "").Replace("o", "").Replace("u", "");
            
query = from n in query 
    where n.Length > 2 
    orderby n 
    select n;

2 into关键字

into关键字在表达式中会解析为两种不同的方式,第一种方式触发继续查询,第二种方式则触发GruopJoin。into关键字可以在映射之后继续执行后续查询,它可以作为构建渐进式查询的快捷途径。例如:

var query =
    from n in names
    select n.Replace("a", "").Replace("e", "").Replace("i", "").Replace("o", "").Replace("u", "")

    into noVowel

    where noVowel.Length > 2
    orderby noVowel
    select noVowel;

貌似into重新创建了一个查询,但在最终转换成流式语法时,它们都是同一个查询,而且不会有性能损失。

注意,使用into时要注意作用域规则。into关键字后面的呢查询语句不能够使用之前定义的范围变量。以下查询无法通过编译:

技术图片

3 查询的包装

渐进式查询可以通过将各个查询进行包装,从而将其构造为一条独立的语句。

var tempQuery = tempQueryExpr
var finalQuery = from ... in tempQuery ...

可以表示为:

var finalQuery = from ... in (tempQueryExpr)

这种包装和into关键字语义上等价。包装后的查询看起来和子查询类似,都有内部查询和外部查询的概念,难以分清。转换为流式语法后,这种包装实际上只是顺序链接运算符的一种方式。最终结果和子查询(将内部查询嵌入到另一个查询的Lambda表达式中)的形式是非常不同的。

在进行包装时,“内部”查询作为前置的传送带,而相对的,子查询就骑在传送带上面,并且会由传送带的Lambda工人按需触发。

技术图片

考虑如下查询:

var names = new string[] { "Tom", "Dick", "Harry", "Mary", "Jay" };

var query =
    from n in names
    select n.Replace("a", "").Replace("e", "").Replace("i", "").Replace("o", "").Replace("u", "");

query = from n in query
        where n.Length > 2
        orderby n
        select n;

foreach (var n in query)
{
    Console.WriteLine(n);
}

可以修改为包装形式:

var query = from n in
    (
        from n in names
        select n.Replace("a", "").Replace("e", "").Replace("i", "").Replace("o", "").Replace("u", "")
    )
    where n.Length > 2
    orderby n
    select n;

映射方式

对象初始化器

如果想要映射比int等标量类型更复杂的类型,比如自己定义的实体类型,可以使用C#的对象初始化器。例如在查询的第一步,希望把名字列表中的元音字母去除,但同时还需要保留原来的名字以便为接下来的查询所用。

首先定义一个辅助类:

class TempProjectionItem
{
    public string Original;
    public string Vowelless;
}

然后将其映射到对象初始化器:

var names = new string[] { "Tom", "Dick", "Harry", "Mary", "Jay" };

var temp =
    from n in names
    select new TempProjectionItem
    {
        Original = n,
        Vowelless = n.Replace("a", "").Replace("e", "").Replace("i", "").Replace("o", "").Replace("u", "")
    };

var query =
    from item in temp
    where item.Vowelless.Length > 2
    select item.Original;

foreach (var n in query)
{
    Console.WriteLine(n);
}

其中temp的类型为IEnumerable。输出结果为去掉元音字母后,字符个数依然大于2的字符串的原始形式(未去掉元音字母的形式)。

匿名类型

使用匿名类型可以避免我们额外定义一次性的TempProjectionItem类。如下:

var names = new string[] { "Tom", "Dick", "Harry", "Mary", "Jay" };

var temp =
    from n in names
    select new
    {
        Original = n,
        Vowelless = n.Replace("a", "").Replace("e", "").Replace("i", "").Replace("o", "").Replace("u", "")
    };

var query =
    from item in temp
    where item.Vowelless.Length > 2
    select item.Original;

foreach (var n in query)
{
    Console.WriteLine(n);
}

此时temp的类型为IEnumerable<<anonymous type: string Original, string Vowelless>> temp

使用into就更加简洁了:

var query =
    from n in names
    select new 
    {
        Original = n,
        Vowelless = n.Replace("a", "").Replace("e", "").Replace("i", "").Replace("o", "").Replace("u", "")
    }
    into temp
    where temp.Vowelless.Length > 2
    select temp.Original;

此时query类型为IEnumerable<string>。使用let关键字更方便书写。

let关键字

let关键字可以在查询中定义一个新变量,它可以和范围变量并存。如下:

var query =
    from n in names
    let vowelless =
        n.Replace("a", "").Replace("e", "").Replace("i", "").Replace("o", "").Replace("u", "")
    where vowelless.Length > 2
    orderby vowelless
    select n;

vowelless与n并存。实际上编译器将let子句映射为一个临时的匿名类型,它不但包含范围变量,还包含新的表达式变量。也就是编译器将查询转换为之前的匿名类型的查询形式。

let同时映射了新元素和已有的元素,它允许在一个查询中无须重写而复用其中的表达式。在select子句中,既能够映射原始名字n,又能够映射其去掉了元音字母的版本vowelless。

where语句前后可以使用任意多个let语句,后面的let语句可以引用前面let语句引入的变量,其作用范围还和into子句引入的边界象关。即let关键字透明地对所有现存的变量进行了重新映射。

let表达式不需要求得标量类型值,有时将其作为子序列反而更加有用。

解释型查询

解释型查询针对远程数据源,它是描述性的,它操作的序列实现了IQuerable接口,并且其查询运算符是定义在Queryable类中的,它们会在运行时解释,并生成表达式树。

Enumerable中的查询运算符也可以接收IQuerable序列,但这种方式产生的查询永远只能在客户端本地执行。所以在Queryable类中创建另一套查询运算符。

IQuerable有如下两种实现,一般的LINQ查询语句在两种实现中都能使用。

  • LINQ to SQL (不需要事先定义实体数据模型)
  • Entity Framework (EF)

对普通的可枚举集合也可以通过调用AsQueryable方法将其包装为IQuerableIQuerable实际上是对IEnumerable的扩展,它在后者的基础上添加了生成表达式树的方法。

解释型查询的构成:

技术图片

解释型查询也会遵循延迟执行的模型。也是只有开始对查询进行枚举时才会生成SQL语句。

注意:当枚举一个IQueryable时,他不会像本地查询那样使整个生产线都开动起来,而是仅仅启动IQueryable那部分。这个部分的专用枚举器会向生产线管理者发出请求,管理者会检视整条生产线,它们并非编译后的代码,而是调用为方法的表达式及其前置指令(表达式树)。然后管理者会遍历所有的表达式,将其转换为一个独立的清单(SQL语句)。并在清单执行后,将结果返回给消费者。整条生产线只有一个传送带在运转,其他部分只是描述既定工作的空壳构成的网络。

可以在一个查询中综合使用解释型查询运算符和本地查询运算符,通常将本地查询运算符放在外层,解释型查询操作放在内层。即令解释型查询为本地查询提供输入。

AsEnumerable方法

Enumerable.AsEnumerable是最简单的查询运算符,它将一个IQueryable转换为一个IEnumerable序列,强制将后续的查询运算符绑定到Enumerable的运算符上,从而使后续查询按本地查询处理。

使用AsEnumerable也可以将两个查询合并为一个:

Regex wordCounter = new Regex (@"(w|[-‘])+");

var query = dataContext.MedicalArticles
    .Where(article => article.Topic == "influenza")

    .AsEnumerable()

    .Where(article => wordCounter.Mathes(article.Abstract).Count < 100);

除了AsEnumerable之外,还可以调用ToArray和ToList达到同样效果。AsEnumerable的优势在于不会立即触发查询的执行,也不会预先创建任何存储结构。


以上是关于Linq的初步了解的主要内容,如果未能解决你的问题,请参考以下文章

七色花基本权限系统 - 初步使用EntityFramework实现用户登录

初步了解URL

LINQ to XML - 从文件加载 XML 片段

错误 3002:映射片段中的问题 | c# linq 到实体

初步了解设计模式

初步了解VUE源码