65.9K
CodeProject 正在变化。 阅读更多。
Home

代码测试平台

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (10投票s)

2010 年 10 月 19 日

Eclipse

8分钟阅读

viewsIcon

53125

downloadIcon

660

通过动态代码生成和编译进行算法实验。

UserSource.jpg

引言

您是否曾经在愉快地编码时遇到一个问题,您*认为*可以通过插入您最喜欢的算法和/或代码片段,只需*少量*调整即可轻松解决?

毋庸置疑,我们都这样做过。我向您展示一个工具,用于测试这些调整,同时完全绕过您正在进行的开发。

恕我直言?为什么要绕过?

虽然这听起来有些离经叛道,但作为开发人员,在编码过程中有时我们并不知道具体要实现什么,直到开始编写代码。大多数时候,我们编写我们知道的并且已经工作的代码,甚至利用项目中已编写和测试的代码(没有人想重复发明轮子,尤其是当他们是第一个发明它的人时)。但偶尔,我们会遇到代码中的一个点,我们真的想退后一步,尝试一下我们刚刚想到的一个想法,看看它是否能给我们带来我们非常确定的结果。

当我想要尝试一些东西时,我真的不关心启动另一个开发环境实例(如果您想知道,是 Visual Studio)。我想要一些轻量级的东西,能让我只编写我需要尝试的代码,而不是别的。把它想象成一种脚本化。

我该怎么做?

好问题!我开发了这个小程序来为我们开发者提供这项服务。有了它,您可以用 C# 或 VB.NET 编写代码,该实用程序会将所有代码打包到一个程序集中并为您执行,让您展示您的才华。

好吧,不完全是。这个实用程序允许您输入您想要尝试的代码,但您也必须编写获取输出的代码行。

好的,我感兴趣了。展示如何使用它

这很简单。运行它,您将看到上面的屏幕(但没有用户输入的源代码)。您可以直接开始输入您的源代码。没有 IntelliSense,没有代码着色,没有自动格式化,没有美化打印,什么都没有。骨架,在这里输入您的代码。

您可以创建任何您能引用和实例化的 .NET 类的实例,甚至可以即时构建 Windows 窗体。所有那些在 C# 最初的 Beta 版本中使用记事本编码的开发者都明白我的意思。这个实用程序与此没有太大区别,只是您无需转到命令行来编译您的代码。

一旦您在语言下拉列表中选择了您正在使用的语言,您就可以通过单击“构建”按钮或按 F5 键来编译您的代码。构建完成后,UI 将切换到“错误”选项卡,向您展示大量的错误。如果您编译无误,您可以单击“运行”按钮或按 Shift+F5 运行您的代码。

当您的代码运行时,您会看到它作为控制台应用程序运行。这是故意的。您的用户代码将被包装到一个控制台应用程序中执行。

等等,我只能做控制台应用程序?

不,您不限于控制台应用程序………………好吧………………是的,是的,您是,但您不是。好吧,我们来澄清一下。

是的,该应用程序只能创建控制台应用程序,但这一切意味着,要在 Windows 窗体中尝试某些内容,您必须编写创建窗体的代码。这并不难,而且我提供了一个示例。之所以使用控制台应用程序,是因为您编写的代码被包装到一个预定义的容器中执行。这是包装器的 C# 版本

namespace OpGenGo {
	using System;
	using System.Collections.Generic;
	using System.Text;	
	
	// <summary>
	// Driver to execute the user entered code
	// </summary>
	public class Program {
		
		#region > Module Entry Point <
		// <summary>
		// Program Entry Point
		// </summary>
		public static void Main(string[] args) {
			AppDomain.CurrentDomain.UnhandledException += 
			new UnhandledExceptionEventHandler
			(OpGenGo.Program.OnUnhandledException);
			try {
				OpGenCompGo uc = new OpGenCompGo();
				uc.UserCode();
			}
			catch (Exception ex) {
				Console.WriteLine();
				Console.WriteLine(">> Exception during 
					processing of user code:");
				Console.WriteLine((ex.Message));
				Console.WriteLine();
			}
			Console.WriteLine();
			Console.WriteLine(">> Press any key to continue <<");
			Console.WriteLine();
			Console.ReadKey(true);
		}
		#endregion
		
		#region > Application Domain Unhandled Exception Handler <
		// <summary>
		// Application Domain exception handler for *any* unhandled exceptions.
		// </summary>
		public static void OnUnhandledException(object sender, 
				UnhandledExceptionEventArgs e) {
			System.Exception uex = ((System.Exception)(e.ExceptionObject));
			Console.WriteLine();
			Console.WriteLine(">> Unhandled Exception during processing:");
			Console.WriteLine((uex.Message));
			Console.WriteLine();
		}
		#endregion
	}
	
	// <summary>
	// Class to encapsulate the execution of the user entered code.
	// </summary>
	public class OpGenCompGo {
		
		#region > Public Methods <
		// <summary>
		// User entered code execution.
		// </summary>
		public void UserCode() {
			// *********************************
			// * B E G I N   U S E R   C O D E *
			// *********************************
		    Console.WriteLine("Hello, World!");
			// *********************************
			// *   E N D   U S E R   C O D E   *
			// *********************************
		}
		#endregion
	}
}

如果您查看代码,您会看到一个清楚标有“用户输入代码”的部分。其余部分是包装器的一部分,但用户输入代码可以是您要尝试的任何内容。这里的想法是离散编码。您只需要编写您想尝试的部分。在上面的代码示例中,它只输出传统的“Hello, World!”消息。

那么这个奇怪的东西究竟是如何工作的?

源代码分为两个部分:用户界面和代码生成器。

用户界面处理用户输入的代码、生成的代码的呈现、错误的呈现、加载用户代码以及保存生成的代码和程序集(是的,我让您将它们保存到文件系统中)的能力。这些是简单部分。

代码生成器都在 DynComp 组件中。当我刚开始构建这个实用程序时,我考虑过有一个小型包装器代码库可供选择,这样我就可以将我的代码包装到各种容器中。这很容易做到,编写起来很快,易于理解,甚至更容易通过在库中创建更多包装器来扩展。好主意,对吧?并非如此。这个想法偏离了只需要编写我想要尝试的代码的需求,所以我把它简化了一点。而且,我真的很想玩玩 CodeDOM。

CodeDOM??你疯了吗??

为什么?你和谁在说话?

说真的,我想使用 CodeDOM 来创建代码,这样我就不需要包装器库。最后,它使该实用程序保持整洁。也就是说,如果您认为大约 275 行代码来生成包装器算整洁的话。

我知道。哑口无言。是的。大约 275 行 CodeDOM 代码来生成包装器。好吧,也许我确实有点疯狂,但通过使用 CodeDOM,我可以修改代码以接受任何 .NET CodeDOM 提供程序,唯一的限制是 CodeDOM 提供程序必须能够实际从 CodeCompileUnit 对象进行编译(令人惊讶的是,并非所有提供程序都能做到,有些会抛出不支持的函数异常)。

这里的技巧之一是,在大多数情况下,用户输入的代码*不会*被解释为 CodeDOM 调用,而是使用 CodeDOM 作为代码片段行插入。看,我给您展示

// Separate our source into lines
string[] SrcLines = _Source.Split(new char[] { '\n' }, 
	StringSplitOptions.RemoveEmptyEntries);

上面这行简单地将用户输入的代码分割成单独的行。我在下面的代码中做的一个假设是,您作为用户,已经在用户源的顶部输入了所有 using/Imports 语句,这使得以下代码能够正常工作

// Set the user added imports
while (SrcIdx < SrcIdxMax)
{
	if (SrcLines[SrcIdx].StartsWith("using ") ||
		SrcLines[SrcIdx].StartsWith
			("Imports ", StringComparison.OrdinalIgnoreCase))
	{
		int IdxStrt = SrcLines[SrcIdx].IndexOf(' ') + 1;
		int IdxStop = SrcLines[SrcIdx].IndexOf(';');
		string ImpName = string.Empty;
		if (IdxStop < 0)
			ImpName = SrcLines[SrcIdx].Substring(IdxStrt);
		else
			ImpName = SrcLines[SrcIdx].Substring
					(IdxStrt, IdxStop - IdxStrt);
		if (!(ImpName.Equals("system", StringComparison.OrdinalIgnoreCase) ||
			  ImpName.Equals("system.collections.generic", 
				StringComparison.OrdinalIgnoreCase)))
			CNS.Imports.Add(new CodeNamespaceImport(ImpName));
		SrcIdx++;
	}
	else
	{
		// No more "using" or "imports" lines
		break;
	}
}

上面用于分隔导入代码行。这些是唯一不是作为代码片段输入的由用户输入的代码行。您会注意到“System”和“System.Collections.Generic”类总是包含在内,因此如果用户输入了它们,它们就会被忽略。任何其他内容都会被作为 import 输入。

处理其余的用户输入代码

while (SrcIdx <= SrcIdxMax)
{
	CodeSnippetStatement Stmt = new CodeSnippetStatement
	(string.Format("{0}{1}", new String('\t', 4) ,SrcLines[SrcIdx]));
	UserTry.TryStatements.Add(Stmt);
	SrcIdx++;
}

上面的代码将剩余的用户输入代码行作为代码片段语句添加到 try 块中。通过将用户代码放入 try 块中,我们可以捕获系统异常并报告它。

举例来说,本文开头用户输入的代码会生成以下代码

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.3615
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace OpGenGo {
	using System;
	using System.Collections.Generic;
	using System.Text;
	
	
	// <summary>
	// Driver to execute the user entered code
	// </summary>
	public class Program {
		
		#region > Module Entry Point <
		// <summary>
		// Program Entry Point
		// </summary>
		public static void Main(string[] args) {
			AppDomain.CurrentDomain.UnhandledException += 
				new UnhandledExceptionEventHandler
				(OpGenGo.Program.OnUnhandledException);
			try {
				OpGenCompGo uc = new OpGenCompGo();
				uc.UserCode();
			}
			catch (Exception ex) {
				Console.WriteLine();
				Console.WriteLine(">> Exception during 
					processing of user code:");
				Console.WriteLine((ex.Message));
				Console.WriteLine();
			}
			Console.WriteLine();
			Console.WriteLine(">> Press any key to continue <<");
			Console.WriteLine();
			Console.ReadKey(true);
		}
		#endregion
		
		#region > Application Domain Unhandled Exception Handler <
		// <summary>
		// Application Domain exception handler for *any* unhandled exceptions.
		// </summary>
		public static void OnUnhandledException(object sender, 
				UnhandledExceptionEventArgs e) {
			System.Exception uex = ((System.Exception)(e.ExceptionObject));
			Console.WriteLine();
			Console.WriteLine(">> Unhandled Exception during processing:");
			Console.WriteLine((uex.Message));
			Console.WriteLine();
		}
		#endregion
	}
	
	// <summary>
	// Class to encapsulate the execution of the user entered code.
	// </summary>
	public class OpGenCompGo {
		
		#region > Public Methods <
		// <summary>
		// User entered code execution.
		// </summary>
		public void UserCode() {
			// *********************************
			// * B E G I N   U S E R   C O D E *
			// *********************************
		    for(int i = 0; i < 21; i++)
			{
				a = i * i * i * i;
				Console.WriteLine(string.Format("{0} : {1}", i, a));
			}
			// *********************************
			// *   E N D   U S E R   C O D E   *
			// *********************************
		}
		#endregion
	}
}

代码切换

我认识的每一个同时使用 C# 和 VB.NET 的开发者都有同样的问题。我们都或多或少地会把 C# 写成 VB,或者把 VB 写成 C#。我个人最大的问题是,当我写 VB 时,会在行尾加一个分号。好吧,与其为此感到恼火,我把下面的代码加进去了

// If the user selected VB and the lines end with a semi-colon (;), 
// strip out the semi-colon
if (_ProviderLanguage == "VB")
{
	for (int sidx = 0; sidx < SrcLines.Length; sidx++)
	{
		if (SrcLines[sidx].EndsWith(";"))
			SrcLines[sidx] = SrcLines[sidx].Substring
				(0, SrcLines[sidx].Length - 1);
	}
}

这样,如果我选择 VB 作为我的源类型并且输入了一个分号,它就会被去掉。除了这段小代码和单独处理 imports/using 语句之外,用户输入的代码没有任何解释或转换。

有趣但不太有价值的注意事项

看看下面显示的这行代码

/// <summary>
/// Generate the source from the namespace.
/// </summary>
/// <param name="CDP">CodeDomProvider object.</param>
/// <returns>Generated code on success, string.Empty otherwise.</returns>
/// <remarks>10) Call one of the generator's GenerateCodeFrom... 
/// methods to emit the code</remarks>
private string GetCode(CodeDomProvider CDP)
{
    // Clear the old
    CleanTemps();

    // Our compiler parameters object
    CompilerParameters parms = new CompilerParameters();

    // Create a code generator options object
    CodeGeneratorOptions opt = new CodeGeneratorOptions();

    // Set the indentation level
    opt.IndentString = new String('\t', 1);

    // Use a StringWriter object to capture the code
    StringBuilder rc = new StringBuilder();
    StringWriter sw = new StringWriter(rc);

    // Generate the code
    CDP.GenerateCodeFromCompileUnit(_CCU, sw, opt);
    //CDP.GenerateCodeFromNamespace(_CCU.Namespaces[0], sw, opt);

    // Return the result
    return rc.ToString();
}

寻找注释 // Generate the code。您会看到两个生成语句,一个被注释掉,一个未被注释掉。

这两个语句之间有一个主要和一个次要的区别。主要区别是,当从 CodeCompileUnit 生成代码时,生成器将为 CodeCompileUnit 中的所有命名空间生成代码。次要区别,也是更大的惊喜是,当您从命名空间生成代码时,您不会得到以下头部块

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.3615
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

我告诉过您,这很小,而且不怎么重要。

警告:使用不安全的代码

实际上不是什么大事。我使用 unsafe 技术通过以下代码将字体 Anonymous Pro(可从 www.ms-studio.com 获取)加载到私有字体集合中,该字体是从嵌入源加载的

_PFC = new PrivateFontCollection();
Stream FontStream = this.GetType().Assembly.GetManifestResourceStream
			("Testbed.Fonts.Anonymous Pro.ttf");
byte[] FontData = new byte[FontStream.Length];
FontStream.Read(FontData, 0, (int)FontStream.Length);
FontStream.Close();
unsafe
{
    fixed (byte* pFontData = FontData)
    {
        _PFC.AddMemoryFont((System.IntPtr)pFontData, FontData.Length);
    }
}

这里唯一不安全的部分是使用指针引用嵌入字体以将其加载到私有字体集合中的代码。这就是您在输入源代码时看到的奇怪而明确的字体。

总结

和所有我写的实用程序一样,我写这个是为了让我的生活更轻松,并在过程中学到更多东西。如果它也能帮助您,那就更好了。如果您喜欢并想分享它,那就更好了。如果您想改进它,那也很棒,但请记住,如果您进行了更改,请检查许可要求(特别是您必须发布代码的部分)。

更新

版本 1.1

  • 更改了用户代码窗口中的制表符(前 20 个)为 4 个字符的制表符
  • 向用户代码窗口添加了剪切/复制/粘贴编辑功能
  • 向用户代码窗口添加了拖放功能
  • 向生成的代码和错误窗口添加了复制功能
  • 对代码进行了一些重构和清理
  • 更改了异常处理,使失败更干净

历史

  • v1 - 首次发布
  • v1.1 - 一些代码重构,更有效的异常处理,添加了编辑便利性
© . All rights reserved.