前几天要解决动态计算问题,尝试着使用了不同的方法。问题是给定一个包含计算的字符串,在程序运行中得到计算结果,当时考虑了动态编译,在网上查了一些资料完成了这项功能,可是基于不同的.NET平台使用的编程代码相差比较大,觉得麻烦就没有使用,用了常规的三种方法,分别是:使用DataTable、使用JavaScript、使用Excel表单元格的计算。
了解这项技术还是值得的,因为我的项目基于.NET6,也就使用了基于.NET6的动态编译来完成计算字符串的动态编译和结果输出。
⑴解决引用问题
在关闭项目的情况下修改项目文件。
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>WinExe</OutputType><TargetFramework>net6.0-windows</TargetFramework><Nullable>enable</Nullable><UseWindowsForms>true</UseWindowsForms><ImplicitUsings>enable</ImplicitUsings></PropertyGroup><ItemGroup><PackageReference Include="Microsoft.Net.Compilers" Version="3.12.0" PrivateAssets="all" /><PackageReference Include="Microsoft.Net.Compilers.Toolset" Version="3.12.0" PrivateAssets="all" />
</ItemGroup></Project>
其中ItemGroup节点及内容是添加的。
保存后再打开项目进行代码编写。
⑵添加引用
using System;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
⑶代码编写
private void button1_Click(object sender, EventArgs e){string StrInputCode=textBox1.Text.Trim();string CompileCode = @"using System;public class Calculator{public static double CalculateResult(){double result = "+StrInputCode+@";return result;}}";// 创建表示代码中的结构和语法的语法树SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(CompileCode);// 创建了一个C#编译实例,定义编译选项,添加编译引用CSharpCompilation compilation = CSharpCompilation.Create("DynamicAssembly").WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)).AddReferences(MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location)).AddReferences(MetadataReference.CreateFromFile(typeof(Action<string>).GetTypeInfo().Assembly.Location)) // 添加对Action的引用.AddReferences(MetadataReference.CreateFromFile(typeof(string).GetTypeInfo().Assembly.Location)) // 添加对string的引用.AddSyntaxTrees(syntaxTree);// 编译代码using (MemoryStream ms = new MemoryStream()){//使用compilation.Emit方法对动态生成的代码进行编译。CompileResult包含编译结果。EmitResult CompileResult = compilation.Emit(ms);if (CompileResult.Success){ms.Seek(0, SeekOrigin.Begin);//使用Assembly.Load方法加载编译后的程序集。Assembly assembly = Assembly.Load(ms.ToArray());//得到类型信息Type type = assembly.GetType("Calculator");//得到方法信息MethodInfo method = type.GetMethod("CalculateResult");//获取计算结果double result1 = (double)method.Invoke(null, null); // 将计算结果输出到TextBox2中OutputStr(result1.ToString()); }else{string StrFalse="";foreach (Diagnostic diagnostic in CompileResult.Diagnostics){StrFalse+= diagnostic.ToString();}//输出编译错误信息textBox2.Text = StrFalse;}}}private void OutputStr(string text){if (textBox2.InvokeRequired){textBox2.Invoke((MethodInvoker)delegate { textBox2.Text = text; });}else{textBox2.Text = text;}}
虽然可以得到正确的结果,但是因为使用的是双精度变量接收结果可能出现结果误差,比如输入1+3-2.2,正确结果应该是1.8,实际输出却是1.7999999999999998;另外,编译的速度也不理想,因为程序中参与运算的量比较大,这一点很成问题了。
也因为如此,担心计算偏差,在程序中我没有使用这项技术,使用DataTable比较稳妥。
上面的程序也可以修改,以便完成更多的需求:
获取计算公式并定义用户方法:
string StrInputCode =textBox1.Text.Trim();string CompileCode = @"using System;public class UserClass{public static void UserMethod(Action<string> OutputStr){double Result="+ StrInputCode + @";string StrResult=Result.ToString();OutputStr(StrResult);}}";
在编译成功后获取输出:
ms.Seek(0, SeekOrigin.Begin);Assembly assembly = Assembly.Load(ms.ToArray());Type type = assembly.GetType("UserClass");MethodInfo method = type.GetMethod("UserMethod", new Type[] { typeof(Action<string>) });method.Invoke(null, new object[] { new Action<string>(OutputStr) });
程序也可以正常运行并获取正确结果。
本来是想通过这项技术应对一些后面的需求变更,但是实现起来还是不理想,应对需求变更也可以使用其他的方法,比如依赖注入或者使用委托定义好方法和参数并将这些方法编译到一个DLL中,后面只需要修改方法代码再编译就可以了。