C#:Linq大赏

C# LINQ 查询语句

筛选操作符

Where

根据指定的条件筛选序列中的元素。

条件操作符Where类似于SQL中的WHERE子句,用于实现条件查询。下列扩展方法表达式查询满足条件"角色不为空"的用户集合:

csharp 复制代码
var user = db.Users.Where(o => o.Roles != null);

对应的标准查询操作符表达式为:

csharp 复制代码
var users = from o in db.Users
            where o.Roles != null
            select o;

Where 操作符根据指定的谓词函数筛选集合中的元素。只有满足谓词条件的元素才会被包含在结果序列中。

csharp 复制代码
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// 使用 Where 操作符筛选出所有的偶数
IEnumerable<int> evenNumbers = numbers.Where(n => n % 2 == 0);

Console.WriteLine("偶数:");
foreach (int number in evenNumbers)
{
    Console.Write(number + " "); // 输出:2 4 6 8 10
}
Console.WriteLine();

// 使用 Where 操作符筛选出大于 5 的奇数
IEnumerable<int> oddNumbersGreaterThanFive = numbers.Where(n => n > 5 && n % 2 != 0);

Console.WriteLine("大于 5 的奇数:");
foreach (int number in oddNumbersGreaterThanFive)
{
    Console.Write(number + " "); // 输出:7 9
}
Console.WriteLine();

OfType

OfType 操作符根据元素的类型筛选集合中的元素。它只返回指定类型的元素。

csharp 复制代码
List<object> mixedList = new List<object> { 1, "hello", 3.14, true, "world", 10 };

// 使用 OfType<int>() 筛选出所有的整数
IEnumerable<int> integers = mixedList.OfType<int>();

Console.WriteLine("整数:");
foreach (int number in integers)
{
    Console.Write(number + " "); // 输出:1 10
}
Console.WriteLine();

// 使用 OfType<string>() 筛选出所有的字符串
IEnumerable<string> strings = mixedList.OfType<string>();

Console.WriteLine("字符串:");
foreach (string str in strings)
{
    Console.Write(str + " "); // 输出:hello world
}
Console.WriteLine();

投影操作符

Select

Select 操作符对集合中的每个元素应用一个转换函数,并返回由该函数的结果组成的新序列。这是最基本的投影操作符。
可以让我们少些一些循环遍历的代码,select方法可以对集合按照指定的条件筛选处理并最终返回一个新的集合。适用于一层循环并判断或运算等场景

投影操作符Select类似于SQL中的SELECT子句,将对象投影为一个匿名类型实例,用于控制指定查询迭代器显示或者处理的对象属性。另外,需要注意的是,扩展方法表达式中的Select操作符并非必须的,省略模式下,会返回完整的被投影对象。下列扩展方法表达式将用户的帐号和密码信息投影为一个匿名类型:

csharp 复制代码
var users = db.Users.Select(o => new { o.Account, o.Password });

对应的标准查询操作符表达式为:

csharp 复制代码
var users = from o in db.Users
            select new { o.Account, o.Password };

所谓投影,比如有一个数据集,想用LINQ语法去操作数据集,会写一个LINQ的表达式,表达式会把数据集合中的数据简单的投影到一个变量中,并且可以通过这个变量去筛选数据。

csharp 复制代码
List<Person> people = new List<Person>
{
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 25 },
    new Person { Name = "Charlie", Age = 35 }
};

// 选择所有人的姓名
IEnumerable<string> names = people.Select(p => p.Name);

Console.WriteLine("姓名:");
foreach (string name in names)
{
    Console.WriteLine(name);
    // 输出:
    // Alice
    // Bob
    // Charlie
}
Console.WriteLine();

// 选择所有人的年龄并加 1
IEnumerable<int> agesPlusOne = people.Select(p => p.Age + 1);

Console.WriteLine("年龄加 1:");
foreach (int age in agesPlusOne)
{
    Console.WriteLine(age);
    // 输出:
    // 31
    // 26
    // 36
}
Console.WriteLine();

// 创建包含姓名和年龄的匿名类型
var nameAndAge = people.Select(p => new { p.Name, p.Age });

Console.WriteLine("姓名和年龄(匿名类型):");
foreach (var item in nameAndAge)
{
    Console.WriteLine($"姓名: {item.Name}, 年龄: {item.Age}");
    // 输出:
    // 姓名: Alice, 年龄: 30
    // 姓名: Bob, 年龄: 25
    // 姓名: Charlie, 年龄: 35
}
Console.WriteLine();

SelectMany

将序列中的每个元素投影到一个 IEnumerable,并将结果序列平展为一个序列。
SelectMany 操作符用于扁平化集合的集合。如果集合中的每个元素本身就是一个集合,SelectMany 会将所有子集合中的元素提取出来,形成一个新的单一集合。

csharp 复制代码
List<Student> students = new List<Student>
{
    new Student { Name = "Alice", Courses = new List<string> { "Math", "Science" } },
    new Student { Name = "Bob", Courses = new List<string> { "English", "History", "Art" } },
    new Student { Name = "Charlie", Courses = new List<string> { "Physics" } }
};

// 选择所有学生的所有课程
IEnumerable<string> allCourses = students.SelectMany(s => s.Courses);

Console.WriteLine("所有课程:");
foreach (string course in allCourses)
{
    Console.WriteLine(course);
    // 输出:
    // Math
    // Science
    // English
    // History
    // Art
    // Physics
}
Console.WriteLine();

// 选择每个学生的姓名和他们选修的每门课程
var studentCourses = students.SelectMany(
    s => s.Courses,
    (student, course) => new { StudentName = student.Name, CourseName = course }
);

Console.WriteLine("学生和课程:");
foreach (var item in studentCourses)
{
    Console.WriteLine($"学生: {item.StudentName}, 课程: {item.CourseName}");
    // 输出:
    // 学生: Alice, 课程: Math
    // 学生: Alice, 课程: Science
    // 学生: Bob, 课程: English
    // 学生: Bob, 课程: History
    // 学生: Bob, 课程: Art
    // 学生: Charlie, 课程: Physics
}
Console.WriteLine();

排序操作符

OrderBy

根据指定的键按升序对序列中的元素排序。

csharp 复制代码
List<string> names = new List<string> { "Charlie", "Alice", "Bob" };

// 按姓名升序排序
IEnumerable<string> sortedNamesAscending = names.OrderBy(name => name);

Console.WriteLine("按姓名升序排序:");
foreach (string name in sortedNamesAscending)
{
    Console.WriteLine(name);
    // 输出:
    // Alice
    // Bob
    // Charlie
}
Console.WriteLine();

List<Person> people = new List<Person>
{
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 25 },
    new Person { Name = "Charlie", Age = 35 }
};

// 按年龄升序排序
IEnumerable<Person> sortedPeopleByAgeAscending = people.OrderBy(p => p.Age);

Console.WriteLine("按年龄升序排序:");
foreach (Person person in sortedPeopleByAgeAscending)
{
    Console.WriteLine($"姓名: {person.Name}, 年龄: {person.Age}");
    // 输出:
    // 姓名: Bob, 年龄: 25
    // 姓名: Alice, 年龄: 30
    // 姓名: Charlie, 年龄: 35
}
Console.WriteLine();

OrderByDescending

根据指定的键按降序对序列中的元素排序。

csharp 复制代码
List<string> names = new List<string> { "Charlie", "Alice", "Bob" };

// 按姓名降序排序
IEnumerable<string> sortedNamesDescending = names.OrderByDescending(name => name);

Console.WriteLine("按姓名降序排序:");
foreach (string name in sortedNamesDescending)
{
    Console.WriteLine(name);
    // 输出:
    // Charlie
    // Bob
    // Alice
}
Console.WriteLine();

List<Person> people = new List<Person>
{
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 25 },
    new Person { Name = "Charlie", Age = 35 }
};

// 按年龄降序排序
IEnumerable<Person> sortedPeopleByAgeDescending = people.OrderByDescending(p => p.Age);

Console.WriteLine("按年龄降序排序:");
foreach (Person person in sortedPeopleByAgeDescending)
{
    Console.WriteLine($"姓名: {person.Name}, 年龄: {person.Age}");
    // 输出:
    // 姓名: Charlie, 年龄: 35
    // 姓名: Alice, 年龄: 30
    // 姓名: Bob, 年龄: 25
}
Console.WriteLine();

ThenBy

在 OrderBy 或 OrderByDescending 操作之后,根据指定的键按升序对序列中的元素执行二级排序。

csharp 复制代码
List<Person> people = new List<Person>
{
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 25 },
    new Person { Name = "Charlie", Age = 30 },
    new Person { Name = "David", Age = 25 }
};

// 先按年龄升序排序,然后按姓名升序排序
IEnumerable<Person> sortedPeopleMultiLevel = people
    .OrderBy(p => p.Age)
    .ThenBy(p => p.Name);

Console.WriteLine("先按年龄升序,再按姓名升序排序:");
foreach (Person person in sortedPeopleMultiLevel)
{
    Console.WriteLine($"姓名: {person.Name}, 年龄: {person.Age}");
    // 输出:
    // 姓名: Bob, 年龄: 25
    // 姓名: David, 年龄: 25
    // 姓名: Alice, 年龄: 30
    // 姓名: Charlie, 年龄: 30
}
Console.WriteLine();

ThenByDescending

在 OrderBy 或 OrderByDescending 操作之后,根据指定的键按降序对序列中的元素执行二级排序。

csharp 复制代码
List<Person> people = new List<Person>
{
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 25 },
    new Person { Name = "Charlie", Age = 30 },
    new Person { Name = "David", Age = 25 }
};

// 先按年龄升序排序,然后按姓名降序排序
IEnumerable<Person> sortedPeopleMultiLevelDescending = people
    .OrderBy(p => p.Age)
    .ThenByDescending(p => p.Name);

Console.WriteLine("先按年龄升序,再按姓名降序排序:");
foreach (Person person in sortedPeopleMultiLevelDescending)
{
    Console.WriteLine($"姓名: {person.Name}, 年龄: {person.Age}");
    // 输出:
    // 姓名: David, 年龄: 25
    // 姓名: Bob, 年龄: 25
    // 姓名: Charlie, 年龄: 30
    // 姓名: Alice, 年龄: 30
}
Console.WriteLine();

Reverse

Reverse 操作符用于反转集合中元素的顺序。它不基于任何键进行排序,只是简单地将现有顺序颠倒过来。

csharp 复制代码
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// 反转列表的顺序
IEnumerable<int> reversedNumbers = numbers.Reverse();

Console.WriteLine("反转后的数字:");
foreach (int number in reversedNumbers)
{
    Console.Write(number + " "); // 输出:5 4 3 2 1
}
Console.WriteLine();

List<string> names = new List<string> { "Alice", "Bob", "Charlie" };

// 先按姓名升序排序,然后再反转顺序
IEnumerable<string> reversedSortedNames = names.OrderBy(n => n).Reverse();

Console.WriteLine("先按姓名升序排序再反转:");
foreach (string name in reversedSortedNames)
{
    Console.WriteLine(name);
    // 输出:
    // Charlie
    // Bob
    // Alice
}
Console.WriteLine();

分组操作符

GroupBy

IEnumerable<IGrouping<TKey, TElement>> 类型的序列,其中每个 IGrouping<TKey, TElement> 对象都包含一个键 (TKey) 和一个包含所有具有该键的元素的集合 (TElement)。

  • GroupBy 操作符 (单个键)

GroupBy 操作符根据指定的键选择器函数对集合中的元素进行分组。

csharp 复制代码
List<Person> people = new List<Person>
{
    new Person { Name = "Alice", Age = 30, City = "Tokyo" },
    new Person { Name = "Bob", Age = 25, City = "Osaka" },
    new Person { Name = "Charlie", Age = 30, City = "Tokyo" },
    new Person { Name = "David", Age = 25, City = "Kyoto" },
    new Person { Name = "Eve", Age = 35, City = "Tokyo" }
};

// 按城市分组
var peopleByCity = people.GroupBy(p => p.City);

Console.WriteLine("按城市分组:");
foreach (var group in peopleByCity)
{
    Console.WriteLine($"城市: {group.Key}");
    foreach (var person in group)
    {
        Console.WriteLine($"  姓名: {person.Name}, 年龄: {person.Age}");
    }
    Console.WriteLine();
    // 输出:
    // 城市: Tokyo
    //   姓名: Alice, 年龄: 30
    //   姓名: Charlie, 年龄: 30
    //   姓名: Eve, 年龄: 35
    //
    // 城市: Osaka
    //   姓名: Bob, 年龄: 25
    //
    // 城市: Kyoto
    //   姓名: David, 年龄: 25
    //
}

  • GroupBy 操作符 (带元素选择器)

GroupBy 操作符还允许你指定一个元素选择器函数,用于在每个分组中选择要包含的元素部分。

csharp 复制代码
List<Person> people = new List<Person>
{
    new Person { Name = "Alice", Age = 30, City = "Tokyo" },
    new Person { Name = "Bob", Age = 25, City = "Osaka" },
    new Person { Name = "Charlie", Age = 30, City = "Tokyo" }
};

// 按城市分组,只选择每个人的姓名
var namesByCity = people.GroupBy(
    p => p.City, // 键选择器
    p => p.Name  // 元素选择器
);

Console.WriteLine("按城市分组(只显示姓名):");
foreach (var group in namesByCity)
{
    Console.WriteLine($"城市: {group.Key}");
    foreach (var name in group)
    {
        Console.WriteLine($"  姓名: {name}");
    }
    Console.WriteLine();
    // 输出:
    // 城市: Tokyo
    //   姓名: Alice
    //   姓名: Charlie
    //
    // 城市: Osaka
    //   姓名: Bob
    //
}

  • GroupBy 操作符 (带结果选择器)

GroupBy 操作符还允许你指定一个结果选择器函数,用于在每个分组创建后对分组的键和元素进行进一步的转换或操作。

csharp 复制代码
List<Person> people = new List<Person>
{
    new Person { Name = "Alice", Age = 30, City = "Tokyo" },
    new Person { Name = "Bob", Age = 25, City = "Osaka" },
    new Person { Name = "Charlie", Age = 30, City = "Tokyo" }
};

// 按城市分组,并统计每个城市的人数
var cityCounts = people.GroupBy(
    p => p.City,
    (city, peopleInCity) => new { City = city, Count = peopleInCity.Count() } // 结果选择器
);

Console.WriteLine("每个城市的人数:");
foreach (var cityCount in cityCounts)
{
    Console.WriteLine($"城市: {cityCount.City}, 人数: {cityCount.Count}");
    // 输出:
    // 城市: Tokyo, 人数: 2
    // 城市: Osaka, 人数: 1
}
Console.WriteLine();

// 按城市分组,并获取每个城市所有人的姓名列表
var cityNames = people.GroupBy(
    p => p.City,
    (city, peopleInCity) => new { City = city, Names = peopleInCity.Select(p => p.Name).ToList() }
);

Console.WriteLine("每个城市的姓名列表:");
foreach (var cityName in cityNames)
{
    Console.WriteLine($"城市: {cityName.City}, 姓名: {string.Join(", ", cityName.Names)}");
    // 输出:
    // 城市: Tokyo, 姓名: Alice, Charlie
    // 城市: Osaka, 姓名: Bob
}
Console.WriteLine();

(city, peopleInCity) => ...: 这是 GroupBy 操作符的结果选择器。它接收两个参数:
city: 这个参数代表当前分组的键,也就是 Person 对象的 City 属性的值(例如:"Tokyo", "Osaka", "Kyoto")。
peopleInCity: 这个参数代表一个 IEnumerable<Person> 类型的集合。这个集合包含了原始 people 列表中所有 City 属性值与当前 city 键相同的 Person 对象。

因此,peopleInCity 本质上是一个临时的、只在结果选择器的 lambda 表达式内部有效的变量,它指向了属于同一个城市分组的所有 Person 对象的集合。
在结果选择器的代码 new { City = city, Count = peopleInCity.Count() } 中:
City = city: 创建一个新的匿名类型的属性 City,其值为当前分组的城市名称。
Count = peopleInCity.Count(): 创建一个新的匿名类型的属性 Count,其值为 peopleInCity 这个 Person 对象集合中的元素数量,也就是该城市的人数。

  • GroupBy 操作符 (使用自定义比较器)

你可以通过实现 IEqualityComparer 接口并将其传递给 GroupBy 方法来使用自定义的键比较逻辑。这在需要基于特定规则进行分组时非常有用

csharp 复制代码
public class CaseInsensitiveStringComparer : IEqualityComparer<string>
{
    public bool Equals(string x, string y)
    {
        return string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
    }

    public int GetHashCode(string obj)
    {
        return obj.ToLowerInvariant().GetHashCode();
    }
}

List<string> fruits = new List<string> { "apple", "Apple", "banana", "BANANA", "Cherry" };

// 使用自定义比较器进行不区分大小写的分组
var fruitsGroupedCaseInsensitive = fruits.GroupBy(f => f, new CaseInsensitiveStringComparer());

Console.WriteLine("不区分大小写的分组:");
foreach (var group in fruitsGroupedCaseInsensitive)
{
    Console.WriteLine($"水果: {group.Key}");
    foreach (var fruit in group)
    {
        Console.WriteLine($"  {fruit}");
    }
    Console.WriteLine();
    // 输出:
    // 水果: apple
    //   apple
    //   Apple
    //
    // 水果: banana
    //   banana
    //   BANANA
    //
    // 水果: Cherry
    //   Cherry
    //
}

  • GroupBy 操作符 (复合键)

可以通过创建匿名类型或使用元组作为键选择器来实现基于多个属性的分组。

csharp 复制代码
List<Order> orders = new List<Order>
{
    new Order { Product = "Laptop", Category = "Electronics", Price = 1200 },
    new Order { Product = "Mouse", Category = "Electronics", Price = 25 },
    new Order { Product = "Book", Category = "Books", Price = 20 },
    new Order { Product = "Keyboard", Category = "Electronics", Price = 75 },
    new Order { Product = "Magazine", Category = "Books", Price = 10 }
};

// 按类别和价格分组
var ordersByCatAndPrice = orders.GroupBy(o => new { o.Category, o.Price });

Console.WriteLine("按类别和价格分组:");
foreach (var group in ordersByCatAndPrice)
{
    Console.WriteLine($"类别: {group.Key.Category}, 价格: {group.Key.Price}");
    foreach (var order in group)
    {
        Console.WriteLine($"  产品: {order.Product}");
    }
    Console.WriteLine();
    // 输出:
    // 类别: Electronics, 价格: 1200
    //   产品: Laptop
    //
    // 类别: Electronics, 价格: 25
    //   产品: Mouse
    //
    // 类别: Books, 价格: 20
    //   产品: Book
    //
    // 类别: Electronics, 价格: 75
    //   产品: Keyboard
    //
    // 类别: Books, 价格: 10
    //   产品: Magazine
    //
}

连接操作符

Join

操作符执行一个内连接。它返回两个集合中键匹配的元素对。只有当两个集合中都存在具有相同键的元素时,这些元素才会被包含在结果中。

假设我们有两个集合:customersorders。我们想根据 CustomerId 将客户和他们的订单连接起来。

csharp 复制代码
public class Customer
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public string City { get; set; }
}

public class Order
{
    public int OrderId { get; set; }
    public int CustomerId { get; set; }
    public string ProductName { get; set; }
    public decimal Price { get; set; }
}

List<Customer> customers = new List<Customer>
{
    new Customer { CustomerId = 1, Name = "Alice", City = "Tokyo" },
    new Customer { CustomerId = 2, Name = "Bob", City = "Osaka" },
    new Customer { CustomerId = 3, Name = "Charlie", City = "Kyoto" }
};

List<Order> orders = new List<Order>
{
    new Order { OrderId = 101, CustomerId = 1, ProductName = "Laptop", Price = 1200 },
    new Order { OrderId = 102, CustomerId = 2, ProductName = "Mouse", Price = 25 },
    new Order { OrderId = 103, CustomerId = 1, ProductName = "Keyboard", Price = 75 },
    new Order { OrderId = 104, CustomerId = 4, ProductName = "Monitor", Price = 300 } // CustomerId 4 不存在于 customers
};

// 使用 Join 操作符根据 CustomerId 连接 customers 和 orders
var customerOrders = customers.Join(
    orders, // 第二个集合
    customer => customer.CustomerId, // 第一个集合的键选择器
    order => order.CustomerId, // 第二个集合的键选择器
    (customer, order) => new // 结果选择器
    {
        CustomerName = customer.Name,
        ProductName = order.ProductName,
        OrderPrice = order.Price
    });

Console.WriteLine("客户及其订单:");
foreach (var item in customerOrders)
{
    Console.WriteLine($"客户: {item.CustomerName}, 产品: {item.ProductName}, 价格: {item.OrderPrice}");
    // 输出:
    // 客户: Alice, 产品: Laptop, 价格: 1200
    // 客户: Bob, 产品: Mouse, 价格: 25
    // 客户: Alice, 产品: Keyboard, 价格: 75
}
Console.WriteLine();

在上面的示例中,只有 CustomerId 在 customers 和 orders 集合中都存在的记录才被连接起来。CustomerId 为 4 的订单由于在 customers 集合中没有匹配的客户而被排除在外。

GroupJoin

GroupJoin 操作符将第一个集合的每个元素与第二个集合中所有键匹配的元素分组在一起。它返回一个 IEnumerable,其中每个元素都包含第一个集合的一个元素以及第二个集合中所有匹配元素的集合。

csharp 复制代码
// 继续使用上面的 customers 和 orders 集合

// 使用 GroupJoin 操作符根据 CustomerId 将客户和他们的订单分组
var customerOrderGroups = customers.GroupJoin(
    orders, // 第二个集合
    customer => customer.CustomerId, // 第一个集合的键选择器
    order => order.CustomerId, // 第二个集合的键选择器
    (customer, orderList) => new // 结果选择器
    {
        CustomerName = customer.Name,
        Orders = orderList.Select(order => order.ProductName).ToList()
    });

Console.WriteLine("客户及其订单列表:");
foreach (var item in customerOrderGroups)
{
    Console.WriteLine($"客户: {item.CustomerName}, 订单: {string.Join(", ", item.Orders)}");
    // 输出:
    // 客户: Alice, 订单: Laptop, Keyboard
    // 客户: Bob, 订单: Mouse
    // 客户: Charlie, 订单:
}
Console.WriteLine();

在这个示例中,即使某个客户没有订单(例如 Charlie),该客户也会出现在结果中,并且其 Orders 属性将是一个空列表。

Left Join

LINQ 本身没有直接提供 Left Outer Join 操作符,但可以通过结合 GroupJoin 和 SelectMany 来实现左外连接的效果。左外连接返回第一个集合中的所有元素以及第二个集合中匹配的元素。如果第二个集合中没有匹配的元素,则为第二个集合的元素使用默认值(通常是 null)。

csharp 复制代码
// 继续使用上面的 customers 和 orders 集合

// 实现 Left Outer Join
var leftOuterJoin = customers.GroupJoin(
    orders,
    customer => customer.CustomerId,
    order => order.CustomerId,
    (customer, orderList) => new { Customer = customer, Orders = orderList })
.SelectMany(
    joined => joined.Orders.DefaultIfEmpty(null), // 如果 orderList 为空,则提供 null
    (joined, order) => new
    {
        CustomerName = joined.Customer.Name,
        ProductName = order?.ProductName ?? "无订单", // 使用 null-coalescing 运算符处理 null 的情况
        OrderPrice = order?.Price
    });

Console.WriteLine("左外连接结果:");
foreach (var item in leftOuterJoin)
{
    Console.WriteLine($"客户: {item.CustomerName}, 产品: {item.ProductName}, 价格: {item.OrderPrice}");
    // 输出:
    // 客户: Alice, 产品: Laptop, 价格: 1200
    // 客户: Alice, 产品: Keyboard, 价格: 75
    // 客户: Bob, 产品: Mouse, 价格: 25
    // 客户: Charlie, 产品: 无订单, 价格:
}
Console.WriteLine();

在这个示例中,即使 Charlie 没有任何订单,他的信息也出现在结果中,并且订单相关的信息显示为 "无订单" 和 null。

连接操作符的参数

所有的连接操作符(JoinGroupJoin)都接受以下参数:

  1. inner: 要连接的第二个集合。
  2. outerKeySelector: 一个函数,用于从第一个集合的每个元素中提取连接键。
  3. innerKeySelector: 一个函数,用于从第二个集合的每个元素中提取连接键。
  4. resultSelector: 一个函数,用于基于两个匹配的元素(或第一个集合的元素和第二个集合的匹配元素集合)创建结果元素。

注意事项

  • 连接操作符通常用于关联来自不同数据源(例如数据库表、不同的列表)的数据。
  • 选择合适的连接键对于获得正确的结果至关重要。
  • 理解不同连接类型的行为(内连接、分组连接、左外连接等)对于解决特定的数据关联问题非常重要。

通过使用 LINQ 的连接操作符,你可以以简洁而强大的方式将来自不同集合的数据整合在一起,从而进行更复杂的查询和分析。


设置操作符

LINQ 设置操作符用于对集合执行基于集合论的操作,例如合并、取交集、取差集、去除重复项等。这些操作符返回一个新的集合,

Distinct

Distinct 操作符用于从集合中移除重复的元素,并返回一个包含唯一元素的新序列。默认情况下,它使用元素的默认相等比较器来确定是否相等。

csharp 复制代码
List<int> numbersWithDuplicates = new List<int> { 1, 2, 2, 3, 4, 4, 5 };

// 使用 Distinct 移除重复的数字
IEnumerable<int> uniqueNumbers = numbersWithDuplicates.Distinct();

Console.WriteLine("去除重复后的数字:");
foreach (int number in uniqueNumbers)
{
    Console.Write(number + " "); // 输出:1 2 3 4 5
}
Console.WriteLine();

List<string> wordsWithCaseDuplicates = new List<string> { "apple", "Apple", "banana", "Banana", "cherry" };

// 默认情况下,Distinct 区分大小写
IEnumerable<string> uniqueWordsCaseSensitive = wordsWithCaseDuplicates.Distinct();

Console.WriteLine("区分大小写去除重复后的单词:");
foreach (string word in uniqueWordsCaseSensitive)
{
    Console.Write(word + " "); // 输出:apple Apple banana Banana cherry
}
Console.WriteLine();

// 使用自定义比较器进行不区分大小写的去重
IEnumerable<string> uniqueWordsCaseInsensitive = wordsWithCaseDuplicates.Distinct(StringComparer.OrdinalIgnoreCase);

Console.WriteLine("不区分大小写去除重复后的单词:");
foreach (string word in uniqueWordsCaseInsensitive)
{
    Console.Write(word + " "); // 输出:apple banana cherry
}
Console.WriteLine();

Union

Union 操作符用于合并两个集合,并返回一个包含两个集合中所有唯一元素的新序列。它也会移除重复项。

csharp 复制代码
List<int> setA = new List<int> { 1, 2, 3, 4, 5 };
List<int> setB = new List<int> { 3, 5, 6, 7, 8 };

// 使用 Union 合并两个集合并去除重复项
IEnumerable<int> unionSet = setA.Union(setB);

Console.WriteLine("两个集合的并集:");
foreach (int number in unionSet)
{
    Console.Write(number + " "); // 输出:1 2 3 4 5 6 7 8
}
Console.WriteLine();

List<string> setC = new List<string> { "apple", "banana", "cherry" };
List<string> setD = new List<string> { "banana", "grape", "kiwi", "APPLE" };

// 默认情况下,Union 区分大小写
IEnumerable<string> unionSetCaseSensitive = setC.Union(setD);

Console.WriteLine("两个字符串集合的并集(区分大小写):");
foreach (string word in unionSetCaseSensitive)
{
    Console.Write(word + " "); // 输出:apple banana cherry grape kiwi APPLE
}
Console.WriteLine();

// 使用自定义比较器进行不区分大小写的并集
IEnumerable<string> unionSetCaseInsensitive = setC.Union(setD, StringComparer.OrdinalIgnoreCase);

Console.WriteLine("两个字符串集合的并集(不区分大小写):");
foreach (string word in unionSetCaseInsensitive)
{
    Console.Write(word + " "); // 输出:apple banana cherry grape kiwi
}
Console.WriteLine();

Intersect

Intersect 操作符用于返回两个集合中共同存在的元素,即交集。

csharp 复制代码
List<int> setA = new List<int> { 1, 2, 3, 4, 5 };
List<int> setB = new List<int> { 3, 5, 6, 7, 8 };

// 使用 Intersect 获取两个集合的交集
IEnumerable<int> intersectionSet = setA.Intersect(setB);

Console.WriteLine("两个集合的交集:");
foreach (int number in intersectionSet)
{
    Console.Write(number + " "); // 输出:3 5
}
Console.WriteLine();

List<string> setC = new List<string> { "apple", "banana", "cherry" };
List<string> setD = new List<string> { "banana", "grape", "APPLE" };

// 默认情况下,Intersect 区分大小写
IEnumerable<string> intersectionSetCaseSensitive = setC.Intersect(setD);

Console.WriteLine("两个字符串集合的交集(区分大小写):");
foreach (string word in intersectionSetCaseSensitive)
{
    Console.Write(word + " "); // 输出:banana
}
Console.WriteLine();

// 使用自定义比较器进行不区分大小写的交集
IEnumerable<string> intersectionSetCaseInsensitive = setC.Intersect(setD, StringComparer.OrdinalIgnoreCase);

Console.WriteLine("两个字符串集合的交集(不区分大小写):");
foreach (string word in intersectionSetCaseInsensitive)
{
    Console.Write(word + " "); // 输出:apple banana
}
Console.WriteLine();

Except

Except 操作符用于返回第一个集合中存在,但第二个集合中不存在的元素,即差集。

csharp 复制代码
List<int> setA = new List<int> { 1, 2, 3, 4, 5 };
List<int> setB = new List<int> { 3, 5, 6, 7, 8 };

// 使用 Except 获取 setA 中不在 setB 中的元素
IEnumerable<int> differenceSetAB = setA.Except(setB);

Console.WriteLine("setA 相对于 setB 的差集:");
foreach (int number in differenceSetAB)
{
    Console.Write(number + " "); // 输出:1 2 4
}
Console.WriteLine();

// 使用 Except 获取 setB 中不在 setA 中的元素
IEnumerable<int> differenceSetBA = setB.Except(setA);

Console.WriteLine("setB 相对于 setA 的差集:");
foreach (int number in differenceSetBA)
{
    Console.Write(number + " "); // 输出:6 7 8
}
Console.WriteLine();

List<string> setC = new List<string> { "apple", "banana", "cherry" };
List<string> setD = new List<string> { "banana", "grape", "APPLE" };

// 默认情况下,Except 区分大小写
IEnumerable<string> differenceSetCDCaseSensitive = setC.Except(setD);

Console.WriteLine("setC 相对于 setD 的差集(区分大小写):");
foreach (string word in differenceSetCDCaseSensitive)
{
    Console.Write(word + " "); // 输出:apple cherry
}
Console.WriteLine();

// 使用自定义比较器进行不区分大小写的差集
IEnumerable<string> differenceSetCDCaseInsensitive = setC.Except(setD, StringComparer.OrdinalIgnoreCase);

Console.WriteLine("setC 相对于 setD 的差集(不区分大小写):");
foreach (string word in differenceSetCDCaseInsensitive)
{
    Console.Write(word + " "); // 输出:banana cherry
}
Console.WriteLine();

使用自定义比较器

Distinct, Union, Intersect, 和 Except 操作符都提供了一个重载,允许你传入一个实现了 IEqualityComparer<T> 接口的自定义比较器。这使得你可以根据自己的逻辑来定义元素的相等性,例如在比较字符串时忽略大小写。

总结

LINQ 设置操作符提供了一种方便的方式来执行集合论操作,可以帮助你:

  • 去除集合中的重复项 (Distinct)
  • 合并两个集合并去除重复项 (Union)
  • 找出两个集合中共有的元素 (Intersect)
  • 找出在一个集合中存在但在另一个集合中不存在的元素 (Except)

通过使用这些操作符,你可以简洁地处理集合之间的关系,并获得符合特定条件的结果集。记住,默认情况下,字符串的比较是区分大小写的,如果需要进行不区分大小写的比较,你需要使用自定义的比较器。


分区操作符

Skip

跳过序列中指定数量的元素,然后返回剩余的元素。

Skip操作符用于跳过指定个数对象并返回序列中的剩余对象,下列扩展方法表达式返回除前10个用户外的剩余用户:

csharp 复制代码
var users = db.Users.OrderBy(o => o.Roles.Count).Skip(10);
csharp 复制代码
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// 跳过前 5 个元素,返回剩余的元素
IEnumerable<int> afterFirstFive = numbers.Skip(5);

Console.WriteLine("跳过前 5 个元素后的剩余元素:");
foreach (int number in afterFirstFive)
{
    Console.Write(number + " "); // 输出:6 7 8 9 10
}
Console.WriteLine();

// 如果指定的数量大于或等于序列的长度,Skip 会返回一个空序列
IEnumerable<int> skipAll = numbers.Skip(10);
IEnumerable<int> skipMoreThanCount = numbers.Skip(15);

Console.WriteLine("跳过所有元素:");
foreach (int number in skipAll)
{
    Console.Write(number + " "); // 不输出任何内容
}
Console.WriteLine();

Console.WriteLine("尝试跳过超过序列长度的元素:");
foreach (int number in skipMoreThanCount)
{
    Console.Write(number + " "); // 不输出任何内容
}
Console.WriteLine();

SkipWhile

只要指定的条件为 true,就跳过序列中的元素,然后返回剩余的元素。

SkipWhile操作符用于跳过条件表达式值为真时的元素,并返回剩下的元素集合,下列扩展方法表达式返回第一个拥有3个角色的用户之后的所有用户集合:

csharp 复制代码
var users = db.Users.OrderBy(o => o.Roles.Count).SkipWhile(o => o.Roles == 3);

SkipWhile 操作符只要指定的条件为真,就跳过序列中的元素。一旦条件变为假,它就会返回剩余的所有元素。

csharp 复制代码
List<int> numbers = new List<int> { 2, 4, 6, 1, 8, 10 };

// 从开头跳过小于 5 的元素,然后返回剩余的元素
IEnumerable<int> skipWhileLessThanFive = numbers.SkipWhile(n => n < 5);

Console.WriteLine("跳过开头小于 5 的元素后的剩余元素:");
foreach (int number in skipWhileLessThanFive)
{
    Console.Write(number + " "); // 输出:6 1 8 10
}
Console.WriteLine();

List<string> words = new List<string> { "apple", "banana", "apricot", "grape", "avocado" };

// 从开头跳过以 "a" 开头的单词,然后返回剩余的元素
IEnumerable<string> skipWhileStartsWithA = words.SkipWhile(w => w.StartsWith("a"));

Console.WriteLine("跳过开头以 'a' 开头的单词后的剩余元素:");
foreach (string word in skipWhileStartsWithA)
{
    Console.Write(word + " "); // 输出:grape avocado
}
Console.WriteLine();

Take

从序列的开头返回指定数量的连续元素。

Take操作符类似于SQL中的TOP操作符,下列扩展方法表达式返回前5个用户对象:

csharp 复制代码
var users = db.Users.OrderBy(o => o.Roles.Count).Take(5);
csharp 复制代码
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// 获取前 5 个元素
IEnumerable<int> firstFive = numbers.Take(5);

Console.WriteLine("前 5 个元素:");
foreach (int number in firstFive)
{
    Console.Write(number + " "); // 输出:1 2 3 4 5
}
Console.WriteLine();

// 如果指定的数量大于序列的长度,Take 会返回所有元素
IEnumerable<int> takeMoreThanCount = numbers.Take(15);

Console.WriteLine("尝试获取超过序列长度的元素:");
foreach (int number in takeMoreThanCount)
{
    Console.Write(number + " "); // 输出:1 2 3 4 5 6 7 8 9 10
}
Console.WriteLine();

TakeWhile

只要指定的条件为 true,就返回序列中的元素。

TakeWhile操作符用于返回条件表达式值为真时的相邻元素集合,下列扩展方法表达式返回第一个拥有3个角色的用户之前的所有用户集合:

csharp 复制代码
var users = db.Users.OrderBy(o => o.Roles.Count).TakeWhile(o => o.Roles.Count == 3);

TakeWhile操作符只要指定的条件为真,就从序列的开头返回元素。一旦条件变为假,它就会停止返回元素。

csharp 复制代码
List<int> numbers = new List<int> { 2, 4, 6, 1, 8, 10 };

// 从开头获取小于 5 的元素,直到遇到第一个不小于 5 的元素
IEnumerable<int> takeWhileLessThanFive = numbers.TakeWhile(n => n < 5);

Console.WriteLine("从开头获取小于 5 的元素:");
foreach (int number in takeWhileLessThanFive)
{
    Console.Write(number + " "); // 输出:2 4
}
Console.WriteLine();

List<string> words = new List<string> { "apple", "banana", "apricot", "grape", "avocado" };

// 从开头获取以 "a" 开头的单词,直到遇到第一个不以 "a" 开头的单词
IEnumerable<string> takeWhileStartsWithA = words.TakeWhile(w => w.StartsWith("a"));

Console.WriteLine("从开头获取以 'a' 开头的单词:");
foreach (string word in takeWhileStartsWithA)
{
    Console.Write(word + " "); // 输出:apple banana apricot
}
Console.WriteLine();

LINQ 分区操作符提供了一种简单而强大的方式来分割序列

  • Take(n): 获取序列的前 n 个元素。
  • Skip(n): 跳过序列的前 n 个元素,返回剩余的元素。
  • TakeWhile(predicate): 从序列开头获取满足谓词条件的元素,直到遇到第一个不满足条件的元素为止。
  • SkipWhile(predicate): 从序列开头跳过满足谓词条件的元素,直到遇到第一个不满足条件的元素为止,然后返回剩余的所有元素。

这些操作符在处理大型数据集、实现分页功能或根据特定条件截取序列时非常有用。它们允许你精确地控制要处理的序列部分,而无需遍历整个集合。


量化操作符

All

确定序列中的所有元素是否都满足条件。
All 操作符用于检查序列中的所有元素是否都满足指定的谓词函数。如果序列为空,All 返回 true(因为没有元素不满足条件)

csharp 复制代码
List<int> numbersAllPositive = new List<int> { 1, 2, 3, 4, 5 };
List<int> numbersWithNegative = new List<int> { 1, 2, -3, 4, 5 };
List<int> emptyNumbers = new List<int>();

// 检查 numbersAllPositive 中的所有数字是否都大于 0
bool allPositive = numbersAllPositive.All(n => n > 0);
Console.WriteLine($"numbersAllPositive 中的所有数字都大于 0: {allPositive}"); // 输出:True

// 检查 numbersWithNegative 中的所有数字是否都大于 0
bool hasNegative = numbersWithNegative.All(n => n > 0);
Console.WriteLine($"numbersWithNegative 中的所有数字都大于 0: {hasNegative}"); // 输出:False

// 检查空序列中的所有数字是否都大于 0
bool allPositiveEmpty = emptyNumbers.All(n => n > 0);
Console.WriteLine($"空序列中的所有数字都大于 0: {allPositiveEmpty}"); // 输出:True

Any

确定序列中是否包含任何元素。
Any 操作符用于检查序列中是否至少存在一个元素满足指定的谓词函数。如果序列为空,Any 返回 false。Any() 的不带谓词的重载用于检查序列是否为空(如果序列包含任何元素,则返回 true,否则返回 false)。

csharp 复制代码
List<int> numbersWithPositive = new List<int> { -1, -2, 3, -4, -5 };
List<int> numbersAllNegative = new List<int> { -1, -2, -3, -4, -5 };
List<int> emptyNumbers = new List<int>();

// 检查 numbersWithPositive 中是否存在大于 0 的数字
bool hasPositive = numbersWithPositive.Any(n => n > 0);
Console.WriteLine($"numbersWithPositive 中是否存在大于 0 的数字: {hasPositive}"); // 输出:True

// 检查 numbersAllNegative 中是否存在大于 0 的数字
bool hasPositiveNegative = numbersAllNegative.Any(n => n > 0);
Console.WriteLine($"numbersAllNegative 中是否存在大于 0 的数字: {hasPositiveNegative}"); // 输出:False

// 检查 emptyNumbers 是否包含任何元素
bool isEmpty = !emptyNumbers.Any();
Console.WriteLine($"emptyNumbers 是否为空: {isEmpty}"); // 输出:True

// 使用不带谓词的 Any() 检查 numbersWithPositive 是否包含任何元素
bool containsElements = numbersWithPositive.Any();
Console.WriteLine($"numbersWithPositive 是否包含任何元素: {containsElements}"); // 输出:True

Contains

确定序列中是否包含指定的元素。
Contains 操作符用于检查序列中是否包含指定的元素。默认情况下,它使用元素的默认相等比较器进行比较。你也可以提供一个自定义的 IEqualityComparer 来指定比较规则。

csharp 复制代码
List<string> fruits = new List<string> { "apple", "banana", "cherry" };

// 检查 fruits 是否包含 "banana"
bool hasBanana = fruits.Contains("banana");
Console.WriteLine($"fruits 是否包含 'banana': {hasBanana}"); // 输出:True

// 检查 fruits 是否包含 "grape"
bool hasGrape = fruits.Contains("grape");
Console.WriteLine($"fruits 是否包含 'grape': {hasGrape}"); // 输出:False

List<string> fruitsCaseSensitive = new List<string> { "apple", "Banana", "cherry" };

// 默认 Contains 区分大小写
bool hasLowercaseBanana = fruitsCaseSensitive.Contains("banana");
Console.WriteLine($"fruitsCaseSensitive 是否包含 'banana' (区分大小写): {hasLowercaseBanana}"); // 输出:False

// 使用 StringComparer.OrdinalIgnoreCase 进行不区分大小写的检查
bool hasIgnoreCaseBanana = fruitsCaseSensitive.Contains("banana", StringComparer.OrdinalIgnoreCase);
Console.WriteLine($"fruitsCaseSensitive 是否包含 'banana' (不区分大小写): {hasIgnoreCaseBanana}"); // 输出:True

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType())
        {
            return false;
        }
        Person other = (Person)obj;
        return (Name == other.Name) && (Age == other.Age);
    }

    public override int GetHashCode()
    {
        return Name.GetHashCode() ^ Age.GetHashCode();
    }
}

List<Person> people = new List<Person>
{
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 25 }
};

Person personToFind = new Person { Name = "Alice", Age = 30 };

// 检查 people 是否包含 personToFind (依赖于 Person 类的 Equals 和 GetHashCode 方法)
bool hasAlice = people.Contains(personToFind);
Console.WriteLine($"people 是否包含 Alice (Name=Alice, Age=30): {hasAlice}"); // 输出:True

LINQ 量化操作符用于对集合中的元素进行条件判断,并返回一个布尔结果:

  • All(predicate): 如果序列中的所有元素都满足谓词,则返回 true;否则返回 false。对于空序列,返回 true
  • Any(predicate): 如果序列中至少有一个元素满足谓词,则返回 true;否则返回 false
  • Any(): 如果序列包含任何元素,则返回 true;否则返回 false
  • Contains(value): 如果序列包含指定的元素,则返回 true;否则返回 false(使用默认相等比较器)。
  • Contains(value, comparer): 如果序列包含指定的元素,则返回 true;否则返回 false(使用自定义的相等比较器)。

聚合操作符

Aggregate

对序列中的元素应用累加器函数。

Average

计算序列中数值元素的平均值。

Count

返回序列中的元素数。

LongCount

返回 Int64 类型的序列中的元素数。

Max

返回序列中的最大值。

Min

返回序列中的最小值。

Sum

计算序列中数值元素的总和。


元素操作符

ElementAt

返回序列中指定索引处的元素。
ElementAt 操作符返回序列中指定索引处的元素。索引从 0 开始。如果索引超出序列的范围,则会抛出 ArgumentOutOfRangeException 异常。

csharp 复制代码
List<string> colors = new List<string> { "red", "green", "blue" };

// 获取索引为 0 的元素
string firstColor = colors.ElementAt(0);
Console.WriteLine($"索引 0 的元素: {firstColor}"); // 输出:red

// 获取索引为 2 的元素
string thirdColor = colors.ElementAt(2);
Console.WriteLine($"索引 2 的元素: {thirdColor}"); // 输出:blue

// 尝试获取超出范围的索引会抛出异常
// try
// {
//     string outOfRangeColor = colors.ElementAt(3);
// }
// catch (ArgumentOutOfRangeException ex)
// {
//     Console.WriteLine($"错误: {ex.Message}"); // 输出:Index was out of range. Must be non-negative and less than the size of the collection.
// }

ElementAtOrDefault

返回序列中指定索引处的元素;如果索引超出范围,则返回默认值。
ElementAtOrDefault 操作符返回序列中指定索引处的元素。如果索引超出序列的范围,则返回类型的默认值。

csharp 复制代码
List<string> colors = new List<string> { "red", "green", "blue" };

// 获取索引为 0 的元素
string firstOrDefaultColor = colors.ElementAtOrDefault(0);
Console.WriteLine($"索引 0 的元素 (ElementAtOrDefault): {firstOrDefaultColor}"); // 输出:red

// 获取索引为 2 的元素
string thirdOrDefaultColor = colors.ElementAtOrDefault(2);
Console.WriteLine($"索引 2 的元素 (ElementAtOrDefault): {thirdOrDefaultColor}"); // 输出:blue

// 获取超出范围的索引会返回默认值
string outOfRangeOrDefaultColor = colors.ElementAtOrDefault(3);
Console.WriteLine($"索引 3 的元素 (ElementAtOrDefault): {outOfRangeOrDefaultColor ?? "null"}"); // 输出:null

First

返回序列的第一个元素
First 操作符返回序列中的第一个元素。如果序列为空,则会抛出 InvalidOperationException 异常。你也可以提供一个谓词函数,First 会返回序列中第一个满足该谓词的元素。如果序列中没有满足谓词的元素,同样会抛出异常.

csharp 复制代码
List<int> numbers = new List<int> { 10, 20, 30, 40, 50 };

// 获取第一个元素
int firstElement = numbers.First();
Console.WriteLine($"第一个元素: {firstElement}"); // 输出:10

// 获取第一个大于 25 的元素
int firstGreaterThan25 = numbers.First(n => n > 25);
Console.WriteLine($"第一个大于 25 的元素: {firstGreaterThan25}"); // 输出:30

List<int> emptyNumbers = new List<int>();
// 尝试在空序列上使用 First() 会抛出异常
// try
// {
//     int firstOfEmpty = emptyNumbers.First();
// }
// catch (InvalidOperationException ex)
// {
//     Console.WriteLine($"错误: {ex.Message}"); // 输出:Sequence contains no elements.
// }

// 尝试在没有满足条件的元素时使用 First(predicate) 会抛出异常
// try
// {
//     int firstGreaterThan50 = numbers.First(n => n > 50);
// }
// catch (InvalidOperationException ex)
// {
//     Console.WriteLine($"错误: {ex.Message}"); // 输出:Sequence contains no matching element.
// }

FirstOrDefault

返回序列的第一个元素;如果序列不包含任何元素,则返回默认值。
FirstOrDefault 操作符返回序列中的第一个元素。如果序列为空,则返回类型的默认值(例如,int 的默认值为 0,string 的默认值为 null,引用类型的默认值也为 null)。你也可以提供一个谓词函数,FirstOrDefault 会返回序列中第一个满足该谓词的元素。如果序列中没有满足谓词的元素,则返回类型的默认值。

csharp 复制代码
List<int> numbers = new List<int> { 10, 20, 30, 40, 50 };
List<int> emptyNumbers = new List<int>();

// 获取第一个元素
int firstOrDefaultElement = numbers.FirstOrDefault();
Console.WriteLine($"第一个元素 (FirstOrDefault): {firstOrDefaultElement}"); // 输出:10

// 获取第一个大于 25 的元素
int firstOrDefaultGreaterThan25 = numbers.FirstOrDefault(n => n > 25);
Console.WriteLine($"第一个大于 25 的元素 (FirstOrDefault): {firstOrDefaultGreaterThan25}"); // 输出:30

// 在空序列上使用 FirstOrDefault() 返回默认值
int firstOrDefaultOfEmpty = emptyNumbers.FirstOrDefault();
Console.WriteLine($"空序列的第一个元素 (FirstOrDefault): {firstOrDefaultOfEmpty}"); // 输出:0

// 在没有满足条件的元素时使用 FirstOrDefault(predicate) 返回默认值
int firstOrDefaultGreaterThan50 = numbers.FirstOrDefault(n => n > 50);
Console.WriteLine($"第一个大于 50 的元素 (FirstOrDefault): {firstOrDefaultGreaterThan50}"); // 输出:0

Last

返回序列的最后一个元素
Last 操作符返回序列中的最后一个元素。如果序列为空,则会抛出 InvalidOperationException 异常。你也可以提供一个谓词函数,Last 会返回序列中最后一个满足该谓词的元素。如果序列中没有满足谓词的元素,同样会抛出异常。

csharp 复制代码
List<int> numbers = new List<int> { 10, 20, 30, 40, 50 };

// 获取最后一个元素
int lastElement = numbers.Last();
Console.WriteLine($"最后一个元素: {lastElement}"); // 输出:50

// 获取最后一个小于 35 的元素
int lastLessThan35 = numbers.Last(n => n < 35);
Console.WriteLine($"最后一个小于 35 的元素: {lastLessThan35}"); // 输出:30

List<int> emptyNumbers = new List<int>();
// 尝试在空序列上使用 Last() 会抛出异常
// try
// {
//     int lastOfEmpty = emptyNumbers.Last();
// }
// catch (InvalidOperationException ex)
// {
//     Console.WriteLine($"错误: {ex.Message}"); // 输出:Sequence contains no elements.
// }

// 尝试在没有满足条件的元素时使用 Last(predicate) 会抛出异常
// try
// {
//     int lastLessThan5 = numbers.Last(n => n < 5);
// }
// catch (InvalidOperationException ex)
// {
//     Console.WriteLine($"错误: {ex.Message}"); // 输出:Sequence contains no matching element.
// }

LastOrDefault

返回序列的最后一个元素;如果序列不包含任何元素,则返回默认值
LastOrDefault 操作符返回序列中的最后一个元素。如果序列为空,则返回类型的默认值。你也可以提供一个谓词函数,LastOrDefault 会返回序列中最后一个满足该谓词的元素。如果序列中没有满足谓词的元素,则返回类型的默认值。

csharp 复制代码
List<int> numbers = new List<int> { 10, 20, 30, 40, 50 };
List<int> emptyNumbers = new List<int>();

// 获取最后一个元素
int lastOrDefaultElement = numbers.LastOrDefault();
Console.WriteLine($"最后一个元素 (LastOrDefault): {lastOrDefaultElement}"); // 输出:50

// 获取最后一个小于 35 的元素
int lastOrDefaultLessThan35 = numbers.LastOrDefault(n => n < 35);
Console.WriteLine($"最后一个小于 35 的元素 (LastOrDefault): {lastOrDefaultLessThan35}"); // 输出:30

// 在空序列上使用 LastOrDefault() 返回默认值
int lastOrDefaultOfEmpty = emptyNumbers.LastOrDefault();
Console.WriteLine($"空序列的最后一个元素 (LastOrDefault): {lastOrDefaultOfEmpty}"); // 输出:0

// 在没有满足条件的元素时使用 LastOrDefault(predicate) 返回默认值
int lastOrDefaultLessThan5 = numbers.LastOrDefault(n => n < 5);
Console.WriteLine($"最后一个小于 5 的元素 (LastOrDefault): {lastOrDefaultLessThan5}"); // 输出:0

Single

返回序列的唯一元素;如果序列不包含任何元素,或者序列包含多个元素,则引发异常
Single 操作符返回序列中唯一的元素。如果序列为空或者包含多个元素,则会抛出 InvalidOperationException 异常。你也可以提供一个谓词函数,Single 会返回序列中唯一满足该谓词的元素。如果序列中没有满足谓词的元素,或者存在多个满足谓词的元素,同样会抛出异常。

csharp 复制代码
 List<int> singleElementList = new List<int> { 25 };
List<int> emptyList = new List<int>();
List<int> multipleElementsList = new List<int> { 10, 20 };

// 获取唯一的元素
int singleElement = singleElementList.Single();
Console.WriteLine($"唯一的元素: {singleElement}"); // 输出:25

// 尝试在空序列上使用 Single() 会抛出异常
// try
// {
//     int singleOfEmpty = emptyList.Single();
// }
// catch (InvalidOperationException ex)
// {
//     Console.WriteLine($"错误: {ex.Message}"); // 输出:Sequence contains no elements.
// }

// 尝试在包含多个元素的序列上使用 Single() 会抛出异常
// try
// {
//     int singleOfMultiple = multipleElementsList.Single();
// }
// catch (InvalidOperationException ex)
// {
//     Console.WriteLine($"错误: {ex.Message}"); // 输出:Sequence contains more than one element.
// }

List<int> numbersWithSingleMatch = new List<int> { 10, 20, 30 };
// 获取唯一大于 25 的元素
int singleGreaterThan25 = numbersWithSingleMatch.Single(n => n > 25);
Console.WriteLine($"唯一大于 25 的元素: {singleGreaterThan25}"); // 输出:30

// 尝试在没有满足条件的元素时使用 Single(predicate) 会抛出异常
// try
// {
//     int singleGreaterThan30 = numbersWithSingleMatch.Single(n => n > 30);
// }
// catch (InvalidOperationException ex)
// {
//     Console.WriteLine($"错误: {ex.Message}"); // 输出:Sequence contains no matching element.
// }

// 尝试在有多个满足条件的元素时使用 Single(predicate) 会抛出异常
// try
// {
//     int singleGreaterThan15 = numbersWithSingleMatch.Single(n => n > 15);
// }
// catch (InvalidOperationException ex)
// {
//     Console.WriteLine($"错误: {ex.Message}"); // 输出:Sequence contains more than one matching element.
// }

SingleOrDefault

返回序列的唯一元素;如果序列不包含任何元素,则返回默认值;如果序列包含多个元素,则引发异常
SingleOrDefault 操作符返回序列中唯一的元素。如果序列为空,则返回类型的默认值。如果序列包含多个元素,则会抛出 InvalidOperationException 异常。你也可以提供一个谓词函数,SingleOrDefault 会返回序列中唯一满足该谓词的元素。如果序列中没有满足谓词的元素,则返回类型的默认值。如果存在多个满足谓词的元素,则会抛出异常。

csharp 复制代码
List<int> singleElementList = new List<int> { 25 };
List<int> emptyList = new List<int>();
List<int> multipleElementsList = new List<int> { 10, 20 };

// 获取唯一的元素
int singleOrDefaultElement = singleElementList.SingleOrDefault();
Console.WriteLine($"唯一的元素 (SingleOrDefault): {singleOrDefaultElement}"); // 输出:25

// 在空序列上使用 SingleOrDefault() 返回默认值
int singleOrDefaultOfEmpty = emptyList.SingleOrDefault();
Console.WriteLine($"空序列的唯一元素 (SingleOrDefault): {singleOrDefaultOfEmpty}"); // 输出:0

// 尝试在包含多个元素的序列上使用 SingleOrDefault() 会抛出异常
// try
// {
//     int singleOrDefaultOfMultiple = multipleElementsList.SingleOrDefault();
// }
// catch (InvalidOperationException ex)
// {
//     Console.WriteLine($"错误: {ex.Message}"); // 输出:Sequence contains more than one element.
// }

List<int> numbersWithSingleMatch = new List<int> { 10, 20, 30 };
// 获取唯一大于 25 的元素
int singleOrDefaultGreaterThan25 = numbersWithSingleMatch.SingleOrDefault(n => n > 25);
Console.WriteLine($"唯一大于 25 的元素 (SingleOrDefault): {singleOrDefaultGreaterThan25}"); // 输出:30

// 在没有满足条件的元素时使用 SingleOrDefault(predicate) 返回默认值
int singleOrDefaultGreaterThan30 = numbersWithSingleMatch.SingleOrDefault(n => n > 30);
Console.WriteLine($"唯一大于 30 的元素 (SingleOrDefault): {singleOrDefaultGreaterThan30}"); // 输出:0

// 尝试在有多个满足条件的元素时使用 SingleOrDefault(predicate) 会抛出异常
// try
// {
//     int singleOrDefaultGreaterThan15 = numbersWithSingleMatch.SingleOrDefault(n => n > 15);
// }
// catch (InvalidOperationException ex)
// {
//     Console.WriteLine($"错误: {ex.Message}"); // 输出:Sequence contains more than one matching element.
// }

转换操作符

AsEnumerable

将序列转换为 IEnumerable。
作用: 将类型为 IEnumerable 的序列显式向上转换为 IEnumerable 类型。
返回值: IEnumerable
延迟执行: 延迟执行,实际上不进行任何转换,只是改变了类型。
使用场景: 在某些情况下,当一个类型同时实现了 IEnumerable 和一个更具体的 LINQ 提供程序接口(例如 IQueryable for LINQ to SQL 或 Entity Framework)时,AsEnumerable() 可以强制后续的 LINQ 操作在 LINQ to Objects 上执行,而不是在特定的 LINQ 提供程序上执行。这对于在内存中处理从数据库或其他外部源检索到的数据很有用。

csharp 复制代码
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// 在内存中对 numbers 进行筛选 (LINQ to Objects)
IEnumerable<int> evenNumbers = numbers.Where(n => n % 2 == 0).AsEnumerable();

// 假设 dbContext 是一个 Entity Framework 上下文
// IQueryable<Order> orders = dbContext.Orders;

// 强制在内存中执行 Take 操作,而不是在数据库中 (LINQ to Objects)
// IEnumerable<Order> firstTenOrders = orders.Take(10).AsEnumerable();

AsQueryable

将序列转换为 IQueryable。
作用: 将一个 IEnumerable 类型的序列显式转换为 IQueryable 类型。
返回值: IQueryable
延迟执行: 延迟执行,它本身不执行任何数据检索或转换,只是改变了序列的类型。

在深入了解 AsQueryable() 之前,理解 IEnumerable<T>IQueryable<T> 的关键区别非常重要:

  • IEnumerable<T>: 表示一个可以枚举的集合,支持在内存中进行 LINQ to Objects 查询。LINQ to Objects 操作符(如 Where, Select, OrderBy 等)在枚举集合时逐个处理元素。
  • IQueryable<T>: 表示一个可以查询的数据源,通常与 LINQ 提供程序(如 LINQ to SQL、Entity Framework)一起使用。IQueryable<T> 继承自 IEnumerable<T>,但它还包含一个 Expression 属性,用于表示查询的逻辑结构树。LINQ 提供程序可以分析这个表达式树,并将其转换为特定数据源(例如 SQL 查询)的查询语言,从而在数据源端执行查询,通常可以提高性能,尤其是在处理大量数据时。

AsQueryable<TSource>() 的作用和使用场景:

AsQueryable() 的主要作用是将一个已经存在于内存中的 IEnumerable<T> 集合转换为 IQueryable<T>. 这样做通常是为了:

  1. 利用标准的 LINQ 查询语法来操作 IEnumerable<T> 数据,但希望某些后续的操作能够像针对 IQueryable 数据源一样进行处理。 虽然 IEnumerable<T> 本身就支持 LINQ 查询,但在某些特定的扩展方法或场景下,期望输入的是 IQueryable<T>.
  2. 创建自定义的 LINQ 提供程序或扩展方法,这些方法需要接收 IQueryable<T> 作为输入。 如果你的数据最初是以 IEnumerable<T> 的形式存在的,你需要使用 AsQueryable() 将其转换为 IQueryable<T> 才能传递给这些特定的扩展方法。
  3. 在某些测试或模拟场景中,你可能需要将内存中的数据模拟成一个可查询的数据源。
csharp 复制代码
using System.Linq;
using System.Collections.Generic;

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class Example
{
    public static void Main(string[] args)
    {
        List<Product> products = new List<Product>
        {
            new Product { Id = 1, Name = "Laptop", Price = 1200 },
            new Product { Id = 2, Name = "Mouse", Price = 25 },
            new Product { Id = 3, Name = "Keyboard", Price = 75 }
        };

        // 将 List<Product> 转换为 IQueryable<Product>
        IQueryable<Product> queryableProducts = products.AsQueryable();

        // 现在可以像操作 IQueryable 数据源一样使用 LINQ 查询操作符
        var expensiveProducts = queryableProducts
            .Where(p => p.Price > 100)
            .OrderByDescending(p => p.Price);

        Console.WriteLine("Expensive Products:");
        foreach (var product in expensiveProducts)
        {
            Console.WriteLine($"{product.Name}: ${product.Price}");
        }

        // 在这个例子中,由于 products 已经在内存中,
        // .Where 和 .OrderByDescending 仍然是作为 LINQ to Objects 执行的。
        // AsQueryable() 只是改变了类型,并没有改变实际的执行方式。

        // 假设有一个需要 IQueryable<Product> 的方法
        ProcessQueryableProducts(queryableProducts);
    }

    public static void ProcessQueryableProducts(IQueryable<Product> productQuery)
    {
        Console.WriteLine("\nProcessing Queryable Products:");
        foreach (var product in productQuery)
        {
            Console.WriteLine(product.Name);
        }
    }
}

关键点:

  • 不执行查询: AsQueryable() 本身不会执行任何查询或数据转换。它只是将一个 IEnumerable<T> 实例包装成一个 IQueryable<T> 实例。
  • 后续操作的执行方式取决于上下文: 如果后续的 LINQ 操作符是 LINQ to Objects 的标准操作符(如 Where, Select 等),它们仍然会在内存中对数据进行操作,即使数据源是 IQueryable<T> 类型。只有当 IQueryable<T> 与特定的 LINQ 提供程序一起使用时,后续的操作才会被转换为该提供程序特定的查询语言并在数据源端执行。
  • 通常不是必需的: 在大多数常见的 LINQ to Objects 场景中,你不需要显式地使用 AsQueryable(). LINQ 的扩展方法通常同时为 IEnumerable<T>IQueryable<T> 提供了重载,因此你可以直接在 IEnumerable<T> 集合上进行 LINQ 查询。
  • 在 LINQ to SQL/EF 中,当你从 DbSet<T> Table<T> 等数据源开始查询时,返回的已经是 IQueryable<T>,你不需要再使用 AsQueryable()

总结:

AsQueryable() 是一个将 IEnumerable<T> 转换为 IQueryable<T> 的转换操作符。它本身不执行任何数据操作,主要用于在需要 IQueryable<T> 类型作为输入的情况下,或者在某些高级场景下控制 LINQ 查询的执行方式。在常见的 LINQ to Objects 场景中,通常不需要显式使用它。

Cast

将 IEnumerable 的元素强制转换为指定的类型。
作用: 将 IEnumerable 的元素强制转换为指定的类型 TResult。
返回值: IEnumerable
延迟执行: 延迟执行。
注意: 如果源序列中的任何元素无法转换为 TResult,则在枚举结果时会抛出 InvalidCastException。Cast 通常用于将非泛型集合(如 ArrayList) 转换为泛型 IEnumerable.

csharp 复制代码
System.Collections.ArrayList numbersList = new System.Collections.ArrayList { 1, 2, 3, 4, 5 };

IEnumerable<int> intNumbers = numbersList.Cast<int>();

foreach (int number in intNumbers)
{
    Console.WriteLine(number);
}

// 如果 numbersList 中包含非 int 类型的元素,以下代码在执行到该元素时会抛出异常
// numbersList.Add("hello");
// foreach (int number in numbersList.Cast<int>())
// {
//     Console.WriteLine(number); // 可能抛出 InvalidCastException
// }

ToArray

从 IEnumerable 创建数组。
作用: 将查询结果转换为一个 TSource 类型的数组。
返回值: TSource[]
延迟执行: 不延迟执行,会立即将查询结果加载到数组中。

复制代码
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

int[] evenNumbersArray = numbers.Where(n => n % 2 == 0).ToArray();

Console.WriteLine(string.Join(", ", evenNumbersArray)); // 输出: 2, 4

ToDictionary

从 IEnumerable 创建 Dictionary<TKey, TValue>。
ToDictionary<TSource, TKey>(this IEnumerable, Func<TSource, TKey>)
作用: 将查询结果转换为一个 Dictionary<TKey, TSource>。你需要提供一个键选择器函数来从每个源元素中提取键。
返回值:Dictionary<TKey, TSource>
延迟执行: 不延迟执行,会立即将查询结果加载到字典中。
注意: 如果键选择器返回重复的键,会抛出 ArgumentException。

csharp 复制代码
List<Product> products = new List<Product>
{
    new Product { Id = 1, Name = "Laptop", Price = 1200 },
    new Product { Id = 2, Name = "Mouse", Price = 25 },
    new Product { Id = 3, Name = "Keyboard", Price = 75 }
};

Dictionary<int, Product> productDictionary = products.ToDictionary(p => p.Id);

Console.WriteLine($"Product with Id 2: {productDictionary[2].Name}"); // 输出: Product with Id 2: Mouse

ToList

从 IEnumerable 创建 List。
作用: 将查询结果转换为一个 List 类型的列表。
返回值: List
延迟执行: 不延迟执行,会立即将查询结果加载到列表中。

csharp 复制代码
string[] names = { "Alice", "Bob", "Charlie" };

List<string> namesList = names.Where(n => n.StartsWith("A")).ToList();

foreach (string name in namesList)
{
    Console.WriteLine(name); // 输出: Alice
}

ToLookup

从 IEnumerable 创建 ILookup<TKey, TElement>。
ToLookup<TSource, TKey>(this IEnumerable, Func<TSource, TKey>)
作用: 将查询结果转换为一个 ILookup<TKey, TSource>。ILookup 类似于 Dictionary,但它可以将一个键映射到多个值。你需要提供一个键选择器函数。
返回值:ILookup<TKey, TSource>
延迟执行: 不延迟执行,会立即将查询结果加载到 ILookup 中。

csharp 复制代码
List<Student> students = new List<Student>
{
    new Student { Name = "Alice", Grade = "A" },
    new Student { Name = "Bob", Grade = "B" },
    new Student { Name = "Charlie", Grade = "A" },
    new Student { Name = "David", Grade = "C" }
};

ILookup<string, Student> studentsByGrade = students.ToLookup(s => s.Grade);

Console.WriteLine("Students in Grade A:");
foreach (var student in studentsByGrade["A"])
{
    Console.WriteLine(student.Name); // 输出: Alice, Charlie
}
相关推荐
Andy工程师1 小时前
Spring Boot 按照以下顺序加载配置(后面的会覆盖前面的):
java·spring boot·后端
繁星蓝雨1 小时前
小试Spring boot项目程序(进行get、post方法、打包运行)——————附带详细代码与示例
java·spring boot·后端
@年年2 小时前
C#十字线小工具
c#
山枕檀痕2 小时前
Spring Boot中LocalDateTime接收“yyyy-MM-dd HH:mm:ss“格式参数的最佳实践
java·spring boot·后端
Java水解2 小时前
【Spring Boot 单元测试教程】从环境搭建到代码验证的完整实践
后端·spring
Lear2 小时前
【JavaSE】动态代理技术详解与案例实战
后端
shark_chili2 小时前
深入剖析Java并发编程中的死锁问题
后端
开心就好20252 小时前
iOS 压力测试的工程化体系 构建多工具协同的极限稳定性验证方案
后端
深紫色的三北六号2 小时前
Quartz 定时任务持久化(重启后自动恢复)
后端