Linq原理

R-C.jpg

委托

委托(delegate):是一种引用类型变量,用于存储某个方法的引用地址。

定义格式:

public delegate 返回值类型 委托类型名字 (参数类型1 参数名字, 参数类型2 参数名字,.....);

案例:

public delegate int Calculate(int x,int y);
static public int Add(int x,int y)
{
    return x+y;
}
static public int Multiply(int x,int y)
{
    return x*y;
}

Calculate cal0=new Calculate(Add);
Calculate cal1=new Calculate(Multiply);
//等同于
Calculate cal0=Add;
Calculate cal1=Multiply;
//调用方法
cal0.Invoke(1,2);
cal1(1,2);
Invoke调用委托方法与()直接调用相同

委托存在的意义

回调方法(CallBack Method):当某个任务执行完毕后或者某事件触发后,调用的方法。

  • 按钮彼此只有皮肤不同,所以肯定是在同一Class Button通过new 出来的对象。
  • 按钮被点击后,所执行的任务彼此不同,即调用了不同的响应方法。
  • 按钮对象中会保持一个回调方法,当点击后调用此回溯方法。
class Button{
    //1.声明一个点击消息响应的方法类型签名(委托)
    public delegate void OnClickDelegate();
    //2.声明一个OnClickDelegate类型委托
    public OnClickDelegate onclick=null;
    //3.点击事件触发后,调用onclick委托存储的方法
    public void Click()
    {
        if(onclick!=null)onclick();
    }
}

注意:

  1. 委托是可以指向方法的类型,调用委托变量时执行的就是变量指向的方法。
  2. .NET中定义了泛型委托Action(无返回值)和Func(有返回值),所以一般不用自定义委托类型。

委托变量不仅可以指向普通方法,还可以指向匿名方法。

 static void Main(string[] args)
 {
	Action f1=delegate(){
        Console.WriteLine("liuliu");
    };
     f1();
     
    Action<string, int> f2 = delegate (string n, int i){
    	Console.WriteLine($"n={n},i={i}");
	};
    f2("liuliu",18);
     
    Func<int, int, int> f3 = delegate (int a, int b){
    	return a + b;
	};
	Console.WriteLine(f3(1, 2)); 
 }

匿名方法可以写成Lambda表达式

Lambda

Lambda表达式(Lambda Expression)是一个匿名函数,即没有函数名的函数。

C#中的Lambda表达式使用Lambda运算符=>表示,该运算符读为goes to

运算符将表达式分为两部分,左边是输入的参数,右边是表达式的主体

(参数列表) => {表达式或者语法块}

static void Main(string[] args)
{
    //可以省略参数数据类型,因为编译能根据委托类型推断出参数的类型,用=>引出来方法体
    Func<int, int, int> f3 = (a, b)=>
    {
        return a + b;
    };
    Console.WriteLine(f3(1, 2)); 
    
    //如果委托没有返回值,且方法体只有一行代码,可省略{} 
	Action f1=()=>Console.WriteLine("liuliu");
     f1();
    
    //如果=>之后的方法体中只有一行代码,且方法有返回值,那么可以省略方法体的{}以及return
    Func<int, int, int> f3 =(a, b)=> a + b;
	Console.WriteLine(f3(1, 2)); 
    
    //如果只有一个参数,参数的()可以省略
    Action<string, int> f2 = n => Console.WriteLine($"n={n},i={i}");
    f2("liuliu");
    Func<int,bool> f3 = n => n>0;
    Console.WriteLine(f3(1));
}

IEnumerable

IEnumerable(可枚举/迭代的)

IEnumerable:是C#中的一个泛型接口,位于System.Collections.Generic命名空间中。

它定义了一个名为GetEnumerator的方法,该方法返回一个实现了IEnumerator接口的枚举器。
通过实现IEnumerable接口,任何类型的集合都可以使用foreach语句或其他基于枚举的技术进行迭代。

image-20241108111548613

任何实现了 IEnumerable 接口的类型都可以使用 foreach 循环进行遍历。这是因为 foreach 循环依赖于 GetEnumerator() 方法来获取迭代器,并使用该迭代器来访问集合中的每个元素。

使用场景:

IEnumerable常用于表示可枚举的集合,如列表(List)、数组(arr[])、字典(Dictionary<TKey, TValue>)的键或值集合等。
它也用于LINQ查询的结果,使得开发者能够以一种统一的方式处理各种数据源。

IEnumerator(迭代器)

IEnumerator接口与IEnumerable紧密相关,它提供了遍历集合的方法。

image-20241108112225245

主要成员:

  1. Current:获取枚举器当前位置的元素。(只读属性)
  2. MoveNext():将枚举器前进到集合的下一个元素。
  3. Reset():将枚举器设置为其初始位置。通常不建议使用,因为它可能不是所有枚举器都支持。

Linq扩展方法

由于IEnumerable的重要性,C#为其定义了许多扩展方法,这些方法可以在任何实现了该接口的集合上调用。

常见的扩展方法:
  • Any(this IEnumerable source): 检查集合是否包含任何元素(是否至少有一条数据),性能高于Count()。
  • Count(this IEnumerable source): 返回集合中的元素数量。
  • Single(this IEnumerable source): 有且只有一条满足要求的数据。
  • SingleOrDefault(this IEnumerable source): 最多只有一条满足要求的数据。
  • First(this IEnumerable source): 返回集合中的第一个元素。
  • FirstOrDefault(this IEnumerable source): 返回集合中的第一个元素,如果集合为空,则返回默认值。
  • Where(this IEnumerable source, Func<T, bool> predicate): 根据指定的条件筛选集合中的元素。
排序补充:
  • Order(): 对数据正序排序;
  • OrderByDescending(): 倒叙排序;

注意:可以在Order()、OrderByDescending() 后继续写ThenBy()、ThenByDescending()。

案例:优先按照Age排序,如果Age相同再按照Salary排序

list.Order(e=>e.Age).ThenBy(e=>e.Salary)

限制结果集,获取部分数据:

Skip(n)跳过n条数据,Take(n)获取n条数据。

案例:获取从第2条开始,获取3条数据

var num=list.Skip(2).Take(3);

分组:

GroupBy() 方法参数是分组条件表达式,返回值为IGrouping<TKey, TSource>类型的泛型IEnumerable,也就是每一组以一个IGrouping对象形式返回。

IGrouping是一个继承自IEnumerable的接口,IGrouping中的Key属性表示这一组的分组数据的值。

投影:

把集合中的每一项转换为另外一种类型。

IEnumerable ages=list.Select(e => e.Age);

大部分都在System.Linq命名空间中。

匿名类型:

不使用匿名类型:Person p = new Person {Name="liuliu" , Id = 1};

使用匿名类型:var p = new {Name="liuliu" , Id = 1};

var p1 = new {name , Id =1 , p.Age };

匿名类型只能写 var 因为不能确定后面new 的类型。

yield

yield关键字用于遍历循环中,yield return用于返回IEnumerable,yield break用于终止循环遍历。

yield 关键字的用途是把指令推迟到程序实际需要的时候再执行,这个特性允许我们更细致地控制集合每个元素产生的时机。如果我们不使用 yield 关键字,则意味着需要把集合数据装载到内存中等待被使用,这可能导致数据在内存中占用较长的时间。

简单地说,当希望获取一个IEnumerable类型的集合,而不想把数据一次性加载到内存,就可以考虑使用yield return实现"按需供给"。

通过 yield 返回的 IEnumerable<T> 类型,表示这是一个可以被遍历的数据集合。它之所以可以被遍历,是因为它实现了一个标准的 IEnumerable 接口。一般,我们把像上面这种包含 yield 语句并返回 IEnumerable<T> 类型的方法称为迭代器(Iterator)

//不用yield
static void Main(string[] args)
{
    int[] arr= { 1, 2, 3 ,11 ,4 ,15};
    IEnumerable<int> arr2 = MyWhere(arr,x => x >10);
    foreach (int x in arr2)
    {
        Console.WriteLine(x);
    }

}
static IEnumerable<int> MyWhere(IEnumerable<int> items ,Func<int,bool> f)
{
    List<int> list = new List<int>();
    foreach (int x in items)
    {
        if(f(x)==true)
        {
            list.Add(x);
        }    
    }
    return list;
}
//用yield
static void Main(string[] args)
{
    int[] arr= { 1, 2, 3 ,11 ,4 ,15};
    IEnumerable<int> arr2 = MyWhere(arr,x => x >10);
    foreach (int x in arr2)
    {
        Console.WriteLine(x);
    }

}
static IEnumerable<int> MyWhere(IEnumerable<int> items ,Func<int,bool> f)
{
    foreach (int x in items)
    {
        if(f(x)==true)
        {
            yield return x;
        }    
    }
}

扩展方法

扩展方法(Extension Methods):是一种特殊的静态方法,它允许我们向现有的类或接口添加新的方法,而无需修改原始类的定义或创建子类。通过扩展方法,我们可以在不改变已有代码的情况下,为现有类型添加新的功能。

扩展方法的定义需要满足以下几个条件:

  1. 扩展方法必须定义在一个静态类中。
  2. 扩展方法必须是静态的。
  3. 扩展方法的第一个参数必须使用this关键字来标识,指定要扩展的类型。
// 定义一个静态类
public static class StringExtensions
{
    // 定义一个扩展方法,用于将字符串反转
    public static string Reverse(this string str)
    {
        char[] charArray = str.ToCharArray();
        Array.Reverse(charArray);
        return new string(charArray);
    }
}
// 不使用扩展方法
string reversedString=StringExtensions.Reverse("Hello World");
// 使用扩展方法
string originalString = "Hello World";
string reversedString = originalString.Reverse();
Console.WriteLine(reversedString);  // 输出:dlroW olleH

需要注意的是,扩展方法只是一种语法糖,编译器会将扩展方法调用转换为对应的静态方法调用。因此,扩展方法并不能真正地修改原始类的定义,只是提供了一种更方便的方式来使用已有类型。

Linq查询方法基本上都是扩展方法,Enumerable类为IEnumerable接口扩展

**注意:**如果扩展方法与类型中原有方法同名,调用原有方法。

LINQ to SQL数据模型

LINQ to SQL 有查询表达式语法(Query Expression)和方法语法(Fluent Synatx)两种可供选择。

1. 查询表达式语法

查询表达式语法是一种接近于SQL语法的查询方式。(个人认为比较适合用于多表查询)

var 结果集 = from c in 数据源 where 过滤表达式 orderby 排序 
select c

注意:

  1. 查询表达式语法于SQL语法相同。
  2. 查询表达式必须以from子句开头,以 select 或 group by 子句结束。
  3. 可以使用过滤、连接、分组、排序等运算符继续筛选操作,构造查找结果。
  4. 可以用隐式 var 类型变量保持查询结果。

2. 方法语法

也就是前面提到的lambda,方法语法又称流利语法,利用System.Linq.Enumerable 类中定义的扩展方法和Lambda表达式进行查询,类似于调用类的扩展方法。

//IEnumerable<T>可以替换为 var 
IEnumerable<T> query=数据源集合.Where(bool类型的过滤表达式).OrderBy(排序条件).Select(选择条件)
    

**注意:**在使用LINQ进行数据处理时需要首先创建数据上下文对象 db,即" DataClasses1DataContext db = new DataClasses1DataContext();"

3.LINQ增删改查

查询
普通查询:

select sname,age from student

//基于Linq查询语句查询
var stu=from s in db.student
        select new{s.sname,s.age };

//基于Lambda表达式查询
var stu = db.student.Select(s => new { s.sname, s.age });

foreach (var s in stu)
{
    Console.WriteLine(s.sname+","+s.age);
}
条件查询:

select * from student where age>18 and sex='女' order by age desc

//基于Linq查询语句查询
var stu = from s in db.student
          where s.age>18 && s.sex =="女"
          orderby s.age descending
          select s;

//基于Lambda表达式查询
var stu = db.student.Where(c => c.age > 18 && c.sex=="女").OrderByDescending(c=>c.age);

foreach (var s in stu)
{
    Console.WriteLine(s.sname+"," +s.sex+ "," + s.age + ","+s.dept);
}
多表查询:

select student.sname,course.cname,sc.grade from sc join student on sc.sno=student.sno join course on sc.cno=course.cno

//基于Linq查询语句查询
 var stu = from sc in db.sc
           join c in db.course on sc.cno equals c.cno
           join s in db.student on sc.sno equals s.sno
           select new
           {
               s.sname,
               c.cname,
               sc.grade
           };

//基于Lambda表达式查询 
var stu = db.sc.Select(g => new { g.student.sname, g.course.cname, g.grade });

 foreach (var s in stu)
 {
     Console.WriteLine(s.sname+"," + "," + s.cname + ","+s.grade);
 }
插入

使用LINQ to SQL 插入数据时,首先创建一条记录,然后判断该记录对应的主键在数据源中是否存在

如果不存在则调用**InsertOnSubmit()方法将该记录添加到数据源,调用SubmitChanges()**方法保持修改

//插入之前先查找有没有该数据
var data = db.student.FirstOrDefault(s=>s.sno=="202204300")

if (data == null)
{
    student xm = new student
    {
        sno=="202204300",
        sname = "小明",
        sex = "男",
        age = 18,
        dept = "宣传部"
    };
    //执行插入操作
    db.student.InsertOnSubmit(xm);
    db.SubmitChanges();
    Console.WriteLine("插入成功");

}
//如果该数据已存在
else
{
    Console.WriteLine("该数据已存在,无法插入");
}

修改

SubmitChanges () 计算要插入、更新或删除的已修改对象的集,并执行相应命令以实现对数据库的更改。

因为数据可能存在外键关系,通常情况下主键不允许修改。

var stu = db.student.SingleOrDefault(s=>s.sname=="小明");
if (stu == null)
{
    Console.WriteLine("不存在该学生信息");
    return;
}
else
{
    stu.sname = "小明";
    stu.sex = "女";
    stu.age = 21;
    stu.dept = "宣传部";
    //执行相应命令以实现对数据库的更改
    db.SubmitChanges();
    Console.WriteLine("信息更新成功");

}
删除

使用LINQ to SQL删除数据时,先检查该记录是否存在,如果存在则调用**DeleteOnSubmit()方法从数据源中移除该记录,然后调用SubmitChanges()**方法对数据源进行保存。

var stu = db.student.SingleOrDefault(s=>s.sname=="小明");
if (stu != null)
{
   db.student.DeleteOnSubmit(stu);
    db.SubmitChanges();
    Console.WriteLine("删除成功!");
}
else
{

    Console.WriteLine("该记录不存在,无法删除!");

}
注意:

在LINQ to SQL中,DeleteOnSubmitDeleteAllOnSubmit是两个用于删除数据的方法,它们的主要区别在于操作的对象数量和方式:

  1. DeleteOnSubmit:此方法用于标记单个实体对象为待删除状态。当你调用DeleteOnSubmit方法时,你只能传递一个对象作为参数,这个方法会将这个对象标记为需要删除。在调用SubmitChanges之后,只有这个特定的对象会被删除。

    示例代码:

    var entity = context.Table.Single(e => e.Id == someId);
    context.Table.DeleteOnSubmit(entity);
    context.SubmitChanges();
    
  2. DeleteAllOnSubmit:此方法用于标记一个实体对象的集合为待删除状态。当你调用DeleteAllOnSubmit方法时,你可以传递一个对象的集合作为参数,这个方法会将这个集合中的所有对象标记为需要删除。在调用SubmitChanges之后,这个集合中的所有对象都会被删除。

    示例代码:

    var entities = from e in context.Table where e.SomeCondition select e;
    context.Table.DeleteAllOnSubmit(entities);
    context.SubmitChanges();
    

总结来说,DeleteOnSubmit用于单个对象的删除,而DeleteAllOnSubmit用于多个对象的批量删除。在使用这些方法时,需要注意它们对数据库操作的影响,以及可能需要处理的外键约束问题。