程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

C#编程技巧:在运行时检查和动态编译.cs为.dll并调用的方法

balukai 2025-04-08 11:41:19 文章精选 4 ℃

喜欢的文章先收藏&转发&点赞。否则根据头条的推荐机制,它会很快消失。


最近在用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编译器

  1. 在Visual Studio中,点击工具 > NuGet 包管理器 > 包管理器控制台。
  2. 在包管理器控制台中,输入以下命令并按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条件永远不会满足,也就无法实现自动编译。一种方案是在编译后手动去创建目录并复制文件过去。这里介绍另一种更方便的方案:

  1. 在“解决方案管理器”中,右键点击项目,选择“属性”。
  2. 在项目属性窗口中,选择“生成事件”选项卡。
  3. 在“后期生成事件命令行”文本框中,输入以下命令:
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#,此文仅作为学习记录。如果您有更好、更简洁的实现方法,欢迎赐教。先行拜谢

最近发表
标签列表