喜欢的文章先收藏&转发&点赞。否则根据头条的推荐机制,它会很快消失。
最近在用C#做一个传感器解调仪的上位机程序。原来的程序是在MATLAB中实现的,选MATLAB的原因不是它有多强大,而是因为我最熟悉。在原来的程序中,为了增强程序的可配置性,几个与解调算法密切相关的函数通过MATLAB的函数句柄关联到主程序中相关对象的*Fcn属性。这样即使整个程序被打包,用户仍可以单独编辑关联到回调属性的.m文件。由于MATLAB是解释执行的,运行时它可以读取.m文件中的指令并顺利执行。这样用户可以根据需要随时更新这些功能函数,比如升级算法,而无需对整个软件进行重新打包。但MATLAB做算法研究还可以,不太适合用来开发通用软件,其可移植性比较差,不同版本间兼容性也不好。虽然有免费的MATLAB Runtime,但体积巨大(>4GB)、界面在不同机器上一致性较差,维护成本实在太高。经充分了解,C#在上位机软件中使用比较普遍。所以借寒假,突击了一下。在AI的助攻下,很快上手并实现了程序的基本功能。
现在要解决的问题是:如何像MATLAB中那样,将几个功能函数以文本格式(.cs)开放给用户,在运行时检测这些cs文件是否比已有的.dll新。如果cs更新,则在运行时自动将其编译为.dll。几经搜索和尝试,最终确定了如下的路径:
准备工作:
- 定义一个接口,比如叫IFunction.cs,在其中声明所有功能函数的接口。
namespace YourAppName.CustomFunctions
{
public interface IFunction
{
double[] CalculateTemperature(double[,] V, double[,] t, params object[] additionalParams);
//其他需要实现的功能函数接口
}
}
- 在Functions.cs中实现这个接口,每个方法对应一个功能函数。
namespace YourAppName.CustomFunctions
{
public class Functions : IFunction
{
public double[] CalculateTemperature(double[,] V, double[,] t, params object[] additionalParams)
{
// 用户自定义的温度计算逻辑
}
}
}
- 定义一个FunctionLoader.cs,利用C#语言的委托(Delegates)和反射(Reflection)机制用来动态加载Functions.cs中已定义的指定方法。
namespace YourAppName.CustomFunctions
{
public class FunctionLoader
{
private readonly string _className = "YourAppName.CustomFunctions.Functions";
private readonly string _dllPath = "CustomFunctions/Dll/Functions.dll";
public Func LoadFunction(string methodName)
{
//用户代码
return (Func)Delegate.CreateDelegate(typeof(Func), instance, method);
}
}
}
这里的代码要与功能函数接口一致。比如此例是加载一个有3个输入(2double二维数组,1double一维数组)和1个返回参数(double一维数组)的函数。
步骤1:安装Roslyn编译器
- 在Visual Studio中,点击工具 > NuGet 包管理器 > 包管理器控制台。
- 在包管理器控制台中,输入以下命令并按Enter:
Install-Package Microsoft.CodeAnalysis.CSharp
步骤2:在FunctionLoader类中添加逻辑
检查Functions.cs是否比Functions.dll新,并在需要时编译Functions.cs为DLL。
public class FunctionLoader
{
private readonly string _className = "YourAppName.CustomFunctions.Functions";
private readonly string _dllPath = "CustomFunctions/Dll/Functions.dll";
private readonly string _csPath = "CustomFunctions/Functions.cs";
public Func LoadFunction(string methodName)
{
if (File.Exists(_csPath) && (!File.Exists(_dllPath) || File.GetLastWriteTime(_csPath) > File.GetLastWriteTime(_dllPath)))
{
CompileCsToDll(_csPath, _dllPath);
}
if (File.Exists(_dllPath)) // 如果.dll文件存在,则使用Functions.dll中的方法
{
Assembly assembly = Assembly.LoadFrom(_dllPath);
Type type = assembly.GetType(_className);
object instance = Activator.CreateInstance(type);
MethodInfo method = type.GetMethod(methodName);
}
else// 否则,使用软件内置的方法
{
Type type = typeof(Functions);
object instance = Activator.CreateInstance(type);
MethodInfo method = type.GetMethod(methodName);
}
return (Func)Delegate.CreateDelegate(typeof(Func), instance, method);
}
private void CompileCsToDll(string csPath, string dllPath)
{
var syntaxTree = CSharpSyntaxTree.ParseText(File.ReadAllText(csPath));
var references = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic)
.Select(a => MetadataReference.CreateFromFile(a.Location))
.Cast();
var compilation = CSharpCompilation.Create(Path.GetFileNameWithoutExtension(dllPath))
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddReferences(references)
.AddSyntaxTrees(syntaxTree);
var result = compilation.Emit(dllPath);
if (!result.Success)
{
var failures = result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error);
foreach (var diagnostic in failures)
{
Console.Error.WriteLine($"{diagnostic.Id}: {diagnostic.GetMessage()}");
}
throw new InvalidOperationException("Compilation failed.");
}
}
}
步骤3:在App主窗口中使用FunctionLoader类,并调用用户自定义函数。
// 加载用户自定义函数
FunctionLoader functionLoader = new FunctionLoader();
sensor.TemperatureFcn = functionLoader.LoadFunction("CalculateTemperature");
// 示例调用用户自定义函数
double[,] V = new double[2, 10]; // 示例输入
double[,] t = new double[2, 10]; // 示例时间输入
double[] temperature = sensor.TemperatureFcn(V, t, new object[] { "additionalParam1", 123 });
// 输出结果
foreach (var temp in temperature)
{
Console.WriteLine(temp);
}
步骤4:创建构件后事件
在默认情形下,IDE 并不会在编译后的项目目录中创建 CustomFunctions,也不会将 Functions.cs 复制至其中。这会导致在调试阶段Initialize函数中的if条件永远不会满足,也就无法实现自动编译。一种方案是在编译后手动去创建目录并复制文件过去。这里介绍另一种更方便的方案:
- 在“解决方案管理器”中,右键点击项目,选择“属性”。
- 在项目属性窗口中,选择“生成事件”选项卡。
- 在“后期生成事件命令行”文本框中,输入以下命令:
if not exist "$(TargetDir)CustomFunctions\Dll" mkdir "$(TargetDir)CustomFunctions\Dll"
copy /Y "$(ProjectDir)CustomFunctions\Functions.cs" "$(TargetDir)CustomFunctions\Functions.cs"
- 第一句指令检查目标目录中是否存在 CustomFunctions\Dll 文件夹,如果不存在则创建它。
- 第二句指令将 Functions.cs 文件从项目目录复制到目标目录中的 CustomFunctions 文件夹,/Y 参数表示覆盖现有文件而不提示
总结
- 利用上述方法,用户可以根据规定好的接口,用文本编辑器修改Functions.cs中的方法。
- 运行时软件会判断:如果存在.cs文件但无.dll文件,或者虽然二者都存在但.cs比.dll新,程序会自动编译.cs为.dll。
- 上述示例中还利用C#中的params,实现了类似MATLAB中varargin的功能。
只学了半个多月C#,此文仅作为学习记录。如果您有更好、更简洁的实现方法,欢迎赐教。先行拜谢