ASP.NET 期末突击


PPT第一章:

C/S结构:使用局域网。客户端响应速度快,但可维护性差。

B/S结构:适用于广域网环境。应用程序及数据库系统都安装在服务器上,客户端只需安装一个浏览器软件。处理功能都运行在服务器端。维护方便,但服务器负担重。

两层架构:用户------表示逻辑层------数据库层

三层架构:用户------表示逻辑层------商务逻辑层------数据库层

N层架构:用户------表示逻辑层------商务逻辑层------服务层------数据库层

.NET FRAMEWORK主要包括CLR(Common Language Runtime,公共语言运行时),还有.NET FRAMEWORK类库。


PPT第二章 ASP.NET基础:

静态页面:HTML格式的网页通常被称为静态页面。以.htm、.html、.shtml、.xml等常见形式为后缀。

动态页面:以.asp、.aspx、.jsp、.php、.perl、.cgi等形式为后缀,语言使用HTML+ASP(ASP.NET) 或 HTML+PHP 或 HTML+JSP 等。

两者区别:
1).更新和维护
静态网页内容一经发布到网站服务器上,无论是否有用户访问,这些网页内容都是保存在网站服务器上的。如果要修改网页的内容,就必须修改其源文件,然后重新上传到服务器上。静态网页没有数据库的支持,当网站信息量很大的时候网页的制作和维护都很困难。

动态网页可以根据不同的用户请求,时间或者环境的需求动态的生成不同的网页内容,并且动态网页一般以数据库技术为基础,可以大大降低网站维护的工作量。
**2)**交互性
静态网页由于很多内容都是固定的,在功能方面有很大的限制,所以交互性较差。

动态网页则可以实现更多的功能,如用户的登录、注册、查询等。
**3)**响应速度
静态网页内容相对固定,容易被搜索引擎检索,且不需要连接数据库,因此响应速度较快。

动态网页实际上并不是独立存在于服务器上的网页文件,只有当用户请求时服务器才返回一个完整的网页,其中涉及到数据的连接访问和查询等一系列过程,所以响应速度相对较慢。

.html文件:.html文件由HTML元素构成,HTML元素可以使用HTML语言或者XHTML语言,后者相比前者语法更加严格。HTML语言称为超文本标记语言,利用标记标识信息,一个标记称为一个元素,每个标记都用一对<>括起。HTML语言是解释型语言,不需要经过编译。

一个有效的HTML文档通常包括三大部分:版本信息,说明性HTML标题(HEAD),文档主体。

<head> </head>是所有头部元素的容器,包含文档信息

<titile>我的个人主页</titile>:定义文档标题,是<head>中必须包含的元素。

<meta>是空元素,空元素不需要像<titile>我的个人主页</titile>那样包裹中间的文本,也不需要</titile>那样的闭合。

<meta charset="UTF-8">:告诉浏览器网页用什么语言编码写成。

<meta content=...>:content属性不能单独使用,必须配合name或者http-equiv一起出现,它是meta标签里的值。

<meta name="description" content="这是一个网页">:name是content的搭档,用来定义元数据的类型。它告诉浏览器,我现在要描述网页的哪个方面。

name="description"是网页的简介,name="keywords"是网页的关键词,content此时应等于"关键词1,关键词2,关键词3"。

http-equiv:也是content的搭档,直接给浏览器发令,告浏览器如何处理页面。

3s后自动跳转:<meta http-equiv="refresh" content="3;url=https://www.baidu.com">。

文档主体(body):文档主体是html文档的主要部分,包含实际的文档内容。在<body></body>之间的内容将显示在浏览器窗口的用户区内。

HTML表单:HTML表单包含了表单内部控件和相应的布局信息。HTML表单是在Web页中的<form>和</form>标记之间定义的控件组,用于让用户输入数据并提交。例如:

html 复制代码
<form method="post" action ="page.html">
	输入您的用户名:
  <input type="text" name="username">
	<input type="submit" name="ok" value="提交" >
</form>

其中action告诉浏览器把数据发给哪个服务器地址(URL),method设定发送方式。如果没有<form>包装控件,那么浏览器不知道讲数据发往何处。

Web表单:Web表单中则包含了表单内部控件、相应的布局信息及数据提交后的数据处理代码。在HTML文档中,控件可以独立存在,也可以有多个表单,但在ASP.NET中,只能有一个Web表单,且所有控件都必须位于表单之中。

html 复制代码
<form runat="server">
	......
</form>

加上runat="server"会让ASP.NET在生成HTML时自动填好action和method。并且ASP.NET会将数据都提交回当前页面自己,由服务器端的C#代码来判断刚才到底是谁点了什么。

注:没带runat="server"的是静态元素,直接发给浏览器,带了runat="server"的是服务器控件,服务器会处理它,把它翻译成HTML。

在.aspx中,虽然我们写的是web表单,但是最终还是由web表单生成html表单。html表单是浏览器唯一能理解的数据提交方式。

ASP开发模式:ASP.NET Web Forms,ASP.NET MVC,ASP.NET Core。

.aspx网页文件格式:一般包含三个独立的部分,页面指令,代码脚本块,页面内容。

页面指令:核心元素是@指令(最常见的是@Page,该指令用于定义ASP.NET页分析器和编译器使用的页特定属性,只能包含在.aspx文件中;还有@Import指令,该指令可将命名控件显式导入到ASP.NET应用程序文件中,导入的命名控件可以是.NET Framework类库或用户定义的命名空间的一部分,该指令语法形式如<%@ Import Namespace="value"%>),不显示给用户看,而是给ASP.NET编译器看。一般写的时候写为<%@ ... %>,%是服务器端界定符,表示其中的内容不是普通的html文字,而是服务器需要执行的代码或指令。@则是指令标记。虽然指令可以位于.aspx文件中的任何位置,但是通常情况下将指令放在文件的开头。每个指令都可以包含一个或者多个属性,它们与相应的值成对出现。

html 复制代码
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="WebApplication1.WebForm1" %>

Language="C#"指定了语言,CodeBehind告诉服务器逻辑代码在哪个文件中。

代码脚本块:这部分包含具体的程序代码,根据运行位置不同,分为两类。

一类是服务器端脚本<script runat=server>,在服务器端运行,通常是C#代码。

另一类是客户端脚本<script type="text/javascript">,处理不需要请求服务器就能完成的动作,就是普通的HTML<script>标签。

页面内容:这是用户在浏览器中真正能看到的部分,也就是<html><body>标签力的内容,它由静态网页元素和服务器空间组成。如果页面包含允许用户交互并提交的控件,则该页面必须包含一个form元素。form元素必须包含runat属性,其属性值设置为server。

注:服务器代码可以选择代码隐藏或者代码内嵌,代码隐藏就是将代码单独放在.aspx.cs文件中,它们通过@Page指令中的CodeBehind属性连接在一起。代码内嵌就是将代码放在上面提到过的<script runat="server"> ... </script>标签中,它会直接把C#代码放在.aspx文件中。

举个例子:

html 复制代码
<%-- 页面指令放在第一行 --%>
<%@ Page Language="C#" %> 

<%-- 版本信息 --%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<script runat="server"> 

<%-- 代码脚本块 --%>
</script> 
<html xmlns="http://www.w3.org/1999/xhtml" >


<%-- 页面内容 --%>
<%-- head --%>
<head runat="server">
    <title>无标题页</title>
</head>

<%-- 文档主体 --%>
<body>
    <form id="form1" runat="server">
    <div>    
    </div>
   </form>
</body>
</html>

ASP.NET编译过程

单文件模型:aspx文件------生成C#新类------生成页面

代码隐藏页模型的页面运行机制:aspx文件------由从aspx生成的类和aspx.cs文件------组合生成一个新的类------生成页面


ppt第三章 C#基础:

C#本身无类库,而是直接使用.NET框架所提供的类库。C#不推荐使用指针,若要使用指针,必须使用关键字unsafe将代码声明为非安全的。C#采用垃圾回收机制,能够在合适时机回收不再使用的内存资源。

隐式using指令:编译器根据项目类型自动把最常用的命名空间引用了。

全局using指令:只需在一个文件写一次global using MyNamespace,项目力所有其他.cs文件都对自动包含这个命名空间,不用再重复写了。

Console.Writeline(string s);

string s=Console.Readline();

访问修饰符:

internal:允许同一程序集(.dll或者.exe)中的应用程序可以访问。

public:任何程序集都可以访问。

protected:允许跨越程序集,但仅限于继承关系。

protected internal:允许同一程序集中的应用程序可以访问、所属类或派生类中能被访问。

readonly:只读变量,初始化后不能修改。和const的区别在于readonly是访问修饰符,而const只是变量修饰符。

整数类型:

sbyte,有符号八位整数 byte,无符号八位整数

short,有符号十六位整数 ushort,无符号十六位整数

int uint

long ulong

object类型:object类型是System.Object类的别名。object类型是所有类型的基类,object类型的变量可以引用任何类型的数据。当我们把值类型赋值给object时,会发生装箱,系统会在堆上分配一块新内存,将栈上的值复制到堆上的这块新内存中,object变量存储的是堆上这块内存的地址。当把object还原回值类型时,系统检查object指向的堆内存中是否真的包含对应值类型的数据,如果匹配,就把堆上的值复制回栈上的变量。

string类型:是System.String类型的别名,实质是一种字符数组。使用ToString()可以转换为string类型,使用Parse()或者Convert类的相应方法可以转换为其他类型。

面向对象编程的优点:程序可维护性好;对象可以多次使用,可重用性好。

base:用于从派生类中访问基类成员,常用于在子类构造函数中初始化基类------base(a),可以用base直接指代基类,或者调用基类中已经被重写的成员。

可空类型:C#中,引用类型天生就是null,但值类型绝对不能是null。我们在值类型后可以加上?,这样int? b=null; 就可以正确运行了。T?本质上是System.Nullable<T>结构的语法糖,上面的代码等价于System.Nullable<int> b = 10; 。Nullable<T>提供两个核心属性,bool HasValue()和T Value()。前一个用于判断是否有值,后一个用于获取具体的值。

和可空类型相关的两个运算符:空条件运算符?.和空合并运算符??。?.用于安全访问,虽然主要用于引用类型,但也常用于可空类型。当使用?.访问成员时,如果实例本身为null,就立刻停止直接返回null。空合并运算符??的含义是如果左边为null,就用右边的值,否则用左边的值。

set和get访问器:在类内声明属性如public int Age{ get; set; }时,可以以这样的方式声明get和set访问器,访问器不能被显式调用,当我们试图对Age赋值时,隐式调用set,当我们试图对Age取值时,隐式调用get。

完整属性:我们需要在get,set里写额外的逻辑。

自动属性:如public string Name { get; set; }。这是C#的语法糖,如果不需要额外的逻辑,C#可以自动生成一个私有字段

只读属性:代码如下,对于外部来说,使用Id属性就像使用一个字段一样。属性的取值和赋值逻辑由get和set的逻辑决定。

cs 复制代码
private int _id; // 显式声明的私有字段

public int Id
{
    get { return _id; }
    // 没有 set
}

计算属性:没有对应的存储字段,值依赖实时计算,代码如下:

cs 复制代码
public class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }

    // 【计算属性】Area
    // 它不存数据,它是算出来的
    public double Area
    {
        get 
        { 
            return Width * Height; 
        }
    }
    
    // 使用 Lambda 表达式简写计算属性(更常见)
    public double Perimeter => (Width + Height) * 2;
}

字段:本质上就是类内变量。属性本质上就是包含get和set方法的代码块,对外提供访问数据的接口,可以在赋值或读取时加入逻辑控制。前者一般定义为private,后者一般定义为public。

多态:使用virtual和override可以实现真正的多态。在子类中使用new关键字修饰方法会隐藏父类中的同名方法。

抽象类和接口:抽象类abstract class不是完整的,但可以有普通方法和字段。接口interface只能有方法声明,但不能有方法定义,也不能有字段。两者都不能直接实例化,都只能使用继承自它们的子类。abstract修饰方法时,父类不能实现该方法,子类必须自己实现该方法。但virtual修饰方法时父类可以自己实现该方法,子类可以选择使用父类的版本或者是自己的版本。如果一个类包含任何一个抽象方法(abstract method),那么这个类必须被定义为抽象类。

sealed关键字:sealed用于修饰类,表明该类不能被继承,sealed修饰方法,表明在子类中该方法不能被重写,只能使用当前版本的方法。

static class:静态类只有静态成员,静态类不能被实例化。

partial class:分部类允许将同一个类的代码拆分在多个不同文件中,编译器在编译时会把这些碎片自动拼在一起。

泛型类:如class Name<T>{}; 其实就是模板,但换了一种语法。


ppt第四章:ASP.NET编程概念详解。

我们在visual studio写的代码(.aspx和.cs后台代码)是服务器端代码,浏览器是看不懂这些代码的。运行流程为:浏览器发起请求------服务器运行你的代码------服务器将运行结果翻译成标准的HTML------发送给浏览器。

回发机制:用户输入网址,服务器生成页面给用户后。当你在页面上点击按钮(提交信息)时,表单数据会被发送给同一个页面(也就是服务器上的自己),服务器接收到数据之后,重新运行这个页面的代码,处理你的逻辑。处理完后,服务器再次生成新的HTML发回给浏览器。

HTTP(Hyper Text Transfer Protocol,超文本传输协议):特点有

1.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。

2.灵活:HTTP允许传输任意类型的数据对象。正在传输的数据由Content-Type加以标记。

3.无连接:无连接的含义是限制每次连接只处理一个请求,服务器处理完客户的请求,并收到客户的应答后马上断开连接。采用这种方式可以节省服务器的资源。

4.无状态:web服务器在生成html并完成发送后,会马上断开连接,并销毁服务器内存中的这个网页对象实例。当你在网页中点击按钮时,服务器会重新创建一个全新的页面对象,它根本不知道上一个对象中存了什么数据,这就是无状态。因此,ASP.NET会把所有状态打包成加密字符串(即视图状态),并藏在HTML中,以让无状态的HTTP协议看上去能够记忆状态。

HTTP方法特性:HttpGetHttpPost特性规定了该方法只接待哪种类型的HTTP请求。我们常常写两个名字一摸一样的方法,但是处理不同的逻辑,这两个标签就是用来区分它们的。HttpGet用来告诉服务器只有当浏览器发送GET请求时,才运行这个方法。HttpPost告诉服务器只有当浏览器提交数据(Post请求)时,才运行这个方法。

复制代码
public class AccountController : Controller
{
    // 场景1:用户仅仅是输入网址想看登录页面
    // 浏览器发起的是 GET 请求 -> 只有贴了 [HttpGet] 的方法会响应
    [HttpGet] 
    public ActionResult Login()
    {
        return View(); // 返回包含空表单的 HTML
    }

    // 场景2:用户填好了用户名密码,点击了"登录"按钮
    // 浏览器发起的是 POST 请求 -> 只有贴了 [HttpPost] 的方法会响应
    [HttpPost]
    public ActionResult Login(string username, string password)
    {
        // 这里写验证逻辑,查数据库等
        if (CheckUser(username, password))
        {
            return RedirectToAction("Index");
        }
        return View();
    }
}

依赖注入(DI):依赖注入就是不要在类中用new创建对象,而是留个接口,让系统把对象传过来。示例如下:

cs 复制代码
// 【接口】 定义合同:只要是工具,必须能干活
public interface ITool 
{ 
    string Work(); 
}

// 【实现】 具体的工具:这是一把锤子
public class Hammer : ITool 
{ 
    public string Work() => "🔨 锤子咣咣砸!"; 
}

builder.Service负责告诉容器,当有人要某个接口时,该给他创建什么具体的类,以及这个类的寿命有多长。常用的有三种方法,分别是AddTransient,AddScoped,AddSingleton。

AddTransient是每次找容器要的时候,都给你用new创建一个全新的对象。

AddScoped是在一个HTTP请求的处理过程中(从浏览器请求到服务器返回),不管你在这个请求中用了多少次这个服务,拿到的都是同一个对象。请求结束,对象销毁。

AddSingleton:整个应用程序启动后,只创建一个对象。以后所有人,所有请求来拿,都是同一个。

cs 复制代码
// ============== Program.cs (启动文件) ==============
var builder = WebApplication.CreateBuilder(args);

// 【Step A: 注册】 (AddTransient / AddScoped / AddSingleton)
// 告诉容器:如果有人要 "ITool",就给他一个新的 "Hammer"
builder.Services.AddTransient<ITool, Hammer>(); 

builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();

此时,类接受的是接口。并且该服务类通过构造函数注入。

cs 复制代码
// ============== WorkerController.cs (使用者) ==============
[Route("/")]
public class WorkerController : ControllerBase
{
    private readonly ITool _tool;

    // 【Step B: 构造函数注入】
    // 并没有写 new Hammer(),而是直接要在构造函数里"我们要一个工具"
    public WorkerController(ITool tool)
    {
        _tool = tool; // 容器会自动把 Hammer 传进来
    }

    [HttpGet]
    public string DoJob()
    {
        return _tool.Work(); // 输出:"🔨 锤子咣咣砸!"
    }
}

FromServices特性:如果你有一个很大的服务,但在WorkController的10个方法中,只有一个方法用到了它。如果使用构造函数注入的话,每次创建Controller都要初始化这个大服务,即使很少用到它。因此,我们可以使用FromServices只在具体方法中临时申请该服务。

cs 复制代码
public class ReportController : Controller
{
    // 注意:构造函数里没有 IBigService

    // 只在这个方法里临时请求注入
    public IActionResult Download([FromServices] IBigService bigService)
    {
        bigService.Run();
        return Ok();
    }
}

如果你的服务自身参数也包含另一个服务,同样通过构造函数注入。我们只需要在builder.Services中add对应的服务,容器会自动完成组装。

如果服务参数是具体的值,需要使用lambda表达式来手动告诉容器如何创建。

cs 复制代码
public class Hammer : ITool 
{ 
    private string _name;
    public Hammer(string name) 
    { 
        _name = name; 
    }
}

// ============== Program.cs ==============

// 这里的写法变了:
// 我们不只是给个类名,而是给了一个"小函数"告诉容器怎么 new
builder.Services.AddTransient<ITool>(provider => 
{
    // 在这里,你可以手动传参
    return new Hammer("雷神"); 
});

DI的好处在于页面或控制器不需要直到具体的类是谁,只需要知道接口。修改具体实现代码时,不需要改动页面代码。你不需要手动用new创建对象,也不需要手动销毁,容器会帮你管理内存。如果一个类的构造函数中注入了太多东西,说明它管得太宽了,违反了单一职责原则。


C#中数据结构的实现:

集合类型 添加元素 删除并返回 获取但不删除 数量属性
List<T> Add(item) Remove(item) / RemoveAt(index) list[index] Count
Dictionary<K,V> Add(key, value) Remove(key) dict[key] Count
HashSet<T> Add(item) Remove(item) (无索引,只能判断存在) Count
Queue<T> (队列) Enqueue(item) Dequeue() Peek() Count
Stack<T> (栈) Push(item) Pop() Peek() Count

所有集合清空都使用Clear(),包含判断都使用Contains(item),计数都使用Count属性(Array使用Length)。

注意:1.Queue使用Enqueue()和Dequeue()方法,Stack使用Push()和Pop()方法,它们都是用Peek()来查看队首/栈顶元素。

2.Dictionary的Add需要两个参数(Key和Value)。

LINQ:提供统一的查询语法、支持筛选/排序/投影、延迟执行、简化代码。

方法语法和查询语法:

特性 查询语法 (Query Syntax) 方法语法 (Method Syntax)
外形 类似 SQL 语句 标准的 C# 方法调用
核心组件 关键字 (from, where, select) Lambda 表达式 (n => n > 10)
可读性 在处理 多表连接 (Join)分组 (Group) 时,非常清晰易读 在处理 简单筛选链式调用 时,非常简洁
完整性 不完整 (有些功能没有对应的关键字) 完整 (包含了 LINQ 的所有功能)
本质 它是语法糖,最终转换为方法语法 它是编译器真正认识的代码

方法语法:

cs 复制代码
IEnumerable<int> firstAndLastFive = numbers.Take(5).Concat(numbers.TakeLast(5));

在C#中,只要实现了IEnumerable<T>接口,那么就可以被视作序列处理。序列可以使用LINQ查询。

常用操作符:

Where:作用是筛选出符合条件的元素,例如:

cs 复制代码
// 找出所有及格(分数大于60)的学生
var pass = students.Where(s => s.Score >= 60);

Select:作用是只拿出想要的部分,例如:

cs 复制代码
// 1. 只想要学生的名字 (变成 List<string>)
var names = students.Select(s => s.Name);

// 2. 想要名字和分数 (变成一个新的匿名对象)
var infos = students.Select(s => new { s.Name, s.Score });

OrderBy:从小到大排序,OrderByDescending,从大到小排序。例如:

cs 复制代码
// 按分数从高到低排
var ranking = students.OrderByDescending(s => s.Score);

GroupBy:归类,把一堆数据切分成好几堆。例如:

cs 复制代码
// 按班级分组
var groups = students.GroupBy(s => s.ClassId);

foreach(var group in groups)
{
    Console.WriteLine($"班级ID: {group.Key}"); // 组的钥匙
    foreach(var s in group) { ... } // 组里的学生
}

GroupBy最终得到的序列可以说是一个由链表实现的哈希表结构。

cs 复制代码
var result = students
    .GroupBy(s => s.ClassId) // 1. 先分成几个袋子
    .Select(g => new         // 2. 针对每个"袋子(g)"进行统计
    {
        BanJi = g.Key,             // 拿标签(几班)
        RenShu = g.Count(),        // 数袋子里有几个元素
        PingJunFen = g.Average(s => s.Score) // 算袋子里分数的平均值
    });

上面代码中Select处理的单位是组,也就是Key相同的元素组成的链表。然后筛选出链表的Key,链表的Count,以及链表中元素的平均值作为匿名类型元素的成员,组成新的序列。

注:var infos = students.Select(s => new { s.Name, s.Score });中序列元素如果不提供名字的话,就沿用之前的名字Name和Score。

Join:连接,把两个集合按某个共同属性拼在一起,例如:

cs 复制代码
// students.Join(目标集合, 自己的外键, 对方的主键, 结果生成器)
var result = students.Join(
    classes,              // 1. 跟谁连?(班级表)
    s => s.ClassId,       // 2. 我用什么连?(我的ClassId)
    c => c.Id,            // 3. 对方用什么连?(它的Id)
    (s, c) => new { s.Name, c.ClassName } // 4. 连好后你要啥?(学生名+班级名)
);

注:在LINQ中操作序列时,想要得到序列中元素数量不能直接使用Count属性,应该使用Count方法!

注:C#中new {}表示创建匿名类型,即编译器生成的临时类(只读)。此时不能用new ()。

注:C#中{}没有构造的功能,大括号是初始化器,只负责设置属性。比如这几行代码:

.Select(g => new // 2. 针对每个"袋子(g)"进行统计
{
BanJi = g.Key, // 拿标签(几班)
RenShu = g.Count(), // 数袋子里有几个元素
PingJunFen = g.Average(s => s.Score) // 算袋子里分数的平均值
});

这里的new后面省略了(),实际上应该是new () { BanJi=...... },()负责构造,大括号只是相当于语法糖的东西,功能为赋值。

聚合函数:

函数 含义 示例代码 备注
Count 数人头 students.Count() students.Count(s => s.Score > 60) 括号里可以加条件,一边过滤一边数
Sum 求和 students.Sum(s => s.Score) 必须告诉它把哪个属性加起来
Average 平均值 students.Average(s => s.Score) 算出所有学生的平均分
Max 最大值 students.Max(s => s.Score) 最高的那个分数是多少
Min 最小值 students.Min(s => s.Score) 最低的那个分数是多少

EF Core:EF Core是由微软推出的轻量级,跨平台的ORM(Object-Relational Mapper)框架。Object是C#代码中的类,Relational指数据库中的表。Mapper负责将这两者对应起来。没有EF Core之前,需要手写SQL语句,有了EF Core之后,只需要操纵C#对象,EF Core自动帮你生成SQL并执行。

持久化:将数据保存下来,使其在程序重启后依然存在。

实体类(Entity Classes):实体类是EF Core中最基础的单元。它们只是普通的C#类,用于定义数据的结构。这里类对应数据库的表,属性对应表中的列,类的实例对应表中的一行数据。值得注意的是,EF Core依靠约定。例如,名为Id的属性会自动被识别为数据库的主键。

DbSet<T>属性:DbSet<T>是DbContext类中的属性,它表示数据库中某个特定表的所有行(实体)的集合。我们通过DbSet执行增删查改的操作。DbSet也是一个IQueryable,意味着可以用LINQ来写查询语句,EF Core会把这些C#代码翻译成SQL语句。

注:IQueryable继承自IEnumerable,如果数据已经在内存里了,使用IEnumerable,如果数据还在数据库里,一定保持IQueryable状态直到过滤完成。

DbContext类:负责协调实体对象和数据库之间的交互。主要职责包括:

  • 数据库连接:配置连接字符串,决定连哪个数据库(SQL Server, SQLite, MySQL 等)。

  • 配置模型 :通过 OnModelCreating 方法,你可以通过代码详细配置表与表之间的关系(Fluent API)。

  • 变更追踪 (Change Tracking) :这是它最神奇的地方。它是"有记忆"的。当你从 DbSet 取出一个对象并修改了属性,DbContext 会默默记下"这个对象变了"。

    • 保存数据 :当你调用 SaveChanges() 时,DbContext 会把所有记下来的变更打包成 SQL 事务,一次性提交给数据库。

      cs 复制代码
      using Microsoft.EntityFrameworkCore;
      
      // 1. 实体类定义 (货物)
      public class Student
      {
          public int Id { get; set; }
          public string Name { get; set; }
          public int Age { get; set; }
      }
      
      // 2. DbContext 类 (仓库管理员)
      public class SchoolContext : DbContext
      {
          // 3. DbSet 属性 (货架)
          // 告诉 EF Core:数据库里有一张表叫 Students,存的是 Student 类型的数据
          public DbSet<Student> Students { get; set; }
      
          // 配置数据库连接
          protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
          {
              // 这里使用简单的 SQLite 数据库作为示例
              optionsBuilder.UseSqlite("Data Source=school.db");
          }
      }
      
      // --- 实际使用场景 ---
      class Program
      {
          static void Main()
          {
              // 创建上下文实例 (开启会话),using(var context...)会确保在离开大括号后自动销毁context
              using (var context = new SchoolContext())
              {
                  // 确保数据库已创建
                  context.Database.EnsureCreated();
      
                  // 创建一个实体对象
                  var newStudent = new Student { Name = "Gemini", Age = 1 };
      
                  // 通过 DbSet 添加数据
                  context.Students.Add(newStudent);
      
                  // 通过 DbContext 保存更改
                  // 此时 EF Core 会生成: INSERT INTO Students (Name, Age) VALUES ('Gemini', 1);
                  context.SaveChanges(); 
              }
          }
      }

关系配置

主键:当前表中每行数据的唯一标识,是一个整数值。

外键:用于在另一张表中进行索引,是另一张表中的主键。

导航属性:它是一个对象引用,或者说,它是数据库中一行数据的引用。通过使用导航属性,我们不需要通过外键在SQL中去查找对应的数据(或者说,我们不需要写那样繁琐的代码),我们可以直接通过对象引用来获取该对象的属性,EF Core会自动完成对应的查找工作。

一对多关系:比如一个班级有多个学生,但是一个学生只属于一个班级。我们需要在两头都写上代码:在一的一端(班级),加上一个列表List<Student>,在多的一端(学生)加上一个引用Class和一个外键ClassId。外键的命名规范为"导航属性名+Id"。

注:这里即使不加上属性ClassId,EF Core也会在后台生成一个隐藏的外键。之所以加上这个外键,是为了编程方便。而一的那方之所以不存外键数组是因为SQL的一个格子只能存一个值,不能存数组或列表。

cs 复制代码
// "一" 的一方:班级
public class Class
{
    public Class{
        Students=new HashSet<Student>();
    }
    public int Id { get; set; }
    public string ClassName { get; set; }

    // 导航属性:一个班级包含"很多"学生
    // List<T> 表示这里有很多个
    public ICollection<Student> Students { get; set; } 
}

// "多" 的一方:学生
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }

    // 1. 外键 (Foreign Key):数据库里实际存的那一列
    // 命名规范:[导航属性名] + Id
    public int ClassId { get; set; } 

    // 2. 导航属性 (Navigation Property):
    // 允许你通过 student.MyClass 直接访问班级对象
    public Class MyClass { get; set; } 
}

注:ICollection继承自IEnumerable,不同于IEnumerable只能读,ICollection定义了标准集合应该有的功能,Add,Remove,Clear,Count。

注:List<Student> 是一个 C# 内存中 的容器,它保存了一组 引用 (References)。 在EF Core的帮助下,操作这些引用间接操作了数据库对应的行。EF Core为了性能,绝对不会去填充这个List,它是空的。虽然List为空,但是其实这时候表与表之间的映射关系已经建立。它们之间的匹配关系为Class.Id=Student.ClassId。想要填充List(填充List就是在建立表与表的关系),需要自己手动进行。

cs 复制代码
var myClass = context.Classes

.Include(c => c.Students) // <--- 下令:填充 List!

.FirstOrDefault(c => c.Id == 1);

Include的作用是修改表达式树,告诉SQL生成器之后需要把关联表中符合匹配条件的行防止在List中。它是延迟执行的方法。

FirstOrDefault的作用是查找结果中符合条件的第一条,或者返回默认值。它会按照Include修改后的条件去进行查找并将结果填充到List中。

多对多关系:例如学生和课程之间的关系,学生可以选多门课,一门课也可以被多个学生选。

cs 复制代码
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    // 我选了很多课
    public List<Course> Courses { get; set; } 
}

public class Course
{
    public int Id { get; set; }
    public string Title { get; set; }
    // 我有很多学生
    public List<Student> Students { get; set; } 
}

隐含着匹配关系:

对于CourseA元素的Students列表中所有Student元素,你都可以在它的List<Course>中找到某个CourseB的Id,它的Id等于CourseA的Id。

对于StudentA元素的Courses列表中所有Course元素,你都可以在它的List<Student>中找到某个StudentB的Id,它的Id等于StudentA的Id。

一切匹配关系都依赖主键和外键之间的匹配。

EF Core中的查询操作

LINQ to Entities:它是一种技术,允许你使用C#语法(LINQ)来编写查询,然后由EF Core自动将其翻译成数据库能听懂的SQL语句。它是连接面向对象代码(C#)和关系型数据库(SQL)的桥梁。

特性 LINQ to Objects LINQ to Entities
操作对象 内存中的集合 (List, Array) 数据库中的表 (DbSet)
接口 IEnumerable<T> IQueryable<T>
执行逻辑 直接运行 C# 代码 (委托 Delegate) 解析表达式树,翻译成 SQL
能力限制 能用所有 C# 方法 只能用能翻译成 SQL 的方法

延迟执行:在EF Core中,查询分为两个完全割裂的阶段------构建阶段和执行阶段。在构建阶段,当你调用如Where,Select时,会返回IQueryable<T>。此时EF Core只是在内存中修改表达式树,但没有生成SQL,也没有连接数据库。在执行阶段,当我们调用ToList,Count,FirstOrDefault时,返回具体的List<T>,int,bool对象,此时会将表达式树翻译成SQL,发送给数据库,并把返回的数据转换成对象。

延迟执行允许我们在C#逻辑中动态组装SQL,而不用担心性能浪费。

类型 方法举例 返回值 行为
延迟执行方法 (Deferred) Where OrderBy Select Skip / Take Include GroupBy Join IQueryable<T> 只修改表达式树。 不查数据库。
立即执行方法 (Immediate) 转换类ToList, ToArray, ToDictionary 单值类First, Single, Find 统计类Count, Any, All, Sum 遍历foreach List, int, bool, Entity 编译 SQL 并发送。 立马查数据库。

延迟加载和立即加载:立即加载就是使用.Include()在查询主数据时就查询关联数据,而延迟加载则是在查询主数据完成后,访问关联数据时才自动完成对关联数据的查询。前者查询完成后,访问速度快,但在关联数据较多时容易耗尽内存,后者内存使用量更小,但在查询次数过多时性能显著下降。

增删查改和SaveChanges方法:新增对象用Add,删除对象用Remove。

SaveChanges():在你调用这个方法之前,所有的Add,Remove等操作,全都是在内存中进行,数据库中一行数据都没变。当你调用context.SaveChanges()时,EF Core会检测变化,根据状态生成对应的SQL语句,然后进行批处理。接着数据库执行SQL,如果执行成功,数据库进行回填。

注:当我们用new创建一个元素时,Id通常默认值为0,大多数数据库的主键被设定为自增,只有数据库有权力知道下一个Id是多少,因此最后需要数据库回填。还有其他的一些东西也需要数据库回填,但不再介绍。

事务处理:数据库事务是指作为单个逻辑工作单元执行的一系列操作。事务必须满足原子性。举个例子,如果事务中包含操作A和操作B,假如操作A执行成功,操作B执行失败,那么将会把操作A撤销,回到完全没有执行过事务的状态。当你调用EF Core时,通常不需要写任何事务代码,EF Core默认帮你完成。当你调用一次SaveChanges()时,EF Core会自动把它生成的所有SQL语句包裹在一个事务中。

有时可能需要显式事务,代码如下:

cs 复制代码
// 1. 手动开启事务
using var transaction = context.Database.BeginTransaction();

try
{
    // --- 第一步:保存订单 ---
    context.Orders.Add(newOrder);
    context.SaveChanges(); // SQL 已执行,但数据只是"暂存"在数据库,别人还看不见

    // --- 第二步:做点别的 (比如再保存日志) ---
    // 假设这里可能通过 context2 或者原生 SQL 操作
    context.Logs.Add(newLog);
    context.SaveChanges(); 

    // --- 第三步:甚至可能有复杂的 C# 逻辑 ---
    if (someCondition == false) 
        throw new Exception("业务检查失败!");

    // 4. 只有运行到这里,才算真正成功!
    transaction.Commit(); // 【提交】:让所有改变永久生效
}
catch (Exception)
{
    // 5. 如果上面任何一行报错,跳到这里
    transaction.Rollback(); // 【回滚】:撤销上面所有的 SaveChanges
    // (注:使用 using 块时,如果没有 Commit,退出时会自动 Rollback,但这行写出来更清晰)
}

连接字符串:如果说DbContext是仓库管理员,那么连接字符串就是记录仓库位置的纸条,没有它,EF Core根本找不到数据库在哪。

EF Core使用连接字符串主要有两种方式:

A.直接把连接字符串写死在OnConfiguring方法中。

B.把连接字符串放在appsettings.json文件里。


线程和异步编程

线程基础:Thread类,必须手动定义一个方法(委托),然后把它交给线程运行。

cs 复制代码
void DoWork() { Console.WriteLine("工作线程正在运行..."); }

Thread t = new Thread(DoWork); // 1. 创建
t.Start();                     // 2. 启动(此时线程进入 Ready 状态,等待 CPU 调度)

线程状态与生命周期:线程的状态有:

Unstarted:new Thread()创立线程之后,Start()之前。

Running:正在被CPU执行,或者准备好被执行。

WaitSleepJoin(阻塞/等待):这是最常见的非运行状态,线程因为某些原因(如Sleep,Join,等待锁释放)暂停,不占用CPU资源。

Stopped:方法执行完毕,线程销毁,不可复活。

关键方法:Thread.Sleep(int ms),让当前线程睡眠指定毫秒数。哪怕设为0,也会触发一次CPU时间片的让出。

t.Join():如果在线程A中调用t.Join(),那么A会暂停,一直等到线程t执行结束,A才会继续往下走。

线程同步:当多个线程同时访问同一个资源时,如果不加控制,数据就会乱套。我们需要锁来保证同一时间只有一个线程能访问关键代码。

A.lock关键字

cs 复制代码
// 最佳实践:定义一个私有的、只读的引用类型对象作为锁
private readonly object _lockObj = new object(); 

public void SafeMethod()
{
    // 只有一个线程能进入这个花括号
    lock (_lockObj) 
    {
        // 临界区:在这里修改共享变量
        // ...
    }
}

B.Monitor,lock实际上是编译器生成的语法糖,他编译后的代码就是Monitor。_lockObj是真正的锁,lockTaken只是记录是否抢到了锁,以应对极端情况下的Bug,防止未获取锁时释放锁。

cs 复制代码
// lock 的真实面目
bool lockTaken = false;
try
{
    Monitor.Enter(_lockObj, ref lockTaken); // 尝试获取锁
    // 临界区代码
}
finally
{
    if (lockTaken)
    {
        Monitor.Exit(_lockObj); // 确保无论是否报错,锁一定会被释放!
    }
}

C.lock和Monitor只能锁住同一个程序(进程)内的线程,如果想要锁住不同的程序,需要用到Mutex,Mutex是操作系统内核级别的对象,比较慢。

cs 复制代码
// "GlobalMyappMutex" 是系统范围的名字
using var mutex = new Mutex(false, "GlobalMyappMutex");
if (!mutex.WaitOne(TimeSpan.FromSeconds(3)))
{
    Console.WriteLine("另一个程序正在运行,我退出了");
    return;
}
// 执行程序...
mutex.ReleaseMutex();

new Mutex(false,"GlobalMyappMutex"):false表示创建锁时不急着占有锁,另一个参数是锁的名字,操作系统依赖这个名字区分是否为同一把锁。

WaitOne函数接受一个表示时间的参数t,返回布尔值。线程会在这里等待时间t,如果时间t内没有抢到锁就放弃,并返回false。

ReleaseMutex(),当然是释放锁。

任务并行库(TPL)

Task类与Task<TResult>类:Task是对线程的高级抽象。Task代表一个异步操作。当你创建一个Task类时,你并不是创建了一个新线程,你只是创建一个新的任务,并丢进线程池的队列里。Task会被自动分配给线程池中空闲的线程执行。Task完成后,线程不会销毁,而是等待下一个Task。

Task类型没有返回值,Task<TResult>类型有返回值。

cs 复制代码
Task t = Task.Run(() => { 
    Console.WriteLine("我干完活了,但我没东西给你。"); 
});
t.Wait(); // 等待做完
cs 复制代码
// 泛型 TResult 指定返回值的类型,这里是 int
Task<int> t = Task.Run(() => {
    return 1 + 1; 
});

// .Result 会阻塞当前线程,直到拿到结果
int result = t.Result; 
Console.WriteLine(result); // 输出 2

Task.Run()接受一个委托类型,用于启动任务。

Task.Factory.StartNew():Task.Run()是Task.Factory.StartNew()的简化封装版,后者是更加底层的构建起,允许配置非常复杂的参数。后者同样接受委托类型。

任务的取消:涉及到三个point。

1.CancellationTokenSource(指挥官)

2.CancellationToken:这是传递给Task的信物,Task只能看,不能修改他。

3.Task内部代码:必须不断地检查CancellationToken。

cs 复制代码
// 1. 创建指挥官
var cts = new CancellationTokenSource();
// 获取令牌
var token = cts.Token;

Task t = Task.Run(() => 
{
    for (int i = 0; i < 100; i++)
    {
        // 2. 执行者:每次循环都检查一下令牌
        // 如果有人发出了 Cancel 信号,这行代码会抛出 OperationCanceledException 异常,结束任务
        token.ThrowIfCancellationRequested(); 
        
        // 或者温柔的写法:
        // if (token.IsCancellationRequested) return;

        Console.WriteLine($"工作进度: {i}%");
        Thread.Sleep(100);
    }
}, token); // 3. 把令牌传进去,让 Task 感知到它关联了这个令牌

// ... 过了 2 秒 ...
Console.WriteLine("不干了,取消!");
cts.Cancel(); // 4. 指挥官:按下停止按钮

cts.Cancel只是把令牌上的一个布尔值属性IsCancellationRequested修改为true,所以仅限使用并检查该令牌的Task会被kill。

异步编程

async和await关键字:虽然Task本身的执行是异步的,但是当我们想要通过获取task.Result时,仍然会阻塞在这里。所以我们引入async和await。

async:async关键字告诉编译器,这个方法中有await,需要把该方法编译成一个状态机类,这个状态机负责记录所有变量和执行位置,以便线程被释放后,知道从哪里恢复。

await关键字:await关键字后接一个Task对象。当遇到await task时,会检查task对象是否完成,如果task未完成,await会将当前线程释放回线程池,并设置一个继续点,等到task完成后,运行时会从线程池中找一个线程,回到之前暂停的继续点继续执行await后面的代码。Task完成后,await task最终会返回Task<TResult>对象最后返回的值。

Task的创建有两种语法:一是Task.Run(),二是通过async Task<T>定义方法,这个方法的核心是非阻塞等待。当调用异步方法时,仍然在当前线程上执行该异步方法,就好像只是调用一个普通的方法一样。当在异步方法中遇到await时,方法状态被保存,异步方法返回一个尚未完成的Task给workerTask,Main线程得到控制权并继续执行下一行代码。

cs 复制代码
using System;
using System.Threading.Tasks;

public class AsyncExample
{
    // 标记方法为 async,允许在内部使用 await
    public static async Task<int> DoWorkAsync(int delayMs)
    {
        Console.WriteLine($"\t[Task] 异步任务开始,等待 {delayMs/1000} 秒..."); // [A]
        
        // 遇到 await:
        // 1. 如果 Task.Delay 没完成,当前线程被释放回线程池。
        // 2. Task.Delay 在后台计时。
        await Task.Delay(delayMs); // [B]
        
        Console.WriteLine("\t[Task] 异步任务等待结束,返回结果。"); // [C]
        return 99; // 返回的结果会被自动封装在 Task<int> 中
    }

    // Main 方法也标记为 async Task (现代 C# 入口点)
    public static async Task Main()
    {
        Console.WriteLine($"[{DateTime.Now.Second}s] 主线程开始工作..."); // [1]
        
        // 调用异步方法,但尚未 await,返回一个 Task 对象
        Task<int> workerTask = DoWorkAsync(3000); // [2]

        // -----------------------------------------------------
        // !!! 关键点 !!! -----------------------------------------
        // -----------------------------------------------------

        // 证明非阻塞:尽管 DoWorkAsync 正在等待 3 秒,主线程会立即执行这一行。
        Console.WriteLine($"[{DateTime.Now.Second}s] 主线程继续执行其他任务..."); // [3]
        
        // await 等待结果:
        // 遇到 await,主线程暂停执行,直到 workerTask 完成。
        int result = await workerTask; // [4]

        Console.WriteLine($"[{DateTime.Now.Second}s] 主线程拿到结果:{result}"); // [5]
    }
}

异步方法和Main函数之间好像全程都只是共用一个线程,从这方面来讲,Main函数就好像在调用一个普通的函数一样。异步方法的价值在于当遇到await task时,如果task未完成,异步方法会返回一个Task,而不是阻塞在这里。接着会执行外层调用异步方法的函数,直到下一个阻塞点。

异步方法的返回值类型:异步方法可以返回Task<T>,Task,void三种类型。返回Task类型时的代码如下:

cs 复制代码
public async Task SaveLogAsync()
{
    await File.WriteAllTextAsync("log.txt", "...");
}

// 调用:我可以 await 它,确保它存完文件我再走
await SaveLogAsync();

当返回void类型时,调用者无法使用await,因为await后面应该接task,因此调用者不知道异步方法何时结束,也无法等待异步方法结束。

ConfigureAwait(false):只有主线程(UI)线程有资格更新界面,await默认设定是在UI线程启动,就一定回到UI线程。ConfigureAwait(false)是在告诉编译器一会儿干完活之后,随便找个线程继续执行后面的代码就行,不用非得回到原来的UI线程。语法形式如下:

cs 复制代码
await DoWorkAsync().ConfigureAwait(false);

并发集合:

由于普通集合线程不安全,多线程同时读写容易导致内部结构损坏。因此在使用的时候必须用lock锁住整个集合,即使保证了安全,性能也是问题,所以,我们引入并发集合。

并发集合的特点是线程安全(不需要手写lock),且具有高性能(内部使用细粒度锁或无锁算法),允许多个线程同时读写,效率极高。

ConcurrentDictionary<TKey,TValue>:线程安全的哈希表。接口有GetOrAdd(key,value),AddOrupdate(key,addValue,updateFunc),后者如果没有就添加,有就调用updateFunc,适合做计数器。

BlockingCollection<T>:他不是一个单一的集合,而是一个包装器,默认包裹着ConcurrentQueue。调用Take取数据时,如果集合为空,会自动阻塞,直到有人放入数据被唤醒。放入数据时调用Add方法,我们在创建BlockingCollection时,可以提供一个参数表示容量上限,如果达到上限,Add会阻塞在这里,直到有人取走数据。可以通过使用它实现生产者-消费者模型。

ConcurrentQueue<T>和ConcurrentStack<T>:仍使用Push和Enqueue作为方法名,因为没有容量上限,默认这两个操作总是会成功的。而TryDequeue,TryPeek,TryPop方法有可能失败。


异常处理和调试

System.Exception基类:所有异常类型都必须继承自System.Exception类。它有三个最核心的属性:Message(string),用于描述错误的文字信息,是给人看的。StackTrace,栈堆跟踪,记录异常发生时的方法调用链。InnerException:内部异常,用于追根溯源。假如在方法A中捕获了一个异常 FileNotFoundException,然后包装成DatabaseLoadException 抛出去,新的异常的InnerException就会指向原始的FileNotFoundException。

ArgumentException:通用的参数错误。

InvalidOperationException:对象状态不对,比如在foreach循环遍历List时,试图Add或Remove它的元素。

FileNotFoundException:文件没有找到。

异常处理结构

try-catch-finally块:

cs 复制代码
FileStream file = null;
try
{
    // 1. 尝试执行
    string content = File.ReadAllText("config.txt"); 
    int num = int.Parse(content);
}
catch (FileNotFoundException ex) // 2. 精准捕获:文件找不到
{
    Console.WriteLine("文件不存在,请检查路径。");
}
catch (FormatException ex) // 3. 精准捕获:格式不对
{
    Console.WriteLine("文件内容不是数字。");
}
catch (Exception ex) // 4. 兜底捕获:抓漏网之鱼
{
    // 注意:Exception 是所有异常的父类,必须放在最后!
    Console.WriteLine($"发生了未知错误: {ex.Message}");
}
finally
{
    // 5. 善后工作:不管上面发生了什么,这里一定执行
    if (file != null)
    {
        file.Close();
    }
    Console.WriteLine("清理完成");
}

throw关键字

throw new Exception("你好你好");

或者throw; 如果不想处理异常,只想把它往外抛的话,就写throw; 。throw 异常会把错误发生地点设定为throw 异常这一行,相当于破坏了现场。

自定义异常类:创建一个类继承自Exception,类名以Exception结尾,然后提供几个标准的构造函数。

cs 复制代码
// 1. 继承 Exception
public class InsufficientFundsException : Exception
{
    // 2. 无参构造
    public InsufficientFundsException() { }

    // 3. 带消息构造 (最常用)
    public InsufficientFundsException(string message) 
        : base(message) { }

    // 4. 带内部异常构造 (用于包裹其他异常)
    public InsufficientFundsException(string message, Exception inner) 
        : base(message, inner) { }
}

// 使用
throw new InsufficientFundsException("余额不足,当前余额: 5元");

异常处理最佳实践:

1.捕获特定异常,而不是为了省事直接捕获Exception。

2.在finally块中进行资源清理(数据库连接,文件流)。

3.异常信息记录:处理并记录 或者 记录并抛出给上层处理。不要使用空的catch块或者只是打印一个错误发生了来进行处理。


文件与流操作

文件操作:

File类:静态类,不需要new,直接调用方法。例如File.ReadAllText(path),File.WriteAllText(path,content),前者会把整个文件都读成一个巨大的string,后者会创建新文件(如果存在则覆盖,旧的文本会被删除),写入字符串。

FileInfo类:必须先new FileInfo(path)创建对象,创建对象后,这个对象就拥有了文件的所有信息,可以反复查看。

流操作:System.IO.Stream是所有流的父类(抽象类)。核心方法有Read()/Write()读写字节,Seek()调整指针位置,Flush()冲刷缓冲区。

FileStream是Stream最常用的子类,专门用于磁盘文件的字节读写。它只认识byte\[\](字节),不认识字符串。

StreamReader/StreamWriter:直接操作byte\[\]比较困难,所以我们会使用StreamReader/StreamWriter,他们会把byte自动翻译成string。

using语句与资源释放:

cs 复制代码
// 经典的块状写法
using (var fs = new FileStream(...))
{
    // 操作...
} // 到了这里自动调用 Dispose(),关闭文件


// 声明式写法
public void ProcessFile()
{
    using var fs = new FileStream("data.bin", ...);
    
    // 操作...
    
} // 方法结束时,自动 Dispose()

序列化:序列化就是把内存里的对象(Object)变成字符串或字节流,更方便存到文件里,或者直接通过网络发送给别人。反序列化就是把文件里的字符串重新变回内存里的C#对象。

JSON序列化(System.Text.Json):JSON序列化更加符合现代标准,速度极快。核心方法都在JsonSerializer静态类中:

JsonSerializer.Serialize(obj):将对象转换为JSON字符串

JsonSerializer.Deserialize<T>(json):将JSON字符串转换为T类型的对象。

XML序列化(System.Text.XML):虽然XML不如JSON流行,但在旧系统集成,配置文件或者银行/政府接口中,它还是占据统治性的低位。XML的规则比JSON严格得多。


委托,事件与lambda表达式

委托(Delegate):委托就是类型安全的函数指针。定义委托必须先定义委托的类型(即定义委托对应的函数签名),然后才能把某个具体方法赋值给委托,接着就可以像调用普通方法一样调用它了。

cs 复制代码
// 1. 定义委托 (规定:只能指向 "返回 void 且 接收一个 string 参数" 的方法)
public delegate void LogHandler(string message);

public class Program
{
    // 一个符合签名的方法
    public static void WriteToConsole(string msg)
    {
        Console.WriteLine($"Console: {msg}");
    }

    // 另一个符合签名的方法
    public static void WriteToFile(string msg)
    {
        // 假装写文件
        Console.WriteLine($"File: {msg}");
    }

    public static void Main()
    {
        // 2. 实例化 (把方法装进变量)
        LogHandler handler = WriteToConsole;
        
        // 3. 调用 (Invoke)
        handler("Hello World"); // 输出: Console: Hello World
        
        // 也可以换绑方法
        handler = WriteToFile;
        handler("Hello File");  // 输出: File: Hello File
    }
}

Action<T>和Func<T>:这两个模板是对delegate的封装,前者没有返回值,后者有返回值。Action<T>的模板参数类型就是函数的参数类型,Func<T>的模板参数类型是<参数类型,返回值类型>。

多播委托:一个委托变量可以包含多个方法,当你调用它时,它会按顺序把里面的方法全跑一遍。我们用+=添加方法(订阅),用-=移除方法(取消订阅)。如果委托有返回值,多播调用后只能拿到最后一个方法的返回值,前面的返回值被覆盖。如果链条中的一个方法报错,后面的方法就不会再执行了。

事件:事件相比委托更具封装性和安全性,它是委托的包装器。定义事件需要在委托类型前面加event关键字。事件的本质是多播。

cs 复制代码
public class Button
{
    // 1. 定义事件
    // 这里的 Action 是委托类型,Click 是事件名
    public event Action Click; 

    // 2. 触发事件的方法 (通常叫 OnEventName)
    // 只有类自己能调用!
    public void OnClick()
    {
        // ?.Invoke 检查是否有人订阅,如果没人订阅(null)就不执行
        Click?.Invoke(); 
    }
}

public class Program
{
    static void Main()
    {
        Button btn = new Button();

        // 3. 订阅事件 (只能用 +=)
        btn.Click += () => Console.WriteLine("按钮被按下了!");
        
        // 4. 取消订阅 (只能用 -=)
        // btn.Click -= ...

        // 模拟用户点击
        btn.OnClick();
    }
}

假设我们不用event关键字,直接用public委托,那么外部代码可以调用btn.Click=null把别人订阅好的事件全部清空,也可以直接调用btn.Click(),尽管用户并没有点击按钮。

加上event关键字后,编译器会禁止外部直接赋值,外部只能调用+=或者-=订阅或取消订阅事件。并且外部不能调用btn.Click(),只有定义事件的类自己才有权触发它。

事件访问器add/remove:事件访问器就像属性get/set一样,默认情况下,编译器会自动帮你生成一个私有的委托变量来存订阅者。如果想要自己控制订阅过程的话,可以显式写出add和remove。

cs 复制代码
public class CustomEventClass
{
    // 私有委托,自己存
    private Action _myDelegate;

    // 自定义事件
    public event Action MyEvent
    {
        add
        {
            Console.WriteLine("有人想订阅事件...");
            // 可以在这里加锁 (lock) 保证线程安全
            if (_myDelegate == null || _myDelegate.GetInvocationList().Length < 3)
            {
                _myDelegate += value; // 允许订阅
            }
            else
            {
                Console.WriteLine("订阅人数已满!");
            }
        }
        remove
        {
            Console.WriteLine("有人取消订阅...");
            _myDelegate -= value;
        }
    }
    
    public void Trigger()
    {
        _myDelegate?.Invoke();
    }
}

事件运行的代码的例子:

cs 复制代码
using System;

// 1. 发布者:只管"响铃"和"发通知"
public class SchoolBell
{
    // 定义事件:Action 代表不带参数的动作
    public event Action OnRing; 

    public void Ring()
    {
        Console.WriteLine("[发布者] 🔔 叮铃铃......");
        // 触发事件:如果有订阅者(不为null),就广播
        OnRing?.Invoke(); 
    }
}

// 客户端
class Program
{
    static void Main()
    {
        SchoolBell bell = new SchoolBell();

        // 2. 订阅者:用 += 挂载逻辑
        // 学生听到铃声 -> 冲向食堂
        bell.OnRing += () => Console.WriteLine("   -> 学生:冲向食堂!");
        
        // 老师听到铃声 -> 拖堂
        bell.OnRing += () => Console.WriteLine("   -> 老师:再讲两分钟...");

        // 3. 触发:铃声一响,上面两个动作自动执行
        bell.Ring();
    }
}

Invoke函数:当你定义了一个委托或事件时,Invoke方法就是让委托或事件开始执行。它和函数调用符()的功能是一样的。

cs 复制代码
Action<string> sayHello = name => Console.WriteLine("Hello " + name);

// 方式 1:隐式调用 (像调用普通方法一样) ------ 常用
sayHello("Tom"); 

// 方式 2:显式调用 (使用 Invoke) ------ 本质完全一样
sayHello.Invoke("Tom");

Invoke真正的用法应该是?.Invoke(),它会执行空检查。而()在委托是null的时候会直接崩溃。

注:当我们调用事件/委托时,所有订阅函数都接受提供给事件/委托的参数。

lambda表达式:结构是 (输入参数) => { 执行逻辑/返回值 }

简化规则:

cs 复制代码
// 原始完整版
(int x) => { return x * 10; }

// 1. 去掉类型 (Type Inference)
(x) => { return x * 10; }

// 2. 去掉参数括号 (只有一个参数时)
x => { return x * 10; }

// 3. 去掉花括号和 return (只有一行代码时)
x => x * 10

特殊情况:

没有参数必须用空括号()

cs 复制代码
() => Console.WriteLine("Hello");

多个参数,必须用括号

cs 复制代码
(x, y) => x + y;

多行逻辑,必须用{}且手写return。

cs 复制代码
x => {
    int temp = x + 1;
    return temp * 2;
};

闭包:Lambda表达式捕获并延长外部变量生命周期的机制叫做闭包。Lambda表达式捕获的是变量本身的引用,而不是值捕获。

cs 复制代码
for (int i = 0; i < 5; i++)
{
    int temp = i; // 【关键】:每次循环 temp 都是一个新的变量
    actions.Add(() => Console.WriteLine(temp)); // 捕获的是 temp
}
// 输出: 0, 1, 2, 3, 4

ASP.NET Core Web API:在传统的应用中,服务器端既负责处理数据,又负责渲染HTML,这意味着前端UI代码和后端业务逻辑高度耦合。ASP.NET Core Web API的作用是将数据层彻底剥离出来,将前端和后端的职责划分开来。

后端仅负责执行业务逻辑,操作数据库,并序列化数据,它不关心数据如何展示。

前端(比如浏览器)负责解析JSON数据并渲染UI控件。

前端和后端可以部署在不同的服务器上,同一套API接口可以同时服务于Web端,App端,无需重复编写业务逻辑。

Web API提供了标准化的HTTP接口,它将C#方法映射为标准的HTTP操作。Web API属于后端的一部分,它是后端暴露给前端的唯一入口。

本质上讲,ASP.NET Core Web API是一个HTTP消息处理器。它监听服务器端口,接收标准的 HTTP 请求(包含 URL、Header、Body),利用路由系统找到对应的 C# 控制器方法(Action),执行逻辑后,将 C# 对象序列化为 JSON 格式,封装进 HTTP 响应中返回给调用者。

ControllerBase基类:ControllerBase是Web API控制器的基础架构提供者,它是一个抽象类。Web API的本质是处理HTTP请求,当控制器被实例化时,会自动注入一些关键属性,比如HttpContext,Request,Response等等。

动作方法:动作方法是控制器类中用于处理传入HTTP请求的执行单元,简单来说,它是C#方法和HTTP网络请求之间的映射终点。当一个具体的URL请求到达服务器时,框架最终会调用这个方法来执行逻辑并生成响应。

动作方法必须满足以下条件才能被框架识别并执行:

  • 它必须是定义在 Controller 类中的 public 方法。

  • 它不能是 static 方法。

  • 它通常(但不强制)被 HTTP 动词特性 (如 [HttpGet], [HttpPost])所标记。

动作方法的返回类型:动作方法应该返回一个描述了完整HTTP响应的对象。一个HTTP响应应该包括状态码(比如200表示成功,404表示没找到,400表示BadRequest,即传递参数错误)和数据(这是给用户看的,比如查出来的User对象,List列表,或者报错的文字信息等等)。

直接返回具体类型:这种方法无法控制状态码。

IActionResult类型:可以使用OK(),NotFound(),BadRequest()等辅助方法,返回一个被包裹的C#对象。我们可以根据不同逻辑返回不同的HTTP状态码。缺点是丢失了类型信息,方法签名只写了IActionResult,我们不知道内部包裹了什么类型的对象。

ActionResult<T>类型:特点是类型安全,可以通过ActionResult<User>明确告知成功时返回的是User类。既可以成功返回T类型的数据(自动转200),也可以返回ActionResult类型的结果。

cs 复制代码
[HttpGet("{id}")]
// 明确告诉外界:我要么给你 User,要么给你 HTTP 状态码
public ActionResult<User> GetById(int id)
{
    var user = _db.Users.Find(id);
    if (user == null)
    {
        return NotFound(); // 返回 404
    }
    
    // 【亮点】不需要写 Ok(user),直接返回对象
    return user; // 自动转换为 200 OK + JSON
}

HTTP方法特性:HTTP方法特性可以让同一个URL根据不同的HTTP动词,分发给不同的C#方法。

特性 (Attribute) HTTP 动词 对应数据库操作 含义 (大白话)
[HttpGet] GET Read (查) "给我看看数据"
[HttpPost] POST Create (增) "我给你一些新数据,帮我存起来"
[HttpPut] PUT Update (改) "把这条数据彻底换成我给你的新的"
[HttpDelete] DELETE Delete (删) "把这条数据删掉"

路由:负责将客户端发来的HTTP请求地址(URL),精确地映射到代码中的某一个控制器的某个一动作方法上。路由分为两种,约定路由和属性路由。

属性路由:在控制器类或动作方法上,直接用Route特性来定义URL长什么样。对于占位符controller,框架会自动把它替换为控制器名字去掉Controller后的部分。例如UsersController会变为users,最终的基础URL就是/api/users。

cs 复制代码
[ApiController]
[Route("api/[controller]")] // 这里的 [controller] 是个占位符
public class UsersController : ControllerBase
{
    // ...
}

在动作方法上,我们结合HTTP动词特性来拼接剩余路径。最终URL=控制器路由+动作路由。

cs 复制代码
// 基础路径: /api/users

[HttpGet] // URL: /api/users
public IActionResult GetAll() { ... }

[HttpGet("{id}")] // URL: /api/users/123
public IActionResult GetOne(int id) { ... }

[HttpGet("search")] // URL: /api/users/search
public IActionResult Search() { ... }

约定路由:它不是写在Controller里的,而是写在Program.cs的全局配置中。它定义了一个通用的模板,试图套用所有的控制器。

cs 复制代码
// 在 Program.cs 中配置
app.MapControllerRoute(
    name: "default",
    pattern: "{controller}/{action}/{id?}");

它假设你的URL永远是 控制器名/方法名/ID 的格式,访问/Users/GetAll会自动找UsersController里的GetAll方法。

路由参数:使用花括号定义,是直接嵌在URL路径里的变量。

cs 复制代码
// 定义: /api/products/{id}
[HttpGet("{id}")]
public IActionResult GetProduct(int id) 
{
    // 如果 URL 是 /api/products/99
    // 这里的 id 就会自动变成 99
    return Ok(id);
}

查询参数:跟在URL的?后面,不同于路由参数,查询参数的使用不需要在路由模板中定义,只要方法参数里有同名的变量,框架会自动找。

cs 复制代码
[Route("api/test")]
public class TestController : ControllerBase
{
    // 1. 路由参数写法
    // URL: GET /api/test/100
    [HttpGet("{id}")] 
    public IActionResult GetByRoute(int id) { ... }

    // 2. 查询参数写法 (注意模板里没有 {id})
    // URL: GET /api/test?id=100
    [HttpGet] 
    public IActionResult GetByQuery(int id) { ... }
}

注:只要在特性中写了"{id}",就强制把这个参数变为了路由参数,该参数必须出现在URL的路径里。路由参数和查询参数冲突。

模型绑定与验证:核心作用是自动化地将HTTP请求中的零散数据(字符串,JSON,URL参数)转换为强类型的C#对象。

HTTP请求中的数据可能藏在URL,Body或者Header里。在写API时,为了明确与安全,我们通常使用特性来显式指定参数数据的来源。

下面是一些参数特性,一个参数特性修饰一个参数。

特性 英文含义 数据来源位置 典型场景
[FromRoute] From URL Path URL 路径 (路由模板里的 {}) 获取唯一资源的 ID (如 /users/5)
[FromQuery] From Query String URL 问号后面 (?key=val) 筛选、分页、搜索 (如 ?page=1&search=Tom)
[FromBody] From Request Body HTTP 请求体 (JSON) 创建修改复杂数据 (如提交用户注册表单)
cs 复制代码
// URL: PUT /api/users/5?version=2
// Body: { "name": "Tom", "email": "tom@abc.com" }

[HttpPut("{id}")]
public IActionResult UpdateUser(
    [FromRoute] int id,                  // 1. 从路径拿 ID (5)
    [FromQuery] int version,             // 2. 从问号后面拿版本号 (2)
    [FromBody] UserUpdateDto userDto     // 3. 从 Body 里拿 JSON 数据
) 
{
    // 此时,id=5, version=2, userDto 已经被自动填好了数据
    // 你直接用就行了!
}

模型状态验证:ModelState.Isvalid是数据经过模型绑定和模型验证后框架生成的报告,告诉你这次接受的数据是合格还是不合格。本质上它是ControllerBase类中的一个属性,类型为布尔值。值为true时说明数据格式正确,且符合验证规则。如果值为false,要么是格式错误,要么违反规则。

自定义验证特性:本质就是,写一个类。这个类必须继承自ValidationAttribute基类,并且重写IsValid方法。

假设我们的要求是,用户注册时用户名不允许叫"admin","root","system"。

cs 复制代码
using System.ComponentModel.DataAnnotations; // 必须引用这个命名空间

// 1. 继承 ValidationAttribute
public class NoAdminAttribute : ValidationAttribute
{
    // 2. 重写 IsValid 方法
    // value: 用户填写的那个具体的值 (比如 "admin")
    // validationContext: 上下文信息 (比如整个 DTO 对象)
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        // 先把 object 转成 string,方便处理
        var strValue = value as string;

        // 如果是空的,通常由 [Required] 去管,这里放行 (return Success)
        if (string.IsNullOrEmpty(strValue))
        {
            return ValidationResult.Success;
        }

        // --- 核心业务逻辑 ---
        var blackList = new[] { "admin", "root", "system" };
        
        // 检查是否在黑名单里 (忽略大小写)
        if (blackList.Contains(strValue.ToLower()))
        {
            // ❌ 验证失败!
            // 返回一个新的 ValidationResult,并附上错误信息
            return new ValidationResult("不允许使用该敏感词作为用户名!");
        }

        // ✅ 验证通过!
        return ValidationResult.Success;
    }
}

现在NoAdmin可以直接被贴在参数上。


老师划的重点,并非真的重点:

1.面向对象三大特性:封装,继承,多态。

封装:把对象的属性和方法结合成一个独立的单位,并隐藏对象内部细节,只暴露有限的接口供外部使用。

继承:子类自动拥有父类的所有属性和方法,子类可以复用父类的代码。

多态:多态通常表现为父类引用指向子类对象。多态可以使同一操作作用于不同对象,产生不同的执行结果。

2.值类型和引用类型的区别:值类型通常存储在栈上,引用类型真实数据通常存储在堆上,栈上只存储指针。在传参时,值类型的参数会重新开辟一块内存空间来存储值,而引用类型只传递内存地址,不会开辟新的内存空间,函数内部仍然是对原本的内存进行操作。

3.接口与抽象类的区别:接口内部不能有数据,只能有方法声明,不能有方法的定义。所有方法都应该由子类实现。抽象类可以有数据,可以有完整的方法定义,抽象类内部由abstract修饰的抽象方法同样只能有声明,不能有定义。

4.异常处理机制:异常处理机制是为了在逻辑发生错误时,不要让程序直接崩溃,而是优雅地处理意外。

省略

5.多线程同步:指协调多个线程对共享资源的访问顺序机制。

省略

6.泛型的类型约束:泛型的类型约束就是虽然泛型T表示任意类型,但我现在强制要求这个任意类型必须满足某些硬性条件。语法形式是public class Myclass<T> where T : 约束条件1,约束条件2, ......

常用的约束如下:

约束写法 含义 作用/解锁的能力
where T : class 引用类型约束 保证 T 必须是类、接口、数组等。 允许你把 T 赋值为 null
where T : struct 值类型约束 保证 T 必须是 int, float, bool 等结构体。 禁止传入 class。
where T : new() 无参构造函数约束 保证 T 可以被 new T() 实例化。 常用于工厂模式。
where T : 基类名 基类约束 保证 T 必须是该类或其子类。 解锁能力:可以直接访问该基类的属性和方法!
where T : 接口名 接口约束 保证 T 必须实现了这个接口。 解锁能力 :可以调用接口定义的方法(如 CompareTo)。
cs 复制代码
public class MyClass<T>
{
    public void DoSomething(T obj)
    {
        // ❌ 报错!
        // 编译器:T 是什么?它有 Name 属性吗?我不敢保证,所以我报错。
        Console.WriteLine(obj.Name); 

        // ❌ 报错!
        // 编译器:T 能不能被 new 出来?万一它是个抽象类怎么办?报错。
        T newObj = new T(); 
    }
}
cs 复制代码
// 定义一个接口
public interface IEntity
{
    int Id { get; set; }
}

// 定义泛型仓储,加上三个约束
// 注意顺序:class/struct 先写,接口中间,new() 必须放最后!
public class Repository<T> where T : class, IEntity, new()
{
    public void Add(T item)
    {
        // 1. 因为有 IEntity 约束,所以我可以大胆访问 .Id
        Console.WriteLine($"正在添加 ID 为 {item.Id} 的数据");
    }

    public T CreateNew()
    {
        // 2. 因为有 new() 约束,所以我可以大胆 new T()
        return new T();
    }
}

易错(我们老师说的易错)

1.String的不可变性:字符串一旦被初始化,就无法改变存储的值。当你在代码中修改一个字符串时,你以为是在修改它,但实际上原地址上存储的字符串会被抛弃,系统会创建一个新的对象,并把指针指向它。现在字符串拥有全新的地址。

这样做的好处有:线程安全,既然字符串的内容根本无法改变,那么多个线程读取同一字符串时也是安全的。(还有其他好处,但是有点复杂)

2.==和Equals的区别:对于值类型,两者表现完全一样,都比较值是否相等。对于引用类型,==默认比较的是引用的内存地址是否相等,Equals同样默认比较内存地址是否相同,但可以在类中重写Equals方法,修改为自己想要的逻辑。

注:String是特殊的引用类型。不论是==还是Equals都比较值。

3.ref和out参数:这两个参数都是按引用传递的实现。ref完全可以当作正常的引用使用,要求调用者必须已经初始化该对象。out则不要求调用者初始化该参数,即使已经初始化,进入方法后也会被清空视为未赋值。在方法内部,要求必须在方法结束之前给out参数一个值,否则编译器报错。

4.using语句的作用:在离开{}后,自动销毁使用using语句声明的变量。using语句本质上是try...finally的语法糖,所以即使发生异常,也能保证资源被释放。

5.Task和Thread的区别:Task类继承自Thread类,Thread类是底层的线程的概念,而Task是它的高层抽象。Thread是重量级的,开销大且功能单一,而Task是轻量级的,支持返回值,和异步等待等特性。

相关推荐
用户03284722207015 小时前
如何搭建本地yum源(上)
运维
大树884 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠4 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质4 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
小宇宙Zz4 天前
Maven依赖冲突
java·服务器·maven
Inhand陈工4 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智4 天前
ARP代理--工作原理
运维·网络·arp·arp代理
shushangyun_4 天前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化
古城小栈4 天前
Unix 与 Linux 异同小叙
linux·服务器·unix
施努卡机器视觉4 天前
SNK施努卡侧滑门锁上滑轮总成自动化装配线,从零件到组件,全流程精密制造方案
运维·自动化·制造