22、为啥要学 Linq
让数据处理变得简单:
// 统计一个字符串中每个字母出现的频率(忽略大小写),然后按照从高到低的顺序输出出现频率高于2次的单词和其出现的频率。
var items = s.Where(c => char.IsLetter(c))//过滤非字母
.Select(c => char.ToLower(c))//大写字母转换为小写
.GroupBy(c => c)//根据字母进行分组
.Where(g=>g.Count()>2)//过滤掉出现次数<=2
.OrderByDescending(g => g.Count())//按次数排序
.Select(g=>new { Char=g.Key,Count=g.Count()});
要想了解 Linq,必须会 Lambda,要想会 Lambda,必须会委托;
委托:是可以指向方法的类型,调用委托变量时执行的就是变量指向的方法。
delegate void D1();
class Program{
static void Main(string[] args){
D1 d = F1;
d();
d = F2;
d();
}
static void F1(){
Console.WriteLine("i am F1.");
}
static void F2(){
Console.WriteLine("i am F2.");
}
}
.NET 中定义了泛型委托Action(无返回值)和Func(有返回值),所以一般不用自定义委托类型。
class Program{
static void Main(string[] args){
Action a = F1;
a += F2;
Func<string, int> b = F3;
Console.ReadKey();
}
static void F1(){
Console.WriteLine("i am F1.");
}
static void F2(){
Console.WriteLine("i am F2.");
}
static int F3(string str){
return str.Length;
}
}
23、Linq是怎么来的
委托 + 匿名方法
Func<int, int, string> f1 = delegate (int i1, int i2) {
return $"{i1}+{i2}={i1 + i2}";
};
string s = f1(1, 2);
匿名方法可以写成lambda表达式
Func<int, int, string> f1 = (i1,i2) =>{
return $"{i1}+{i2}={i1 + i2}";
};
可以省略参数数据类型,因为编译能根据委托类型推断出参数的类型
如果委托没有返回值,且方法体只有一行代码,可省略 {}
Action<int, string> a1 = (age, name) => Console.WriteLine($"年龄{age},姓名{name}");
a1(18, "yzk");
如果=>之后的方法体中只有一行代码,且方法有返回值,那么可以省略方法体的{}以及return。
Func<int, int, string> f1 = (i1, i2) => $"{i1}+{i2}={i1 + i2}";
如果只有一个参数,参数的()可以省略。
Action<string> f1 = s => Console.WriteLine(s);
24、揭秘LINQ方法的背后
LINQ中提供了很多集合的扩展方法,配合lambda能简化数据处理。
int[] nums = new int[] { 3,99,88,77,7,8,9,66,15,7};
IEnumerable<int> items = nums.Where(i=>i>10); //using System.Linq;
使用var让编译器的“类型推断”来简化类型的声明。在LINQ中常用。
C#的var和JavaScript的var不一样,仍然是强类型的。
(*)C#中的弱类型是dynamic。
编写自己的扩展方法MyWhere来模拟Where的实现。
static IEnumerable<int> MyWhere(int[] items, Func<int, bool> filter)
{
List<int> result = new();
foreach (int i in items)
{
if (filter(i))
{
result.Add(i);
}
}
return result;
}
int[] nums = new int[] { 3, 99, 88, 77, 7, 8, 9, 66, 15, 7 };
IEnumerable<int> items = MyWhere(nums, i => i > 10);
通过yield return来让MyWhere“流水线”处理。
static IEnumerable<int> MyWhere(IEnumerable<int> items, Func<int, bool> filter)
{
List<int> result = new();
foreach (int i in items)
{
if (filter(i))
{
yield return i;
}
}
}
int[] nums = new int[] { 3, 99, 88, 77, 7, 8, 9, 66, 15, 7 };
IEnumerable<int> items = MyWhere(nums, i => i > 10);
foreach (int i in items)
{
Console.Out.WriteLine(i.ToString());
}
Console.ReadKey();
25、常用扩展方法1 Where/Count/Any
LINQ中提供了大量类似Where的扩展方法,简化数据处理。且这些扩展方法都是对 IEnumerable<T>
的扩展方法;
数组、List、Dictionary、Set等都实现了 IEnumerable 接口;
class Employee
{
public long Id { get; set; }
public string Name { get; set; }//姓名
public int Age { get; set; }//年龄
public bool Gender { get; set; }//性别
public int Salary { get; set; }//月薪
public override string ToString()
{
return $"Id={Id},Name={Name},Age={Age},Gender={Gender},Salary={Salary}";
}
}
List<Employee> list = new List<Employee>();
list.Add(new Employee { Id = 1, Name = "jerry", Age = 28, Gender = true, Salary = 5000 });
list.Add(new Employee { Id = 2, Name = "jim", Age = 33, Gender = true, Salary = 3000 });
list.Add(new Employee { Id = 3, Name = "lily", Age = 35, Gender = false, Salary = 9000 });
list.Add(new Employee { Id = 4, Name = "lucy", Age = 16, Gender = false, Salary = 2000 });
list.Add(new Employee { Id = 5, Name = "kimi", Age = 25, Gender = true, Salary = 1000 });
list.Add(new Employee { Id = 6, Name = "nancy", Age = 35, Gender = false, Salary = 8000 });
list.Add(new Employee { Id = 7, Name = "zack", Age = 35, Gender = true, Salary = 8500 });
list.Add(new Employee { Id = 8, Name = "jack", Age = 33, Gender = true, Salary = 8000 });
Where方法:每一项数据都会经过predicate的测试,如果针对一个元素,predicate执行的返回值为true,那么这个元素就会放到返回值中。
public static int Count<TSource>(this.IEnumerable source, Func<TSource, bool> predicate);
Where参数是一个lambda表达式格式的匿名方法,方法的参数e表示当前判断的元素对象。参数的名字不一定非要叫e,不过一般lambda表达式中的变量名长度都不长。
IEnumerable<Employee> items1 = list.Where(e => e.Age > 30);
Count()方法:获取数据条数
int count1 = list.Count(e => e.Salary > 5000 || e.Age < 30);
int count2 = list.Where(e => e.Salary > 5000 || e.Age < 30).Count();
Any()方法:是否至少有一条数据,如果一条都没有,返回False,否则返回True
bool b1 = list.Any(e => e.Salary > 8000);
bool b2 = list.Where(e => e.Salary > 8000).Any();
有可能比Count()实现效率高。
26、常用扩展方法2 Single/First/Order/Skip/Take
获取一条数据(是否带参数的两种写法):
- Single:有且只有一条满足要求的数据;查询1条数据,如果查询出有多条或者没有就报错;
- SingleOrDefault :最多只有一条满足要求的数据;查询1条数据,查询有多条就报错,没有就返回数据默认值比如 null/0;
- First :至少有一条,返回第一条;查询1条数据,有多条也返回1条,没有就报错;
- FirstOrDefault :返回第一条或者默认值;查询1条数据,有多条也返回1条,没有就返回数据默认值比如 null/0;
排序:
- Order() 对数据正序排序;
- OrderByDescending() 倒序排序;
list.OrderBy(e => e.Age);
对于简单类型排序
nums.OrderBy(e => e);
特殊案例:
按照最后一个字符排序;
list.OrderBy(e=> e.Name[e.Name.Length-1]);
用Guid或者随机数进行随机排序。
list.OrderBy(e=>Guid.NewGuid());
或 list.OrderBy(e=>new Random().Next());
多规则排序:
可以在Order()、OrderByDescending()后继续写ThenBy () 、ThenByDescending()。
案例:优先按照Age排序,如果Age相同再按照Salary排序,如下:
list.OrderBy(e => e.Age).ThenByDescending(e => e.Salary)
千万不要写成:list.OrderBy(e => e.Age). OrderByDescending (e => e.Salary)
限制结果集,获取部分数据:
Skip(n)跳过n条数据,Take(n)获取n条数据。
案例:获取从第2条(不包括)开始获取3条数据
var orderedItems1 = list.Skip(2).Take(3);
Skip()、Take()也可以单独使用。
27、常用扩展方法3 聚合函数Max/Min/Average/Sum/Count 分组GroupBy
聚合函数Max/Min/Average/Sum/Count 分组GroupBy
LINQ中所有的扩展方法几乎都是针对IEnumerable接口的,而几乎所有能返回集合的都返回IEnumerable,所以是可以把几乎所有方法“链式使用”的。
年龄大于30岁的最低工资:list.Where(e => e.Age > 30).Min(e=>e.Salary)
年龄大于30岁的平均工资:list.Where(e => e.Age > 30).Average(e=>e.Salary)
GroupBy()方法参数是分组条件表达式,返回值为 IGrouping<TKey, TSource>
类型的泛型IEnumerable(即:IEnumerable<IGrouping<TKey, TSource>>
),也就是每一组以一个IGrouping对象的形式返回。IGrouping是一个继承自IEnumerable的接口,IGrouping中Key属性(TKey Key{get;set;}
)表示这一组的分组数据的值。
例子:根据年龄分组,获取每组人数、最高工资、平均工资。用var简化编程。
IEnumerable<IGrouping<int, Employee>> items = list.GroupBy(e=>e.Age);
foreach(IGrouping<int, Employee> g in items){
Console.WriteLine(g.Key);
Console.WriteLine("人数:" + g.Count());
Console.WriteLine("最大工资:" + g.Max(a=>a.Salary));
Console.WriteLine("平均工资:" + g.Average(a=>a.Salary));
foreach(Employee e in g){
Console.WriteLine(e);
}
Console.WriteLine("********************");
}
28、常用扩展方法4 投影/匿名类型/
什么是投影:把集合中的每一项转换为另外一种类型。(说白了就是 Select 用法)
IEnumerable<int> ages = list.Select(e => e.Age);
IEnumerable<string> names = list.Select(e=>e.Gender?"男":"女");
IEnumerable<Dog> dogs = list.Select(p => new Dog{NickName=e.Name,Age=e.Age});
匿名类型:
var p = new {Name="tom",Id=1};
var p1 = new {name,Id=1,p.Age};
通过反编译看匿名类型原理。
投影与匿名类型结合:
var items = list.Select(e=>new {e.Name,e.Age,XingBie= e.Gender ? "男" : "女"});
var items = list.GroupBy(e => e.Gender)
.Select(g=>
new { Gender=g.Key,Count=g.Count(),AvgSalary= g.Average(e => e.Salary),MinAge= g.Min(e => e.Age)}a
);
29、集合转换/链式调用
集合转换:有一些地方需要数组类型或者List类型的变量,我们可以用ToArray()方法和ToList()分别把 IEnumerable<T>
转换为数组类型和 List<T>
类型。
List<Employee> lst = list.Where(e=>e.Salary>1000).ToList();
Employee[] arr = list.Where(e=>e.Salary>1000).ToArray();
链式调用
Where、Select、OrderBy、GroupBy、Take、Skip等返回值都是IEnumerable<T>
类型,所以可以链式调用。
例子:“获取Id>2的数据,然后按照Age分组,并且把分组按照Age排序,然后取出前3条,最后再投影取得年龄、人数、平均工资”
var items = list.Where(x=>x.Id>2)
.GroupBy(x=>x.Age)
.OrderBy(x=>x.Key)
.Take(3)
.Select(g=>
new {NL=g.Key, RS=g.Count(), PJ=g.Average(e=>e.Salary)}
);
30、LINQ另一种倩影
查询语法
使用Where、OrderBy、Select等 扩展方法进行数据查询的写法叫做 “LINQ方法语法”。还有一种“查询语法”的写法。
var items2 = from e in list
where e.Salary > 3000
orderby e.Age
select new { e.Name, e.Age, Gender = e.Gender ? "男" : "女" };
// 注意这里的 new {e.Name 等同于 new {Name = e.Name
区别
运行时没有区别,编译后都是一样的。反编译一下看看。
“查询语法”看起来更酷,但是“方法语法”更实用,因此.NET开发者大部分还是用“方法语法”。
31、LINQ 面试问题
性能与面试
LINQ大部分时间不会影响性能,不过我曾经遇到过,讲讲。
比如图像像素渲染,有三个整数,取最大值,如果用linq:
int r =2, g=3, b=5;
var nums = new int[]{r, g, b};
nums.Max();
// 因为渲染的每个像素都要执行上述创建数据操作,极大消耗资源
// 可以用三目运算 r>g ? (r>b ? r: b) : (g > b ? g : b); 或者 Math.Max(r, Math.Max(g, b))
面试时候的算法题一般尽量避免使用正则表达式、LINQ等这些高级的类库。
大饭店面试大厨的故事。大厨面试一般考察蛋炒饭,不会让你做鲍鱼龙虾,因为鲍鱼龙虾本来就鲜美,是其本身材料问题,而蛋炒粉恰好能考察这个厨师对这种家常菜的最好吃做法;
案例1
有一个用逗号分隔的表示成绩的字符串,如"61,90,100,99,18,22,38,66,80,93,55,50,89",计算这些成绩的平均值。
"61,90,100,99,18,22,38,66,80,93,55,50,89".Split(',').Average(x=>x.Convert.ToInt32(e));
案例2
统计一个字符串中每个字母出现的频率(忽略大小写),然后按照从高到低的顺序输出出现频率高于2次的单词和其出现的频率。
"hello,world.".ToUpper().ToList().GroupBy(t => t).Where(t => t.Count() > 2).OrderByDescending(o => o.Count());