ZetCode

C# LINQ let

最后修改于 2025 年 5 月 14 日

本文探讨如何使用 LINQ 的 let 子句在查询表达式中引入中间变量,从而提高可读性、效率和可维护性。

let 子句允许开发者在 LINQ 查询中定义临时变量。 这些变量存储计算值,减少冗余计算并提高复杂查询的清晰度。 无需多次重复表达式,您可以将结果分配给 let 变量并在整个查询中引用它。

使用 let 的好处

let 子句通常用于 LINQ 查询中,以存储中间结果,例如计算值、转换后的数据或格式化的输出。 它可以简化过滤条件、帮助分组操作并提高数据处理任务的整体清晰度。

虽然 let 子句可以提高查询效率,但应谨慎使用。 通过 let 引入的变量在每次迭代中计算一次,这意味着它们不会被持久存储,而是为集合中的每个项目重新计算。 这使得它非常适合轻量级计算,但不适用于需要频繁修改值的情况。

C# LINQ let 基本示例

let 最简单的用法是创建一个临时变量来避免重复计算。

Program.cs
string[] words = ["apple", "banana", "cherry", "date", "elderberry"];

var query = from word in words
            let length = word.Length
            where length > 5
            select new { Word = word, Length = length };

foreach (var item in query)
{
    Console.WriteLine($"{item.Word} - {item.Length} letters");
}

我们计算一次单词长度,并在 where 子句和最终投影中重用该值。

let length = word.Length

let 子句创建一个临时变量 length,可以在查询的其余部分中使用。

$ dotnet run 
banana - 6 letters
cherry - 6 letters
elderberry - 10 letters

C# LINQ let 与复杂表达式

let 可以存储更复杂表达式的结果以供重用。

Program.cs
List<Product> products = 
[
    new("Laptop", 999.99m, 5),
    new("Phone", 699.99m, 10),
    new("Tablet", 349.99m, 3),
    new("Monitor", 249.99m, 7)
];

var query = from p in products
            let totalValue = p.Price * p.Stock
            where totalValue > 2000
            orderby totalValue descending
            select new { p.Name, TotalValue = totalValue.ToString("C") };

foreach (var product in query)
{
    Console.WriteLine($"{product.Name}: {product.TotalValue}");
}

record Product(string Name, decimal Price, int Stock);

我们计算每个产品的总库存价值,并在多个子句中使用它。

let totalValue = p.Price * p.Stock

中间计算执行一次,但在过滤和排序操作中都使用。

$ dotnet run 
Phone: $6,999.90
Laptop: $4,999.95
Monitor: $1,749.93

C# LINQ let 与字符串操作

在处理字符串操作时,let 特别有用。

Program.cs
List<string> names = 
[
    "John Smith",
    "Alice Johnson",
    "Robert Brown",
    "Emily Davis",
    "Michael Wilson"
];

var query = from name in names
            let parts = name.Split(' ')
            let firstName = parts[0]
            let lastName = parts[1]
            let initials = $"{firstName[0]}{lastName[0]}"
            select new { FullName = name, Initials = initials };

foreach (var person in query)
{
    Console.WriteLine($"{person.Initials}: {person.FullName}");
}

我们将名称分解为组件并创建首字母缩写,而无需重复字符串操作。

let parts = name.Split(' ')
let firstName = parts[0]
let lastName = parts[1]
let initials = $"{firstName[0]}{lastName[0]}"

多个 let 子句创建一个转换管道,每个转换都建立在前一个转换的基础上。

$ dotnet run 
JS: John Smith
AJ: Alice Johnson
RB: Robert Brown
ED: Emily Davis
MW: Michael Wilson

C# LINQ let 与方法调用

let 可以存储方法调用的结果以避免重复执行。

Program.cs
List<DateTime> dates = 
[
    new DateTime(2023, 1, 15),
    new DateTime(2023, 3, 22),
    new DateTime(2023, 6, 8),
    new DateTime(2023, 9, 30),
    new DateTime(2023, 12, 5)
];


var query = from date in dates
            let quarter = GetQuarter(date)
            where quarter > 2
            group date by quarter into dateGroup
            select new { Quarter = dateGroup.Key, Dates = dateGroup };

foreach (var group in query)
{
    Console.WriteLine($"Quarter {group.Quarter}:");
    foreach (var date in group.Dates)
    {
        Console.WriteLine($"  {date:d}");
    }
}

static int GetQuarter(DateTime date) => (date.Month - 1) / 3 + 1;

我们计算每个日期的季度一次,并将其用于过滤和分组。

let quarter = GetQuarter(date)

方法结果缓存在 quarter 变量中,确保每个元素只计算一次。

$ dotnet run 
Quarter 3:
  30. 9. 2023
Quarter 4:
  5. 12. 2023

C# LINQ let 与匿名类型

您可以使用 let 在查询中创建具有多个计算属性的匿名类型。

Program.cs
string[] words = ["mountain", "river", "forest", "valley", "desert"];

var query = from word in words
            let upper = word.ToUpper()
            let reversed = new string([.. word.Reverse()])
            select new { Original = word, Upper = upper, Reversed = reversed };

foreach (var item in query)
{
    Console.WriteLine($"{item.Original} | {item.Upper} | {item.Reversed}");
}

此示例使用 let 存储每个单词的大写版本和反转版本,然后将它们投影到匿名类型中。

C# LINQ let 与嵌套集合

let 子句可以帮助展平和过滤嵌套集合,例如学生及其成绩。

Program.cs
var students = new[]
{
    new { Name = "Anna", Grades = new[] { 90, 85, 92 } },
    new { Name = "Ben", Grades = new[] { 78, 81, 86 } },
    new { Name = "Cara", Grades = new[] { 88, 94, 91 } }
};

var query = from student in students
            let highGrades = student.Grades.Where(g => g >= 90)
            where highGrades.Any()
            select new { student.Name, HighGrades = highGrades };

foreach (var s in query)
{
    Console.WriteLine($"{s.Name}: {string.Join(", ", s.HighGrades)}");
}

在这里,let 用于提取每个学生的高分,并且只有至少有一个高分的学生才会包含在结果中。

C# LINQ let vs 多个 from 子句

let 与多个 from 子句的不同之处在于它如何影响查询结构。

Program.cs
List<List<int>> numberLists = 
[
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
];

// Using multiple from clauses (cross join)
var query1 = from list in numberLists
             from number in list
             where number % 2 == 0
             select number;

// Using let to reference the inner list
var query2 = from list in numberLists
             let evenNumbers = list.Where(n => n % 2 == 0)
             where evenNumbers.Any()
             select new { List = list, Evens = evenNumbers };

Console.WriteLine("Multiple from clauses (flattened):");
foreach (var num in query1) Console.WriteLine(num);

Console.WriteLine("\nUsing let (structured):");
foreach (var item in query2)
{
    Console.WriteLine($"List: [{string.Join(", ", item.List)}]");
    Console.WriteLine($"Even numbers: [{string.Join(", ", item.Evens)}]");
}

let 保留原始结构,而多个 from 子句展平结果。

let evenNumbers = list.Where(n => n % 2 == 0)

let 子句维护每个列表与其偶数之间的关系。

$ dotnet run 
Multiple from clauses (flattened):
2
4
6
8

Using let (structured):
List: [1, 2, 3]
Even numbers: [2]
List: [4, 5, 6]
Even numbers: [4, 6]
List: [7, 8, 9]
Even numbers: [8]

来源

let 子句 (C# 参考)

在本文中,我们展示了如何使用 LINQ let 子句来创建中间变量,从而提高查询的可读性和性能。

作者

我叫 Jan Bodnar,是一位充满激情的程序员,拥有丰富的编程经验。 我自 2007 年以来一直在撰写编程文章。 至今,我已撰写了超过 1,400 篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有 C# 教程