本講從 **腳本技術本質** 出發,對比傳統編程與腳本差異,闡述 **為何選 VB.NET 做腳本語言**,並完整實現一個基於 **動態編譯** 的輕量級 VB.NET 腳本引擎核心 (XVBAEngine),涵蓋 **代碼生成器、編譯參數控制、快取機制、反射調用、全域物件語法支援** 等核心模組。
| 維度 | 傳統程式 | 腳本 |
|---|---|---|
| 運行形態 | 預編譯獨立 EXE/DLL | 宿主容器內即時編譯/解釋執行 |
| 部署 | 修改需重編譯、重部署、重啟宿主 | 純文字修改即時生效,宿主無需重啟 |
| 語言特性 | 靜態類型 (C#、C++) | 動態類型/晚期綁定 (VB.NET、JS、Lua) |
| 權限 | 完整信任 | 受宿主沙箱限制 |
| 用途 | 核心業務邏輯 | 業務規則自訂、工作流、報表模板、自動化腳本 |
結論:腳本需 **動態語言特性** (晚期綁定、不嚴格型別檢查),VB.NET 繼承自 VB6 保留此特性,C# 則為靜態語言,**VB.NET 更適合作腳本語言**。
Option Strict Off、Imports、Namespace/Module 包裝)。Microsoft.VisualBasic.VBCodeProvider.CompileAssemblyFromSource **動態編譯** 產生記憶體組件。Public Shared Sub (靜態方法) 建立方法索引表。MethodInfo.Invoke 或 **委託快取** 執行腳本方法。Module mdlXVBAScriptEngine
Sub MyScript() ... End Sub ' 編譯為 public static void MyScript()
Dim GlobalVar As Integer ' 編譯為 public static int GlobalVar
End Module
引擎自動在使用者代碼外層包裝 Module ... End Module,使用者只寫 Sub Name()/Dim Var 即可得到靜態成員,無需手寫 Public Shared。
標記類別 [StandardModuleAttribute] class GlobalObject { public static Window Window {get;} },VB 編譯器會將腳本中的 Window.Alert("hi") 自動擴展為 GlobalObject.Window.Alert("hi")。**編譯期完成,運行期零開銷**。C# 無此特性。
vbc.exe /imports:MyVBAScript.Global,System,Microsoft.VisualBasic ... 讓編譯器識別附加 StandardModuleAttribute 類別的靜態成員為全域可用。源碼 Imports 僅簡化寫法,/imports 才啟用全域語義。
public class XVBAEngine
{
// 參數控制屬性
public string ScriptText { get; set; } // 原始腳本
public StringCollection SourceImports { get; } // 代碼內 Imports
public StringCollection VBCompilerImports { get; } // 編譯器 /imports
public StringCollection ReferencedAssemblies { get; } // 參考組件
public bool OutputDebug { get; set; } = true;
// 內部狀態
private bool _scriptModified = true;
private Assembly _compiledAssembly;
private readonly List _methods = new();
private static readonly Hashtable _assemblyCache = new(); // 代碼文本->組件
// 核心流程
public bool Compile() { /* 生成代碼 -> 動態編譯 -> 反射掃描 -> 快取 */ }
public object Execute(string methodName, object[] args, bool throwOnError) { /* 查表 -> Invoke */ }
}
private string GenerateRuntimeSource()
{
var sb = new StringBuilder();
sb.AppendLine("Option Strict Off");
foreach (string ns in SourceImports) sb.AppendLine($"Imports {ns}");
sb.AppendLine("Namespace NameSpaceXVBAScriptEngine");
sb.AppendLine("Module mdlXVBAScriptEngine");
sb.AppendLine(); // 用戶代碼插入點
sb.AppendLine(ScriptText);
sb.AppendLine();
sb.AppendLine("End Module");
sb.AppendLine("End Namespace");
return sb.ToString();
}
SourceImports 含 System, Microsoft.VisualBasic。System.Drawing 等減少腳本前綴。
private bool BuildAssembly(string sourceText, string outputFile = null)
{
// 1. 檢查快取
if (_assemblyCache[sourceText] is Assembly cached)
{ _compiledAssembly = cached; return true; }
// 2. 編譯參數
var cp = new CompilerParameters
{
GenerateExecutable = false,
GenerateInMemory = true,
IncludeDebugInformation = true,
ReferencedAssemblies = { "mscorlib.dll", "System.dll", "System.Data.dll",
"System.Xml.dll", "System.Drawing.dll",
"System.Windows.Forms.dll", "Microsoft.VisualBasic.dll" }
};
// 加入用戶參考組件
foreach (string asm in ReferencedAssemblies) cp.ReferencedAssemblies.Add(asm);
// 3. /imports 指令
if (VBCompilerImports.Count > 0)
{
var opt = new StringBuilder("/imports:");
for (int i = 0; i < VBCompilerImports.Count; i++)
{
if (i > 0) opt.Append(',');
opt.Append(VBCompilerImports[i].Trim());
}
cp.CompilerOptions = opt.ToString();
}
// 4. 編譯
var provider = new VBCodeProvider();
CompilerResults result;
#if NET20_OR_GREATER
result = provider.CompileAssemblyFromSource(cp, sourceText); // .NET 2.0+
#else
result = provider.CreateCompiler().CompileAssemblyFromSource(cp, sourceText); // .NET 1.1
#endif
// 5. 結果處理
_compilerOutput = string.Join(Environment.NewLine, result.Output.Cast());
if (result.Errors.HasErrors) return false;
_compiledAssembly = result.CompiledAssembly;
_assemblyCache[sourceText] = _compiledAssembly; // 快取
if (outputFile != null) File.WriteAllText(outputFile, sourceText, Encoding.GetEncoding(936));
return true;
}
private void ScanMethods()
{
_methods.Clear();
var moduleType = _compiledAssembly.GetType("NameSpaceXVBAScriptEngine.mdlXVBAScriptEngine");
if (moduleType == null) return;
foreach (var mi in moduleType.GetMethods(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))
{
if (mi.IsSpecialName) continue; // 略過 property accessor 等
_methods.Add(new ScriptMethodInfo
{
MethodName = mi.Name,
MethodInfo = mi,
ReturnType = mi.ReturnType,
Delegate = Delegate.CreateDelegate(
Expression.GetFuncType(mi.GetParameters().Select(p=>p.ParameterType).Append(mi.ReturnType).ToArray()),
mi) // 建立強型別委託加速後續調用
});
}
}
委託快取:MethodInfo.Invoke 較慢,首次掃描建立 Delegate (對應簽名的 Func<...>/Action<...>),後續調用 delegate.DynamicInvoke(args) 顯著提速。
public object Execute(string name, object[] args, bool throwEx)
{
if (!_scriptModified && _methods.Count == 0) return null; // 未編譯
if (_scriptModified) { if (!Compile()) return null; }
var mi = _methods.FirstOrDefault(m => m.MethodName.Equals(name, StringComparison.OrdinalIgnoreCase));
if (mi == null) { if (throwEx) throw new MissingMethodException(name); return null; }
try { return mi.Delegate?.DynamicInvoke(args) ?? mi.MethodInfo.Invoke(null, args); }
catch (TargetInvocationException tie) when (tie.InnerException != null)
{ if (throwEx) throw tie.InnerException; return null; }
catch { if (throwEx) throw; return null; }
}
Alert/AlertError/Confirm/Prompt/BrowseFile/BrowseFolder — 受 UserInteractive 控制,Service/ASP.NET 環境設 false 自動禁用。Left/Top/Width/Height/Title 映射 ParentWindow (IWin32Window)。ScreenWidth/Height。SetTimeout(ms, method) (一次)、SetInterval(ms, method) (週期) — 基於 System.Windows.Forms.Timer,UI 執行緒安全。Engine 屬性暴露 XVBAEngine,腳本可存取 Window.Engine.ScriptText 等。上集完整構建了腳本引擎核心:
VBCodeProvider 編譯 → 反射掃描 → 委託快取 → 記憶體組件快取。