C# Advanced Tutorial 1-8-LambdaLinq


主要内容概要

1 匿名方法 lambda表达式
2 匿名类 var 扩展方法
3 linq to object

匿名方法

lambda演变历史

首先我们定义了如下这么些委托,还有DoNothing和Study方法:

public delegate void NoReturnNoPara();
public delegate void NoReturnWithPara(string x, string y);//1 声明委托
public delegate int WithReturnNoPara();
public delegate string WithReturnWithPara(out int x, ref int y);

第一阶段: .NetFramework1.0/1.1时代,是像下面这么写的:

NoReturnNoPara method = new NoReturnNoPara(this.DoNothing);
NoReturnWithPara noReturnWithPara = new NoReturnWithPara(this.Study);
noReturnWithPara.Invoke("Chaoqiang", "ZhengChaoqiang");

这时候是需要先定义一个方法DoNothing,然后进行绑定到委托的。

第二阶段: .NetFramework2.0 匿名方法,增加了一个delgate关键字 可以访问局部变量

int i = 10;
NoReturnWithPara noReturnWithPara = new NoReturnWithPara(delegate (string x, string y)
{
  Console.WriteLine(i);//匿名方法可以访问局部变量
  Console.WriteLine("This is Study");
});
noReturnWithPara.Invoke("Chaoqiang", "ZhengChaoqiang");

第三阶段: .NetFramework3.0 去掉delegate关键字,添加了一个=> goes to

NoReturnWithPara noReturnWithPara = new NoReturnWithPara((string x, string y) =>
{
  Console.WriteLine(i);
  Console.WriteLine("This is Study");
});
noReturnWithPara.Invoke("Chaoqiang", "ZhengChaoqiang");

更进一步的优化,省略参数类型,是编译器的语法糖(根据委托约束的):

NoReturnWithPara noReturnWithPara = new NoReturnWithPara((x, y) => 
{
  Console.WriteLine(i);
  Console.WriteLine("This is Study");
});
noReturnWithPara.Invoke("Chaoqiang", "ZhengChaoqiang");

如果方法体只有一行,可以去掉大括号和分号去掉,new NoReturnWithPara也可以省略:

NoReturnWithPara noReturnWithPara = (x, y) => Console.WriteLine("This is Study");
noReturnWithPara.Invoke("Chaoqiang", "ZhengChaoqiang");

针对一些特殊情况:

Action<string, string> action = (x, y) => Console.WriteLine($"{x}_{y}");
Action<string> action1 = x => Console.WriteLine($"{x}"); // 如果只有一个参数的时候,可以省略小括号 
Func<string, string> func = x => "杰克";  //如果有返回值,而且方法体只有一行,可以省略return; 
Func<string> func1 = () => "杰克";  //如果有返回值,而且方法体只有一行,可以省略return;

lambda表达式就是什么?
其实是一个方法, 在中间语言中,为其分配了一个方法名称;
通过反编译之后,发现Lambda表达式生成在一个名称为<> 密封类中的一个方法;
LambdaIL

lambda在多播委托中的应用

NoReturnWithPara noReturnWithPara = new NoReturnWithPara(this.Study);
noReturnWithPara += this.Study;
noReturnWithPara += (x, y) =>
{
Console.WriteLine($"{x}_{y}");
};
noReturnWithPara -= this.Study;
noReturnWithPara -= (x, y) => { Console.WriteLine($"{x}_{y}"); };  //lambda表达式在多播委托中不能被移除
noReturnWithPara("Chaoqiang", "ZhengChaoqiang");

lambda表达式在多播委托中不能被移除,因为在编译器中生成了两个不同的方法。

匿名类

第一阶段:3.0出了个匿名类

object model = new//3.0    
{
  Id = 2,
  Name = "undefined",
  Age = 25,
  ClassId = 2,
  Teacher = "Richard"
};

这时候我们发现,是无法访问属性值。object中肯定有Id/Name,C#是强类型语言,编译器不认可,

//会出错
Console.WriteLine(model.Id); 
Console.WriteLine(model.Name);

第二阶段:4.0的时候出了dynamic这个动态类型,可以解决上面匿名类的不能访问属性的问题,因为dynamic避开编译器检查。

dynamic dModel = new//
{
  Id = 2,
  Name = "undefined",
  Age = 25,
  ClassId = 2
};

dModel.Id = 134;
Console.WriteLine(dModel.Id);
Console.WriteLine(dModel.Name);
// Console.WriteLine(dModel.Js);// 编译没问题,运行会报异常

第三阶段:var语法糖
利用var构造匿名类:

var model1 = new //不能声明方法
{
  Id = 2,
  Name = "undefined",
  Age = 25,
  ClassId = 2,
  Teacher = "Richard"
};
Console.WriteLine(model1.Id);
Console.WriteLine(model1.Name);
model1.Id=3;//会出错

需要注意的是:var匿名不能声明方法,字段也只有在初始化的时候指定。
1 var 配合匿名类型使用
2 var 偷懒,复杂类型的使用

int i2 = 2;
var i1 = 1;//var就是个语法糖,由编译器自动推算
var s = "Richard";
//var aa;//var声明的变量必须初始化,必须能推算出类型

扩展方法

扩展方法:添加一个第三方静态类,把当前需要扩展的类作为参数添加方法,在第一个参数前加一个this
下面我们先看一个实例,一开始有个Student类:

public class Student
{
  public int Id { get; set; }
  public int ClassId { get; set; }
  public string Name { get; set; }
  public int Age { get; set; }

  public void Study()
  {
    Console.WriteLine("{0} {1}我们一起学习.net开发", this.Id, this.Name);
  }

  public void StudyHard()
  {
    Console.WriteLine("{0} {1}我们一起学习.net开发", this.Id, this.Name);
  }
}

随着业务的修改,如果我们需要增加一些方法,会频繁修改原来的代码,代码就很不稳定。这时候,常见的方法就是增加一个扩展方法,可以像下面这么来操作:
首先有个静态类,然后有个静态方法,再在第一个参数前面加上this

public static class ExtendMethod
{
  public static void StudyPractise(this Student student)
  {
    Console.WriteLine("{0} {1}我们一起学习.net开发-扩展方法"", student.Id, student.Name);
  }
}

这样一来,我们可以在外面调用这个扩展方法,看起来就像和实例方法是一样的:

Student student = new Student()
{
  Id = 1,
  Name = "Mr.zhang",
  Age = 25,
  ClassId = 2
};
student.Study();
student.StudyPractise(); //扩展方法;看起来跟实例方法调用方式一样

扩展方法调用,用起来很像实例方法,就像扩展了Student的逻辑,一般来说,有以下场景:

  1. 第三方的类,不适合修改源码,可以通过扩展方法增加逻辑;需要注意的是:有限调用实例方法,最怕扩展方法增加了,然后原来的类后来有被修改了。
  2. 适合组建时开发的扩展(。NET Core),定义接口或者类,是按照最小需求,但是在开发的时候又经常需要一些方法,就是通过扩展方法context.Response.WriteAsync 中间件的注册。
  3. 扩展一些常见的操作

例如下面对int的一个非空判断扩展方法:

public static int ToInt(this int? k)
{
  return k ?? 0;
  //if (k == null)
  //{
  //    return 0;
  //}
  //else
  //{
  //    return k.Value;
  //}
}

用的时候就可以直接int x = k.ToInt() + y;这么来用。

又例如对string做一个扩展方法,用于截取特定长度的字符串:

public static string ToStringNew(this string str, int length = 5)// 默认参数
{
  if (string.IsNullOrWhiteSpace(str))
  {
    return string.Empty;
  }
  else if (str.Length > length)
  {
    return $"{str.Substring(0, length)}....";
  }
  else
  {
    return str;
  }
}

用的时候可以这么来用:

string str = "中华人名共和国万岁";
string str1 = "中华人名共和国..."; 
str1.ToStringNew();

又例如泛型扩展方法,对任意类型都可以增加一个扩展方法:

public static string GenericExtend<T>(this T t)// 默认参数
{
  if (t is Guid)
  {
    return t.ToString().Replace("-","");
  }
  ///.....
  else
  {
    return t.ToString();
  }
}

用的时候可以这么来写str1.GenericExtend,对object进行扩展的时候会污染基础类型,一般少为object ,没有约束的泛型去扩展。

思考:匿名方法或者lamdba表达式是不是只能是无返回值的

Action action = () => { };
Action<string> action1 = s => Console.WriteLine("test");

Func<int> func0 = () => DateTime.Now.Day;//如果方法只有一行,去掉大括号分号return
int iResult = func0.Invoke();
Func<int,int> func1 = (s) => s;
iResult=func1.Invoke(1);

Linq

Linq To Object
.NetFramework3.0的一个非常重大的改变
Linq专门用来做数据处理

Where筛选

先看一个例子体会一下,首先有个student的列表List<Student> studentList = new List<Student>(),然后要做一些数据过滤,正常来说,下面两个都是常见的操作。

{
  var list = studentList.Where<Student>(s => s.Age < 30);
  foreach (var item in list)
  {
    Console.WriteLine("Name={0}  Age={1}", item.Name, item.Age);
  }
}

{
  Console.WriteLine("********************");
  var list = from s in studentList
             where s.Age < 30
             select s;

  foreach (var item in list)
  {
    Console.WriteLine("Name={0}  Age={1}", item.Name, item.Age);
  }
}

看到这里的时候,可能会赞叹,这个Where<Student>真不错,不仅见名知意,并且代码简介。我们先通过委托泛型表达式的知识想一想,我们自己要实现这个扩展方法,可以怎么做?

public static List<T> JerryWhere<T>(this List<T> resource, Func<T, bool> func)
{
  var list = new List<T>();
  foreach (var item in resource)
  {
    if (func.Invoke(item)) //  /* if (item.Age < 30)*///bool //item做为一个参数  返回一个bool 
    {
      list.Add(item);
    }
  }
  return list;
}

这个扩展方法就是传递一个Func委托进去,然后逐个遍历,通过委托的方法去判断是否添加到list里面去,最后统一返回一个list容器。
更进一步的,这里的List还不够generic,我们可以采用这个IEnumerable,并结合迭代器(模式),写成下面这样:

public static IEnumerable<T> JerryWhereIterator<T>(this IEnumerable<T> resource, Func<T, bool> func)
{
  foreach (var item in resource)
  {
    Console.WriteLine("开始检查");
    Thread.Sleep(300);
    if (func.Invoke(item))
    {
      yield return item; // yield 和Enumerabl 是配合使用,按需获取/延时加载
    }
  }
}

通过在foreach中加Console.WriteLine("开始检查")可以发现,上述两种方法,前者是全部检查完了再返回,后者是按需获取,在用的时候采取执行,也就是延迟加载,逐个返回的。
后者的另外一个优点是只要实现了IEnumerable接口的类型,都可以用,例如数据,集合等。

Select投影

例如在上例中,筛选出的一堆学生要转换成另一个对象:
写法一:

//var 在这里是为了匿名类
var list = from s in studentList
           where s.Age < 30
           select new
           {
             IdName = s.Id + s.Name,
             ClassName = s.ClassId == 2 ? "Normal Class" : "Other Class"
           };

foreach (var item in list)
{
  Console.WriteLine("Name={0}  Age={1}", item.ClassName, item.IdName);
}

写法二:

var list = studentList.Where<Student>(s => s.Age < 30)//条件过滤
                      .Select(s => new//投影
                      {
                      Id = s.Id,
                      ClassId = s.ClassId,
                      IdName = s.Id + s.Name,
                      ClassName = s.ClassId == 2 ?  "Normal Class" : "Other Class"
                      })
                      .OrderBy(s => s.Id)//排序
                      .OrderByDescending(s=>s.ClassId)//降序 只会生效最后一个
                      //.ThenBy//2个都生效
                      .OrderByDescending(s => s.ClassId)//倒排 只会生效最后一个
                      .Skip(2)//跳过几条 
                      .Take(3)//获取几条
                      ;

分组group by

同样有两种写法,首先筛选年级小于30的学生集合s,其次按照学生的班级进行分组,得到sg。
方法一:

var list = from s in studentList
           where s.Age < 30
           group s by s.ClassId into sg
           select new
           {
             key = sg.Key,
             maxAge = sg.Max(t => t.Age)
           };
foreach (var item in list)
{
  Console.WriteLine($"key={item.key}  maxAge={item.maxAge}");
}

方法二:

var list = studentList.GroupBy(s => s.ClassId).Select(sg => new
{
  key = sg.Key,
  maxAge = sg.Max(t => t.Age)
});
foreach (var item in list)
{
  Console.WriteLine($"key={item.key}  maxAge={item.maxAge}"); 
}

值得注意的是,这里的sg,实现了IEnumerable接口,可以进行foreach操作;并且这个key就是班级编号,可以这么进行验证:

foreach(var data in sg)
{
  Console.WriteLine(data.Key);
  foreach (var item in data)
  {
    Console.WriteLine($"{item.Id} {item.Name} {item.Age}"); 
  } 
}

join查询

多表查询join学生的班级关联班级,然后组成匿名类,包含学生姓名,班级名称:
方法一:

var list = from s in studentList
           join c in classList on s.ClassId equals c.Id //条件的比较 只能使用 equals  不能用==
           select new
           {
             Name = s.Name,
             CalssName = c.ClassName
           };
foreach (var item in list)
{
  Console.WriteLine($"Name={item.Name},CalssName={item.CalssName}");
}

方法二:

var list = studentList.Join(classList, s => s.ClassId, c => c.Id, (s, c) => new
{
  Name = s.Name,
  CalssName = c.ClassName
});
foreach (var item in list)
{
  Console.WriteLine($"Name={item.Name},CalssName={item.CalssName}");
}

左连接

从学生列表的班级Id去join班级,是以学生列表为准,如果学生列表中的班级ID在班级列表中没有,则班级为空。
方法一:

 var list = from s in studentList
            join c in classList on s.ClassId equals c.Id
            into scList
            from sc in scList.DefaultIfEmpty()//  在Linq语句中只有左连接  DefaultIfEmpty 关键字
            select new
            {
              Name = s.Name,
              CalssName = sc == null ? "无班级" : sc.ClassName//c变sc,为空则用
            };
foreach (var item in list)
{
  Console.WriteLine($"Name={item.Name},CalssName={item.CalssName}");
}
Console.WriteLine(list.Count());

方法二:

var list = studentList.Join(classList, s => s.ClassId, c => c.Id, (s, c) => new
{
  Name = s.Name,
  CalssName = c.ClassName
}).DefaultIfEmpty();//为空就没有了

总结一下:
Linq to object
在Enumerable类,正对满足IEnumerable数据,指的是内存数据
Linq–Where: 把对数据过滤的通用操作完成,把可变的过滤逻辑交给委托;
Linq–Select:把对数据转化的通用操作完成,把可变的转换逻辑交给委托;
Linq–OrderBy,…
Linq其实就是把对数据操作的通用部分完成,把可变的交给委托,使用者只关心可变部分即可,Linq就是这样一个封装。

Linq to SQL
针对IQueryable数据,操作数据库
程序访问数据库,都需要Ado.net+Sql
封装一下通用数据库操作,可变的SQL语句,是通过表达式目录树来传递,这个可以被解析的。

IQueryable<Student> list = studentList.AsQueryable();
list.Where<Student>(s => s.Age > 30);

需要注意的是:这里传递的是表达式目录树,不是匿名方法,需要可以被解析的。

Linq to XML
封装与喜爱通用的对XML文件的操作,可变的操作通过委托来传递

这是一种伟大的封装思想,希望通过一种模式完成一切数据源的访问。


文章作者: Chaoqiang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Chaoqiang !
评论
 上一篇
C# Advanced Tutorial 1-9-Expression C# Advanced Tutorial 1-9-Expression
主要内容概要1 什么是表达式目录树Expression2 动态拼装Expression3 基于Expression扩展应用4 ExpressionVisitor解析表达式目录树5 解析Expression生成Sql6 Expression扩
下一篇 
C# Advanced Tutorial 1-7-Event C# Advanced Tutorial 1-7-Event
主要内容概要一只猫的故事这里有个例子,一只猫叫了一声,会有一系列的动作,如下: public void Miao() { Console.WriteLine("{0} Miao", this.GetType().Name); new
  目录