字段临时缓存包装器

前言

在实际开发中,我们有时候存在一种需求,例如对于某个字段,我们希望在某个明确的保存节点前对字段的修改都仅作为缓存保留,最终是否应用这些修改取决于某些条件,比如玩家对游戏设置的修改可能需要玩家明确确认应用修改后才会保存下来,在此之前玩家在游戏界面上的所有修改都是临时的。


本文基于这个需求探索出了一种解决方案“字段临时缓存包装器”,通过创建字段或用于存储字段临时数据的数据结构的副本来实现临时缓存,虽然我们同样可以采用直接声明一个副本字段的方式来达到同样的目的,但是这可能会增加冗余代码,且不利于代码的维护,通过包装器来封装临时缓存的通用逻辑,与具体业务逻辑隔离。

代码

v1.0
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using Newtonsoft.Json;/// <summary>
/// 临时包装器
/// </summary>
/// <typeparam name="T">字段类型</typeparam>
/// <remarks>
/// 该类主要用于创造某个字段的副本作为该字段的临时缓存,避免直接修改源字段。
/// </remarks>
public class TempWrapper<T> : IDisposable
{/// <summary>/// 是否为值类型/// <para>提示:若为true则表示包装字段为值类型,否则为引用类型</para>/// </summary>public static bool isValueType => _isValueType;/// <summary>/// 缓存字段/// <para>提示:对于值类型而言,该属性涉及拷贝</para>/// </summary>public T value{get{if (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");return _value;}set{if (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");_value = value;}}/// <summary>/// 获取引用/// <para>提示:对于值类型而言,该属性直接返回引用从而避免拷贝</para>/// </summary>public ref T refrence{get{if (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");return ref _value;}}/// <summary>/// 是否已经释放/// </summary>public bool isDisposed => _isDisposed;static readonly bool _isValueType = typeof(T).IsValueType;static readonly bool _isDisposable = typeof(IDisposable).IsAssignableFrom(typeof(T));static readonly object _key = new object();T _value;bool _isDisposed;TempWrapper() { }/// <summary>/// 包装指定字段并返回包装类/// </summary>/// <param name="value">待包装字段的引用</param>/// <remarks>/// <para>提示:采用二进制序列化和反序列化生成字段副本</para>/// <para>提示:该方法仅可用于被 <c>Serializable</c> 标记的字段类型</para>/// </remarks>public static TempWrapper<T> WrapByBinary(ref T value){lock (_key){try{TempWrapper<T> wrapper = new TempWrapper<T>();if (_isValueType) wrapper._value = value;else{using (MemoryStream ms = new MemoryStream()){IFormatter formatter = new BinaryFormatter();formatter.Serialize(ms, value);ms.Seek(0, SeekOrigin.Begin);wrapper._value = (T)formatter.Deserialize(ms);}}return wrapper;}catch (Exception e){throw new InvalidOperationException("Failed to wrap.", e);}}}/// <summary>/// 包装指定字段并返回包装类/// <para>提示:采用JSON序列化和反序列化生成字段副本</para>/// </summary>/// <param name="value">待包装字段的引用</param>public static TempWrapper<T> WrapByJson(ref T value){lock (_key){try{TempWrapper<T> wrapper = new TempWrapper<T>();if (_isValueType) wrapper._value = value;else{string jsonStr = JsonConvert.SerializeObject(value);wrapper._value = JsonConvert.DeserializeObject<T>(jsonStr);}return wrapper;}catch (Exception e){throw new InvalidOperationException("Failed to wrap.", e);}}}/// <summary>/// 包装生成器所生成的字段并返回包装类/// </summary>/// <param name="creator">生成器</param>public static TempWrapper<T> WrapByCustom(Func<T> creator){lock (_key){try{TempWrapper<T> wrapper = new TempWrapper<T>() { _value = creator() };return wrapper;}catch (Exception e){throw new InvalidOperationException("Failed to wrap.", e);}}}/// <summary>/// 解包包装器并赋值给指定的字段/// </summary>/// <remarks>/// <para>提示:采用二进制序列化和反序列化解包</para>/// <para>提示:该方法仅可用于被 <c>Serializable</c> 标记的字段类型</para>/// </remarks>public void UnWrapByBinary(ref T value){if (_isValueType) value = _value;else{lock (_key){using (MemoryStream ms = new MemoryStream()){IFormatter formatter = new BinaryFormatter();formatter.Serialize(ms, _value);ms.Seek(0, SeekOrigin.Begin);value = (T)formatter.Deserialize(ms);}}}}/// <summary>/// 解包包装器并赋值给指定的字段/// <para>提示:采用JSON序列化和反序列化解包</para>/// </summary>public void UnwrapByJson(ref T value){if (_isValueType) value = _value;else{lock (_key){string jsonStr = JsonConvert.SerializeObject(_value);value = JsonConvert.DeserializeObject<T>(jsonStr);}}}/// <summary>/// 释放包装器所包装的字段/// <para>提示:当所包装字段实现了IDisposable接口时该方法才有效</para>/// </summary>public void Dispose(){if (_isDisposed) return;DoDispose(true);GC.SuppressFinalize(this);}void DoDispose(bool disposing){if (_isDisposed) return;_isDisposed = true;if (disposing && _isDisposable && _value is IDisposable ds)ds.Dispose();}~TempWrapper(){DoDispose(false);}
}
v1.1 
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using Newtonsoft.Json;/// <summary>
/// 临时包装器
/// </summary>
/// <typeparam name="T">字段类型</typeparam>
/// <remarks>
/// 该类主要用于创造某个字段的副本作为该字段的临时缓存,避免直接修改源字段。
/// </remarks>
public class TempWrapper<T> : IDisposable
{/// <summary>/// 是否为值类型/// <para>提示:若为true则表示包装字段为值类型,否则为引用类型</para>/// </summary>public static bool isValueType => _isValueType;/// <summary>/// 缓存字段/// <para>提示:对于值类型而言,该属性涉及拷贝</para>/// </summary>public T value{get{if (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");return _value;}set{if (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");_value = value;}}/// <summary>/// 获取引用/// <para>提示:对于值类型而言,该属性直接返回引用从而避免拷贝</para>/// </summary>public ref T refrence{get{if (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");return ref _value;}}static readonly bool _isValueType = typeof(T).IsValueType;static readonly bool _isDisposable = typeof(IDisposable).IsAssignableFrom(typeof(T));static readonly object _key = new object();T _value;bool _isDisposed;TempWrapper() { }/// <summary>/// 包装指定字段并返回包装类/// </summary>/// <param name="value">待包装字段的引用</param>/// <remarks>/// <para>提示:采用二进制序列化和反序列化生成字段副本</para>/// <para>提示:该方法仅可用于被 <c>Serializable</c> 标记的字段类型</para>/// </remarks>public static TempWrapper<T> WrapByBinary(ref T value){lock (_key){try{TempWrapper<T> wrapper = new TempWrapper<T>();if (_isValueType) wrapper._value = value;else{using (MemoryStream ms = new MemoryStream()){IFormatter formatter = new BinaryFormatter();formatter.Serialize(ms, value);ms.Seek(0, SeekOrigin.Begin);wrapper._value = (T)formatter.Deserialize(ms);}}return wrapper;}catch (Exception e){throw new InvalidOperationException("Failed to wrap.", e);}}}/// <summary>/// 包装指定字段并返回包装类/// <para>提示:采用JSON序列化和反序列化生成字段副本</para>/// </summary>/// <param name="value">待包装字段的引用</param>public static TempWrapper<T> WrapByJson(ref T value){lock (_key){try{TempWrapper<T> wrapper = new TempWrapper<T>();if (_isValueType) wrapper._value = value;else{string jsonStr = JsonConvert.SerializeObject(value);wrapper._value = JsonConvert.DeserializeObject<T>(jsonStr);}return wrapper;}catch (Exception e){throw new InvalidOperationException("Failed to wrap.", e);}}}/// <summary>/// 包装生成器所生成的字段并返回包装类/// </summary>/// <param name="creator">生成器</param>public static TempWrapper<T> WrapByCustom(Func<T> creator){lock (_key){try{TempWrapper<T> wrapper = new TempWrapper<T>() { _value = creator() };return wrapper;}catch (Exception e){throw new InvalidOperationException("Failed to wrap.", e);}}}/// <summary>/// 解包包装器并赋值给指定的字段/// </summary>/// <remarks>/// <para>提示:采用二进制序列化和反序列化解包</para>/// <para>提示:该方法仅可用于被 <c>Serializable</c> 标记的字段类型</para>/// </remarks>public void UnWrapByBinary(ref T value){if (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");if (_isValueType) value = _value;else{using (MemoryStream ms = new MemoryStream()){IFormatter formatter = new BinaryFormatter();formatter.Serialize(ms, _value);ms.Seek(0, SeekOrigin.Begin);value = (T)formatter.Deserialize(ms);}}}/// <summary>/// 解包包装器并赋值给指定的字段/// <para>提示:采用JSON序列化和反序列化解包</para>/// </summary>public void UnwrapByJson(ref T value){if (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");if (_isValueType) value = _value;else{string jsonStr = JsonConvert.SerializeObject(_value);value = JsonConvert.DeserializeObject<T>(jsonStr);}}/// <summary>/// 释放包装器所包装的字段/// <para>提示:当所包装字段实现了IDisposable接口时该方法才有效</para>/// </summary>public void Dispose(){if (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");DoDispose(true);GC.SuppressFinalize(this);}void DoDispose(bool disposing){if (_isDisposed) return;_isDisposed = true;if (disposing && _isDisposable && _value is IDisposable ds)ds.Dispose();}~TempWrapper(){DoDispose(false);}
}

测试

#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;// TempWrapper测试脚本
public class TempWrapperTest : MonoBehaviour
{[SerializeField] int[] intArray;[SerializeField] string[] strArray;[SerializeField] StructA[] structaArray;[SerializeField] ClassA classA;[SerializeField] StructA structA;[SerializeField] Transform tf;[Serializable]struct StructA{public string key;public int value;public override string ToString(){return $"(key:{key},value:{value})";}}[Serializable]class ClassA{public string key;public StructA structA;public override string ToString(){StringBuilder builder = new StringBuilder("[key:");builder.Append(key).Append(",").Append($"StructA:{structA}");builder.Append("]");return builder.ToString();}}TempWrapper<int[]> intArrayWrapper;TempWrapper<string[]> strArrayWrapper;TempWrapper<StructA[]> structaArrayWrapper;TempWrapper<ClassA> classaWrapper;TempWrapper<StructA> structaWrapper;TempWrapper<Transform> tfWrapper;void Awake(){if (Application.isPlaying){intArrayWrapper = TempWrapper<int[]>.WrapByBinary(ref intArray);strArrayWrapper = TempWrapper<string[]>.WrapByJson(ref strArray);structaArrayWrapper = TempWrapper<StructA[]>.WrapByBinary(ref structaArray);classaWrapper = TempWrapper<ClassA>.WrapByBinary(ref classA);structaWrapper = TempWrapper<StructA>.WrapByBinary(ref structA);tfWrapper = TempWrapper<Transform>.WrapByCustom(() => Instantiate(tf));Instantiate(transform);}}void OnDestroy(){intArrayWrapper.Dispose();strArrayWrapper.Dispose();structaArrayWrapper.Dispose();classaWrapper.Dispose();structaWrapper.Dispose();tfWrapper.Dispose();}void Update(){if (Input.GetKeyDown(KeyCode.Q)){PrintWrapper();PrintHashCode();PrintWrapperHashCode();}if (Input.GetKeyDown(KeyCode.W)){WriteWrapper();UnWrap();}}void PrintWrapper(){intArrayWrapper.value.LogC("IntArrayWrapper:");strArrayWrapper.value.LogC("StrArrayWrapper:");structaArrayWrapper.value.LogC(s => $"[key:{s.key},value:{s.value}]", "StructaArrayWrapper:");LogUtility.Log("ClassAWrapper:" + classaWrapper.value);LogUtility.Log("StructAWrapper:" + structaWrapper.value);LogUtility.Log("TfWrapper:" + tfWrapper.value.position);}void PrintHashCode(){LogUtility.Log("IntArray:" + intArray.GetHashCode());LogUtility.Log("StrArray:" + strArray.GetHashCode());LogUtility.Log("StructaArray:" + structaArray.GetHashCode());LogUtility.Log("ClassA:" + classA.GetHashCode());LogUtility.Log("StructA:" + structA.GetHashCode());LogUtility.Log("Tf:" + tf.GetHashCode());}void PrintWrapperHashCode(){LogUtility.Log("IntArrayWrapper:" + intArrayWrapper.value.GetHashCode());LogUtility.Log("StrArrayWrapper:" + strArrayWrapper.value.GetHashCode());LogUtility.Log("StructaArrayWrapper:" + structaArrayWrapper.value.GetHashCode());LogUtility.Log("ClassAWrapper:" + classaWrapper.value.GetHashCode());LogUtility.Log("StructAWrapper:" + structaWrapper.value.GetHashCode());LogUtility.Log("TfWrapper:" + tfWrapper.value.GetHashCode());}void WriteWrapper(){List<int> ints = new List<int>(intArrayWrapper.value) { 99, 100 };intArrayWrapper.value = ints.ToArray();List<string> strs = new List<string>(strArrayWrapper.value) { "D", "E" };strArrayWrapper.value = strs.ToArray();List<StructA> strcutAs = new List<StructA>(structaArrayWrapper.value){new StructA { key = "D", value = 99 },new StructA { key = "E", value = 100 }};structaArrayWrapper.value = strcutAs.ToArray();structaWrapper.refrence.key = "E";structaWrapper.refrence.value = 1000;classaWrapper.value.key = "DE";classaWrapper.value.structA.key = "D";classaWrapper.value.structA.value = 999;tfWrapper.value.position = Vector3.zero;}void UnWrap(){intArrayWrapper.UnWrapByBinary(ref intArray);strArrayWrapper.UnwrapByJson(ref strArray);structaArrayWrapper.UnWrapByBinary(ref structaArray);structaWrapper.UnWrapByBinary(ref structA);classaWrapper.UnWrapByBinary(ref classA);}
}
#endif
v1.0 
用例ID用例名称前者测试预期结果是否通过
1简单值类型数组可缓存通过
2不可变引用类型数组可缓存通过
3复合值类型数组可缓存通过
4自定义引用类型可缓存通过
5自定义值类型可缓存通过
6Unity对象可缓存通过
v1.1
用例ID用例名称前者测试预期结果是否通过
1简单值类型数组可缓存通过
2不可变引用类型数组可缓存通过
3复合值类型数组可缓存通过
4自定义引用类型可缓存通过
5自定义值类型可缓存通过
6Unity对象可缓存通过

分析

字段临时缓存包装器有三种包装字段的方式,分别是WrapByBinary、WrapByJson和WrapByCustom,三种方式各有优缺点,择优而用。WrapByBinary采用二进制序列化和反序列化生成字段副本,该方法仅可用于被 Serializable 标记的字段类型。WrapByJson采用JSON序列化和反序列化生成字段副本,它虽然比前者包装范围更广,但是不可避免可能会依赖第三方用于JSON序列化和反序列化的库。WrapByCustom则是对前两种方式的补充,当前两种方式都不适用时,则可以自定义包装方式,例如对于Unity对象来说,需要通过Instantiate方法创建对象副本,这个时候就只能用自定义的方法进行包装。


返回的包装器提供了一些属性和方法,可用于判断是否为值类型、缓存的字段和缓存字段的引用(值类型),提供了针对WrapByBinary和WrapByJson包装方法的解包方法,还提供了显式释放包装器的方法。对于包装值类型时,我们可以通过获取缓存字段的引用来避免拷贝,解包方法用于将临时缓存的数据重新写入被包装字段中。通过显式释放包装器可以保证那些使用了非托管资源的类型(实现了IDisposable接口)进行资源的释放工作,从而避免内存泄漏等问题。


但是该包装器存在一些不可避免的限制,若所包装字段越复杂,其性能损耗越高,这是不可避免的。而对于复杂类型,建议自定义一个数据结构作为临时缓存的包装类型。


注意:不要使用包装器去包装其声明所在的类,特别是对于自定义包装逻辑要避免无限递归,否则会导致栈溢出或内存泄漏问题。示例代码如下:

// 该方法将导致栈溢出
public class A
{TempWrapper<A> wrapper;public A(){wrapper = TempWrapper<A>.WrapByCustom(() => new A());}
}// 该方法将导致内存泄漏
public class B:Monobehaviour
{TempWrapper<Transform> wrapper;void Awake(){// Instantiate方法去克隆当前组件对象的Transform组件就会导致无限递归wrapper = TempWrapper<Transform>.WrapByCustom(() => Instantiate(transform));}
}

版本改进

版本号改进内容
v1.1

1.实例方法不使用线程锁,仅对静态方法使用线程锁;

2.手动执行Dispose方法释放包装器后,所有对公开实例成员的访问都将触发异常;

3.删除IsDisposed属性;

............

系列文章

......

如果这篇文章对你有帮助,请给作者点个赞吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/439857.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一个真实可用的登录界面!

需要工具&#xff1a; MySQL数据库、vscode上的php插件PHP Server等 项目结构&#xff1a; login | --backend | --database.sql |--login.php |--welcome.php |--index.html |--script.js |--style.css 项目开展 index.html&#xff1a; 首先需要一个静态网页&#x…

机器学习(5):机器学习项目步骤(二)——收集数据与预处理

1. 数据收集与预处理的任务&#xff1f; 为机器学习模型提供好的“燃料” 2. 数据收集与预处理的分步骤&#xff1f; 收集数据-->数据可视化-->数据清洗-->特征工程-->构建特征集和数据集-->拆分数据集、验证集和测试集 3. 数据可视化工作&#xff1f; a. 作用&…

基于SpringBoot+Vue的在线投票系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

JDBC 概述

JDBC 概述 JDBC的基本概念与功能JDBC的工作原理JDBC的组件与类JDBC的类型与特性JDBC的应用场景 JDBC&#xff08;Java Database Connectivity&#xff09;即Java数据库连接&#xff0c;是Java编程语言用于与数据库进行连接和操作的API&#xff08;应用程序编程接口&#xff09;…

C++面试速通宝典——7

150. 数据库连接池的作用 数据库连接池的作用包括以下几个方面&#xff1a; 资源重用&#xff1a;连接池允许多个客户端共享有限的数据库连接&#xff0c;减少频繁创建和销毁连接的开销&#xff0c;从而提高资源的利用率。 统一的连接管理&#xff1a;连接池集中管理数据库连…

python交互式命令时如何清除

在交互模式中使用Python&#xff0c;如果要清屏&#xff0c;可以import os&#xff0c;通过os.system()来调用系统命令clear或者cls来实现清屏。 [python] view plain copy print? >>> import os >>> os.system(clear) 但是此时shell中的状态是&#xff1a;…

Java 面向对象设计一口气讲完![]~( ̄▽ ̄)~*(上)

目录 Java 类实例 Java面向对象设计 - Java类实例 null引用类型 访问类的字段的点表示法 字段的默认初始化 Java 访问级别 Java面向对象设计 - Java访问级别 Java 导入 Java面向对象设计 - Java导入 单类型导入声明 按需导入声明 静态导入声明 例子 Java 方法 J…

msvcp140.dll丢失的解决方法,详细解读6种解决方法

在使用电脑时&#xff0c;我们可能会遇到提示缺少msvcp140.dll的错误信息。这个提示意味着我们的电脑中缺少MSVCP140.dll这个文件&#xff0c;它是某些程序运行所必需的。如果我们遇到这个问题&#xff0c;应该如何解决呢&#xff1f;本文将详细解析如何解决msvcp140.dll丢失的…

Study-Oracle-10-ORALCE19C-RAC集群搭建

一路走来,所有遇到的人,帮助过我的、伤害过我的都是朋友,没有一个是敌人。 ORACLE --RAC 搭建理念:准备工作要仔细,每个参数及配置都到仔细核对。环境准备完成后,剩下的就是图像化操作,没啥难度,所以图形化操作偷懒不续写了。 一、硬件信息及配套软件 1、硬件设置 RAC…

springboot系列--web相关知识探索二

一、映射 指的是与请求处理方法关联的URL路径&#xff0c;通过在Spring MVC的控制器类&#xff08;使用RestController注解修饰的类&#xff09;上使用注解&#xff08;如 RequestMapping、GetMapping&#xff09;来指定请求映射路径&#xff0c;可以将不同的HTTP请求映射到相应…

【React】事件机制

事件机制 react 基于浏览器的事件机制自身实现了一套事件机制&#xff0c;称为合成事件。比如&#xff1a;onclick -> onClick 获取原生事件&#xff1a;e.nativeEvent onClick 并不会将事件代理函数绑定到真实的 DOM节点上&#xff0c;而是将所有的事件绑定到结构的最外层…

Mysql数据库约束

前言 数据库是用户数据的最后一道保护屏障&#xff0c;所以数据库存在大量的约束&#xff0c;保证数据库中数据的完整性和可预期性。数据库中&#xff0c;数据类型本身就是一种约束&#xff0c;除此在外还有&#xff1a; null/not null&#xff0c;default&#xff0c; comment…

【大模型 AI 学习】大模型 AI 部署硬件配置方案(本地硬件配置 | 在线GPU)

最近想部署一个开源深度学习项目&#xff0c;但是小编的笔记本电脑是8G的集成显存&#xff0c;且没有GPU&#xff0c;性能肯定是不够的。于是小编在小po站上粗浅了解了一下当前: 1. 大模型 AI本地硬件配置和 2. 云上申请GPU算力的两种方式。简单记录一下&#xff1a; 参考视频…

openEuler 24.03 (LTS) 部署 K8s(v1.31.1) 高可用集群(Kubespray Ansible 方式)

写在前面 实验需要一个 CNI 为 flannel 的 K8s 集群之前有一个 calico 的版本有些旧了,所以国庆部署了一个v1.31.1 版本 3 * master 5 * work时间关系直接用的工具 kubespray博文内容为部署过程以及一些躺坑分享需要科学上网理解不足小伙伴帮忙指正 &#x1f603;,生活加油 99…

Android2024.2.1升级错误

提示 Gradle 版本不兼容&#xff0c;升级后就报错了 。 1.gradle安装包镜像 distributionBaseGRADLE_USER_HOME distributionPathwrapper/dists //distributionUrlhttps\://services.gradle.org/distributions/gradle-8.5-bin.zip distributionUrlhttps://mirrors.cloud.tencen…

一个月学会Java 第2天 认识类与对象

Day2 认识类与对象 第一章 初识类 经过一个程序的编写&#xff0c;应该对程序的结构有点好奇了吧&#xff0c;如果你有基础&#xff0c;接下来的肯定非常的易懂&#xff0c;如果你没有基础也没有关系&#xff0c;反复琢磨一下也就懂了&#x1f606; 我们来重复一下第一个程序 …

Pikachu-unsafe upfileupload-getimagesize

什么是getimagesize()&#xff1f; getimagesize()是PHP中用于获取图像的大小和格式的函数。它可以返回一个包含图像的宽度、高度、类型和MIME类型的数组。 由于返回的这个类型可以被伪造&#xff0c;如果用这个函数来获取图片类型&#xff0c;从而判断是否时图片的话&#xff…

4.资源《Arduino UNO R3 proteus 电机PID参数整定工程文件(含驱动代码)》说明。

资源链接&#xff1a; Arduino UNO R3 proteus 电机PID参数整定工程文件&#xff08;含驱动代码&#xff09; 1.文件明细&#xff1a; 2.文件内容说明 包含&#xff1a;proteus工程&#xff0c;内含设计图和工程代码。 3.内容展示 4.简述 工程功能可以看这个视频 PID仿真调…

Graphiti:如何让构建知识图谱变得更快、更具动态性?

扩展大语言模型数据提取&#xff1a;挑战、设计决策与解决方案 Graphiti 是一个用于构建和查询动态、时间感知的知识图谱的 Python 库。它可以用于建模复杂、不断演变的数据集&#xff0c;并确保 AI 智能体能够访问它们完成非平凡任务所需的数据。它是一个强大的工具&#xff…

java将word转pdf

总结 建议使用aspose-words转pdf,poi的容易出问题还丑… poi的(多行的下边框就不对了) aspose-words的(基本和word一样) poi工具转换 <!-- 处理PDF --><dependency><groupId>fr.opensagres.xdocreport</groupId><artifactId>fr.opensagres…