C# .NET 实战技巧:深入理解 LINQ 的强大功能
大家好,这是 [C#.NET 拾遗补漏] 系列的第 08 篇文章,今天讲 C# 强大的 LINQ 查询。LINQ 是我最喜欢的 C# 语言特性之一。
LINQ 是 Language INtegrated Query 单词的首字母缩写,翻译过来是语言集成查询。它为查询跨各种数据源和格式的数据提供了一致的模型,所以叫集成查询。由于这种查询并没有制造新的语言而只是在现有的语言基础上来实现,所以叫语言集成查询。
一些基础
在 C# 中,从功能上 LINQ 可分为两类:LINQ to Object 和 LINQ to XML;从语法上 LINQ 可以分为 LINQ to Object 和 LINQ 扩展方法
纠正:
从功能上 LINQ 可分为两类:
- LINQ to Object,查询内存集合,直接把查询编译成 .NET 代码执行。
- LINQ to Provider,查询自定义数据源,由开发者提供相应数据源的 Provider 并翻译和执行自定义查询,如 XML、JSON 等都可以作为 Provider 对应的数据源,数据源对应的 LINQ 查询叫 LINQ to <数据源>,比如:LINQ to XML。
从语法上 LINQ 可以分为:
-
SQL风格:语法和 SQL 相似,部分复杂查询用 SQL 风格语义会更清晰明了,比如 SelectMany 和 Join 查询。SQL 风格的可读性有绝对优势,但不支持全部标准 LINQ 函数,不支持自定义函数。纯粹的语法糖。
-
函数风格:以 C# 扩展方法的方式实现,扩展方法即可以是标准库提供的也可以是自己实现的,完全的原生编程风格,编译后的代码都是函数调用。支持全部标准 LINQ 函数和任何自定义函数。随着查询复杂度的提高,可读性不如 SQL 风格。
感谢 @coredx 的纠正。
所有 LINQ to Object 都可以用 LINQ 扩展方法实现等同的效果,而且平时开发中用的最多的是 LINQ 扩展方法。
LINQ to Object 多用于映射数据库的查询,LINQ to XML 用于查询 XML 元素数据。使用 LINQ 查询的前提是对象必须是一个 IEnumerable 集合(注意,为了描述方便,本文说的集合都是指 IEnumerable 对象,包含字面上的 ICollection 对象)。另外,LINQ 查询大多是都是链式查询,即操作的数据源是 IEnumerable<T1>
类型,返回的是 IEnumerable<T2>
类型,T1 和 T2 可以相同,也可以不同。
形如下面这样的查询就是 LINQ to Object:
var list = from user in users
where user.Name.Contains("Wang")
select user.Id;
等同于使用下面的 LINQ 扩展方法:
var list = users
.Where(u => user.Name.Contains("Wang"))
.Select(u => u.id);
LINQ 查询支持在语句中间根据需要定义变量,比如取出数组中平方值大于平均值的数字:
int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var result = from number in numbers
let average = numbers.Average()
let squared = Math.Pow(number, 2)
where squared > average
select number;
// 平均值为 4.5, result 为 { 3, 4, 5, 6, 7, 8, 9 }
其中的 Select 方法接收的参数用的最多的是 Func<TSource, TResult>
,它还可以接收 Func<TSource, int, TResult>
参数,示例:
var collectionWithRowNumber = collection.
.Select((item, index) => new { Item = item, RowNumber =index })
.ToList();
再来看一下 LINQ to XML 的示例。假如我们有如下 XML 文件:
<?xml version="1.0" encoding="utf-8" ?>
<Employees>
<Employee>
<EmpId>1</EmpId>
<Name>Liam</Name>
<Sex>男</Sex>
</Employee>
<Employee>
<EmpId>2</EmpId>
...
</Employee>
</Employees>
使用 LINQ to XML 查询所有含有指定节点值的元素:
XElement xelement = XElement.Load("Employees.xml");
var els = from el in xelement.Elements("Employee")
where (string)el.Element("Sex") == "Male"
select el;
等同于使用 LINQ 扩展方法:
var els = xelement.Elements("Employee")
.Where(el => (string)el.Element("Sex") == "Male");
LINQ to XML 操作 XML 非常方便和灵活,大家可以在具体使用的时候去探索,这里就不展开讲了。
LINQ 查询有很多方法,由于篇幅原因,就不一一列举演示了,这里只选取一些强大的查询方法,这些方法若使用非 LINQ 来实现可能会比较麻烦。
LINQ 之所以强大,是因为它可以轻松实现复杂的查询,下面我们来总结一下 C# LINQ 的强大之处。
First、Last 和 Single 等
First、FirstOrDefault、Last、LastOrDefault、Single 和 SingleOrDefault 是快速查询集合中的第一个或最后一个元素的方法。如果集合是空的,Fist、Last 和 Single 都会报错,如果使其不报错而在空集合时使用默认值可以使用 FirstOrDefault、LastOrDefault 和 SingleOrDefault。Single/SingleOrDefault 和其它方法的区别是,它限定查询结果只有一个元素,如果查询结果集合中包含多个元素时会报错。具体看下面几个示例:
new[] { "a", "b" }.First(x => x.Equals("b")); // 返回 ”b“
new[] { "a", "b" }.First(x => x.Equals("c")); // 抛出 InvalidOperationException 异常
new[] { "a", "b" }.FirstOrDefault(x => x.Equals("c")); // 返回 null
new[] { "a", "b" }.Single(x => x.Equals("b")); // 返回 ”b“
new[] { "a", "b" }.Single(x => x.Equals("c")); // 抛出 InvalidOperationException 异常
new[] { "a", "b" }.SingleOrDefault(x => x.Equals("c")); // 返回 null
new[] { "a", "a" }.Single(); // 抛出 InvalidOperationException 异常
在实际应用中,如果要确保查询结果的唯一性(比如通过手机号查询用户),使用 Single/SingleOrDefaut,其它情况应尽量使用 First/FirstOrDefault。虽然 FirstOrDefault 也可以根据条件判断元素是否存在,但使用 Any 更高效。
Except 取差集
LINQ 的 Except 方法用来取差集,即取出集合中与另一个集合所有元素不同的元素。
示例:
int[] first = { 1, 2, 3, 4 };
int[] second = { 0, 2, 3, 5 };
IEnumerable<int> result = first.Except(second);
// result = { 1, 4 }
注意 Except 方法会去除重复元素:
int[] second = { 0, 2, 3, 5 };
int[] third = { 1, 1, 1, 2, 3, 4 };
IEnumerable<int> result = third.Except(second);
// result = { 1, 4 }
对于简单类型(int、float、string 等)使用 Except 很简单,但对于自定义类型(或者叫复合类型,下同)的 Object 如何使用 Except 呢?此时需要将自定义类型实现IEquatable<T>
接口,示例:
class User : IEquatable<User>
{
public string Name { get; set; }
public bool Equals(User other)
{
return Name == other.Name;
}
public override int GetHashCode()
{
return Name?.GetHashCode() ?? 0;
}
}
class Program
{
static void Main(string[] args)
{
var list1 = new List<User>
{
new User{ Name = "User1"},
new User{ Name = "User2"},
};
var list2 = new List<User>
{
new User{ Name = "User2"},
new User{ Name = "User3"},
};
var result = list1.Except(list2);
result.ForEach(u => Console.WriteLine(u.Name));
// 输出:User1
}
}
SelectMany 集合降维
SelectMany 可以把多维集合降维,比如把二维的集合平铺成一个一维的集合。举例:
var collection = new int[][]
{
new int[] {1, 2, 3},
new int[] {4, 5, 6},
};
var result = collection.SelectMany(x => x);
// result = [1, 2, 3, 4, 5, 6]
再来举个更贴合实际应用的例子。例如有如下实体类(一个部门有多个员工):
class Department
{
public Employee[] Employees { get; set; }
}
class Employee
{
public string Name { get; set; }
}
此时,我们拥有一个这样的数据集合:
var departments = new[]
{
new Department()
{
Employees = new []
{
new Employee { Name = "Bob" },
new Employee { Name = "Jack" }
}
},
new Department()
{
Employees = new []
{
new Employee { Name = "Jim" },
new Employee { Name = "John" }
}
}
};
现在我们可以使用 SelectMany 把各部门的员工查询到一个结果集中:
var allEmployees = departments.SelectMany(x => x.Employees);
foreach(var emp in allEmployees)
{
Console.WriteLine(emp.Name);
}
// 依次输出:Bob Jack Jim John
SelectMany 迪卡尔积运算
SelectMany 不光适用于单个包含多维集合对象的降维,也适用于多个集合之前的两两相互操作,比如进行迪卡尔积运算。比如我们有这样两个集合:
var list1 = new List<string> { "a1", "a2" };
var list2 = new List<string> { "b1", "b2", "b3" };
现在我们需要把它进行两两组合,使用普通的方法,我们需要用嵌套循环语句来实现:
var result = newList<string>();
foreach (var s1 in list1)
foreach (var s2 in list2)
result.Add($"{s1}{s2}");
// result = ["a1b1", "a1b2", "a1b3", "a2b1", "a2b2", "a2b3"]
改用 SelectMany 实现:
var result = list1.SelectMany(x => list2.Select(y => $"{x}{y}"));
// result = ["a1b1", "a1b2", "a1b3", "a2b1", "a2b2", "a2b3"]
具有挑战性的问题来了,如何对 N 个集合进行迪卡尔积运算呢,比如有这样的集合数据:
var arrList = new List<string[]>
{
new string[] { "a1", "a2" },
new string[] { "b1", "b2", "b3" },
new string[] { "c1" },
// ...
};
如何对上面的 arrList 中的各个集合进行两两组合呢?在电商业务尤其是零售业务中的产品组合促销中这种需求很常见。
下面是一个使用 SelectMany 的实现,需要用到递归:
class Program
{
static void Main(string[] args)
{
var arrList = new List<string[]>
{
new string[] { "a1", "a2" },
new string[] { "b1", "b2", "b3" },
new string[] { "c1" },
// ...
};
var result = Recursion(arrList, 0, new List<string>());
result.ForEach(x => Console.WriteLine(x));
}
static List<string> Recursion(List<string[]> list, int start, List<string> result)
{
if (start >= list.Count)
return result;
if (result.Count == 0)
result = list[start].ToList();
else
result = result.SelectMany(x => list[start].Select(y => x + y)).ToList();
result = Recursion(list, start + 1, result);
return result;
}
}
输出:
a1b1c1
a1b2c1
a1b3c1
a2b1c1
a2b2c1
a2b3c1
类似这种集合的迪卡尔积运算操作,也可以用 LINQ to Object 来代替 SelectMany 实现:
result = result.SelectMany(x => list[start].Select(y => x + y)).ToList();
// 等同使用扩展方法:
result = (from a in result from b in list[start] select a + b).ToList();
LINQ to Object 比扩展方法看上去易读性更好,但写起来扩展方法更方便。
Aggregate 聚合
Aggregate 扩展方法可以对一个集合依次执行类似累加器的操作,就像滚雪球一样把数据逐步聚集在一起。比如实现从 1 加到 10,用 Aggregate 扩展方法就很方便:
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int sum = numbers.Aggregate((prevSum, current) => prevSum + current);
// sum = 55
我们来解析一下它的执行步骤
- 第一步,prevSum 取第一个元素的值,即 prevSum = 1
- 第二步,把第一步得到的 prevSum 的值加上第二个元素,即 prevSum = prevSum + 2
- 依此类推,第 i 步把第 i-1 得到的 prevSum 加上第 i 个元素
再来看一个字符串的例子加深理解:
string[] stringList = { "Hello", "World", "!" };
string joinedString = stringList.Aggregate((prev, current) => prev + " " + current);
// joinedString = "Hello World !"
Aggregate 还有一个重载方法,可以指定累加器的初始值。我们来看一个比较综合的复杂例子。假如我们有如下 1-12 的一个数字集合:
var items = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
现在我们想做如下计算:
- 计算集合元素的总数个数
- 计算值为偶数的元素个数
- 收集每第 4 个元素
当然通过普通的循环遍历也可以实现这三个计算,但使用 Aggregate 会更简洁,下面是 Aggregate 的实现:
var result = items.Aggregate(new { Total = 0, Even = 0, FourthItems = new List<int>() },
(accum, item) =>
new
{
Total = accum.Total + 1,
Even = accum.Even + (item % 2 == 0 ? 1 : 0),
FourthItems = (accum.Total + 1) % 4 == 0 ? new List<int>(accum.FourthItems) { item } : accum.FourthItems
}
);
// result:
// Total = 12
// Even = 6
// FourthItems = [4, 8, 12]
这里为了简单起见使用匿名类型作为累加器的初始值,由于匿名类型的属性是只读的,所以在累加的过程都 new 了一个新对象。如果初始值使用的是自定义类型,那累加时就不需 new 新对象了。
Join 关联查询
和 SQL 查询一样,LINQ 同样支持 Inner Join、Left Join、Right Join、Cross Join 和 Full Outer Join,有时候你可能看到不同的写法,其实是同一个意思,比如 Left Outer Join 就是 Left Join,Join 是 Inner Join 省略了 Inner 等。
假设我们有下面两个集合,分别表示左边的数据和右边的数据。
var first = new List<string>() { "a","b","c" }; // 左边
var second = new List<string>() { "a", "c", "d" }; // 右边
下面以此数据为例来演示各种关联查询。
Inner Join
var result = from f in first
join s in second on f equals s
select new { f, s };
// 等同使用扩展方法:
var result = first.Join(second,
f => f,
s => s,
(f, s) => new { f, s });
// result: {"a","a"}
// {"c","c"}
Left Join
var result = from f in first
join s in second on f equals s into temp
from t in temp.DefaultIfEmpty()
select new { First = f, Second = t };
// 或者:
var result = from f in first
from s in second.Where(x => x == f).DefaultIfEmpty()
select new { First = f, Second = s };
// 等同使用扩展方法:
var result = first.GroupJoin(second,
f => f,
s => s,
(f, s) => new { First = f, Second = s })
.SelectMany(temp => temp.Second.DefaultIfEmpty(),
(f, s) => new { First = f.First, Second = s });
// result: {"a","a"}
// {"b", null}
// {"c","c"}
Right Join
var result = from s in second
join f in first on s equals f into temp
from t in temp.DefaultIfEmpty()
select new { First = t, Second = s };
// 其它和 Left Join 类似
// result: {"a","a"}
// {"c","c"}
// {null,"d"}
Cross Join
var result = from f in first
from s in second
select new { f, s };
// result: {"a","a"}
// {"a","c"}
// {"a","d"}
// {"b","a"}
// {"b","c"}
// {"b","d"}
// {"c","a"}
// {"c","c"}
// {"c","d"}
Full Outer Join
var leftJoin = from f in first
join s in second on f equals s into temp
from t in temp.DefaultIfEmpty()
select new { First = f, Second = t };
var rightJoin = from s in second
join f in first on s equals f into temp
from t in temp.DefaultIfEmpty()
select new { First = t, Second = s };
var fullOuterJoin = leftJoin.Union(rightJoin);
根据多个键关联
在 SQL 中,表与表进行关联查询时 on 条件可以指定多个键的逻辑判断,用 and 或 or 连接。但 C# 的 LINQ 不支持 and 关键字,若要根据多键关联,需要把要关联的键值分别以相同的属性名放到匿名对象中,然后使用 equals 比较两个匿名对象是否相等。示例:
var stringProps = typeof(string).GetProperties();
var builderProps = typeof(StringBuilder).GetProperties();
var query =
from s in stringProps
join b in builderProps
on new { s.Name, s.PropertyType } equals new { b.Name, b.PropertyType }
select new
{
s.Name,
s.PropertyType
};
以上均使用两个集合做为示例,LINQ 关联查询也支持多个集合关联,就像 SQL 的多表关联,只需往后继续追加 join 操作即可,不再累述。
LINQ 关联查与 SQL 相似,但使用上有很大区别。LINQ 关联查询的用法有很多,也很灵活,不用刻意去记住它们,只要熟悉简单常用的,其它的在实际用到的时候再查询相关文档。
Skip & Take 分页
Skip 扩展方法用来跳过从起始位置开始的指定数量的元素读取集合;Take 扩展方法用来从集合中只读取指定数量的元素。
var values = new[] { 5, 4, 3, 2, 1 };
var skipTwo = values.Skip(2); // { 3, 2, 1 }
var takeThree = values.Take(3); // { 5, 4, 3 }
var skipOneTakeTwo = values.Skip(1).Take(2); // { 4, 3 }
Skip 与 Take 两个方法结合即可实现我们常见的分页查询:
public IEnumerable<T> GetPage<T>(this IEnumerable<T> collection, int pageNumber, int pageSize)
{
int startIndex = (pageNumber - 1) * pageSize;
return collection.Skip(startIndex).Take(pageSize);
}
使用过 EF (Core) 的同学一定很熟悉。
另外,还有 SkipWhile 和 TakeWhile 扩展方法,它与 Skip 和 Take 不同的是,它们的参数是具体的条件。SkipWhile 从起始位置开始忽略元素,直到遇到不符合条件的元素则停止忽略,往后就是要查询的结果;TakeWhile 从起始位置开始读取符合条件的元素,一旦遇到不符合条件的就停止读取,即使后面还有符合条件的也不再读取。示例:
SkipWhile:
int[] list = { 42, 42, 6, 6, 6, 42 };
var result = list.SkipWhile(i => i == 42);
// result: 6, 6, 6, 42
TakeWhile:
int[] list = { 1, 10, 40, 50, 44, 70, 4 };
var result = list.TakeWhile(item => item < 50).ToList();
// result = { 1, 10, 40 }
Zip 拉链
Zip 扩展方法操作的对象是两个集合,它就像拉链一样,根据位置将两个系列中的每个元素依次配对在一起。其接收的参数是一个 Func 实例,该 Func 实例允许我们成对在处理两个集合中的元素。如果两个集合中的元素个数不相等,那么多出来的将会被忽略。
示例:
int[] numbers = { 3, 5, 7 };
string[] words = { "three", "five", "seven", "ignored" };
IEnumerable<string> zip = numbers.Zip(words, (n, w) => n + "=" + w);
foreach (string s in zip)
{
Console.WriteLine(s);
}
输出:
3=three
5=five
7=seven
OfType 和 Cast 类型过滤与转换
OfType 用于筛选集合中指定类型的元素,Cast 可以把集合转换为指定类型,但要求源类型必须可以隐式转换为目标类型。假如有如下数据:
interface IFoo { }
class Foo : IFoo { }
class Bar : IFoo { }
var item0 = new Foo();
var item1 = new Foo();
var item2 = new Bar();
var item3 = new Bar();
var collection = new IFoo[] { item0, item1, item2, item3 };
OfType 示例:
var foos = collection.OfType<Foo>(); // result: item0, item1
var bars = collection.OfType<Bar>(); // result: item2, item3
var foosAndBars = collection.OfType<IFoo>(); // result: item0, item1, item2, item3
// 等同于使用 Where
var foos = collection.Where(item => item is Foo); // result: item0, item1
var bars = collection.Where(item => item is Bar); // result: item2, item3
Cast 示例:
var bars = collection.Cast<Bar>(); // InvalidCastException 异常
var foos = collection.Cast<Foo>(); // InvalidCastException 异常
var foosAndBars = collection.Cast<IFoo>(); // OK
ToLookup 索引式查找
ToLookup 扩展方法返回的是可索引查找的数据结构,它是一个 ILookup 实例,所有元素根据指定的键进行分组并可以按键进行索引。这样说有点抽象,来看具体示例:
string[] array = { "one", "two", "three" };
// 根据元素字符串长度创建一个查找对象
var lookup = array.ToLookup(item => item.Length);
// 查找字符串长度为 3 的元素
var result = lookup[3];
// result: one,two
再来一个示例:
int[] array = { 1,2,3,4,5,6,7,8 };
// 创建一个奇偶查找(键为 0 和 1)
var lookup = array.ToLookup(item => item % 2);
// 查找偶数
var even = lookup[0];
// even: 2,4,6,8
// 查找奇数
var odd = lookup[1];
// odd: 1,3,5,7
Distinct 去重
Distinct 方法用来去除重复项,这个容易理解。示例:
int[] array = { 1, 2, 3, 4, 2, 5, 3, 1, 2 };
var distinct = array.Distinct();
// distinct = { 1, 2, 3, 4, 5 }
简单类型的集合调用 Distinct 方法使用的是默认的比较器,Distinct 方法用此比较器来判断元素是否与其它元素重复,但对于自定义类型要实现去重则需要自定义比较器。示例:
public class IdEqualityComparer : IEqualityComparer<Person>
{
public bool Equals(Person x, Person y) => x.Id == y.Id;
public int GetHashCode(Person p) => p.Id;
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
class Program
{
static void Main(string[] args)
{
var people = new List<Person>();
var distinct = people.Distinct(new IdEqualityComparer());
}
}
ToDictionary 字典转换
ToDictionary 扩展方法可以把集合 IEnumerable<TElement>
转换为 Dictionary<TKey, TValue>
结构的字典,它接收一个 Func<TSource, TKey>
参数用来返回每个元素指定的键与值。示例:
IEnumerable<User> users = GetUsers();
Dictionary<int, User> usersById = users.ToDictionary(x => x.Id);
如果不用 ToDictionary,你需要这样写:
IEnumerable<User> users = GetUsers();
Dictionary<int, User> usersById = new Dictionary<int, User>();
foreach (User u in users)
{
usersById.Add(u.Id, u);
}
上面 ToDictionary 返回的字典数据中的值是整个元素,你也可以通过它的第二个参数来自定义字典的值。示例:
Dictionary<int, string> userNamesById = users.ToDictionary(x => x.Id, x => x.Name);
你也可以为转换的字典指定其键是否区分大小写,即自定义字典的 IComparer,示例:
Dictionary<string, User> usersByCaseInsenstiveName = users.ToDictionary(x =>x.Name,
StringComparer.InvariantCultureIgnoreCase);
var user1 =usersByCaseInsenstiveName["liam"];
var user2 =usersByCaseInsenstiveName["LIAM"];
user1 == user2; // true
注意,字典类型要求所有键不能重复,所以在使用 ToDictionary 方法时要确保作为字典的键的元素属性不能有重复值,否则会抛出异常。
其它常见扩展方法
LINQ 还有很多其它常见的扩展方法,大家在平时应该用的比较多,比如 Where、Any、All 等,这里也选几个简单举例介绍一下。
Range 和 Repeat
Range 和 Repeat 用于生成简单的数字或字符串系列。示例:
// 生成 1-100 的数字,即结果为 [1, 2, ..., 99, 100]
var range = Enumerable.Range(1, 100);
// 生成三个重复的字符串“a”,即结果为 ["a", "a", "a"]
var repeatedValues = Enumerable.Repeat("a", 3);
Any 和 All
Any 用来判断集合中是否存在任一一个元素符合条件,All 用来判断集合中是否所有元素符合条件。示例:
var numbers = new int[] {1, 2, 3, 4, 5 };
bool result = numbers.Any(); // true
bool result = numbers.Any(x => x == 6); // false
bool result = numbers.All(x => x > 0); // true
bool result = numbers.All(x => x > 1); // false
Concat 和 Union
Concat 用来拼接两个集合,不会去除重复元素,示例:
List<int> foo = newList<int> { 1, 2, 3 };
List<int> bar = newList<int> { 3, 4, 5 };
// 通过 Enumerable 类的静态方法
var result = Enumerable.Concat(foo, bar).ToList(); // 1,2,3,3,4,5
// 通过扩展方法
var result = foo.Concat(bar).ToList(); // 1,2,3,3,4,5
Union 也是用来拼接两个集合,与 Concat 不同的是,它会去除重复项,示例:
var result = foo.Union(bar); // 1,2,3,4,5
GroupBy 分组
GroupBy 扩展方法用来对集合进行分组,下面是一个根据奇偶进行分组的示例:
var list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var grouped = list.GroupBy(x => x % 2 == 0);
// grouped: [1, 3, 5, 7, 9] 和 [2, 4, 6, 8]
还可以根据指定属性进行分组:
public class Person
{
public int Age { get; set; }
public string Name { get; set; }
}
var people = new List<Person>();
var query = people
.GroupBy(x => x.Age)
.Select(g => { Age = g.Key, Count = g.Count() });
DefaultIfEmpty 空替换
在上面的关联查询中我们使用了 DefaultIfEmpty 扩展方法,它表示在没有查询到指定条件的元素时使用元素的默认值代替。其实 DefaultIfEmpty 还可以指定其它的默认值,示例:
var chars = new List<string>() { "a", "b", "c", "d" };
chars.Where(s => s.Length > 1).DefaultIfEmpty().First(); // 返回 null
chars.DefaultIfEmpty("N/A").FirstOrDefault(); // 返回 "a"
chars.Where(s => s.Length > 1).DefaultIfEmpty("N/A").FirstOrDefault(); // 返回 "N/A"
SequenceEqual 集合相等
SequenceEqual 扩展方法用于比较集合系列各个相同位置的元素是否相等。示例:
int[] a = new int[] {1, 2, 3};
int[] b = new int[] {1, 2, 3};
int[] c = new int[] {1, 3, 2};
bool result1 = a.SequenceEqual(b); // true
bool result2 = a.SequenceEqual(c); // false
最后
还有一些常用和简单的扩展方法就不举例了,比如 OrderBy(排序)、Sum(求和)、Count(计数)、Reverse(反转)等,同时欢迎大家补充本文遗漏的强大或好用的 LINQ 语法糖。
上一篇: 使用 LINQ to SQL 进行多条件动态查询(上)
下一篇: 轻松上手C#中的LINQ教程
推荐阅读
-
全面了解C# .NET的强大LINQ功能
-
用16个实战练习,深入理解.NET的Linq和Lambda技术
-
C# .NET 实战技巧:深入理解 LINQ 的强大功能
-
C# .NET 实战技巧:深入理解 LINQ 的强大功能
-
【2022新手指南】Java编程进阶之路 - 六、技术架构篇 ### MySQL索引底层解析与优化实战 - 你会讲解MySQL索引的数据结构吗?性能调优技巧知多少? - Redis深度揭秘:你知道多少?从基础到哨兵、主从复制全梳理 - Redis持久化及哨兵模式详解,还有集群搭建和Leader选举黑箱打开 - Zookeeper是个啥?特性和应用场景大公开 - ZooKeeper集群搭建攻略及 Leader选举、读写一致性、共享锁实现细节 - 探究ZooKeeper中的Leader选举机制及其在分布式环境中的作用 - Zab协议深入剖析:原理、功能与在Zookeeper中的核心地位 - RabbitMQ全方位解读:工作模式、消费限流、可靠投递与配置策略 - 设计者视角:RabbitMQ过期时间、死信队列与延时队列实践指南 - RocketMQ特性和应用场景揭示:理解其精髓与差异化优势 - Kafka详细介绍:特性及广泛应用于实时数据处理的场景解析 - ElasticSearch实力揭秘:特性概述与作为搜索引擎的广泛应用 - MongoDB认知升级:非关系型数据库的优势阐述,安装与使用实战教学 - BIO/NIO/AIO网络模型对比:掌握它们的区别与在网络编程中的实际应用 - Netty带你飞:理解其超快速度背后的秘密,包括线程模型分析 - 网络通信黑科技:Netty编解码原理与常用编解码器的应用,Protostuff实战演示 - 解密Netty粘包与拆包现象,怎样有效应对这一常见问题 - 自定义Netty心跳检测机制,轻松调整检测间隔时间的艺术 - Dubbo轻骑兵介绍:核心特性概览,服务降级实战与其实现益处 - Dubbo三大神器解读:本地存根与本地伪装的实战运用与优势呈现 ----------------------- 七、结语与回顾
-
F#探险之旅(二):函数式编程(上)-函数式编程范式简介 F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“*代有语言出,各领风骚数十年”。 尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。 纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。 FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。 关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。F#中的函数式编程 从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。标识符(Identifier) 在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如: let x = 42 这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。 标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数: let add x y = x + y 这里共有三个标识符,add表示函数名,x和y表示它的参数。关键字和保留字关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是: abstract and as asr assert begin class default delegate do donedowncast downto elif else end exception extern false finally forfun function if in inherit inline interface internal land lazy letlor lsr lxor match member mod module mutable namespace new nullof open or override private public rec return sig static structthen to true try type upcast use val void when while with yield 保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是: atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixinobject parallel process protected pure sealed trait virtual volatile 文字值(Literals) 文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。 与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示: let message = "Hello World"r"n!" // 常规字符串let dir = @"C:"FS"FP" // 逐字字符串let bytes = "bytes"B // byte 数组let xA = 0xFFy // sbyte, 16进制表示let xB = 0o777un // unsigned native-sized integer,8进制表示let print x = printfn "%A" xlet main = print message; print dir; print bytes; print xA; print xB; main Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。值和函数(Values and Functions) 在F#中函数也是值,F#处理它们的语法也是类似的。 let n = 10let add a b = a + blet addFour = add 4let result = addFour n printfn "result = %i" result 可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。 当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过: let sub(a, b) = a - blet subFour = sub 4 必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。 对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。 如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进: let halfWay a b = let dif = b - a let mid = dif / 2 mid + a 需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。作用域(Scope)作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。 let defineMessage = let message = "Help me" print_endline message // error 对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。 但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。 let changeType = let x = 1 let x = "change me" let x = x + 1 print_string x 在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。 另外,如果在嵌套函数中重定义标识符就更有趣了。 let printMessages = let message = "fun value" printfn "%s" message; let innerFun = let message = "inner fun value" printfn "%s" message innerFun printfn "%s" message printMessages 打印结果: fun value inner fun valuefun value 最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。递归(Recursion)递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。 使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数: let rec factorial x = match x with | x when x < 0 -> failwith "value must be greater than or equal to 0" | 0 -> 1 | x -> x * factorial(x - 1) 这里使用了模式匹配(F#的一个很棒的特性),其C#版本为: public static long Factorial(int n) { if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); } if (n == 0) { return 1; } return n * Factorial (n - 1); } 递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。匿名函数(Anonymous Function) 定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子: let x = (fun x y -> x + y) 1 2let x1 = (function x -> function y -> x + y) 1 2let x2 = (function (x, y) -> x + y) (1, 2) 我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。 注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。 F#系列随笔索引页面