
语法1// 2. 序列化 → 反序列化 深拷贝varjsonJsonConvert.SerializeObject(obj,_defaultSettings);// 对象 → JSON字符串returnJsonConvert.DeserializeObjectT(json,_defaultSettings)!;// JSON字符串 → 新对象深拷贝 序列化 反序列化对象 → JSON字符串 → 新对象│ │ ││ JsonConvert JsonConvert│ .Serialize .Deserialize│ │ │└─────────┴──────────┘完全独立的新对象varcustomer1newCustomer{Name张三,Age25};varcustomer2deepCopy(customer1);customer2.Name李四;Console.WriteLine(customer1.Name);// 张三不变Console.WriteLine(customer2.Name);// 李四深拷贝 先把对象转成字符串 → 再把字符串转成新对象→ 新旧对象完全独立互不影响语法2services.AddTransientI_jsonHelper,jsonHelper();services.AddTransientI_jsonHelper,jsonHelper();// │ │ │// 生命周期 接口 实现类注册services.AddTransientI_jsonHelper, jsonHelper();使用var helper ServiceProvider.GetRequiredServiceI_jsonHelper();↓ 系统自动做 ↓系统new jsonHelper()↓返回给你为什么 jsonHelper 用 TransientjsonHelper 是工具类每次用创建新实例就行不需要保留状态。如果是带状态的用 Singleton不带状态的用 Transient。语法3[global::System.Configuration.UserScopedSettingAttribute()][global::System.Diagnostics.DebuggerNonUserCodeAttribute()][global::System.Configuration.DefaultSettingValueAttribute()]这三个是 .NET 平台设置系统Settings自动生成代码的标准特性标签常见于 WinForms/WPF 老项目的Settings.Designer.cs文件中用来标注配置项的作用域、调试行为和默认值。下面逐个拆解含义与实际作用1.[global::System.Configuration.UserScopedSettingAttribute()]全称用户范围设置特性核心作用标记该配置项属于用户级配置每个 Windows 登录用户拥有独立的配置副本修改后仅对当前用户生效。存储位置保存在用户目录下路径类似C:\Users\你的用户名\AppData\Local\程序发布商\程序名_版本哈希\user.config对应概念与之相对的是ApplicationScopedSettingAttribute应用程序级配置全局共用、运行时只读所有用户共享同一份值。典型场景窗口位置、UI缩放比例、上次打开的机种、操作习惯等个性化配置适合用用户级设置。2.[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]全称调试器非用户代码特性核心作用告诉 Visual Studio 调试器这段代码是框架自动生成的模板代码不是业务手写代码调试时会自动跳过。实际效果你按 F11 单步调试时不会进入这些自动生成的属性 get/set 内部直接跳过避免调试过程中被大量自动生成代码干扰专注业务逻辑。性质纯调试辅助特性完全不影响程序运行和功能。3.[global::System.Configuration.DefaultSettingValueAttribute()]全称默认设置值特性核心作用为该配置项指定初始默认值。程序第一次运行、本地配置文件不存在时就会使用这个值作为初始配置。这里括号内参数为表示该配置项的默认值是空字符串。示例如果写成[DefaultSettingValue(localhost)]代表配置项默认值为localhost。整体补充说明代码来源这三行通常是 Visual Studio 自动生成的你在项目中添加「设置文件Settings.settings」并配置项后VS 会自动生成带这些特性的属性代码一般不需要手动编写。老代码改造建议不要直接修改Settings.Designer.cs里的自动生成代码下次保存设置文件会被覆盖如果要封装老配置、适配新接口参考之前的适配器模式在外部包一层配置服务类不要动自动生成文件。常见坑用户级配置会随程序版本号变化而丢失路径包含版本哈希程序升级时需要额外做配置迁移逻辑否则用户之前保存的参数会重置。语法3console.Writeline(a,b,c); int 不能转为char[] 数组核心报错原因参数不匹配Console.WriteLine没有「直接接收 3 个 int 参数」的方法重载。编译器在尝试匹配最近的重载时其中包含char[]参数的重载就会提示int 无法转换为 char[]的类型不匹配错误。一、正确输出多个 int 变量的写法调试最常用假设你有三个 int 变量inta10;intb20;intc30;写法1字符串插值首推清晰不易错工业上位机调试首选可读性最强方便定位变量Console.WriteLine($a{a}, b{b}, c{c});输出结果a10, b20, c30写法2格式化占位符老项目通用写法Console.WriteLine(a{0}, b{1}, c{2},a,b,c);效果和字符串插值完全一致.NET Framework 老项目常用。写法3分多行单独输出只看数值、不需要标注变量名时使用Console.WriteLine(a);Console.WriteLine(b);Console.WriteLine(c);写法4拼接成一行输出Console.WriteLine(string.Join(, ,a,b,c));输出结果10, 20, 30二、如果你确实需要把 int 转成 char[] 再输出如果你的业务需求是把 int 数字的每一位转为字符数组再输出到控制台正确写法如下单个 int 转 char[] 输出intnum12345;// 先转字符串再转字符数组char[]charArraynum.ToString().ToCharArray();// 直接传入 char[] 输出Console.WriteLine 有对应重载Console.WriteLine(charArray);输出结果12345多个 int 批量转 char[] 输出inta111,b222,c333;char[]arrAa.ToString().ToCharArray();char[]arrBb.ToString().ToCharArray();char[]arrCc.ToString().ToCharArray();Console.WriteLine(arrA);Console.WriteLine(arrB);Console.WriteLine(arrC);ScopedScoped作用域生命周期 一句话Scoped 在同一个作用域内只创建一个实例 三种生命周期对比// 1. Singleton - 全局唯一 services.AddSingletonIMyService, MyService(); // 整个程序运行期间只有一个实例 // 2. Transient - 每次新建 services.AddTransientIMyService, MyService(); // 每次 GetRequiredService 都创建新实例 // 3. Scoped - 作用域内唯一 services.AddScopedIMyService, MyService(); // 同一个 Scope 内只创建一次不同 Scope 是不同实例 图解Singleton全局一个: ┌─────────────────────────────────────────┐ │ 整个应用程序 │ │ │ │ ┌─────────┐ │ │ │ 实例 A │ ← 所有请求都用这一个 │ │ └─────────┘ │ └─────────────────────────────────────────┘ Transient每次新建: ┌─────────────────────────────────────────┐ │ 整个应用程序 │ │ │ │ 请求1 → 实例A │ │ 请求2 → 实例B新的 │ │ 请求3 → 实例C新的 │ └─────────────────────────────────────────┘ Scoped作用域内唯一: ┌─────────────────────────────────────────┐ │ 整个应用程序 │ │ │ │ Scope 1: │ │ 请求1 → 实例A │ │ 请求2 → 实例A同一个 │ │ │ │ Scope 2: │ │ 请求3 → 实例B新的 │ │ 请求4 → 实例B同一个 │ └─────────────────────────────────────────┘ 为什么 DbContext 用 Scopedservices.AddDbContextUserPermissionContext(..., ServiceLifetime.Scoped);原因 代码示例// 注册 Scoped 服务 services.AddScopedIMyService, MyService(); // 使用 using var scope _host.Services.CreateScope(); var service1 scope.ServiceProvider.GetRequiredServiceIMyService(); var service2 scope.ServiceProvider.GetRequiredServiceIMyService(); Console.WriteLine(service1 service2); // True同一个 Scope同一个实例 // Scope 结束实例释放 何时用哪种 一句话总结usingvarscope_host.Services.CreateScope();// │ │ │ │ │// │ │ │ │ └─ 创建一个新作用域// │ │ │ └─ 服务容器// │ │ └─ 主机// │ └─ 作用域对象// └─ 用完自动释放vardbContextscope.ServiceProvider.GetRequiredServiceUserPermissionContext();// │ │ │ │ │// │ │ │ │ └─ 要获取的服务类型// │ │ │ └─ 获取必需的服务不存在则抛异常// │ │ └─ 作用域内的服务提供者// │ └─ 从这个作用域获取// └─ 返回 DbContext 实例语法4SQLite 数据库上下文 一句话DbContext 操作数据库的 C# 类不用写 SQL用 C# 对象就能操作数据库 什么是 DbContextDbContext 数据库的管家 你 ├─ dbContext.Users.Add(user) → 自动生成 INSERT INTO Users... ├─ dbContext.Users.ToList() → 自动生成 SELECT * FROM Users ├─ dbContext.SaveChanges() → 自动提交到数据库 └─ 不用写 SQL 语句 对比❌ 传统方式手写 SQLvar connection new SQLiteConnection(Data Sourceusers.db); connection.Open(); var command new SQLiteCommand(SELECT * FROM Users WHERE user_id id, connection); command.Parameters.AddWithValue(id, 1); var reader command.ExecuteReader(); while (reader.Read()) { var user new User { UserId reader.GetInt32(0), Username reader.GetString(1), Password reader.GetString(2) }; } connection.Close();✅ DbContext 方式不用写 SQLvar user dbContext.Users.Find(1); // 一行搞定 UserPermissionContext 的作用public class UserPermissionContext : DbContext { // DbSet 表 public DbSetUser Users { get; set; } // 对应 Users 表 public DbSetRole Roles { get; set; } // 对应 Roles 表 } code复制 DbSetUser Users ↓ C# 代码dbContext.Users.Where(u u.Username admin) ↓ 自动生成 SQLSELECT * FROM Users WHERE username admin ↓ 返回ListUser SQLite 是什么数据库特点SQLite文件数据库无需安装服务器适合单机应用MySQL服务器数据库需要安装 MySQL Server适合多用户并发SQLite ├─ 数据存在一个 .db 文件里 ├─ 不用安装数据库服务器 ├─ 程序启动就能用 └─ 适合用户权限、本地配置 MySQL ├─ 数据存在 MySQL Server 里 ├─ 需要安装和启动服务 ├─ 支持多用户并发 └─ 适合业务数据、大数据量 这个项目为什么用两个数据库数据库存什么为什么SQLite用户、角色、权限轻量、本地、无需服务MySQL客户、配件业务数据大、需要并发 一句话总结DbContext 是什么操作数据库的 C# 类不用写 SQLSQLite 是什么文件数据库数据存在 .db 文件里语法5services.AddSingletonMainWindow(spnewMainWindow{DataContextsp.GetRequiredServiceMainWindow_VM()});我换一个最直白的方式解释我们用“买电脑”来比喻。先看普通的注册简单但有缺陷代码services.AddSingletonMainWindow();比喻这就像你去电脑城买电脑老板直接给你一个空机箱。机箱 MainWindow (界面)内存、硬盘、CPU MainWindow_VM (数据/逻辑)问题你拿回家也没法用因为里面没有零件没有 DataContext屏幕是黑的。2. 再看你问的这行代码高级全自动代码services.AddSingletonMainWindow(sp new MainWindow { DataContext sp.GetRequiredServiceMainWindow_VM() });比喻这就像你去电脑城跟老板说“我要一台装好内存、硬盘、CPU 的电脑。”老板也就是 sp即服务提供者会做以下几步从仓库里拿出内存、硬盘、CPUsp.GetRequiredServiceMainWindow_VM() —— 去容器里把 ViewModel 拿出来。把这些零件装进机箱new MainWindow { DataContext … } —— 创建 Window 并把 VM 赋值给 DataContext。把装好的整机交给你。逐字翻译这行代码为什么不能直接写 AddSingleton()因为在 C# 里MainWindow 是个类它的默认构造函数无参并不知道 DataContext 是什么。如果你只写 AddSingleton()容器就会调用那个“啥也不知道”的无参构造函数结果就是界面出来了。但是界面背后没有数据因为 DataContext 是 null。你点按钮没反应因为按钮绑定的命令都在 MainWindow_VM 里但界面找不到它。总结这句代码的核心目的是在创建界面View的那一瞬间顺手把它的“大脑”ViewModel装进去。这样当你以后写 var window ServiceProvider.GetRequiredService(); 时拿到的这个 window 已经是有脑子的、能干活的窗口了。你可以理解成这是依赖注入容器提供的“自定义组装”功能。 默认的组装方式是“空壳”你用 Lambda 表达式告诉容器“按我的要求组装。”语法6services.AddTransientLogin(spnewLogin{DataContextsp.GetRequiredServiceLogin_VM()});这行代码和刚才那行很像但有一个关键区别它是 AddTransient瞬时而不是 AddSingleton单例。用你的项目场景来解释为什么 Login 窗口要用 Transient场景模拟用户打开软件 → 弹出 登录窗口Login。输入密码登录成功 → 进入主界面。用户点击“注销” → 软件需要再次弹出登录窗口。如果用 AddSingleton容器里只有一个 Login 实例。用户注销后那个 Login 窗口虽然关了但实例还在内存里单例嘛。再次登录时容器会把旧的那个给你。问题旧的 Login 窗口可能状态没清比如密码框里还有刚才输的星号或者有个错误提示还没消导致界面显示不正常。如果用 AddTransient现在的代码每次登录容器都会 new 一个全新的 Login 窗口。全新的窗口干干净净密码框是空的状态是初始的。注销后再登录又是新的不会留上次操作的“脏数据”。代码逐字翻译services.AddTransientLogin(sp new Login { DataContext sp.GetRequiredServiceLogin_VM() });对比 MainWindow 和 Login一句话总结这行代码是在说“以后每次有人要登录窗口都给我现场做一个新的别忘了把**新的大脑ViewModel**也装进去。”这样能保证用户每次看到的登录界面都是干干净净、没填过密码、没报过错的。语法7services.AddKeyedTransient(“User”, (sp, key) new UsersDialog { DataContext sp.GetRequiredServiceUserDialog_VM() });这行代码引入了一个新概念“带钥匙”的注册。通俗地说同一个 UsersDialog 窗口根据“钥匙Key”的不同装上不同的“大脑ViewModel”。先看你的代码里做了什么你注册了两次 UsersDialog但用了不同的钥匙// 钥匙 User services.AddKeyedTransientUsersDialog(User, (sp, key) new UsersDialog { DataContext sp.GetRequiredServiceUserDialog_VM() }); // 钥匙 Role services.AddKeyedTransientUsersDialog(Role, (sp, key) new UsersDialog { DataContext sp.GetRequiredServiceRoleDialog_VM() });这意味着什么你的项目里只有一个 UsersDialog.xaml 文件界面但你用它干了两件事管理用户时弹出 UsersDialog但里面显示的是用户列表用 UserDialog_VM。管理角色时弹出 UsersDialog但里面显示的是角色列表用 RoleDialog_VM。为什么要这样写省代码如果不这样写传统做法你得创建两个几乎一样的 XAML 界面UsersDialog.xaml用户管理界面RolesDialog.xaml角色管理界面然后分别注册csharp复制services.AddTransient();services.AddTransient();缺点界面长得差不多但你要维护两套代码改样式要改两次。现在的写法Keyed 做法只写一个 UsersDialog.xaml。想管用户拿钥匙 “User” → 装上 UserDialog_VM。想管角色拿钥匙 “Role” → 装上 RoleDialog_VM。怎么使用这把“钥匙”当你想弹出这个窗口时不能再用 GetRequiredService 了得用 GetRequiredKeyedService// 场景1点击“用户管理”按钮 var dialog ServiceProvider.GetRequiredKeyedServiceUsersDialog(User); dialog.Show(); // 场景2点击“角色管理”按钮 var dialog ServiceProvider.GetRequiredKeyedServiceUsersDialog(Role); dialog.Show();逐字翻译services.AddKeyedTransientUsersDialog( // 注册 UsersDialog而且是瞬时的每次 new User, // 给它一把钥匙名叫 User (sp, key) // 制作说明书sp管理员key刚才那把钥匙这里没用上但签名要有 new UsersDialog { DataContext sp.GetRequiredServiceUserDialog_VM() // 装上用户管理的大脑 });总结一句话这行代码是 “一把钥匙开一把锁”。它让你用一个 UsersDialog 界面通过切换钥匙“User”或“Role”变身成“用户管理界面”或“角色管理界面”。语法8varpermissionConnectionStringcontext.Configuration.GetConnectionString(PermissionDefaultConnection);context.Configuration 是什么context是 HostBuilderContext 对象。它是 .NET Generic Host 在构建时传递给你的**“环境快照”**。Configuration是一个配置管理器。它里面装满了从各个地方如 appsettings.json、环境变量、命令行参数读进来的配置数据。它是怎么来的回想一下 App.xaml.cs 里的这行_host Host.CreateDefaultBuilder() .ConfigureAppConfiguration((context, config) { // 这里可以加载配置文件 config.AddJsonFile(appsettings.json, optional: false, reloadOnChange: true); }) .Build();Host.CreateDefaultBuilder() 默认就会去读 appsettings.json。读进来之后就放在 context.Configuration 里供后面的 ConfigureServices 使用。3. GetConnectionString 在干什么它专门读取配置文件里的 ConnectionStrings 这一节。你的 appsettings.json 里应该有这样的内容json复制{“ConnectionStrings”: {“PermissionDefaultConnection”: “Data Sourcepermissions.db”,“RepairDefaultConnection”: “Serverlocalhost;DatabaseRepairDB;…”}}代码执行结果permissionConnectionString 的值会变成“Data Sourcepermissions.db”repairConnectionString 的值会变成“Serverlocalhost;DatabaseRepairDB;…”接下来的代码连贯理解拿到这两个字符串后你的代码立马把它们传给了数据库上下文的注册// 配置UserPermissionContextSQLite 延迟加载 services.AddDbContextUserPermissionContext(options options.UseSqlite(permissionConnectionString) // ← 用刚才读到的字符串 .UseLazyLoadingProxies(), ServiceLifetime.Scoped);总结这两行代码是**“桥梁”**左手从 appsettings.json 文件里把数据库地址拿出来。右手把这些地址塞给 Entity Framework Core 的数据库上下文配置。这样你的程序就知道该连哪个数据库了。而且以后换数据库比如从 SQLite 换到 SQL Server只需要改 appsettings.json不用动代码。