7.2:深拷贝;标准特性标签; Transient;Scoped;数据库上下文

语法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>();
//       │              │           │
//    生命周期         接口          实现类
  1. 注册:services.AddTransient<I_jsonHelper, jsonHelper>();

  2. 使用:

    var helper = ServiceProvider.GetRequiredService<I_jsonHelper>();

    ↓ 系统自动做 ↓

  3. 系统:new jsonHelper()

  4. 返回给你

    为什么 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

整体补充说明

  1. 代码来源:这三行通常是 Visual Studio 自动生成的,你在项目中添加「设置文件(Settings.settings)」并配置项后,VS 会自动生成带这些特性的属性代码,一般不需要手动编写。
  2. 老代码改造建议
    • 不要直接修改 Settings.Designer.cs 里的自动生成代码,下次保存设置文件会被覆盖;
    • 如果要封装老配置、适配新接口,参考之前的适配器模式,在外部包一层配置服务类,不要动自动生成文件。
  3. 常见坑:用户级配置会随程序版本号变化而丢失(路径包含版本哈希),程序升级时需要额外做配置迁移逻辑,否则用户之前保存的参数会重置。

语法3

console.Writeline(a,b,c); int 不能转为char\[\] 数组

核心报错原因

  1. 参数不匹配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>() });

我换一个最直白的方式解释,我们用"买电脑"来比喻。

  1. 先看普通的注册(简单,但有缺陷)
    代码:
csharp复制 复制代码
services.AddSingleton<MainWindow>();

比喻:

这就像你去电脑城买电脑,老板直接给你一个空机箱。

机箱 = MainWindow (界面)

内存、硬盘、CPU = MainWindow_VM (数据/逻辑)

问题:你拿回家也没法用,因为里面没有零件(没有 DataContext),屏幕是黑的。

  1. 再看你问的这行代码(高级,全自动)

代码:

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)。

把装好的整机交给你。

  1. 逐字翻译这行代码
  2. 为什么不能直接写 AddSingleton()?
    因为在 C# 里,MainWindow 是个类,它的默认构造函数(无参)并不知道 DataContext 是什么。
    如果你只写 AddSingleton(),容器就会调用那个"啥也不知道"的无参构造函数,结果就是:

界面出来了。

但是界面背后没有数据(因为 DataContext 是 null)。

你点按钮没反应,因为按钮绑定的命令都在 MainWindow_VM 里,但界面找不到它。

  1. 总结
    这句代码的核心目的是:在创建界面(View)的那一瞬间,顺手把它的"大脑"(ViewModel)装进去。
    这样,当你以后写 var window = ServiceProvider.GetRequiredService(); 时,拿到的这个 window 已经是有脑子的、能干活的窗口了。
    你可以理解成:这是依赖注入容器提供的"自定义组装"功能。 默认的组装方式是"空壳",你用 Lambda 表达式(=>)告诉容器:"按我的要求组装。"

语法6

csharp 复制代码
services.AddTransient<Login>(sp =>
new Login { DataContext = sp.GetRequiredService<Login_VM>() });

这行代码和刚才那行很像,但有一个关键区别:它是 AddTransient(瞬时),而不是 AddSingleton(单例)。

用你的项目场景来解释:

  1. 为什么 Login 窗口要用 Transient?
    场景模拟:

用户打开软件 → 弹出 登录窗口(Login)。

输入密码,登录成功 → 进入主界面。

用户点击"注销" → 软件需要再次弹出登录窗口。

如果用 AddSingleton:

容器里只有一个 Login 实例。

用户注销后,那个 Login 窗口虽然关了,但实例还在内存里(单例嘛)。

再次登录时,容器会把旧的那个给你。

问题:旧的 Login 窗口可能状态没清(比如密码框里还有刚才输的星号,或者有个错误提示还没消),导致界面显示不正常。

如果用 AddTransient(现在的代码):

每次登录,容器都会 new 一个全新的 Login 窗口。

全新的窗口,干干净净,密码框是空的,状态是初始的。

注销后再登录,又是新的,不会留上次操作的"脏数据"。

  1. 代码逐字翻译
csharp复制 复制代码
services.AddTransient<Login>(sp =>
    new Login 
    { 
        DataContext = sp.GetRequiredService<Login_VM>() 
    });
  1. 对比 MainWindow 和 Login
  2. 一句话总结

这行代码是在说:"以后每次有人要登录窗口,都给我现场做一个新的,别忘了把**新的大脑(ViewModel)**也装进去。"

这样能保证用户每次看到的登录界面都是干干净净、没填过密码、没报过错的。

语法7

services.AddKeyedTransient("User", (sp, key) =>

new UsersDialog { DataContext = sp.GetRequiredService<UserDialog_VM>() });

这行代码引入了一个新概念:"带钥匙"的注册。

通俗地说:

同一个 UsersDialog 窗口,根据"钥匙(Key)"的不同,装上不同的"大脑(ViewModel)"。

  1. 先看你的代码里做了什么
    你注册了两次 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)。

  1. 为什么要这样写?(省代码!)
    如果不这样写(传统做法):
    你得创建两个几乎一样的 XAML 界面:

UsersDialog.xaml(用户管理界面)

RolesDialog.xaml(角色管理界面)

然后分别注册:csharp复制services.AddTransient();

services.AddTransient();

缺点:界面长得差不多,但你要维护两套代码,改样式要改两次。

现在的写法(Keyed 做法):

只写一个 UsersDialog.xaml。

想管用户?拿钥匙 "User" → 装上 UserDialog_VM。

想管角色?拿钥匙 "Role" → 装上 RoleDialog_VM。

  1. 怎么使用这把"钥匙"?
    当你想弹出这个窗口时,不能再用 GetRequiredService 了,得用 GetRequiredKeyedService:
csharp复制 复制代码
// 场景1:点击"用户管理"按钮
var dialog = ServiceProvider.GetRequiredKeyedService<UsersDialog>("User");
dialog.Show();

// 场景2:点击"角色管理"按钮
var dialog = ServiceProvider.GetRequiredKeyedService<UsersDialog>("Role");
dialog.Show();
  1. 逐字翻译
csharp复制 复制代码
services.AddKeyedTransient<UsersDialog>(  // 注册 UsersDialog,而且是瞬时的(每次 new)
    "User",                               // 给它一把钥匙,名叫 "User"
    (sp, key) =>                          // 制作说明书:sp=管理员,key=刚才那把钥匙(这里没用上,但签名要有)
        new UsersDialog 
        { 
            DataContext = sp.GetRequiredService<UserDialog_VM>() // 装上用户管理的大脑
        });
  1. 总结

一句话:

这行代码是 "一把钥匙开一把锁"。它让你用一个 UsersDialog 界面,通过切换钥匙("User"或"Role"),变身成"用户管理界面"或"角色管理界面"。

语法8

csharp 复制代码
  var permissionConnectionString = context.Configuration.GetConnectionString("PermissionDefaultConnection");
  1. 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 使用。

  1. 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;..."

  1. 接下来的代码(连贯理解)
    拿到这两个字符串后,你的代码立马把它们传给了数据库上下文的注册:
csharp复制 复制代码
// 配置UserPermissionContext(SQLite + 延迟加载)
services.AddDbContext<UserPermissionContext>(options =>
    options.UseSqlite(permissionConnectionString) // ← 用刚才读到的字符串
           .UseLazyLoadingProxies(), ServiceLifetime.Scoped);

总结

这两行代码是**"桥梁"**:

左手从 appsettings.json 文件里把数据库地址拿出来。

右手把这些地址塞给 Entity Framework Core 的数据库上下文配置。

这样,你的程序就知道该连哪个数据库了。而且以后换数据库(比如从 SQLite 换到 SQL Server),只需要改 appsettings.json,不用动代码。