语法1
csharp
// 2. 序列化 → 反序列化 = 深拷贝
var json = JsonConvert.SerializeObject(obj, _defaultSettings); // 对象 → JSON字符串
return JsonConvert.DeserializeObject<T>(json, _defaultSettings)!; // JSON字符串 → 新对象
深拷贝 = 序列化 + 反序列化
对象 → JSON字符串 → 新对象
│ │ │
│ JsonConvert JsonConvert
│ .Serialize .Deserialize
│ │ │
└─────────┴──────────┘
完全独立的新对象
csharp
var customer1 = new Customer { Name = "张三", Age = 25 };
var customer2 = deepCopy(customer1);
customer2.Name = "李四";
Console.WriteLine(customer1.Name); // 张三(不变)
Console.WriteLine(customer2.Name); // 李四
深拷贝 = 先把对象转成字符串 → 再把字符串转成新对象
→ 新旧对象完全独立,互不影响
语法2
csharp
services.AddTransient<I_jsonHelper, jsonHelper>();
csharp
services.AddTransient<I_jsonHelper, jsonHelper>();
// │ │ │
// 生命周期 接口 实现类

-
注册:services.AddTransient<I_jsonHelper, jsonHelper>();
-
使用:
var helper = ServiceProvider.GetRequiredService<I_jsonHelper>();
↓ 系统自动做 ↓
-
系统:new jsonHelper()
↓
-
返回给你

为什么 jsonHelper 用 Transient?
jsonHelper 是工具类,每次用创建新实例就行,不需要保留状态。
如果是带状态的,用 Singleton;不带状态的,用 Transient。
语法3
csharp
[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里的自动生成代码,下次保存设置文件会被覆盖; - 如果要封装老配置、适配新接口,参考之前的适配器模式,在外部包一层配置服务类,不要动自动生成文件。
- 不要直接修改
- 常见坑:用户级配置会随程序版本号变化而丢失(路径包含版本哈希),程序升级时需要额外做配置迁移逻辑,否则用户之前保存的参数会重置。
语法3
console.Writeline(a,b,c); int 不能转为char\[\] 数组
核心报错原因
- 参数不匹配 :
Console.WriteLine没有「直接接收 3 个 int 参数」的方法重载。编译器在尝试匹配最近的重载时(其中包含char[]参数的重载),就会提示int 无法转换为 char[]的类型不匹配错误。
一、正确输出多个 int 变量的写法(调试最常用)
假设你有三个 int 变量:
csharp
int a = 10;
int b = 20;
int c = 30;
写法1:字符串插值(首推,清晰不易错)
工业上位机调试首选,可读性最强,方便定位变量:
csharp
Console.WriteLine($"a={a}, b={b}, c={c}");
输出结果:a=10, b=20, c=30
写法2:格式化占位符(老项目通用写法)
csharp
Console.WriteLine("a={0}, b={1}, c={2}", a, b, c);
效果和字符串插值完全一致,.NET Framework 老项目常用。
写法3:分多行单独输出
只看数值、不需要标注变量名时使用:
csharp
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
写法4:拼接成一行输出
csharp
Console.WriteLine(string.Join(", ", a, b, c));
输出结果:10, 20, 30
二、如果你确实需要把 int 转成 char\[\] 再输出
如果你的业务需求是把 int 数字的每一位转为字符数组,再输出到控制台,正确写法如下:
单个 int 转 char\[\] 输出
csharp
int num = 12345;
// 先转字符串,再转字符数组
char[] charArray = num.ToString().ToCharArray();
// 直接传入 char[] 输出,Console.WriteLine 有对应重载
Console.WriteLine(charArray);
输出结果:12345
多个 int 批量转 char\[\] 输出
csharp
int a = 111, b = 222, c = 333;
char[] arrA = a.ToString().ToCharArray();
char[] arrB = b.ToString().ToCharArray();
char[] arrC = c.ToString().ToCharArray();
Console.WriteLine(arrA);
Console.WriteLine(arrB);
Console.WriteLine(arrC);
Scoped
Scoped(作用域生命周期)
🎯 一句话
Scoped = 在同一个作用域内,只创建一个实例
📋 三种生命周期对比
csharp复制
// 1. Singleton - 全局唯一
services.AddSingleton<IMyService, MyService>();
// 整个程序运行期间只有一个实例
// 2. Transient - 每次新建
services.AddTransient<IMyService, MyService>();
// 每次 GetRequiredService 都创建新实例
// 3. Scoped - 作用域内唯一
services.AddScoped<IMyService, MyService>();
// 同一个 Scope 内只创建一次,不同 Scope 是不同实例
🔄 图解
code复制
Singleton(全局一个):
┌─────────────────────────────────────────┐
│ 整个应用程序 │
│ │
│ ┌─────────┐ │
│ │ 实例 A │ ← 所有请求都用这一个 │
│ └─────────┘ │
└─────────────────────────────────────────┘
Transient(每次新建):
┌─────────────────────────────────────────┐
│ 整个应用程序 │
│ │
│ 请求1 → 实例A │
│ 请求2 → 实例B(新的) │
│ 请求3 → 实例C(新的) │
└─────────────────────────────────────────┘
Scoped(作用域内唯一):
┌─────────────────────────────────────────┐
│ 整个应用程序 │
│ │
│ Scope 1: │
│ 请求1 → 实例A │
│ 请求2 → 实例A(同一个) │
│ │
│ Scope 2: │
│ 请求3 → 实例B(新的) │
│ 请求4 → 实例B(同一个) │
└─────────────────────────────────────────┘
🎯 为什么 DbContext 用 Scoped?
csharp复制
services.AddDbContext<UserPermissionContext>(..., ServiceLifetime.Scoped);
原因:

📝 代码示例
csharp复制
// 注册 Scoped 服务
services.AddScoped<IMyService, MyService>();
// 使用
using var scope = _host.Services.CreateScope();
var service1 = scope.ServiceProvider.GetRequiredService<IMyService>();
var service2 = scope.ServiceProvider.GetRequiredService<IMyService>();
Console.WriteLine(service1 == service2); // True(同一个 Scope,同一个实例)
// Scope 结束,实例释放
📊 何时用哪种?

🎯 一句话总结

csharp
using var scope = _host.Services.CreateScope();
// │ │ │ │ │
// │ │ │ │ └─ 创建一个新作用域
// │ │ │ └─ 服务容器
// │ │ └─ 主机
// │ └─ 作用域对象
// └─ 用完自动释放
var dbContext = scope.ServiceProvider.GetRequiredService<UserPermissionContext>();
// │ │ │ │ │
// │ │ │ │ └─ 要获取的服务类型
// │ │ │ └─ 获取必需的服务(不存在则抛异常)
// │ │ └─ 作用域内的服务提供者
// │ └─ 从这个作用域获取
// └─ 返回 DbContext 实例
语法4
SQLite 数据库上下文
🎯 一句话
DbContext = 操作数据库的 C# 类,不用写 SQL,用 C# 对象就能操作数据库
📋 什么是 DbContext?
code复制
DbContext = 数据库的"管家"
你:
├─ dbContext.Users.Add(user) → 自动生成 INSERT INTO Users...
├─ dbContext.Users.ToList() → 自动生成 SELECT * FROM Users
├─ dbContext.SaveChanges() → 自动提交到数据库
└─ 不用写 SQL 语句!
🔄 对比
❌ 传统方式(手写 SQL)
csharp复制
var connection = new SQLiteConnection("Data Source=users.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 方式(不用写 SQL)
csharp复制
var user = dbContext.Users.Find(1); // 一行搞定
📊 UserPermissionContext 的作用
csharp复制
public class UserPermissionContext : DbContext
{
// DbSet = 表
public DbSet<User> Users { get; set; } // 对应 Users 表
public DbSet<Role> Roles { get; set; } // 对应 Roles 表
}
``
```code复制
DbSet<User> Users
↓
C# 代码:dbContext.Users.Where(u => u.Username == "admin")
↓
自动生成 SQL:SELECT * FROM Users WHERE username = 'admin'
↓
返回:List<User>
🎯 SQLite 是什么?
| 数据库 | 特点 |
|---|---|
| SQLite | 文件数据库,无需安装服务器,适合单机应用 |
| MySQL | 服务器数据库,需要安装 MySQL Server,适合多用户并发 |
code复制
SQLite:
├─ 数据存在一个 .db 文件里
├─ 不用安装数据库服务器
├─ 程序启动就能用
└─ 适合:用户权限、本地配置
MySQL:
├─ 数据存在 MySQL Server 里
├─ 需要安装和启动服务
├─ 支持多用户并发
└─ 适合:业务数据、大数据量
📋 这个项目为什么用两个数据库?
| 数据库 | 存什么 | 为什么 |
|---|---|---|
| SQLite | 用户、角色、权限 | 轻量、本地、无需服务 |
| MySQL | 客户、配件 | 业务数据大、需要并发 |
🎯 一句话总结
DbContext 是什么?
操作数据库的 C# 类,不用写 SQL
SQLite 是什么?
文件数据库,数据存在 .db 文件里
语法5
csharp
services.AddSingleton<MainWindow>(sp =>new MainWindow { DataContext = sp.GetRequiredService<MainWindow_VM>() });
我换一个最直白的方式解释,我们用"买电脑"来比喻。
- 先看普通的注册(简单,但有缺陷)
代码:
csharp复制
services.AddSingleton<MainWindow>();
比喻:
这就像你去电脑城买电脑,老板直接给你一个空机箱。
机箱 = MainWindow (界面)
内存、硬盘、CPU = MainWindow_VM (数据/逻辑)
问题:你拿回家也没法用,因为里面没有零件(没有 DataContext),屏幕是黑的。
- 再看你问的这行代码(高级,全自动)
代码:
csharp复制
services.AddSingleton<MainWindow>(sp =>
new MainWindow
{
DataContext = sp.GetRequiredService<MainWindow_VM>()
});
比喻:
这就像你去电脑城,跟老板说:"我要一台装好内存、硬盘、CPU 的电脑。"
老板(也就是 sp,即服务提供者)会做以下几步:
从仓库里拿出内存、硬盘、CPU(sp.GetRequiredService<MainWindow_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 表达式(=>)告诉容器:"按我的要求组装。"
语法6
csharp
services.AddTransient<Login>(sp =>
new Login { DataContext = sp.GetRequiredService<Login_VM>() });
这行代码和刚才那行很像,但有一个关键区别:它是 AddTransient(瞬时),而不是 AddSingleton(单例)。
用你的项目场景来解释:
- 为什么 Login 窗口要用 Transient?
场景模拟:
用户打开软件 → 弹出 登录窗口(Login)。
输入密码,登录成功 → 进入主界面。
用户点击"注销" → 软件需要再次弹出登录窗口。
如果用 AddSingleton:
容器里只有一个 Login 实例。
用户注销后,那个 Login 窗口虽然关了,但实例还在内存里(单例嘛)。
再次登录时,容器会把旧的那个给你。
问题:旧的 Login 窗口可能状态没清(比如密码框里还有刚才输的星号,或者有个错误提示还没消),导致界面显示不正常。
如果用 AddTransient(现在的代码):
每次登录,容器都会 new 一个全新的 Login 窗口。
全新的窗口,干干净净,密码框是空的,状态是初始的。
注销后再登录,又是新的,不会留上次操作的"脏数据"。
- 代码逐字翻译
csharp复制
services.AddTransient<Login>(sp =>
new Login
{
DataContext = sp.GetRequiredService<Login_VM>()
});

- 对比 MainWindow 和 Login

- 一句话总结
这行代码是在说:"以后每次有人要登录窗口,都给我现场做一个新的,别忘了把**新的大脑(ViewModel)**也装进去。"
这样能保证用户每次看到的登录界面都是干干净净、没填过密码、没报过错的。
语法7
services.AddKeyedTransient("User", (sp, key) =>
new UsersDialog { DataContext = sp.GetRequiredService<UserDialog_VM>() });
这行代码引入了一个新概念:"带钥匙"的注册。
通俗地说:
同一个 UsersDialog 窗口,根据"钥匙(Key)"的不同,装上不同的"大脑(ViewModel)"。
- 先看你的代码里做了什么
你注册了两次 UsersDialog,但用了不同的钥匙:
csharp复制
// 钥匙 "User"
services.AddKeyedTransient<UsersDialog>("User", (sp, key) =>
new UsersDialog { DataContext = sp.GetRequiredService<UserDialog_VM>() });
// 钥匙 "Role"
services.AddKeyedTransient<UsersDialog>("Role", (sp, key) =>
new UsersDialog { DataContext = sp.GetRequiredService<RoleDialog_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:
csharp复制
// 场景1:点击"用户管理"按钮
var dialog = ServiceProvider.GetRequiredKeyedService<UsersDialog>("User");
dialog.Show();
// 场景2:点击"角色管理"按钮
var dialog = ServiceProvider.GetRequiredKeyedService<UsersDialog>("Role");
dialog.Show();
- 逐字翻译
csharp复制
services.AddKeyedTransient<UsersDialog>( // 注册 UsersDialog,而且是瞬时的(每次 new)
"User", // 给它一把钥匙,名叫 "User"
(sp, key) => // 制作说明书:sp=管理员,key=刚才那把钥匙(这里没用上,但签名要有)
new UsersDialog
{
DataContext = sp.GetRequiredService<UserDialog_VM>() // 装上用户管理的大脑
});
- 总结

一句话:
这行代码是 "一把钥匙开一把锁"。它让你用一个 UsersDialog 界面,通过切换钥匙("User"或"Role"),变身成"用户管理界面"或"角色管理界面"。
语法8
csharp
var permissionConnectionString = context.Configuration.GetConnectionString("PermissionDefaultConnection");
- context.Configuration 是什么?
context:是 HostBuilderContext 对象。它是 .NET Generic Host 在构建时传递给你的**"环境快照"**。
Configuration:是一个配置管理器。它里面装满了从各个地方(如 appsettings.json、环境变量、命令行参数)读进来的配置数据。
它是怎么来的?
回想一下 App.xaml.cs 里的这行:
csharp复制
_host = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, config) =>
{
// 这里可以加载配置文件
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
})
.Build();
Host.CreateDefaultBuilder() 默认就会去读 appsettings.json。读进来之后,就放在 context.Configuration 里,供后面的 ConfigureServices 使用。
- GetConnectionString 在干什么?
它专门读取配置文件里的 ConnectionStrings 这一节。
你的 appsettings.json 里应该有这样的内容:
json复制{
"ConnectionStrings": {
"PermissionDefaultConnection": "Data Source=permissions.db",
"RepairDefaultConnection": "Server=localhost;Database=RepairDB;..."
}
}
代码执行结果:
permissionConnectionString 的值会变成:"Data Source=permissions.db"
repairConnectionString 的值会变成:"Server=localhost;Database=RepairDB;..."
- 接下来的代码(连贯理解)
拿到这两个字符串后,你的代码立马把它们传给了数据库上下文的注册:
csharp复制
// 配置UserPermissionContext(SQLite + 延迟加载)
services.AddDbContext<UserPermissionContext>(options =>
options.UseSqlite(permissionConnectionString) // ← 用刚才读到的字符串
.UseLazyLoadingProxies(), ServiceLifetime.Scoped);
总结
这两行代码是**"桥梁"**:
左手从 appsettings.json 文件里把数据库地址拿出来。
右手把这些地址塞给 Entity Framework Core 的数据库上下文配置。
这样,你的程序就知道该连哪个数据库了。而且以后换数据库(比如从 SQLite 换到 SQL Server),只需要改 appsettings.json,不用动代码。