语法1
// 2. 序列化 → 反序列化 = 深拷贝varjson=JsonConvert.SerializeObject(obj,_defaultSettings);// 对象 → JSON字符串returnJsonConvert.DeserializeObject<T>(json,_defaultSettings)!;// JSON字符串 → 新对象深拷贝 = 序列化 + 反序列化
对象 → JSON字符串 → 新对象
│ │ │
│ JsonConvert JsonConvert
│ .Serialize .Deserialize
│ │ │
└─────────┴──────────┘
完全独立的新对象
varcustomer1=newCustomer{Name="张三",Age=25};varcustomer2=deepCopy(customer1);customer2.Name="李四";Console.WriteLine(customer1.Name);// 张三(不变)Console.WriteLine(customer2.Name);// 李四深拷贝 = 先把对象转成字符串 → 再把字符串转成新对象
→ 新旧对象完全独立,互不影响
语法2
services.AddTransient<I_jsonHelper,jsonHelper>();services.AddTransient<I_jsonHelper,jsonHelper>();// │ │ │// 生命周期 接口 实现类注册:services.AddTransient<I_jsonHelper, jsonHelper>();
使用:
var helper = ServiceProvider.GetRequiredService<I_jsonHelper>();↓ 系统自动做 ↓
系统:new jsonHelper()
↓返回给你
为什么 jsonHelper 用 Transient?
jsonHelper 是工具类,每次用创建新实例就行,不需要保留状态。
如果是带状态的,用 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里的自动生成代码,下次保存设置文件会被覆盖; - 如果要封装老配置、适配新接口,参考之前的适配器模式,在外部包一层配置服务类,不要动自动生成文件。
- 不要直接修改
- 常见坑:用户级配置会随程序版本号变化而丢失(路径包含版本哈希),程序升级时需要额外做配置迁移逻辑,否则用户之前保存的参数会重置。
语法3
console.Writeline(a,b,c); int 不能转为char[] 数组
核心报错原因
- 参数不匹配:
Console.WriteLine没有「直接接收 3 个 int 参数」的方法重载。编译器在尝试匹配最近的重载时(其中包含char[]参数的重载),就会提示int 无法转换为 char[]的类型不匹配错误。
一、正确输出多个 int 变量的写法(调试最常用)
假设你有三个 int 变量:
inta=10;intb=20;intc=30;写法1:字符串插值(首推,清晰不易错)
工业上位机调试首选,可读性最强,方便定位变量:
Console.WriteLine($"a={a}, b={b}, c={c}");输出结果:a=10, b=20, c=30
写法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[] 输出
intnum=12345;// 先转字符串,再转字符数组char[]charArray=num.ToString().ToCharArray();// 直接传入 char[] 输出,Console.WriteLine 有对应重载Console.WriteLine(charArray);输出结果:12345
多个 int 批量转 char[] 输出
inta=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 = 在同一个作用域内,只创建一个实例
📋 三种生命周期对比
// 1. Singleton - 全局唯一 services.AddSingleton<IMyService, MyService>(); // 整个程序运行期间只有一个实例 // 2. Transient - 每次新建 services.AddTransient<IMyService, MyService>(); // 每次 GetRequiredService 都创建新实例 // 3. Scoped - 作用域内唯一 services.AddScoped<IMyService, 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 用 Scoped?
services.AddDbContext<UserPermissionContext>(..., ServiceLifetime.Scoped);原因:
📝 代码示例
// 注册 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 结束,实例释放📊 何时用哪种?
🎯 一句话总结
usingvarscope=_host.Services.CreateScope();// │ │ │ │ │// │ │ │ │ └─ 创建一个新作用域// │ │ │ └─ 服务容器// │ │ └─ 主机// │ └─ 作用域对象// └─ 用完自动释放vardbContext=scope.ServiceProvider.GetRequiredService<UserPermissionContext>();// │ │ │ │ │// │ │ │ │ └─ 要获取的服务类型// │ │ │ └─ 获取必需的服务(不存在则抛异常)// │ │ └─ 作用域内的服务提供者// │ └─ 从这个作用域获取// └─ 返回 DbContext 实例语法4
SQLite 数据库上下文
🎯 一句话
DbContext = 操作数据库的 C# 类,不用写 SQL,用 C# 对象就能操作数据库
📋 什么是 DbContext?
DbContext = 数据库的"管家" 你: ├─ dbContext.Users.Add(user) → 自动生成 INSERT INTO Users... ├─ dbContext.Users.ToList() → 自动生成 SELECT * FROM Users ├─ dbContext.SaveChanges() → 自动提交到数据库 └─ 不用写 SQL 语句!🔄 对比
❌ 传统方式(手写 SQL)
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)
var user = dbContext.Users.Find(1); // 一行搞定📊 UserPermissionContext 的作用
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,适合多用户并发 |
SQLite: ├─ 数据存在一个 .db 文件里 ├─ 不用安装数据库服务器 ├─ 程序启动就能用 └─ 适合:用户权限、本地配置 MySQL: ├─ 数据存在 MySQL Server 里 ├─ 需要安装和启动服务 ├─ 支持多用户并发 └─ 适合:业务数据、大数据量📋 这个项目为什么用两个数据库?
| 数据库 | 存什么 | 为什么 |
|---|---|---|
| SQLite | 用户、角色、权限 | 轻量、本地、无需服务 |
| MySQL | 客户、配件 | 业务数据大、需要并发 |
🎯 一句话总结
DbContext 是什么?
操作数据库的 C# 类,不用写 SQL
SQLite 是什么?
文件数据库,数据存在 .db 文件里
语法5
services.AddSingleton<MainWindow>(sp=>newMainWindow{DataContext=sp.GetRequiredService<MainWindow_VM>()});我换一个最直白的方式解释,我们用“买电脑”来比喻。
- 先看普通的注册(简单,但有缺陷)
代码:
services.AddSingleton<MainWindow>();比喻:
这就像你去电脑城买电脑,老板直接给你一个空机箱。
机箱 = MainWindow (界面)
内存、硬盘、CPU = MainWindow_VM (数据/逻辑)
问题:你拿回家也没法用,因为里面没有零件(没有 DataContext),屏幕是黑的。
2. 再看你问的这行代码(高级,全自动)
代码:
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
services.AddTransient<Login>(sp=>newLogin{DataContext=sp.GetRequiredService<Login_VM>()});这行代码和刚才那行很像,但有一个关键区别:它是 AddTransient(瞬时),而不是 AddSingleton(单例)。
用你的项目场景来解释:
- 为什么 Login 窗口要用 Transient?
场景模拟:
用户打开软件 → 弹出 登录窗口(Login)。
输入密码,登录成功 → 进入主界面。
用户点击“注销” → 软件需要再次弹出登录窗口。
如果用 AddSingleton:
容器里只有一个 Login 实例。
用户注销后,那个 Login 窗口虽然关了,但实例还在内存里(单例嘛)。
再次登录时,容器会把旧的那个给你。
问题:旧的 Login 窗口可能状态没清(比如密码框里还有刚才输的星号,或者有个错误提示还没消),导致界面显示不正常。
如果用 AddTransient(现在的代码):
每次登录,容器都会 new 一个全新的 Login 窗口。
全新的窗口,干干净净,密码框是空的,状态是初始的。
注销后再登录,又是新的,不会留上次操作的“脏数据”。
- 代码逐字翻译
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,但用了不同的钥匙:
// 钥匙 "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:
// 场景1:点击“用户管理”按钮 var dialog = ServiceProvider.GetRequiredKeyedService<UsersDialog>("User"); dialog.Show(); // 场景2:点击“角色管理”按钮 var dialog = ServiceProvider.GetRequiredKeyedService<UsersDialog>("Role"); dialog.Show();- 逐字翻译
services.AddKeyedTransient<UsersDialog>( // 注册 UsersDialog,而且是瞬时的(每次 new) "User", // 给它一把钥匙,名叫 "User" (sp, key) => // 制作说明书:sp=管理员,key=刚才那把钥匙(这里没用上,但签名要有) new UsersDialog { DataContext = sp.GetRequiredService<UserDialog_VM>() // 装上用户管理的大脑 });- 总结
一句话:
这行代码是 “一把钥匙开一把锁”。它让你用一个 UsersDialog 界面,通过切换钥匙(“User”或“Role”),变身成“用户管理界面”或“角色管理界面”。
语法8
varpermissionConnectionString=context.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 Source=permissions.db”,
“RepairDefaultConnection”: “Server=localhost;Database=RepairDB;…”
}
}
代码执行结果:
permissionConnectionString 的值会变成:“Data Source=permissions.db”
repairConnectionString 的值会变成:“Server=localhost;Database=RepairDB;…”
- 接下来的代码(连贯理解)
拿到这两个字符串后,你的代码立马把它们传给了数据库上下文的注册:
// 配置UserPermissionContext(SQLite + 延迟加载) services.AddDbContext<UserPermissionContext>(options => options.UseSqlite(permissionConnectionString) // ← 用刚才读到的字符串 .UseLazyLoadingProxies(), ServiceLifetime.Scoped);总结
这两行代码是**“桥梁”**:
左手从 appsettings.json 文件里把数据库地址拿出来。
右手把这些地址塞给 Entity Framework Core 的数据库上下文配置。
这样,你的程序就知道该连哪个数据库了。而且以后换数据库(比如从 SQLite 换到 SQL Server),只需要改 appsettings.json,不用动代码。