前言
为了提升一下自己的姿势水平,买了 Effective C# 和 More Effective C# 两本书,想一边阅读一边强迫自己做点笔记(主要是自己的理解),不然怕是看了就忘。另外手头还有一本《深入解析C# ( C# in Depth )》其实我已经阅读了一遍,但感觉没咋读懂,下次有机会也用这种方式重读一遍。
忙着搞秋招和毕业论文,以及自己是在是太能摸了,导致距离上次更新已经快一年了。不过以后工作还是要写.NET的,所以还是不能把相关学习落下,今天开始边写变更吧。今天这篇是 Effective C# 的第四章。
第四章 合理地运用LINQ
LINQ全名Language Integrated Query,中文译为语言集成查询,简单来说就是用类似SQL语言的方式来对数据进行查询和操作,C#里提供了两种可使用的语法,分别是查询语法和方法语法。查询语法更贴近SQL语法,但其实我从来没写过...方法语法就是通过扩展方法实现了一系列方法,在数据集合之后调用,比如list.Where(n => n > 0).ToList();
,我觉得使用起来更加方便,而且这个应该能算作是函数式编程吧。LINQ给数据操作还是带来了很大的方便的,尤其是写后端的时候操作数据库,像Java也是从8开始引进了stream,用起来感觉也很相似,之前实习的时候基本都是在写stream。
第29条 优先考虑提供迭代器方法,而不要返回集合
当我们要返回一系列对象的时候,应该优先考虑提供迭代器方法而不是集合,这样可以立刻返回,并且等调用方需要实际使用的时候才会创建对应数据,节省时间和空间(就是yield return返回IEnumerable<T>
而不是直接返回一个ListList<int>
很划不来,而通过Enumerable.Range(0, int.MaxValue)
会返回IEnumerable<int>
,可以在后续使用的时候再去生成数值。而如果调用方真的需要对这些数据反复使用或者缓存下来,那让他们自己调用ToList()
或者ToArray()
就好了,相当于提供了两种选择,方便灵活使用,因此即使调用方确实需要把数据保存到集合里,我们还是应该优先提供迭代器方法(其实LINQ就是基于这个原则的)。
另外在编写迭代器方法的时候要注意一个问题,就是参数检查和生成序列的逻辑分开写。因为如果合在一起的话:
// 求正数序列
public static IEnumerable<int> GeneratePositiveNumber(int first)
{
// 参数检查
if (first <= 0)
{
throw new ArgumentException("first must be postive", nameof(first));
}
// 序列生成
var num = first;
while (num <= int.MaxValue)
{
yield return num;
num++;
}
}
如果按照上面这么写的话,当进行var nums = GeneratePositiveNumber(-11);
的时候,并不会抛出异常,只有当真正使用到nums
里面的数值的时候,才会抛异常,这样很不合理而且难以诊断错误。因为编译器固定如此,无法修改,但我们可以把检查和生成分成两段函数来规避这个问题,例如:
// 求正数序列
public static IEnumerable<int> GeneratePositiveNumber(int first)
{
// 参数检查
if (first <= 0)
{
throw new ArgumentException("first must be postive", nameof(first));
}
// 序列生成
return GeneratePositiveNumberImpl(first);
}
// 序列生成的实现
private static IEnumerable<int> GeneratePositiveNumberImpl(int first)
{
var num = first;
while (num <= int.MaxValue)
{
yield return num;
num++;
}
}
这样的话,调用的时候,在进入到迭代器方法之前就能抛出异常。
第30条 优先考虑通过查询语句来编写代码,而不要使用循环语句
C#提供了经典的比如for、foreazh、while等循环结构,但他们的功能其实也可以通过查询语句来实现。与采用循环语句所编写的命令式结构相比,查询语句能够更为清晰地表现开发着的意图。比我想要打印一批成绩高于80分的学生的ID和姓名,用循环语句可以写成:
// 学生的定义
class Student
{
public int Id { get; set; }
public string Name { get; set; }
public int Score { get; set; }
}
// 假设有100个学生的列表
var students = Enumerable.Repeat(new Student(), 100).ToList();
// 生成存放结果的列表
var idAndNameTextList = new List<string>();
// 逐个保存学生的ID和姓名
for (var i = 0; i < students.Count; i++)
{
var student = students[i];
if (student.Score >= 80)
{
idAndNameTextList.Add($"{students[i].Id}: {students[i].Name}");
}
}
// 逐个打印学生的ID和姓名
foreach (var text in idAndNameTextList)
{
Console.WriteLine(text);
}
可以发现这种写法更注重的是操作的方式而非操作的意图,上面这个例子因为简单所以还比较好能够一眼看出是在干什么,但是复杂的情况下可能就比较麻烦。而查询语句的写法如下,一句话解决:
// 挑选出学生中分数大于80分的,合并ID和姓名后把每个都打印出来
// ToList()是因为微软没给IEnumerable<T>提供ForEach的扩展方法,我不懂为什么,不过不少工具库里都会提供
students.Where(s => s.Score >= 80).Select(s => $"{s.Id}: {s.Name}").ToList().ForEach(t => Console.WriteLine(t));
可以看到这种写法的可读性很好,语义清晰。而且LINQ还提供了比如Max()、Min()、OrderBy()这些方法,可以大幅简化原来使用循环语句的写法(而且我觉的这样的链式调用真的很爽)。
第31条 把针对序列的API设计得更加易于拼接
未完待续(起码等我把毕业的事情搞完吧 )