简而言之,问题是:如何引用包含可重用脚本代码的第二个脚本,在需要能够卸载和重新加载脚本的约束下,如果它们中的任何一个更改而不重新启动主机应用程序?
我正在尝试使用CS-Script“编译器作为服务”(CSScript.Evaluator)编译脚本类,同时引用刚刚从第二个“库”脚本编译的程序集。目的是库脚本应包含可以重用于不同脚本的代码。
下面是一个示例代码,用于说明该想法,但也会在运行时导致CompilerException。
using CSScriptLibrary;
using NUnit.Framework;
[TestFixture]
public class ScriptReferencingTests
{
private const string LibraryScriptCode = @"
public class Helper
{
public static int AddOne(int x)
{
return x + 1;
}
}
";
private const string ScriptCode = @"
using System;
public class Script
{
public int SumAndAddOne(int a, int b)
{
return Helper.AddOne(a+b);
}
}
";
[Test]
public void CSScriptEvaluator_CanReferenceCompiledAssembly()
{
var libraryEvaluator = CSScript.Evaluator.CompileCode(LibraryScriptCode);
var libraryAssembly = libraryEvaluator.GetCompiledAssembly();
var evaluatorWithReference = CSScript.Evaluator.ReferenceAssembly(libraryAssembly);
dynamic scriptInstance = evaluatorWithReference.LoadCode(ScriptCode);
var result = scriptInstance.SumAndAddOne(1, 2);
Assert.That(result, Is.EqualTo(4));
}
}
要运行代码,您需要NuGet包 NUnit和cs-script 。
此行在运行时导致 CompilerException :
dynamic scriptInstance = evaluatorWithReference.LoadCode(ScriptCode);
{interactive}(7,23): error CS0584: Internal compiler error: The invoked member is not supported in a dynamic assembly.
{interactive}(7,9): error CS0029: Cannot implicitly convert type '<fake$type>' to 'int'
同样,使用 CSScript.Evaluator.LoadCode 而不是CSScript.LoadCode的原因是,可以随时重新加载脚本,而无需在任何一个脚本时重新启动主机应用程序变化即可。 (CSScript.LoadCode已根据http://www.csscript.net/help/Importing_scripts.html支持包含其他脚本)
以下是CS-Script Evaluator的文档:http://www.csscript.net/help/evaluator.html
缺乏谷歌的结果是令人沮丧的,但我希望我错过了一些简单的事情。任何帮助将不胜感激。
(这个问题应该在不存在的标签cs-script下提交。)
答案 0 :(得分:3)
这里有一些轻微的混乱。 Evaluator
不是实现可重载脚本行为的唯一方法。 CSScript.LoadCode
也允许重新加载。
我确实建议将CSScript.Evaluator.LoadCode
视为托管模型的第一个候选者,因为它提供了更少的开销,并且可以说是更方便的重新加载模型。然而它带来了成本。您几乎无法控制重新加载和依赖项包含(程序集,脚本)。内存泄漏不是100%可避免的。而且它也使得脚本调试完全不可能(Mono bug)。
在你的情况下,我真的建议你转向更传统的托管模式:CodeDOM。
请查看"[cs-script]\Samples\Hosting\CodeDOM\Modifying script without restart"
示例。
"[cs-script]\Samples\Hosting\CodeDOM\InterfaceAlignment"
还可以让您了解如何在重新加载时使用接口。
CodeDOM多年来一直是默认的CS脚本托管模式,实际上它非常强大,直观且易于管理。唯一的真正的缺点是,您传递给(或从中获取)脚本的所有对象都需要可序列化或从MarshalByRef继承。这是在“自动”单独域中执行的脚本的副作用。因此,人们必须处理Remoting的所有“乐趣”。 顺便说一句,这是我实施基于Mono的评估器的唯一原因。
CodeDOM模型还将自动管理依赖项,并在需要时重新编译它们。但无论如何,你似乎都知道这一点。
CodeDOM还允许您精确定义检查更改依赖关系的机制:
//the default algorithm "recompile if script or dependency is changed"
CSScript.IsOutOfDateAlgorithm = CSScript.CachProbing.Advanced;
或
//custom algorithm "never recompile script"
CSScript.IsOutOfDateAlgorithm = (s, a) => false;
答案 1 :(得分:0)
CompilerException的快速解决方案似乎不是使用Evaluator来编译程序集,而是只使用CSScript.LoadCode
这样
var compiledAssemblyName = CSScript.CompileCode(LibraryScriptCode);
var evaluatorWithReference = CSScript.Evaluator.ReferenceAssembly(compiledAssemblyName);
dynamic scriptInstance = evaluatorWithReference.LoadCode(ScriptCode);
但是,如前面的答案中所述,这限制了CodeDOM模型提供的依赖控制的可能性(如css_include)。此外,看不到对LibraryScriptCode的任何更改,这再次限制了Evaluator方法的有用性。
我选择的解决方案是AsmHelper.CreateObject
和AsmHelper.AlignToInterface<T>
方法。这使您可以在脚本中使用常规css_include,同时允许您随时通过释放AsmHelper重新加载脚本并重新开始。我的解决方案看起来像这样:
AsmHelper asmHelper = new AsmHelper(CSScript.Compile(filePath), null, false);
object obj = asmHelper.CreateObject("*");
IMyInterface instance = asmHelper.TryAlignToInterface<IMyInterface>(obj);
// Any other interfaces you want to instantiate...
...
if (instance != null)
instance.MyScriptMethod();
检测到更改后(我使用FileSystemWatcher
),您只需致电asmHelper.Dispose
并再次运行上述代码。
此方法要求脚本类使用Serializable
属性标记,或者只是从MarshalByRefObject
继承。
请注意,您的脚本类不需要继承任何接口。 AlignToInterface可以使用和不使用它。你可以在这里使用dynamic
,但我更喜欢使用强类型接口来避免错误。
我无法使用内置的try
- 方法,所以当不知道接口是否已实现时,我使用这种扩展方法以减少混乱:
public static class InterfaceExtensions
{
public static T TryAlignToInterface<T>(this AsmHelper helper, object obj) where T : class
{
try
{
return helper.AlignToInterface<T>(obj);
}
catch
{
return null;
}
}
}
主要指南http://www.csscript.net/help/script_hosting_guideline_.html中解释了大部分内容,并且在上一篇文章中提到了有用的示例。
我觉得我可能错过了关于脚本更改检测的内容,但这种方法非常有效。