November 30, 2023
By: Kevin'

Linq编程指南

  1. 第一章Linq基础介绍
    1. Linq的优势
    2. Linq的组成元素
    3. Linq是Lzay的
    4. Lambda表达式在查询操作符中的应用
    5. 本地和解释型查询
  2. 第二章 两种风格的Linq
  3. 第三章 操作符
    1. 操作符
    2. 性能
    3. 异常
  4. 第四章 LINQ to XML
    1. X-DOM概览
    2. 关键X-DOM类型
    3. 创建X-DOM
    4. 手动过程式创建
    5. 函数式构造
    6. 通过投影创建
    7. 使用LINQ查询X-DOM
    8. 查找子节点
    9. 查找父节点
    10. 查找同级节点
  5. 第五章 解释查询(Interpreted Queries)
    1. 解释型查询
    2. Entity Framework 示例
  6. 第六章 Linq并发

linq

Language Integrated Query (Linq), 是C#语言集成的数据查询, 是C#语言函数编程的基本体现, 广泛用于结构化数据(xml, sql, json, etc.)的操作.

本文的所有代码都是可以独立以脚本方式执行得到结果的. 形式上第一段是代码, 第二段是代码的执行结果.

第一章Linq基础介绍

Linq的优势

  1. 比之命令式的风格, 可以少写代码

  2. 更好的表述作者意图

  3. 是一个可以广泛应用于各类不同的数据(容器,数据库,Json,XML, etc.)的通用技巧

  4. Linq是组合式的, 可以从子表达式开始, 一步步建立自己需要的语句

  5. 类型检查帮助我们建立类型安全的查询代码

Linq的组成元素

  • 元素 序列中的元素
  • 序列
    • IEnumerable<T> : 对应这种序列的是local query, 查询对象是本地对象结构.
    • IQuerable<T> : 称为remote sequence, 比如数据库服务器, 第五章会详细讲解
  • 查询结果 可以是一个value, 某个Element或者一个新的 IEnumable<T>
int[] fibonacci = { 0, 1, 1, 2, 3, 5 };

// Scalar return value
int numberOfElements = fibonacci.Count();
Console.WriteLine("元素总数: {0}", numberOfElements);

// Output sequence return value
IEnumerable<int> distinctNumbers = fibonacci.Distinct();
Console.WriteLine("处理得到的序列的元素:");
foreach (var number in distinctNumbers)
{
    Console.WriteLine(number);
}
元素总数: 6
处理得到的序列的元素:
0
1
2
3
5

Linq是Lzay的

以下代码中虽然linq表达式已经定义, 单尚未执行, 所以99会出现在最终的结果当中.

int[] fibonacci = { 0, 1, 1, 2, 3, 5 };

IEnumerable<int> numbersGreaterThanTwoQuery = fibonacci.Where(x => x > 2);
// Linq已经创建, 但是并没有得到执行
// 修改原始输入的首元素
fibonacci[0] = 99;
// 真正遍历结果的时候Linq才会执行
foreach (var number in numbersGreaterThanTwoQuery) {
Console.WriteLine(number); }
99
3
5

而下面执行结果中则不包含99.

int[] fibonacci = { 0, 1, 1, 2, 3, 5 };
// 构造查询
IEnumerable<int> numbersGreaterThanTwoQuery = fibonacci.Where(x => x > 2).ToArray();
// 此时查询已经执行,因为使用了 .ToArray()
// 更改输入序列的第一个元素
fibonacci[0] = 99;
// 枚举结果
foreach (var number in numbersGreaterThanTwoQuery)
{
    Console.WriteLine(number);
}
3
5

本质的区别在于ToArray操作符会强迫求值(类比ClojureSequence的doall等操作).

Lambda表达式在查询操作符中的应用

一些查询操作符允许提供自定义逻辑。这种自定义逻辑可以通过Lambda表达式传递给查询操作符。

在前面的代码示例中的 fibonacci.Where(x => x > 2) 就是一个向查询操作符提供自定义逻辑的例子。这里的Lambda表达式 x => x > 2 将只返回大于2的元素(在这个案例中是整数)。

当查询操作符接收一个Lambda表达式时,它会将Lambda表达式中的逻辑应用于输入序列中的每个单独元素。

传递给查询操作符的Lambda表达式的类型取决于查询操作符执行的任务。

我们可以看到Where查询操作符的签名;这里提供了输入元素int,并需要返回一个布尔值来决定该元素是否会被包含在输出序列中。

Lambda表达式和使用传统的委托指向一个方法本质上是等价的.

本地和解释型查询

LINQ提供了两种不同的架构:本地和解释型。

本地查询操作于 IEnumerable<T> 序列,并在编译时编译到结果程序集中。

顾名思义,本地查询可以被认为是在执行查询的机器上的本地序列(例如,查询内存中的对象列表)。

解释型查询在运行时被解释,并且可以操作来自远程源的序列,如数据库。解释型查询操作于 IQueryable<T> 序列。

解释型查询将在第5章接续讨论。

以下代码演示了使用 Attribute(XName) 方法来定位特定的 XAttribute 并通过读取其 Value 属性获取其值。

此示例还展示了如何使用 FirstAttribute 方法获取元素的第一个声明属性,以及如何结合标准查询操作符(如Skip)来查询 Attributes 方法提供的 IEnumerable<XAttribute>

using System.Xml.Linq;
  var xml = @"
  <ingredients>
      <ingredient name='milk' quantity='200' price='2.99' />
      <ingredient name='sugar' quantity='100' price='4.99' />
      <ingredient name='safron' quantity='1' price='46.77' />
  </ingredients>";
  XElement xmlData = XElement.Parse(xml);
  XElement milk =
      xmlData.Descendants("ingredient")
      .First(x => x.Attribute("name").Value == "milk");
  XAttribute nameAttribute = milk.FirstAttribute; // name属性
  XAttribute priceAttribute = milk.Attribute("price");
  string priceOfMilk = priceAttribute.Value; // 2.99
  XAttribute quantity = milk.Attributes()
      .Skip(1)
      .First(); // quantity属性
  Console.WriteLine(quantity);
quantity="200"

第二章 两种风格的Linq

  • 传统风格(Fluent style)

      public record Ingredient(string Name, int Calories);
    
      Ingredient[] ingredients =  {
          new Ingredient("Sugar", 500),
          new Ingredient("Egg",   100),
          new Ingredient("Milk",  150),
          new Ingredient("Flour", 50),
          new Ingredient("Butter",200)
      };
    
    var highCalorieIngredientNamesQuery = ingredients.Where(x => x.Calories >= 150)
                                           .OrderBy(x => x.Name)
                                           .Select(x => x.Name);
    foreach (var ingredientName in highCalorieIngredientNamesQuery) {
    Console.WriteLine(ingredientName); }
    
    Butter
    Milk
    Sugar
    
  • 查询表达式(Query expression style)

    public record Ingredient(string Name, int Calories);
    
    Ingredient[] ingredients =  {
        new Ingredient("Sugar", 500),
        new Ingredient("Egg",   100),
        new Ingredient("Milk",  150),
        new Ingredient("Flour", 50),
        new Ingredient("Butter",200)
    };
    
    
    var highCalorieIngredientNamesQuery = from i in ingredients
        where i.Calories >= 150
        orderby i.Name
        select i.Name;
    foreach (var ingredientName in highCalorieIngredientNamesQuery) {
        Console.WriteLine(ingredientName); }
    
    Butter
    Milk
    Sugar
    
  • let表达式

    public record Ingredient(string Name, int Calories);
    
    Ingredient[] ingredients =  {
        new Ingredient("Sugar", 500),
        new Ingredient("Egg",   100),
        new Ingredient("Milk",  150),
        new Ingredient("Flour", 50),
        new Ingredient("Butter",200)
    };
    
    var highCalDairyQuery =
        from i in ingredients
        let isDairy = i.Name == "Milk" || i.Name == "Butter" where i.Calories >= 150 && isDairy
        select i;
    foreach (var ingredientName in highCalDairyQuery) {
        Console.WriteLine(ingredientName); }
    
    Ingredient { Name = Milk, Calories = 150 }
    Ingredient { Name = Butter, Calories = 200 }
    
    string[] csvRecipes =
    {
        "milk,sugar,eggs",
        "flour,BUTTER,eggs",
        "vanilla,ChEEsE,oats"
    };
    var dairyQuery =
    from csvRecipe in csvRecipes
    let ingredients = csvRecipe.Split(',')
    from ingredient in ingredients
    let uppercaseIngredient = ingredient.ToUpper() where uppercaseIngredient == "MILK" ||
              uppercaseIngredient == "BUTTER" ||
    uppercaseIngredient == "CHEESE" select uppercaseIngredient;
    foreach (var dairyIngredient in dairyQuery) {
    Console.WriteLine("{0} is dairy", dairyIngredient); }
    
    MILK is dairy
    BUTTER is dairy
    CHEESE is dairy
    
  • into 关键词

    public record Ingredient(string Name, int Calories);
    
    Ingredient[] ingredients =  {
        new Ingredient("Sugar", 500),
        new Ingredient("Egg",   100),
        new Ingredient("Milk",  150),
        new Ingredient("Flour", 50),
        new Ingredient("Butter",200)
    };
    IEnumerable<Ingredient> highCalDairyQuery = from i in ingredients
        select new // 匿名类型
        {
            OriginalIngredient = i,
            IsDairy = i.Name == "Milk" || i.Name == "Butter", IsHighCalorie = i.Calories >= 150
        } into temp
        where temp.IsDairy && temp.IsHighCalorie
          // 后面就不能用selete i了, 因为已经被遮蔽
        select temp.OriginalIngredient;
    
    foreach (var ingredient in highCalDairyQuery) {
        Console.WriteLine(ingredient.Name);
    }
    
    Milk
    Butter
    
  • join

    record Recipe (int Id, string Name);
    
    record Review (int RecipeId, string ReviewText);
    
    Recipe[] recipes =
    {
        new Recipe(1, "宫保鸡丁"),
        new Recipe(2, "葱烧海参"),
        new Recipe(3, "红烧肉"),
    
    };
    
    Review[] reviews = {
        new Review (1, "非常赞"),
        new Review (2, "对得起价格"),
        new Review (1, "太酸"),
        new Review (2, "海参太小"),
    };
    var query = from recipe in recipes
        join review in reviews on recipe.Id equals review.RecipeId select new // anonymous type
        {
            RecipeName = recipe.Name, RecipeReview = review.ReviewText
        };
    foreach (var item in query)
    {
        Console.WriteLine("{0} - '{1}'", item.RecipeName, item.RecipeReview);
    }
    
    宫保鸡丁 - '非常赞'
    宫保鸡丁 - '太酸'
    葱烧海参 - '对得起价格'
    葱烧海参 - '海参太小'
    
  • group join

    record Recipe (int Id, string Name);
    
    record Review (int RecipeId, string ReviewText);
    
    Recipe[] recipes =
    {
        new Recipe(1, "宫保鸡丁"),
        new Recipe(2, "葱烧海参"),
        new Recipe(3, "红烧肉"),
    
    };
    
    Review[] reviews = {
        new Review (1, "非常赞"),
        new Review (2, "对得起价格"),
        new Review (1, "太酸"),
        new Review (2, "海参太小"),
    };
    
    var query = from recipe in recipes
        join review in reviews on recipe.Id equals review.RecipeId
        into reviewGroup // 匿名类型
        select new //匿名类型
        {
            RecipeName = recipe.Name,
            Reviews = reviewGroup
        };
    
    foreach (var item in query)
    {
        Console.WriteLine("{0}", item.RecipeName);
        foreach (var review in item.Reviews) {
            Console.WriteLine("- {0}", review.ReviewText);
        }
    }
    
    宫保鸡丁
    - 非常赞
    - 太酸
    葱烧海参
    - 对得起价格
    - 海参太小
    红烧肉
    
  • left outer join

    record Recipe (int Id, string Name);
    
    record Review (int RecipeId, string ReviewText);
    
    Recipe[] recipes =
    {
        new Recipe(1, "宫保鸡丁"),
        new Recipe(2, "葱烧海参"),
        new Recipe(3, "红烧肉"),
    
    };
    
    Review[] reviews = {
        new Review (1, "非常赞"),
        new Review (2, "对得起价格"),
        new Review (1, "太酸"),
        new Review (2, "海参太小"),
    };
    
    var query = from recipe in recipes
        join review in reviews on recipe.Id equals review.RecipeId
        into reviewGroup // 匿名类型
        from rg in reviewGroup.DefaultIfEmpty()
        select new //匿名类型
        {
            RecipeName = recipe.Name,
            Reviews = (rg == null? "n/a" : rg.ReviewText)
        };
    
    foreach (var item in query)
    {
        Console.WriteLine("{0} - '{1}'", item.RecipeName, item.Reviews);
    }
    
    宫保鸡丁 - '非常赞'
    宫保鸡丁 - '太酸'
    葱烧海参 - '对得起价格'
    葱烧海参 - '海参太小'
    红烧肉 - 'n/a'
    
  • 其他表达式 group, orderby, ascending, descending, by…

    • group

      record Ingredient (string Name, int Calories);
      
      Ingredient[] ingredients = {
          new Ingredient("糖",       500),
          new Ingredient("五花肉",   500),
          new Ingredient("黄油",     500),
          new Ingredient("鸡蛋",     100),
          new Ingredient("牛奶",     100),
          new Ingredient("面粉",     50),
          new Ingredient("燕麦",     50)
      };
      
      //实际类型是IEnumerable<IGrouping<int, Ingredient>>
      var query =
          from i in ingredients
          group i by i.Calories;  // query 语句必须以select或者group语句来结束
      
      foreach (var group in query)
      {
          Console.WriteLine("原料的卡路里数: {0} 卡", group.Key);
          foreach (var ingredient in group) {
              Console.WriteLine("- {0}", ingredient.Name);
          }
      }
      
      原料的卡路里数: 500 卡
      - 糖
      - 五花肉
      - 黄油
      原料的卡路里数: 100 卡
      - 鸡蛋
      - 牛奶
      原料的卡路里数: 50 卡
      - 面粉
      - 燕麦
      
    • orderby 注意orderby的排序

      record Ingredient (string Name, int Calories);
      
      Ingredient[] ingredients = {
          new Ingredient("糖",       500),
          new Ingredient("五花肉",   500),
          new Ingredient("黄油",     500),
          new Ingredient("鸡蛋",     100),
          new Ingredient("牛奶",     100),
          new Ingredient("面粉",     50),
          new Ingredient("燕麦",     50)
      };
      
      var query =
          from i in ingredients
          orderby i.Calories //descending
          select i;
      
      foreach (var item in query)
      {
          Console.WriteLine(item);
      }
      
      #+results:
      
      Ingredient { Name = 面粉, Calories = 50 }
      Ingredient { Name = 燕麦, Calories = 50 }
      Ingredient { Name = 鸡蛋, Calories = 100 }
      Ingredient { Name = 牛奶, Calories = 100 }
      Ingredient { Name = 糖, Calories = 500 }
      Ingredient { Name = 五花肉, Calories = 500 }
      Ingredient { Name = 黄油, Calories = 500 }
      
    • group后组内排序

      record Ingredient (string Name, int Calories);
      
      Ingredient[] ingredients = {
          new Ingredient("糖",       500),
          new Ingredient("五花肉",   500),
          new Ingredient("黄油",     500),
          new Ingredient("鸡蛋",     100),
          new Ingredient("牛奶",     100),
          new Ingredient("面粉",     50),
          new Ingredient("燕麦",     50)
      };
      
      var query =
          from i in ingredients
          group i by i.Calories
          into calorieGroup
          orderby calorieGroup.Key
          select calorieGroup;
      
      
      foreach (var group in query)
      {
          Console.WriteLine("原料的卡路里数: {0} 卡", group.Key);
          foreach (var ingredient in group) {
              Console.WriteLine("- {0}", ingredient.Name);
          }
      }
      
      原料的卡路里数: 50 卡
      - 面粉
      - 燕麦
      原料的卡路里数: 100 卡
      - 鸡蛋
      - 牛奶
      原料的卡路里数: 500 卡
      - 糖
      - 五花肉
      - 黄油
      

第三章 操作符

本章着眼于操作符, 即操作符的分类, 操作中的处理异常, 性能考虑.

操作符

  1. 类型

    • 过滤(Restriction): Where
    • 投影(Projection): Select, SelectMany
    • 分段(Partitioning): Take, Skip, TakeWhile, SkipWhile
    • 顺序(Ordering): OrderBy, OrderByDescending, ThenBy, ThenByDescending, Reverse
    • 分组(Grouping): GroupBy
    • 集合(Set): Concat, Union, Intersect, Except
    • 转化(Conversion): ToArray, ToList, ToDictionary, ToLookup, OfType, Cast
    • 索引(Element): First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault, ElementAt, ElementAtOrDefault, DefaultIfEmpty
    • 生成(Generation): Empty, Range, Repeat
    • 限定(Quantifiers): Any, All, Contains, SequenceEqual
    • 统计(Aggregate): Count, LongCount, Sum, Min, Max, Average, Aggregate
    • 连接(Joining): Join, GroupJoin, Zip
    1. 过滤, 对应于其他函数时编程语言中的filter

      record Ingredient (string Name, int Calories);
      
      Ingredient[] ingredients = {
          new Ingredient("糖",       500),
          new Ingredient("五花肉",   500),
          new Ingredient("黄油",     500),
          new Ingredient("鸡蛋",     100),
          new Ingredient("牛奶",     100),
          new Ingredient("面粉",     50),
          new Ingredient("燕麦",     50)
      };
      
      IEnumerable<Ingredient> query = ingredients
          .Where(x => x.Calories >= 200);
      foreach (var ingredient in query) {
          Console.WriteLine(ingredient.Name);
      }
      
      糖
      五花肉
      黄油
      

      对应于filter-indexed

      record Ingredient (string Name, int Calories);
      
      Ingredient[] ingredients = {
          new Ingredient("糖",       500),
          new Ingredient("五花肉",   500),
          new Ingredient("黄油",     500),
          new Ingredient("鸡蛋",     100),
          new Ingredient("牛奶",     100),
          new Ingredient("面粉",     50),
          new Ingredient("燕麦",     50)
      };
      
      IEnumerable<Ingredient> queryUsingIndex = ingredients
      .Where( (ingredient, index) => ingredient.Name == "糖" || index == 0);
      foreach (var ingredient in queryUsingIndex) {
          Console.WriteLine(ingredient);
      }
      
      Ingredient { Name = 糖, Calories = 500 }
      
    2. 映射

      1. Select

        对应于map

        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.Select(x => x.Name);
        
        foreach (var name in query) {
            Console.WriteLine(name);
        }
        
        糖
        五花肉
        黄油
        鸡蛋
        牛奶
        面粉
        燕麦
        
      2. SelectMany

        相当于mapcat

        string[] ingredients = {"糖", "鸡蛋", "牛奶", "面粉", "黄油"};
        var query = ingredients.SelectMany(x => x.ToCharArray());
        query.ToList().ForEach(Console.WriteLine);
        
        
        Console.WriteLine("\n对比map的结果:\n");
        var query1 = ingredients.Select(x => x.ToCharArray());
        query1.ToList().ForEach(Console.WriteLine);
        
        糖
        鸡
        蛋
        牛
        奶
        面
        粉
        黄
        油
        
        对比map的结果:
        
        糖
        鸡蛋
        牛奶
        面粉
        黄油
        
    3. 分段

      1. Take 对应于clojure的 take

        string[] ingredients = {"糖", "鸡蛋", "牛奶", "面粉", "黄油"};
        var query = ingredients.Take(3);
        query.ToList().ForEach(Console.WriteLine)
        
        糖
        鸡蛋
        牛奶
        
      2. TakeWhile对应于clojure的 take-while?

        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("面粉",     50),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.TakeWhile(x => x.Calories >= 100);
        
        query.ToList().ForEach(Console.WriteLine)
        
        Ingredient { Name = 糖, Calories = 500 }
        Ingredient { Name = 五花肉, Calories = 500 }
        Ingredient { Name = 黄油, Calories = 500 }
        
      3. Skip 对应于clojure的 drop

        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.Skip(3);
        
        query.ToList().ForEach(Console.WriteLine)
        
        Ingredient { Name = 鸡蛋, Calories = 100 }
        Ingredient { Name = 牛奶, Calories = 100 }
        Ingredient { Name = 面粉, Calories = 50 }
        Ingredient { Name = 燕麦, Calories = 50 }
        
      4. SkipTake用来分页, 相当于droptake.

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        static IEnumerable<IEnumerable<Ingredient>> part (Ingredient[] xs, int groupSize){
            int groupCount = xs.Length/groupSize;
            var rg = Range(0, groupCount + 1);
            return rg.Select((n) => xs.Skip(n * groupSize).Take(groupSize));
        }
        
        part(ingredients, 2).ToList().ForEach(
            (x) => {Console.WriteLine();
                    x.ToList().ForEach(Console.WriteLine);});
        
        Ingredient { Name = 糖, Calories = 500 }
        Ingredient { Name = 五花肉, Calories = 500 }
        
        Ingredient { Name = 黄油, Calories = 500 }
        Ingredient { Name = 鸡蛋, Calories = 100 }
        
        Ingredient { Name = 牛奶, Calories = 100 }
        Ingredient { Name = 面粉, Calories = 50 }
        
        Ingredient { Name = 燕麦, Calories = 50 }
        
      5. SkipWhile 类同于clojure的 drop-while

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.SkipWhile(x => x.Name != "牛奶");
        
        query.ToList().ForEach(Console.WriteLine);
        
        Ingredient { Name = 牛奶, Calories = 100 }
        Ingredient { Name = 面粉, Calories = 50 }
        Ingredient { Name = 燕麦, Calories = 50 }
        
    4. 顺序

      1. OrderBy, 类比于order-by

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.OrderBy(x => x.Calories);
        
        query.ToList().ForEach(Console.WriteLine);
        
        Ingredient { Name = 面粉, Calories = 50 }
        Ingredient { Name = 燕麦, Calories = 50 }
        Ingredient { Name = 鸡蛋, Calories = 100 }
        Ingredient { Name = 牛奶, Calories = 100 }
        Ingredient { Name = 糖, Calories = 500 }
        Ingredient { Name = 五花肉, Calories = 500 }
        Ingredient { Name = 黄油, Calories = 500 }
        
      2. ThenBy, 根据多个条件排序

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.OrderBy(x => x.Calories).ThenBy(x => x.Name);
        
        query.ToList().ForEach(Console.WriteLine);
        
        Ingredient { Name = 燕麦, Calories = 50 }
        Ingredient { Name = 面粉, Calories = 50 }
        Ingredient { Name = 牛奶, Calories = 100 }
        Ingredient { Name = 鸡蛋, Calories = 100 }
        Ingredient { Name = 五花肉, Calories = 500 }
        Ingredient { Name = 糖, Calories = 500 }
        Ingredient { Name = 黄油, Calories = 500 }
        
        

        与之等价的:

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = from i in ingredients
                    orderby i.Calories, i.Name
                    select i;
        
        query.ToList().ForEach(Console.WriteLine);
        
        Ingredient { Name = 燕麦, Calories = 50 }
        Ingredient { Name = 面粉, Calories = 50 }
        Ingredient { Name = 牛奶, Calories = 100 }
        Ingredient { Name = 鸡蛋, Calories = 100 }
        Ingredient { Name = 五花肉, Calories = 500 }
        Ingredient { Name = 糖, Calories = 500 }
        Ingredient { Name = 黄油, Calories = 500 }
        
      3. OrderByDescending & ThenByDescending(升降序)

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.OrderByDescending(x => x.Calories).ThenBy(x => x.Name);
        
        query.ToList().ForEach(Console.WriteLine);
        
        Ingredient { Name = 五花肉, Calories = 500 }
        Ingredient { Name = 糖, Calories = 500 }
        Ingredient { Name = 黄油, Calories = 500 }
        Ingredient { Name = 牛奶, Calories = 100 }
        Ingredient { Name = 鸡蛋, Calories = 100 }
        Ingredient { Name = 燕麦, Calories = 50 }
        Ingredient { Name = 面粉, Calories = 50 }
        
      4. Reverse, 类比于reverse

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.Reverse();
        
        query.ToList().ForEach(Console.WriteLine);
        
        Ingredient { Name = 燕麦, Calories = 50 }
        Ingredient { Name = 面粉, Calories = 50 }
        Ingredient { Name = 牛奶, Calories = 100 }
        Ingredient { Name = 鸡蛋, Calories = 100 }
        Ingredient { Name = 黄油, Calories = 500 }
        Ingredient { Name = 五花肉, Calories = 500 }
        Ingredient { Name = 糖, Calories = 500 }
        
    5. GroupBy分组类比于 group-py

      using static System.Linq.Enumerable;
      record Ingredient (string Name, int Calories);
      
      Ingredient[] ingredients = {
          new Ingredient("糖",       500),
          new Ingredient("五花肉",   500),
          new Ingredient("黄油",     500),
          new Ingredient("鸡蛋",     100),
          new Ingredient("牛奶",     100),
          new Ingredient("面粉",     50),
          new Ingredient("燕麦",     50)
      };
      
      var query = ingredients.GroupBy(x => x.Calories);
      
      query.ToList().ForEach(group => {Console.WriteLine(group.Key);
                                       group.ToList().ForEach(Console.WriteLine);});
      
      500
      Ingredient { Name = 糖, Calories = 500 }
      Ingredient { Name = 五花肉, Calories = 500 }
      Ingredient { Name = 黄油, Calories = 500 }
      100
      Ingredient { Name = 鸡蛋, Calories = 100 }
      Ingredient { Name = 牛奶, Calories = 100 }
      50
      Ingredient { Name = 面粉, Calories = 50 }
      Ingredient { Name = 燕麦, Calories = 50}
      
    6. 集合操作

      1. Concat并不是集合操作, 只是个单纯的拼接, 类比于concat

        string[] applePie = {"Apple", "Sugar", "Pastry", "Cinnamon"};
        string[] cherryPie = { "Cherry", "Sugar", "Pastry", "Kirsch" };
        IEnumerable<string> query = applePie.Concat(cherryPie);
        foreach (string item in query)
        {
            Console.WriteLine(item);
        }
        
        Apple
        Sugar
        Pastry
        Cinnamon
        Cherry
        Sugar
        Pastry
        Kirsch
        
      2. Union, 类比于clojure.set/union

        string[] applePie = {"Apple", "Sugar", "Pastry", "Cinnamon"};
        string[] cherryPie = { "Cherry", "Sugar", "Pastry", "Kirsch" };
        IEnumerable<string> query = applePie.Union(cherryPie);
        foreach (string item in query)
        {
            Console.WriteLine(item);
        }
        
        Apple
        Sugar
        Pastry
        Cinnamon
        Cherry
        Sugar
        Pastry
        Kirsch
        
      3. Intersect 类比于 clojure.set/intersect

        string[] applePie = {"Apple", "Sugar", "Pastry", "Cinnamon"};
        string[] cherryPie = { "Cherry", "Sugar", "Pastry", "Kirsch" };
        IEnumerable<string> query = applePie.Intersect(cherryPie);
        foreach (string item in query)
        {
            Console.WriteLine(item);
        }
        
        Sugar
        Pastry
        
      4. Except 类比set/difference

        string[] applePie = {"Apple", "Sugar", "Pastry", "Cinnamon"};
        string[] cherryPie = { "Cherry", "Sugar", "Pastry", "Kirsch" };
        IEnumerable<string> query = applePie.Except(cherryPie);
        foreach (string item in query)
        {
            Console.WriteLine(item);
        }
        
        Apple
        Cinnamon
        
    7. 类型转化

      1. ofType, 根据类型过滤, 选择特定类型的元素

        var input = new object[]
        {
            "Apple", 33, "Sugar", 44, 'a', new DateTime()
        };
        IEnumerable<string> query = input.OfType<string>();
        foreach (string item in query)
        {
            Console.WriteLine(item);
        }
        
        Apple
        Sugar
        
      2. Cast 转化为特定类型, 失败的话, 会抛出异常

        var input = new object[]
        {
            "Apple", 33, "Sugar", 44, 'a', new DateTime()
        };
        IEnumerable<string> query = input.Cast<string>();
        foreach (string item in query)
        {
            Console.WriteLine(item);
        }
        
        System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.String'.
           at System.Linq.Enumerable.CastIterator[TResult](IEnumerable source)+MoveNext()
        
        
      3. ToArray

        IEnumerable<string> input = new List<string> { "Apple", "Sugar", "Flour" };
        string[] array = input.ToArray();
        array.ToList().ForEach(Console.WriteLine);
        
        Apple
        Sugar
        Flour
        
      4. ToList

        IEnumerable<string> input = new List<string> { "Apple", "Sugar", "Flour" };
        input.ToList().ForEach(Console.WriteLine);
        
        Apple
        Sugar
        Flour
        
      5. ToDictionary

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.ToDictionary(x=>x.Name);
        
        query.ToList().ForEach(item =>
                               Console.WriteLine("Key = {0}, ingredient = {1}", item.Key, item.Value));
        
        Key = 糖, ingredient = Ingredient { Name = 糖, Calories = 500 }
        Key = 五花肉, ingredient = Ingredient { Name = 五花肉, Calories = 500 }
        Key = 黄油, ingredient = Ingredient { Name = 黄油, Calories = 500 }
        Key = 鸡蛋, ingredient = Ingredient { Name = 鸡蛋, Calories = 100 }
        Key = 牛奶, ingredient = Ingredient { Name = 牛奶, Calories = 100 }
        Key = 面粉, ingredient = Ingredient { Name = 面粉, Calories = 50 }
        Key = 燕麦, ingredient = Ingredient { Name = 燕麦, Calories = 50 }
        
      6. ToLookup, 等价于group-by

        record Recipe (int Id, string Name, int Rating);
        
        Recipe[] recipes = {
            new Recipe(1, "炒鸡蛋", 5),
            new Recipe(1, "炖排骨", 5),
            new Recipe(1, "西湖醋鱼", 2),
            new Recipe(1, "青椒肉", 3),
        };
        
        var look = recipes.ToLookup(x=>x.Rating);
        
        look.ToList().ForEach(item=> {Console.WriteLine(item.Key);
                item.ToList().ForEach(Console.WriteLine);});
        
        5
        Recipe { Id = 1, Name = 炒鸡蛋, Rating = 5 }
        Recipe { Id = 1, Name = 炖排骨, Rating = 5 }
        2
        Recipe { Id = 1, Name = 西湖醋鱼, Rating = 2 }
        3
        Recipe { Id = 1, Name = 青椒肉, Rating = 3 }
        
    8. 元素操作

      1. First, 找到第一个满足条件的类比于some

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.First();
        
        Console.WriteLine(query);
        
        Ingredient { Name = 糖, Calories = 500 }
        
      2. FirstOrDefault 避免出现异常, 找不到则返回默认值

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.FirstOrDefault(x => x.Name == "不存在");
        
        Console.WriteLine(query == null);
        
        True
        
      3. Last

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.Last(x => x.Calories == 100);
        
        Console.WriteLine(query);
        
        Ingredient { Name = 牛奶, Calories = 100 }
        
      4. LastOrDefault

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.LastOrDefault(x => x.Name == "不存在");
        
        Console.WriteLine(query == null);
        
        True
        
      5. Single

        Enumerable中有且只有一个元素, 则返回该元素, 否则抛出异常

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
        };
        
        var query = ingredients.Single();
        
        Console.WriteLine(query);
        
        Ingredient { Name = 糖, Calories = 500 }
        
      6. SingleOrDefault

        可以找不到

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("糖",       500),
        };
        
        var query = ingredients.SingleOrDefault(x => x.Name == "hahaha");
        
        Console.WriteLine(query == null);
        
        True
        
      7. ElementAt

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.ElementAt(1);
        
        Console.WriteLine(query);
        
        Ingredient { Name = 五花肉, Calories = 500 }
        
      8. ElementAtOrDefault

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {
            new Ingredient("糖",       500),
            new Ingredient("五花肉",   500),
            new Ingredient("黄油",     500),
            new Ingredient("鸡蛋",     100),
            new Ingredient("牛奶",     100),
            new Ingredient("面粉",     50),
            new Ingredient("燕麦",     50)
        };
        
        var query = ingredients.ElementAtOrDefault(1000);
        
        Console.WriteLine(query == null);
        
        True
        
      9. DefaultIfEmpty

        using static System.Linq.Enumerable;
        record Ingredient (string Name, int Calories);
        
        Ingredient[] ingredients = {};
        
        var query = ingredients.DefaultIfEmpty();
        
        
        foreach(Ingredient item in query)
        {
              Console.WriteLine(item == null);
        }
        
        True
        
    9. 生成

      1. Empty

        record Ingredient (string Name, int Calories);
        IEnumerable<Ingredient> ingredients = Enumerable.Empty<Ingredient>();
        Console.WriteLine(ingredients.Count());
        
        0
        
      2. Range

        IEnumerable<int> fiveToTen = Enumerable.Range(5, 6);
        foreach (int num in fiveToTen)
        {
            Console.WriteLine(num);
        }
        
        
        5
        6
        7
        8
        9
        10
        
      3. Repeat

        IEnumerable<int> fiveToTen = Enumerable.Repeat(5, 4);
        foreach (int num in fiveToTen)
        {
            Console.WriteLine(num);
        }
        
        5
        5
        5
        5
        
    10. 条件判断

      1. Contains 集合是否包含某元素.

        int[] nums = {1, 2, 3};
        bool isTwoThere = nums.Contains(2);
        bool isFiveThere = nums.Contains(5);
        Console.WriteLine(isTwoThere);
        Console.WriteLine(isFiveThere);
        
        True
        False
        
      2. Any 集合中是否含有某元素.

        int[] nums = { 1, 2, 3 };
        bool areAnyEvenNumbers = nums.Any(x => x % 2 == 0);
        Console.WriteLine(areAnyEvenNumbers);
        
        True
        
      3. All 是否集合中所有的元素都满足某条件.

        record Ingredient (string Name, int Calories);
        Ingredient[] ingredients =
        {
            new Ingredient ("Sugar",  500),
            new Ingredient ("Egg",    100),
            new Ingredient ("Milk",   150),
            new Ingredient ("Flour",  50),
            new Ingredient ("Butter", 400)
        };
        bool isLowFatRecipe = ingredients.All(x => x.Calories < 200);
        Console.WriteLine(isLowFatRecipe);
        
        False
        
      4. SequenceEqual 全面相等, 每个元素都相等, 且顺序一致.

        IEnumerable<int> sequence1 = new[] {1, 2, 3};
        IEnumerable<int> sequence2 = new[] {1, 2, 3};
        bool isSeqEqual = sequence1.SequenceEqual(sequence2);
        Console.WriteLine(isSeqEqual);
        
        True
        
        IEnumerable<int> sequence1 = new[] {1, 2, 3};
        IEnumerable<int> sequence2 = new[] {3, 2, 1};
        bool isSeqEqual = sequence1.SequenceEqual(sequence2);
        Console.WriteLine(isSeqEqual);
        
        False
        
    11. 统计

    12. Count, 集合中所有的元素数量

      int[] nums = {1, 2, 3};
      int numberOfElements = nums.Count();
      Console.WriteLine(numberOfElements);
      
      3
      
      int[] nums = { 1, 2, 3 };
      int numberOfEvenElements = nums.Count(x => x % 2 == 0);
      Console.WriteLine(numberOfEvenElements);
      
      1
      
    13. LongCount

      和count是一样的, 不过返回类型是long

    14. Sum, 求和, 可回传入一个返回值为long的函数.

      int[] nums = { 1, 2, 3 };
      int total = nums.Sum();
      Console.WriteLine(total);
      
      6
      
      record Ingredient (string Name, int Calories);
      
      Ingredient[] ingredients = {
          new Ingredient("糖",       500),
          new Ingredient("五花肉",   500),
          new Ingredient("黄油",     500),
          new Ingredient("鸡蛋",     100),
          new Ingredient("牛奶",     100),
          new Ingredient("面粉",     50),
          new Ingredient("燕麦",     50)
      };
      
      int totalCalories = ingredients.Sum(x => x.Calories);
      Console.WriteLine(totalCalories);
      
      1800
      
    15. Min, 最小值, 可回传入一个返回值为long的函数.

      int[] nums = { 1, 2, 3 };
      int min = nums.Min();
      Console.WriteLine(min);
      
      1
      
      record Ingredient (string Name, int Calories);
      
      Ingredient[] ingredients = {
          new Ingredient("糖",       500),
          new Ingredient("五花肉",   500),
          new Ingredient("黄油",     500),
          new Ingredient("鸡蛋",     100),
          new Ingredient("牛奶",     100),
          new Ingredient("面粉",     50),
          new Ingredient("燕麦",     50)
      };
      
      int minCalories = ingredients.Min(x => x.Calories);
      Console.WriteLine(minCalories);
      
      50
      
    16. Max, 最大值, 可回传入一个返回值为long的函数.

      int[] nums = { 1, 2, 3 };
      int max = nums.Max();
      Console.WriteLine(max);
      
      6
      
      record Ingredient (string Name, int Calories);
      
      Ingredient[] ingredients = {
          new Ingredient("糖",       500),
          new Ingredient("五花肉",   500),
          new Ingredient("黄油",     500),
          new Ingredient("鸡蛋",     100),
          new Ingredient("牛奶",     100),
          new Ingredient("面粉",     50),
          new Ingredient("燕麦",     50)
      };
      
      int maxCalories = ingredients.Max(x => x.Calories);
      Console.WriteLine(maxCalories);
      
      500
      
    17. Average, 平均值

      int[] nums = { 1, 2, 3 };
      var avg = nums.Average();
      Console.WriteLine(avg);
      
      2
      
      record Ingredient (string Name, int Calories);
      
      Ingredient[] ingredients = {
          new Ingredient("糖",       500),
          new Ingredient("五花肉",   500),
          new Ingredient("黄油",     500),
          new Ingredient("鸡蛋",     100),
          new Ingredient("牛奶",     100),
          new Ingredient("面粉",     50),
          new Ingredient("燕麦",     50)
      };
      
      var avgCalories = ingredients.Average(x => x.Calories);
      Console.WriteLine(avgCalories);
      
      257.14285714285717
      
    18. Aggregate, 聚合.

      int[] nums = { 1, 2, 3 };
      var result = nums.Aggregate(0, (currentElement, runningTotal) => runningTotal + currentElement);
      Console.WriteLine(result);
      
      6
      
    19. 联接操作

      1. Join 连接查询操作符接收两个输入序列,以某种方式组合它们,并输出单个序列。

        Join 操作符接受多个参数:

        • IEnumerable<TInner> inner — 内部序列。
        • Func<TOuter, TKey> outerKeySelector — 在外部序列元素中用于连接的键。
        • Func<TInner, TKey> innerKeySelector — 在内部序列元素中用于连接的键。
        • Func<TOuter, TInner, TResult> resultSelector — 输出元素的外观。

        注意:还有一个重载允许使用特定的 IEqualityComparer 用来进行判等。

        以下代码展示了 Join 查询操作符的使用。注意在键选择 lambda 表达式中的显式类型定义;这是为了演示代码清晰度。例如:(Recipe outerKey) => outerKey.Id 可以简化为 outerKey => outerKey.Id

        record Recipe (int Id, string Name);
        record Review (int RecipeId, string ReviewText);
        
        Recipe[] recipes = // outer sequence
        {
            new Recipe (1, "辣炒蛤蜊"),
            new Recipe (2, "香煎鸡柳"),
            new Recipe (3, "葱爆羊肉")
        };
        Review[] reviews = // inner sequence
        {
            new Review (1, "真香!"),
            new Review (1, "一点也不好吃 :("),
            new Review (1, "还不错"),
            new Review (2, "火大了,咬不动"),
            new Review (2, "爱了爱了")
        };
        var query = recipes // recipes 是 outer sequence
        .Join(
            reviews, // reviews 是 the inner sequence
            (Recipe outerKey) => outerKey.Id, // outer key
            (Review innerKey) => innerKey.RecipeId, // inner key
            (recipe, review) => recipe.Name + " - " + review.ReviewText);
        foreach (string item in query)
        {
            Console.WriteLine(item);
        }
        
        辣炒蛤蜊 - 真香!
        辣炒蛤蜊 - 一点也不好吃 :(
        辣炒蛤蜊 - 还不错
        香煎鸡柳 - 火大了,咬不动
        香煎鸡柳 - 爱了爱了
        

        请注意,此输出中不包括“葱爆羊肉”。这是因为 Join 执行的是左连接,所以在外部序列中没有在内部序列中找到匹配项的元素将不会包含在输出序列中。

        查询表达式使用 在使用查询表达式风格时,使用 join 关键字。以下代码演示了如何使用查询表达式风格执行内连接。

        请参阅第二章,表达式风格部分深入 join 关键字的用法。

        record Recipe (int Id, string Name);
        record Review (int RecipeId, string ReviewText);
        
        Recipe[] recipes = // outer sequence
        {
            new Recipe (1, "辣炒蛤蜊"),
            new Recipe (2, "香煎鸡柳"),
            new Recipe (3, "葱爆羊肉")
        };
        Review[] reviews = // inner sequence
        {
            new Review (1, "真香!"),
            new Review (1, "一点也不好吃 :("),
            new Review (1, "还不错"),
            new Review (2, "火大了,咬不动"),
            new Review (2, "爱了爱了")
        };
        
        var query = from recipe in recipes
        join review in reviews on recipe.Id equals review.RecipeId
        select new // anonymous type
        {
            RecipeName = recipe.Name,
            RecipeReview = review.ReviewText
        };
        
        foreach (var item in query)
        {
            Console.WriteLine(item);
        }
        
        { RecipeName = 辣炒蛤蜊, RecipeReview = 真香! }
        { RecipeName = 辣炒蛤蜊, RecipeReview = 一点也不好吃 :( }
        { RecipeName = 辣炒蛤蜊, RecipeReview = 还不错 }
        { RecipeName = 香煎鸡柳, RecipeReview = 火大了,咬不动 }
        { RecipeName = 香煎鸡柳, RecipeReview = 爱了爱了 }
        
      2. GroupJoin, 分组连结

        record Recipe (int Id, string Name);
        record Review (int RecipeId, string ReviewText);
        
        Recipe[] recipes =
        {
            new Recipe (1, "辣炒蛤蜊"),
            new Recipe (2, "香酥鸭"),
            new Recipe (3, "孜然羊肉")
        };
        Review[] reviews =
        {
            new Review (1, "真香!"),
            new Review (1, "一点也不好吃 :("),
            new Review (1, "还不错"),
            new Review (2, "火大了,咬不动"),
            new Review (2, "爱了爱了")
        };
        var query = recipes
        .GroupJoin(
            reviews,
            (Recipe outerKey) => outerKey.Id,
            (Review innerKey) => innerKey.RecipeId,
            (Recipe recipe, IEnumerable<Review> revs) =>
             new {
                RecipeName = recipe.Name,
                Reviews = revs
            }
                );
        foreach (var item in query)
        {
            Console.WriteLine("{0} 的热评", item.RecipeName);
            foreach (var review in item.Reviews)
            {
                Console.WriteLine(" - {0}", review.ReviewText);
            }
        }
        
        辣炒蛤蜊 的热评
         - 真香!
         - 一点也不好吃 :(
         - 还不错
        香酥鸭 的热评
         - 火大了,咬不动
         - 爱了爱了
        孜然羊肉 的热评
        
        record Recipe (int Id, string Name);
        record Review (int RecipeId, string ReviewText);
        
        Recipe[] recipes =
        {
            new Recipe (1, "辣炒蛤蜊"),
            new Recipe (2, "香酥鸭"),
            new Recipe (3, "孜然羊肉")
        };
        Review[] reviews =
        {
            new Review (1, "真香!"),
            new Review (1, "一点也不好吃 :("),
            new Review (1, "还不错"),
            new Review (2, "火大了,咬不动"),
            new Review (2, "爱了爱了")
        };
        
        var query = from recipe in recipes
        join review in reviews on recipe.Id equals review.RecipeId
        into reviewGroup
        select new // anonymous type
        {
            RecipeName = recipe.Name,
            Reviews = reviewGroup // collection of related reviews
        };
        foreach (var item in query)
        {
            Console.WriteLine("{0} 的热评", item.RecipeName);
            foreach (var review in item.Reviews)
            {
                Console.WriteLine(" - {0}", review.ReviewText);
            }
        }
        
        辣炒蛤蜊 的热评
         - 真香!
         - 一点也不好吃 :(
         - 还不错
        香酥鸭 的热评
         - 火大了,咬不动
         - 爱了爱了
        孜然羊肉 的热评
        
      3. Zip

        record Ingredient (string Name, int Calories);
        string[] names = {"Flour", "Butter", "Sugar"};
        int[] calories = {100, 400, 500};
        IEnumerable<Ingredient> ingredients = names.Zip(calories, (name, calorie) =>
                                                        new Ingredient (name, calorie));
        foreach (var item in ingredients)
        {
            Console.WriteLine("{0} has {1} calories", item.Name, item.Calories);
        }
        
        Flour has 100 calories
        Butter has 400 calories
        Sugar has 500 calories
        

性能

https://learn.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code 性能优化的的原则,

  1. 避免heap内存分配
  2. 避免额外copy

异常

#nullable enable
// 获得数据源从来都是个危险操作
IEnumerable<int> GetData() => throw new InvalidOperationException();

// 在query之前提前处理
IEnumerable<int>? dataSource = null;
try{
    dataSource = GetData();
}
catch (InvalidOperationException){
    // 以最合适的方法处理异常
    Console.WriteLine("Invalid operation");
}

if (dataSource is not null){
    // If we get here, it is safe to proceed.
    var query =
        from i in dataSource
        select i * i;

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

第四章《LINQ Succinctly》的内容涉及LINQ to XML,主要介绍了LINQ to XML的构建块、X-DOM架构、以及如何使用LINQ查询和操作XML数据。以下是该章节的详细翻译:

第四章 LINQ to XML

LINQ to XML的构建块允许创建、修改、查询和保存XML数据。LINQ to XML主要包括两个关键的架构元素:

  1. XML文档对象模型(X-DOM):这不是W3C DOM,而是一组表示内存中XML数据的.NET类型。这些类型可以独立于LINQ查询使用,但设计时考虑了与LINQ的易用性和互操作性。
  2. 额外的LINQ to XML查询操作符

X-DOM概览

X-DOM的类型用于表示XML文档或片段的结构。它们代表了XML数据的内存模型结构和内容。

关键X-DOM类型

构成X-DOM对象模型的关键类型包括:

  • XObject
  • XAttributeXNode
  • XContainer
  • XElementXDocument

XContainer代表XML层次结构中可能有零个、一个或多个子XNode对象的项。XElement代表XML文档中的元素,如 <ingredient> 元素,可以包含值(例如“Butter”)和零个、一个或多个XAttributes。XElement继承自XContainer,因此也可以有属于它的子XNode对象。XDocument则包装一个根XElement,提供额外的XML声明功能,如XML版本和编码,以及其他头部项,如DTD。

创建X-DOM

可以通过加载XML文件和代码实例化来创建内存中的X-DOM。例如,使用 XElement.Parse 静态方法可以从字符串创建一个XElement,而 XElement.Load 方法则用于从物理XML文件中加载XElement。

手动过程式创建

可以像使用常规.NET类型一样实例化X-DOM类型。例如,可以编写代码来创建代表 <ingredients> XML的X-DOM。

函数式构造

使用函数式构造风格可以使得结果XML结构在阅读代码时更易于理解。例如,可以使用函数式构造来创建相同的 <ingredients> XML。

通过投影创建

函数式构造的一个优点是可以将LINQ查询的结果投影到X-DOM中。例如,可以编写代码来填充X-DOM,其结果来自LINQ查询。

使用LINQ查询X-DOM

查询和导航X-DOM主要有两种方式实现:一种是X-DOM类型本身的方法,另一种是定义在 System.Xml.Linq.Extensions 类中的额外查询操作符(即扩展方法)。X-DOM类型的方法通常返回IEnumerable序列,然后可以使用额外的LINQ-to-XML查询操作符对这些序列进行进一步处理。

查找子节点

可以使用多种属性和方法(在某些情况下,包括实例方法和查询操作符扩展方法)来定位子节点。

查找父节点

从XNode继承的XDOM类型包含一个返回父XElement的Parent属性。

查找同级节点

XNode类中定义了一些方法,用于处理同级节点。

这一章节为使用LINQ to XML提供了全面的指导,包括如何创建和查询XML数据,以及如何在LINQ查询中使用这些数据【55†source】。

第五章 解释查询(Interpreted Queries)

到第四章以前的都是本地查询, 不论查询XML还是对象树.

返回的都是IEnumerable<T>类型. 本章介绍返回IQuerable<T>的情况, 由于不太常用,不做过多展开, 仅仅做了一下内容总结.

解释型查询

  • 概述:与本地查询(基于 IEnumerable<T>)不同,解释型查询描述的是在运行时解释的查询“形状”,基于 IQueryable<T>
  • 表达式树:在解释型查询中,查询操作符使用表达式树(expression trees)作为参数,而不是委托(delegates),如本地查询所用的那样。
  • 示例:提供了 IQueryable<T>IEnumerable<T>Where 查询操作符的示例。
  • 编译器决策:由于 IQueryable<T>IEnumerable<T> 的子类型,编译器会根据情况选择适用的版本。
  • 支持性:并非所有标准查询操作符都被所有解释型查询提供者支持。
  • 查询提供者:例如 Microsoft 提供的 LINQ to SQL、Entity Framework,以及第三方的 LINQ to Twitter、flickr、JSON 等。

Entity Framework 示例

  • 实体类:介绍了使用 Entity Framework 查询数据库的示例,包括定义 RecipeReview 类。
  • 数据上下文:创建 RecipeDbContext 类来管理数据库操作。
  • 数据库初始化器:使用 RecipeDbInitializer 类来初始化和填充演示数据。
  • 查询演示:展示了如何使用 Entity Framework 进行查询。
  • 查询表达式:展示了结合 Entity Framework 模型和函数式构建的查询表达式。
  • 注意事项:介绍了 Entity Framework 特定的一些注意事项。
IQueryable<int> interpretedQuery = remoteData.Where(x => x > 100);
IEnumerable<int> localQuery = localData.Where(x => x > 100);

// Local version of Where from System.Linq.Enumerable
public static IEnumerable<TSource> Where<TSource>
(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
// Interpreted version of Where from System.Linq.Queryable
public static IQueryable<TSource> Where<TSource>
(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)

第六章 Linq并发

并行编程领域既广泛又可能相当复杂。本章介绍并行LINQ(PLINQ)作为一种减少执行LINQ查询时处理时间的方法。

PLINQ是一个高层次的抽象,位于各种低层级的.NET多线程相关组件之上,旨在抽象掉多线程的低层次细节,同时提供熟悉的通用LINQ查询语义。

需要注意的是,由于PLINQ位于更高的抽象层级,程序员仍需要具备基本的多线程编程知识。 将LINQ查询转换为PLINQ查询时,PLINQ处理以下并行化的低层次方面:

  • 将查询工作(输入序列)分割为若干个更小的子段。
  • 在不同线程上针对每个子段执行查询代码。
  • 一旦所有子段都处理完毕,将所有子段的结果重新组装回单个输出序列。 通过这种方式,PLINQ可以通过利用执行机器上的多个核心来帮助减少整体查询处理时间。还应注意,PLINQ适用于本地查询,而非远程解释查询。

并不是所有查询都会从PLINQ中受益,实际上,根据输入元素的数量和所需处理的量,由于分割/线程/重组的开销,PLINQ查询可能实际上需要更长时间来运行。与所有与性能相关的任务一样,应该进行测量/分析和有条不紊的性能调整,而不是随机地将代码库中的所有LINQ查询转换为PLINQ查询。

还应该注意,仅仅使用PLINQ并不保证查询实际上会并行执行。这是因为并不是所有查询操作符都能被并行化。即便是那些可以并行化的查询操作符,在执行期间,PLINQ可能仍然决定顺序执行它们.

var someNumbers = Enumerable.Range(1, 10000000).ToArray();

var sw = new Stopwatch();
sw.Start();
someNumbers.Where(x => x.ToString().Contains("3")).ToArray(); sw.Stop();
Console.WriteLine("串行版本花费 {0} ms", sw.ElapsedMilliseconds);

sw.Restart();
someNumbers.AsParallel().Where(x => x.ToString().Contains("3")).ToArray();
sw.Stop();
Console.WriteLine("并行版本花费 {0} ms", sw.ElapsedMilliseconds);
串行版本花费 394 ms
并行版本花费 182 ms

并发天然是无序的

var inputNumbers = Enumerable.Range(1, 10).ToArray();

Console.WriteLine("Input numbers");
foreach (var num in inputNumbers)
{
    Console.Write(num + " ");
}
var outputNumbers = inputNumbers.AsParallel().Select(x => x);
Console.WriteLine();
Console.WriteLine("Output numbers");
foreach (var num in outputNumbers)
{
    Console.Write(num + " ");
}

强制顺序

var inputNumbers = Enumerable.Range(1, 10).ToArray();

var outputNumbers = inputNumbers.AsParallel().AsOrdered().Select(x => x);
Console.WriteLine("Output numbers"); foreach (var num in outputNumbers)
{
    Console.Write(num + " ");
}
Input numbers
1 2 3 4 5 6 7 8 9 10
Output numbers
1 3 5 6 7 8 9 10 2 4

混合使用

IEnumerable<int> inputNumbers = Enumerable.Range(1, 10).ToArray();
IEnumerable<int> outputNumbers = inputNumbers .AsParallel()
.Select(x => x) // PLINQ version of Select .AsSequential()
.Select(x => x); // LINQ version of Select
Tags: c# linq