news 2026/7/3 12:05:57

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
7.2:深拷贝;标准特性标签; Transient;Scoped;数据库上下文

语法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>();// │ │ │// 生命周期 接口 实现类

  1. 注册:services.AddTransient<I_jsonHelper, jsonHelper>();

  2. 使用:
    var helper = ServiceProvider.GetRequiredService<I_jsonHelper>();

    ↓ 系统自动做 ↓

  3. 系统:new jsonHelper()

  4. 返回给你

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

整体补充说明

  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 变量:

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>()});

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

  1. 先看普通的注册(简单,但有缺陷)
    代码:
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)。
把装好的整机交给你。

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

界面出来了。
但是界面背后没有数据(因为 DataContext 是 null)。
你点按钮没反应,因为按钮绑定的命令都在 MainWindow_VM 里,但界面找不到它。

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

语法6

services.AddTransient<Login>(sp=>newLogin{DataContext=sp.GetRequiredService<Login_VM>()});

这行代码和刚才那行很像,但有一个关键区别:它是 AddTransient(瞬时),而不是 AddSingleton(单例)。
用你的项目场景来解释:

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

用户打开软件 → 弹出 登录窗口(Login)。
输入密码,登录成功 → 进入主界面。
用户点击“注销” → 软件需要再次弹出登录窗口。

如果用 AddSingleton:

容器里只有一个 Login 实例。
用户注销后,那个 Login 窗口虽然关了,但实例还在内存里(单例嘛)。
再次登录时,容器会把旧的那个给你。
问题:旧的 Login 窗口可能状态没清(比如密码框里还有刚才输的星号,或者有个错误提示还没消),导致界面显示不正常。

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

每次登录,容器都会 new 一个全新的 Login 窗口。
全新的窗口,干干净净,密码框是空的,状态是初始的。
注销后再登录,又是新的,不会留上次操作的“脏数据”。

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

一句话:

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

语法8

varpermissionConnectionString=context.Configuration.GetConnectionString("PermissionDefaultConnection");
  1. 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;…”

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

总结
这两行代码是**“桥梁”**:

左手从 appsettings.json 文件里把数据库地址拿出来。
右手把这些地址塞给 Entity Framework Core 的数据库上下文配置。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/3 12:05:25

RTX Spark深度解析:AI原生PC如何重塑个人计算与AI代理开发

&#x1f680; 30款热门AI模型一站整合&#xff0c;DeepSeek/GLM/Claude 随心用&#xff0c;限时 5 折。 &#x1f449; 点击领海量免费额度 过去几年&#xff0c;我们一直在谈论“AI PC”&#xff0c;但很多时候&#xff0c;它更像是一个营销概念。你可能会在电脑上运行一个…

作者头像 李华
网站建设 2026/7/3 12:04:03

Struts2漏洞扫描器:从原理到实战的专项安全检测工具解析

1. 项目概述&#xff1a;为什么我们需要一个专门的Struts2扫描器&#xff1f;如果你在安全行业待过几年&#xff0c;尤其是2016年前后&#xff0c;一定对Struts2这个名字记忆犹新。那段时间&#xff0c;几乎每隔几个月就会爆出一个新的Struts2高危漏洞&#xff0c;从S2-016到S2…

作者头像 李华
网站建设 2026/7/3 12:00:04

终极解决方案:彻底解决Quark-Auto-Save转存失败的空间不足问题

终极解决方案&#xff1a;彻底解决Quark-Auto-Save转存失败的空间不足问题 【免费下载链接】quark_auto_save 夸克网盘签到、自动转存、命名整理、发推送提醒和刷新媒体库一条龙 项目地址: https://gitcode.com/gh_mirrors/qu/quark_auto_save 夸克网盘自动转存工具Quar…

作者头像 李华
网站建设 2026/7/3 11:59:10

电玩城设备再不更新就落伍了:聪明的老板为什么开始安排拳击机器人

电玩城设备更新&#xff0c;不是把娃娃机、赛车机、篮球机、街机这些传统设备一口气否定掉&#xff0c;而是要补上一类更容易让人停下来看、上手玩、围观拍摄的新体验。拳击机器人和实体机器人对战设备值得进入老板候选清单&#xff0c;原因不在“机器人”三个字够新&#xff0…

作者头像 李华
网站建设 2026/7/3 11:58:04

KoiWeave — 构建企业级持续进化的研发知识中枢(LLM-WIKI)

KoiWeave — 编织。把分散的代码仓库、零散的知识、不同时间尺度的工作流&#xff0c;织成一张有韧性、不断进化的网 当代码仓库越 split&#xff0c;知识越需要 unite。 KoiWeave 点个赞&#xff01;给个支持&#xff01; 一、写在前面 先承认一个事实&#xff1a;AI 写代码…

作者头像 李华