Andrew W. Troelsen,Microsoft MVP
摘要:本文分析了使用 C# 命令行编译器 csc.exe 生成应用程序的过程。同时,还将向读者介绍很多为 C# 2.0 独有的编译器选项,例如,extended/reference 标志和强名称支持。阅读完本文的内容之后,您将能够轻松地在没有向导的环境中生成单文件程序集和多文件程序集。
适用于:
Microsoft Visual C# 2.0
注 本文假定您熟悉 C# 编程语言和 .NET Framework 的结构。体验一下使用命令行工具的感觉还将证明很有帮助。
本页内容
scsc.exe 带来的乐趣
C# 编译器选项概览
配置环境变量
命令行基础知识
用于指定输入和控制输出的选项
编译 .NET 代码库
使用 C# 响应文件
使用 /reference 引用外部程序集
理解 C# 2.0 引用别名
使用 /addmodule 生成多文件程序集
创建 Windows 窗体应用程序
通过 csc.exe 使用资源
使用 /define 定义预处理器符号
csc.exe 的以调试为中心的选项
杂项
小结
scsc.exe 带来的乐趣
几乎没有人会否认集成开发环境 (IDE)(例如,Visual Studio 2005 和 Visual C# Express 2005)所提供的能使编程工作变得相当简单的诸多功能。但是,实际上 IDE 自己通常不能提供对基础编译器的所有方面的访问。例如,Visual Studio 2005 不支持生成多文件程序集。
此外,了解在命令行编译代码的过程,对于具有以下特征的用户可能有用:
-
偏爱最简单的生成 .NET Framework 应用程序的方法。
-
希望揭开 IDE 处理源代码文件的方法的秘密。
-
没有集成开发环境,例如,Visual Studio(但实际上具有免费提供的 .NET Framework SDK)。
-
正在基于 Unix的系统(在该系统中,命令行是必须使用的工具)上使用 .NET Framework,并且希望更好地了解 Mono 和/或 Portable .NET ECMA 兼容 C# 编译器。
-
正在研究当前未集成到 Visual Studio 中的备选 .NET 编程语言。
-
只是希望扩展他们的 C# 编程语言知识。
如果您属于上面所述的这些用户,那么就忠实于自己的选择并继续读下去吧。
C# 编译器选项概览
C# 编译器 csc.exe 提供了大量用于对创建 .NET 程序集的方式进行控制的选项。站在一个较高层次来看,命令行选项属于下列八个类别之一(表 1)。
表 1. csc.exe 提供的标记的类别
C# 编译器类别 | 定义 |
---|---|
输出文件 | 用于控制所生成的程序集的格式、可选的 XML 文档文件和强名称信息的选项。 |
输入文件 | 使用户可以指定输入文件和引用的程序集的选项。 |
资源 | 用于将任何必需的资源(例如,图标和字符串表)嵌入到程序集中的选项。 |
代码生成 | 这些选项控制调试符号的生成。 |
错误和警告 | 控制编译器处理源代码错误/警告的方式。 |
语言 | 启用/禁用 C# 语言功能(例如,不安全代码)以及条件编译符号的定义。 |
杂项 | 该类别的最有趣的选项使您可以指定 csc.exe 响应文件。 |
高级 | 该类别指定一些更加深奥并且通常不太重要的编译器选项。 |
注 1.0 和 1.1 版本的 C# 编译器中存在的 /incremental 标志现在已过时。
在阅读本文的过程中,您将了解每个编译器类别中存在的核心 标志(最重要的词是核心)。对于大多数开发方案,可以安全地忽略 C# 编译器的很多高级选项。如果您需要有关本文未予讨论的 csc.exe 功能的详细信息,请尽管放心,您可以参阅 Microsoft Visual Studio 2005 文档帮助系统(只须从“Search”选项卡中搜索“csc.exe”并深入查阅)。
注 MSDN 文档也会对您也很所帮助,因为它描述了如何在 Visual Studio(如果可用)内部设置 csc.exe 的特定选项。
配置环境变量
在使用任何 .NET SDK 命令行工具(包括 C# 编译器)之前,需要配置开发计算机以识别它们的存在。最简单的方法是使用 Start | All Programs | Visual Studio 2005 | Visual Studio Tools 菜单选项,启动预配置的 Visual Studio 命令提示。这一特定的控制台能够自动初始化必要的环境变量,而无须您执行任何操作。(Visual Studio .NET 2003 用户需要启动他们各自的命令提示)。
注 如果您没有 Visual Studio,但是已经安装了 .NET Framework SDK,则可以从 Start | All Programs | Microsoft .NET Framework SDK 2.0 菜单选项启动预配置的命令提示。
如果您希望从任意的 命令提示使用 .NET 命令行工具,则需要手动更新计算机的 Path 变量。做法是,请右键单击桌面上的 My Computer 图标并选择 Properties 菜单选项。从出现的对话框中,单击位于 Advanced 选项卡下面的 Environment Variables 按钮。从出现的对话框中,在 System 变量列表框中的当前 Path 变量的结尾添加以下目录清单(请注意,必须用分号分隔各个条目):
C:/Windows/Microsoft.NET/Framework/v2.0.40607 C:/Program Files/Microsoft Visual Studio 8/SDK/v2.0/Bin
注 上面的列表指向我的当前 .NET 2.0 测试版的路径。您的路径可能因 Visual Studio 和/或 .NET SDK 的安装和版本的不同而略有不同,因此请确保执行完整性检查。
在更新 Path 变量之后,请立即关闭所有对话框和当前打开的任何 Console 窗口,以便提交设置。您现在应当能够从任何命令提示执行 csc.exe 和其他 .NET 工具了。要进行测试,请输入以下命令:
csc -? ildasm -?
如果您看到有大量信息显示出来,那么您就可以继续了。
命令行基础知识
已经能够熟练地在命令行工作的用户在使用 csc.exe 时不会有任何问题,因而可以跳到下一节。但是,如果您使用命令行的次数很有限,那么请让我说明一些基本的详细信息,以便进行必要的准备。
首先,可以使用反斜杠或单个短划线指定 csc.exe 的选项。其次,在 / 或 - 以及随后的标志之间具有额外的空格是非法 的。因此,“-help”是完全正确,而“- help”就行不通了。为了加以说明,让我们使用 help 标志检查完整的命令行选项集:
csc –help csc /help
如果一切正常,则您应当看到所有可能的标志,如图 1 所示。
很多选项都提供了简写表示法,以便为您节省一些键入时间。假设 help 标志的简写表示法是 ?,则您可以如下所示列出 csc.exe 的选项:
csc –? csc /?
很多选项都需要附加的修饰,例如,目录路径和文件名。这种性质的标志使用冒号将标志与它的修饰分隔开来。例如,/reference 选项要求将 .NET 程序集的名称包括在程序集清单中:
csc /reference:MyLibrary.dll ...
其他标志在性质上是二元 的,因为它们或者被启用 (+),或者被禁用 (-)。二元标志总是默认为它们的禁用状态,因此您通常只需要使用加号标记。例如,要将所有编译警告都视为错误,可以启用 warnaserror 标志:
csc /warnaserror+ ...
标志的顺序无关紧要,但是在指定输入文件集合之前,必须列出所有标志的集合。另外值得说明的是,在修饰和它的关联数据之间具有额外的空格是非法 的。例如,请使用 /reference:MyLibrary.dll,而不要使用 /reference:MyLibrary.dll。
用于指定输入和控制输出的选项
我们将要分析的第一组命令行选项用于指定编译器输入(表 2)和控制得到的输出(表 3)。请注意,下面的表还标记了特定于 C# 2.0 的选项和任何可用的简写表示法。
表 2. csc.exe 的以输入为中心的选项
输入标志 | 定义 | 是否特定于 C# 2.0? |
---|---|---|
/recurse | 通知 csc.exe 编译位于项目的子目录结构中的 C# 文件。该标志支持通配符语法。 | 否 |
/reference (/r) | 用于指定要在当前编译中引用的外部程序集。 | 否,但是 2.0 编译器提供了别名变体。 |
/addmodule | 用于指定要包括在多文件程序集中的模块。 | 否 |
表 3. csc.exe 的以输出为中心的选项
输出标志 | 定义 | 是否特定于 C# 2.0? |
---|---|---|
/out | 指定要生成的程序集的名称。如果省略该标志,则输出文件的名称基于初始输入文件的名称(对于 *.dll 程序集而言)或定义 Main() 方法的类的名称(对于 *.exe 程序集而言)。 | 否 |
/target (/t) | 指定要创建的程序集的文件格式。 | 否 |
/doc | 用于生成 XML 文档文件。 | 否 |
/delaysign | 使您可以使用强名称的延迟签名生成程序集。 | 是 |
/keyfile | 指定用于对程序集进行强命名的 *.snk 文件的路径。 | 是 |
/keycontainer | 指定包含 *.snk 文件的容器的名称。 | 是 |
/platform | 指定必须存在以便承载程序集的 CPU(x86、Itanium、x64 或 anycpu)。默认为 anycpu。 | 是 |
也许用途最多的输入/输出选项是 /target。该标志通过使用附加的修饰(表 4)告诉编译器您对生成哪个类型的 .NET 程序集感兴趣。
表 4. /target 标志的变体
目标修饰 | 定义 |
---|---|
/target:exe | 创建基于控制台的程序集。如果未指定 /target 选项,则这是默认选项。 |
/target:winexe | 创建基于 Windows 窗体的可执行程序集。尽管您可以使用 /target:exe 创建 Windows 窗体应用程序,但控制台窗口将在主窗体的后台隐现。 |
/target:library | 用于生成 .NET 代码库 (*.dll)。 |
/target:module | 创建将成为多文件程序集的一部分的模块。 |
编译 .NET 代码库
为了说明使用 csc.exe 的输入/输出选项的过程,我们将创建一个强命名的单文件程序集 (MyCodeLibrary.dll),以定义一个名为 SimpleType 的类类型。为了展示 /doc 选项的作用,我们还将生成一个 XML 文档文件。
首先,请在驱动器 C 上创建一个名为 MyCSharpCode 的新文件夹。在该文件夹中,创建一个名为 MyCodeLibrary 的子目录。使用您选择的文本编辑器(notepad.exe 就完全合乎需要)输入以下代码,并将该文件保存为刚刚创建的 C:/MyCSharpCode/MyCodeLibrary 目录中的 simpleType.cs。
// simpleType.cs using System; namespace MyCodeLibrary { /// <summary> /// Simple utility type. /// </summary> public class SimpleType { /// <summary> /// Print out select environment information /// </summary> public static void DisplayEnvironment() { Console.WriteLine("Location of this program: {0}", Environment.CurrentDirectory); Console.WriteLine("Name of machine: {0}", Environment.MachineName); Console.WriteLine("OS of machine: {0}", Environment.OSVersion); Console.WriteLine("Version of .NET: {0}", Environment.Version); } } }
现在,打开命令提示,并且使用 cd(更改目录)命令导航到 simpleType.cs 文件的位置 (C:/MyCSharpCode/MyCodeLibrary):
cd MyCSharpCode/MyCodeLibrary
或
cd C:/MyCSharpCode/MyCodeLibrary
要将该源代码文件编译为名为 MyCodeLibrary.dll 的单文件程序集,请指定以下命令集:
csc /t:library /out:MyCodeLibrary.dll simpleType.cs
此时,您应当在应用程序目录中具有一个全新的 .NET 代码库,如图 2 所示。
当在命令行编译多个 C# 文件时,可以分别列出每个文件 — 如果您希望编译包含在单个目录中的 C# 文件的子集,则这可能有所帮助。假设我们已经创建了另外一个名为 asmInfo.cs 的 C# 代码文件(保存在同一目录中),它定义了下列程序集级别属性以描述我们的代码库:
// asmInfo.cs using System; using System.Reflection; // A few assembly level attributes. [assembly:AssemblyVersion("1.0.0.0")] [assembly:AssemblyDescription("Just an example library")] [assembly:AssemblyCompany("Intertech Training")]
要只编译 simpleType.cs 和 asmInfo.cs 文件,请键入:
csc /t:library /out:MyCodeLibrary.dll simpleType.cs asmInfo.cs
正如您可能希望的那样,csc.exe 支持通配符表示法。因而,要编译单个目录中的所有文件,请仅将 *.cs 指定为输入选项:
csc /t:library /out:MyCodeLibrary.dll *.cs
使用 /recurse 指定子目录
在创建应用程序时,您肯定喜欢为您的项目创建逻辑目录结构。您可以通过将代码文件放到特定的子目录(/Core、/AsmInfo、/MenuSystem 等等)中对它们进行组织,而不是将多达 25 个文件转储到名为 myApp 的单个目录中。尽管我们的当前示例只包含几个文件,但假设您将 AsmInfo.cs 文件放到一个名为 /AsmInfo 的新的子目录(如图 3 所示)中。
要告诉 C# 编译器编译位于根目录以及 AsmInfo 子目录中的所有 C# 文件,请使用 /recurse 选项:
csc /t:library /out:MyCodeLibrary.dll /recurse:AsmInfo /doc:myDoc.xml *.cs
这里,/recurse 已经用特定子目录的名称限定。要指定多个子目录,我们可以再次使用通配符语法。如果我们要将 simpleType.cs 文件移到一个新的名为 Core 的子目录中,则我们可以用以下命令集编译所有子目录中的所有 C# 文件:
csc /t:library /out:MyCodeLibrary.dll /recurse:*.cs
在任何一种情况下,输出都是相同的。
使用 /doc 生成 XML 文档文件
SimpleType 类已经配备了各种 XML 元素。就像您很可能知道的那样,C# 编译器将使用这些带有三条斜杠的代码注释生成 XML 文档文件。要告诉 csc.exe 创建这样的文件,必须提供 /doc 选项,并且用要生成的文件的名称修饰它:
csc /t:library /out:MyCodeLibrary.dll /recurse:*.cs /doc:myDoc.xml
在应用程序目录中,您现在应当看到一个名为 myDoc.xml 的新文件。如果您打开该文件,则会发现您的类型以 XML 的形式进行了说明,如图 5 所示。
注 如果您希望了解 C# XML 代码注释的详细信息,则请参阅文章 XML Comments Let You Build Documentation Directly From Your Visual Studio .NET Source Files。
使 用 /keyfile 建立强名称
当前示例的最后一项任务是为我们的程序集分配一个强名称。在 .NET 1.1 下,创建强命名程序集需要使用 [AssemblyKeyFile] 属性。尽管这样做就很好了,但 C# 2.0 编译器现在提供了 /keyfile 标志,以指定强名称密钥文件 (*.snk) 的位置。
在驱动器 C 上创建一个名为 MyKeyPair 的新文件夹,然后使用命令提示更改至该目录。接下来,使用 sn.exe 实用工具的 –k 选项创建一个名为 myKeyPair.snk 的新密钥对。
sn -k myKeyPair.snk
要使用 csc.exe 对 MyCodeLibrary.dll 进行强命名,请发出以下命令集:
csc /t:library /out:MyCodeLibrary.dll /recurse:*.cs /doc:myDoc.xml /keyfile:C:/MyKeyPair/myKeypair.snk
要验证该程序集的确具有强名称,请使用安全实用工具 (secutil.exe) 和 –s 选项显示强名称信息:
secutil /sMyCodeLibrary.dll
您应当发现,程序集清单中记录的公钥值被显示为如图 6 所示的输出。
C# 2.0 编译器确实还有其他一些以强名称为中心的标志(/delaysign 和 /keycontainer),您可能希望在空闲时加以研究。特别地,如果您希望启用延迟签名,则请使用 /delaysign 选项。
使用 C# 响应文件
尽管通过命令行工作时可以体验到其与生俱来的优势,但没有人能够否认键入数十个编译器选项可能导致手指抽筋和录入错误。为了有助于减轻这两个问题,C# 编译器支持使用响应文件。
注 所有命令提示都允许您使用 Up 和 Down 箭头键遍历以前的命令。
响应文件(它们按照约定采用 *.rsp 文件扩展名)包含您希望供给到 csc.exe 中的所有选项。在创建了该文件以后,您就可以将它的名称指定为 C# 编译器的唯一选项。为了便于说明,下面提供了一个将用于生成 MyCodeLibrary.dll 的响应文件(请注意,您可以使用 # 符号指定注释)。
# MyCodeLibraryArgs.rsp # # These are the options used # to compile MyCodeLibrary.dll # Output target and name. /t:library /out:MyCodeLibrary.dll # Location of C# files. /recurse:*.cs # Give me an XML doc. /doc:myDoc.xml # Give me a strong name as well. /keyfile:C:/MyKeyPair/myKeypair.snk
给定该文件以后,您现在就可以使用 @ 选项指定 MyCodeLibraryArgs.rsp 了:
csc @MyCodeLibraryArgs.rsp
如果您愿意,则可以指定多个响应文件:
csc @MyCodeLibraryArgs.rsp @MoreArgs.rsp @EvenMoreArgs.rsp
请记住,按照遇到的顺序对响应文件进行处理。因此,以前的文件中的设置可能被以后的文件中的设置重写。
默认的响应文件和 /noconfig 选项
最后,请记住有一个默认的响应文件 — csc.rsp,它由 csc.exe 在每次编译期间自动处理。如果您分析该文件(它与 csc.exe 本身位于相同的文件夹中)的内容,则您将只是发现一组经常引用的程序集(System.Windows.Forms.dll、System.Data.dll 等等)。
在您希望禁止包括 csc.rsp 的极少数的场合中,您可以指定 /noconfig 标志:
csc /noconfig @MyCodeLibraryArgs.rsp
注 如果您引用程序集,而实际上并不使用它,则它将不会在程序集清单中列出。因此,请不要担心代码膨胀问题,因为它们根本不存在。
使用 /reference 引用外部程序集
此时,我们已经使用命令行编译器创建了具有强名称(并且进行了说明)的单文件代码库。现在,我们需要一个客户端应用程序以便使用它。请在 C:/MyCSharpCode 中创建一个名为 MyClient 的新文件夹。在该文件夹中,创建一个新的 C# 代码文件 (simpleTypeClient.cs),该文件从程序的入口点调用静态的 SimpleType.DisplayEnvironment() 方法:
// simpleTypeClient.cs using System; // Namespace in MyCodeLibrary.dll using MyCodeLibrary; namespace MyClientApp { public class MyApp { public static void Main() { SimpleType.DisplayEnvironment(); Console.ReadLine(); } } }
因为我们的客户端应用程序要使用 MyCodeLibrary.dll,所以我们需要使用 /reference(或只是使用 /r)选项。该标志很灵活,因为您可以指定所讨论的 *dll 的完整路径,如下所示:
csc /t:exe /r:C:/MyCSharpCode/MyCodeLibrary/MyCodeLibrary.dll *.cs
或者,如果私有程序集的副本与输入文件位于相同的文件夹中,则可以只指定程序集名称:
csc /t:exe /r:MyCodeLibrary.dll *.cs
请注意,我没有指定 /out 选项。给定该条件,csc.exe 基于我们的初始输入文件 (simpleTypeClient.cs) 创建了一个名称。此外,已知 /target 的默认行为是生成基于控制台的应用程序,所以 /t:exe 参数是可选的。
在任何情况下,因为 MyCodeLibrary.dll 是私有程序集,所以您需要将该库的一个副本放到 MyClient 目录中。在您完成该工作以后,您就能够执行 simpleTypeClient.exe 应用程序。图 7 显示了可能的测试运行。
注 请回忆一下这个问题,不必将具有强名称的程序集部署到全局程序集缓存 (GAC) 中。实际上,因为强名称具有天然的安全性方面的好处,所以向每个程序集(无论共享与否)提供强名称是一种 .NET 最佳策略。
引用多个外部程序集
如果您希望在命令行引用大量程序集,则可以指定多个 /reference 选项。为了说明这一点,假设我们的客户端应用程序需要使用包含在名为 NewLib.dll 的库中的类型:
csc /t:exe /r:MyCodeLibrary.dll /r:NewLib.dll *.cs
作为一种稍微简单一些的替代方法,您可以使用单个 /reference 选项,并且使用分号分隔的列表指定每个程序集:
csc /t:exe /r:MyCodeLibrary.dl;NewLib.dll *.cs
当然,在创作 C# 响应文件时使用相同的语法。
关于 /lib 的简短说明
在查看 C# 2.0 程序集别名的作用之前,请允许我对 /lib 标志加以简短说明。该选项可用于将包含由 /reference 选项指定的程序集的目录告诉给 csc.exe。为了进行说明,假设您具有三个位于驱动器 C 的根目录中的 *.dll 程序集。要指示 csc.exe 在 C:/ 下查找 asm1.dll、asm2.dll 和 asm3.dll,需要发出以下命令集:
csc /lib:c:/ /reference:asm1.dll;asm2.dll;asm3.dll *.cs
如果您未使用 /lib,则需要将这三个 .NET 代码库手动复制到包含输入文件的目录中。还请注意,如果在给定的命令集中多次发出 /lib 标志,则结果将累积起来。
理解 C# 2.0 引用别名
关于 /reference 选项需要进行的最后一点说明是,在 C# 2.0 中,现在可以为引用的程序集创建别名。通过该功能,可以解决在唯一命名的程序集中包含的名称完全相同的类型之间存在的名称冲突问题。
为了说明该功能的实用性,请在 C:/MyCSharpCode 目录中创建一个名为 MyCodeLibrary2 的新文件夹。将现有的 simpleType.cs 和 AsmInfo.cs 文件的副本放到新目录中。现在,向 SimpleType 中添加一个能够显示客户端提供的字符串的新方法:
/// <summary> /// Display a user supplied message. /// </summary> public static void PrintMessage(string msg) { Console.WriteLine("You said: {0}", msg); }
编译这些文件,以创建一个名为 MyCodeLibrary2.dll 的新程序集,如下所示:
csc /t:library /out:MyCodeLibrary2.dll *.cs
最后,将这一新代码库的副本放到 MyClient 文件夹(图 8)中。
现在,如果我们的当前客户端程序希望引用 MyCodeLibrary.dll 以及 MyCodeLibrary2.dll,则我们执行以下操作:
csc /t:exe /r:MyCodeLibrary.dll;MyCodeLibrary2.dll *.cs
编译器告诉我们,我们已经引入了名称冲突,因为这两个程序集都定义了一个名为 SimpleType 的类:
simpleTypeClient.cs(13,7): error CS0433: The type 'MyCodeLibrary.SimpleType' exists in both 'c:/MyCSharpCode/MyClient/MyCodeLibrary.dll' and 'c:/MyCSharpCode/MyClient/MyCodeLibrary2.dll'
乍看起来,您可能认为可以通过在客户端代码中使用完全限定名称来修复该问题。但是,这样做无法纠正该问题,因为这两个程序集定义了相同的完全限定名称 (MyCodeLibrary。SimpleType)。
使用 /reference 标志的新别名选项,我们可以为引用的每个代码库生成唯一的名称。在我们这样做之后,我们就可以更新客户端代码,以便将某个类型与特定的程序集相关联。
第一步是修改 simpleTypeClient.cs 文件,以使用我们将通过新的 extern alias 语法在命令行指定的别名:
// Extern alias statements must be // listed before all other code! extern alias ST1; extern alias ST2; using System; namespace MyClientApp { public class MyApp { public static void Main() { // Bind assembly to type using the '::' operator. ST1::MyCodeLibrary.SimpleType.DisplayEnvironment(); ST2::MyCodeLibrary.SimpleType.PrintMessage("Hello!"); Console.ReadLine(); } } }
请注意,我们已经使用 C# 2.0 extern alias 语句捕获了在命令行定义的别名。这里,ST1(简单类型 1)是为 MyCodeLibrary.dll 定义的别名,而 ST2 是 MyCodeLibrary2.dll 的别名:
csc /r:ST1=MyCodeLibrary.dll /r:ST2=MyCodeLibrary2.dll *.cs
给定这些别名,请注意 Main() 方法如何使用 C# 2.0 范围解析运算符 (::) 将程序集别名绑定到类型本身:
// This says "I want the MyCodeLibrary.SimpleType class // that is defined in MyCodeLibrary.dll". ST1::MyCodeLibrary.SimpleType.DisplayEnvironment();
进而,/reference 选项的这一变体可以提供一种避免名称冲突(当两个具有唯一名称的程序集包含名称完全相同的类型时发生)的方式。
使用 /addmodule 生成多文件程序集
就像您可能已经知道的那样,多文件程序集提供了一种将单个 .NET 二进制文件分解为多个较小的小文件的方式,这在远程下载 .NET 模块时证明很有帮助。多文件程序集的最终效果是让一组文件像一个单独引用和进行版本控制的单元那样工作。
多文件程序集包含一个主 *.dll,它包含程序集清单。该多文件程序集的其他模块(按照约定,它们采用 *.netmodule 文件扩展名)被记录在主模块的清单中,并且由 CLR 按需加载。迄今为止,生成多文件程序集的唯一方式是使用命令行编译器。
为了说明该过程,请在 C:/MyCSharpCode 目录中创建一个名为 MultiFileAsm 的新子目录。我们的目标是创建一个名为 AirVehicles 的多文件程序集。主模块 (Airvehicles.dll) 将包含单个名为 Helicopter 的类类型(稍后定义)。程序集清单编录了一个附加模块 (ufos.netmodule),该模块定义了一个名为 UFO 的类类型:
// ufo.cs using System; using System.Windows.Forms; namespace AirVehicles { public class UFO { public void AbductHuman() { MessageBox.Show("Resistance is futile"); } } }
要将 ufo.cs 编译为 .NET 模块,请指定 /t:module 作为目标类型,这会自动遵循 *.netmodule 命名约定(请回想一下,默认的响应文件自动引用 System.Windows.Forms.dll,因此我们不需要显式引用该库):
csc /t:module ufo.cs
如果您要将 ufo.netmodule 加载到 ildasm.exe 中,您会发现一个记载了该模块的名称和外部引用程序集的模块级别清单(请注意,*.netmodules 没有指定版本号,因为那是主模块的工作):
.assembly extern mscorlib{...} .assembly extern System.Windows.Forms{...} .module ufo.netmodule
现在,请创建一个名为 helicopter.cs 的新文件,该文件将用于编译主模块 Airvehicles.dll:
// helicopter.cs using System; using System.Windows.Forms; namespace AirVehicles { public class Helicopter { public void TakeOff() { MessageBox.Show("Helicopter taking off!"); } } }
假设 Airvehicles.dll 是该多文件程序集的主模块,则您将需要指定 /t:library 标志。但是,因为您还希望将 ufo.netmodule 二进制文件编码到程序集清单中,所以您还必须指定 /addmodule 选项:
csc /t:library /addmodule:ufo.netmodule /out:airvehicles.dll helicopter.cs
如果您要分析 airvehicles.dll 二进制文件内部包含的程序集级别清单(使用 ildasm.exe),则会发现 ufo.netmodule 确实是使用 .file CIL 指令记录的:
.assembly airvehicles{...} .file ufo.netmodule
多文件程序集的使用者可以较少关心他们要引用的程序集由大量二进制文件组成这一事实。实际上,在句法上将看起来与使用单文件程序集的行为完全相同。为了使本文变得更有趣一些,让我们创建一个基于 Windows 窗体的客户端应用程序。
创建 Windows 窗体应用程序
我们的下一个示例项目将是一个使用 airvehicles.dll多文件程序集的 Windows 窗体应用程序。在 C:/MyCSharpCode 目录中创建一个名为 WinFormClient 的新的子目录。创建一个派生自 Form 的类,该类定义了单个 Button 类型,当它被单击时,将创建 Helicopter 和 UFO 类型,并且调用它们的成员:
using System; using System.Windows.Forms; using AirVehicles; public class MyForm : Form { private Button btnUseVehicles = new Button(); public MyForm() { this.Text = "My Multifile Asm Client"; btnUseVehicles.Text = "Click Me"; btnUseVehicles.Width = 100; btnUseVehicles.Height = 100; btnUseVehicles.Top = 10; btnUseVehicles.Left = 10; btnUseVehicles.Click += new EventHandler(btnUseVehicles_Click); this.Controls.Add(btnUseVehicles); } private void btnUseVehicles_Click(object o, EventArgs e) { Helicopter h = new Helicopter(); h.TakeOff(); UFO u = new UFO(); u.AbductHuman(); } private static void Main() { Application.Run(new MyForm()); } }
注 在继续操作之前,请确保将 airvehicals.dll 和 ufo.netmodule 二进制文件复制到 WinFormClient 目录中。
要将该应用程序编译为 Windows 窗体可执行文件,请确保指定 winexe 作为 /target 标志的修饰。请注意,在引用多文件程序集时,只须指定主模块的名称:
csc /t:winexe /r:airvehicles.dll *.cs
在运行您的程序并单击按钮之后,您就应当看到按照预期显示的每个消息框。图 9 显示了我创建的一个消息框。
通过 csc.exe 使用资源
下一个议程是分析如何使用 csc.exe 将资源(例如,字符串表或图像文件)嵌入到 .NET 程序集中。首先,可以使用 /win32Icon 选项指定 Win32 *.ico 文件的路径。假设您已经将一个名为 HappyDude.ico 的图标文件放到当前 Windows 窗体应用程序的应用程序目录中。要将 HappyDude.ico 设置为可执行文件的图标,请发出以下命令集:
csc /t:winexe /win32icon:HappyDude.ico /r:airvehicles.dll *.cs
此时,应当更新可执行程序集,如图 10 所示。
除了使用 /win32icon 分配应用程序图标以外,csc.exe 还提供了三个附加的以资源为中心的选项(表 5)。
表 5. csc.exe 的以资源为中心的选项
csc.exe 的以资源为中心的选项 | 定义 |
---|---|
/resource | 将 *.resources 文件内部包含的资源嵌入到当前程序集中。请注意,通过该选项可以“以公共方式”嵌入资源以供所有使用者使用,或者“以私有方式”嵌入资源以便仅供包含程序集使用。 |
/linkresource | 在程序集清单中记录指向外部资源文件的链接,但实际上并不嵌入资源自身。 |
/win32res | 使您可以嵌入存在于旧式 *.res 文件中的资源。 |
在我们了解如何将资源嵌入到我们的程序集中以前,请让我对 .NET 平台中的资源的性质进行简短的介绍。正如您可能已经知道的那样,.NET 资源开始时通常呈现为一组被记录为 XML(*.resx 文件)或简单文本 (*.txt) 的名称/值对。该 XML/文本文件随后被转换为等效的二进制文件,它采用 *.resources 文件扩展名。然后,这些二进制文件被嵌入到程序集中并且被记录在清单中。当需要以编程方式从该程序集中读取资源的时候,System.Resources 命名空间会提供很多类型来完成该工作,其中最值得注意的是 ResourceManager 类。
尽管您肯定可以手动创建 *.resx 文件,但您最好使用 resgen.exe 命令行工具(或者,您当然可以使用 Visual Studio .NET 本身)。虽然本文不打算描述 resgen.exe 的全部详细信息,但是让我们演练一个简单的示例。
使用 /resource 嵌入资源
在 MyCSharpCode 下面创建一个名为 MyResourceApp 的新目录。在该目录中,使用 notepad.exe 创建一个名为 myStrings.txt 的新文件,并且使其包含您选择的一些有趣的名称/值对。例如:
# A list of personal data # company=Intertech Training lastClassTaught=.NET Security lastPresentation=SD East Best Practices favoriteGameConsole=XBox favoriteComic=Cerebus
现在,使用命令提示,通过以下命令将 *.txt 文件转换为一个基于 XML 的 *.resx 文件:
resgen myStrings.txt myStrings.resx
如果您使用 notepad.exe 打开该文件,则会找到许多描述名称/值对的 XML 元素,例如:
<data name="company"> <value xml:space="preserve">Intertech Training</value> </data> <data name="lastClassTaught"> <value xml:space="preserve">.NET Security</value> </data>
要将该 *.resx 文件转换为二进制的 *.resources 文件,只须将文件扩展名作为 resgen.exe 的参数进行更新:
resgen myStrings.resx myStrings.resources
此时,我们具有了一个名为 myStrings.resources 的二进制资源文件。通过 /resource 标志可以达到使用 csc.exe 将该数据嵌入到 .NET 程序集中的目的。假设我们已经创作了位于 MyResourceApp 目录中的以下 C# 文件 (resApp.cs):
// This simple app reads embedded // resources and displays them to the // console. using System; using System.Resources; using System.Reflection; public class ResApp { private static void Main() { ResourceManager rm = new ResourceManager("myStrings", Assembly.GetExecutingAssembly()); Console.WriteLine("Last taught a {0} class.", rm.GetString("lastClassTaught")); } }
要相对于您的 myStrings.resources 文件编译该程序集,请输入以下命令:
csc /resource:myStrings.resources *.cs
因为我尚未指定 /out 标志,所以在该示例中,我们的可执行文件的名称将基于定义了 Main() 的文件 resApp.exe。如果一切正常,则在执行以后,您应当发现类似于图 11 的输出。
我希望您能够轻松地使用 csc.exe 和所选的文本编辑器创建单文件和多文件 .NET 程序集(带有资源!)。您已经学习了 csc.exe 的最常见的命令行选项,而本文的其余部分将分析一些不太常用但仍然有帮助的选项。
如果您愿意继续学习,请在 MyCSharpCode 文件夹中创建一个名为 FinalExample 的新目录。
使用 /define 定义预处理器符号
尽管 C# 编译器没有真正预处理代码,但该语言的确允许我们使用类似于 C 的预处理器符号来定义该编译器以及与其进行交互。使用 C# 的 #define 语法,可以创建能够控制应用程序内部的执行路径的标记。
注 必须在使用任何语句或其他 C# 类型定义之前列出所定义的符号。
为了利用常见的示例,假设您希望定义一个名为 DEBUG 的符号。为此,请创建一个名为 finalEx.cs 的新文件,并将其保存在 MyCSharpCode/FinalExample 目录中:
// Define a 'preprocessor' symbol named DEBUG. #define DEBUG using System; public class FinalExample { public static void Main() { #if DEBUG Console.WriteLine("DEBUG symbol defined"); #else Console.WriteLine("DEBUG not defined"); #endif } }
请注意,在我们使用 #define 定义了符号以后,我们就能够使用 #if、#else 和 #endif 关键字来有条件地检查和响应该符号。如果您现在编译该程序,则应当看到在 finalEx.exe 执行时,消息“DEBUG symbol defined”显示到控制台上:
csc *.cs
但是,如果您注释掉符号定义:
// #define DEBUG
则输出将会像您预料的那样(“DEBUG not defined”)。
在 .NET 程序集的开发和测试期间,在命令行定义符号可能有所帮助。这样做可以快速地即时指定符号,而不必更新代码基。为了进行说明,假设您希望在命令行定义 DEBUG 符号,则请使用 /define 选项:
csc /define:DEBUG *.cs
当您再次运行该应用程序时,您应当看到显示“DEBUG symbol defined”— 即使 #define 语句已经被注释掉。
csc.exe 的以调试为中心的选项
即使是最好的程序员,有时也会发现有对他们的代码基进行调试的需要。尽管我假设大多数读者更喜欢使用 Visual Studio .NET 进行调试活动,但对 csc.exe 的一些以调试为中心的选项(表 6)进行说明是值得的。
表 6. csc.exe 的以调试为中心的选项
csc.exe 的以调试为中心的选项 | 定义 |
---|---|
/debug | 指示 csc.exe 发出一个 *.pdb 文件,以供调试工具(例如,cordbg.exe、dbgclr.exe 或 Visual Studio)使用。 |
/warnaserror | 将所有警告视为严重错误。 |
/warn | 使您可以指定当前编译的警告级别(0、1、2、3 或 4)。 |
/nowarn | 使您可以禁用特定的 C# 编译器警告。 |
/bugreport | 如果应用程序在运行时出现故障,则该选项可生成错误日志。该选项将提示您输入纠正信息以发送到您希望的任何地方(例如,QA 小组)。 |
要说明 /debug 选项的用法,我们首先需要在我们的 finalEx.cs 代码文件中插入一些编码错误。请将以下代码添加到当前的 Main() 方法中:
// Create an array. string[] myStrings = {"Csc.exe is cool"}; // Go out of bounds. Console.WriteLine(myStrings[1]);
正如您可以看到的那样,我们试图使用越界索引访问我们的数组的内容。如果您重新编译和运行该程序,则会得到 IndexOutOfRangeException。尽管我们可以明显地指出该错误,但假如是一个不那么明显的更为复杂的错误,又该怎么办呢?
要调试使用 csc.exe 创建的程序集,第一步是生成包含各种 .NET 调试实用工具所需信息的 *.pdb 文件。为此,请输入下列命令(它们在功能上是等效的)之一:
csc /debug *.cs csc /debug+ *.cs
此时,您应当在应用程序目录中看到一个名为 finalEx.pdb 的新文件,如图 12 所示。
可以根据情况用 full 或 pdbonly 标记限定 /debug 标志。当您指定 /debug:full(它是默认标记)时,将以适当的方式对程序集进行修改,以使其可以附加到当前正在执行的调试器。既然该选项能够 影响所产生的 .NET 程序集的大小和速度,那么请确保只在调试过程中指定该选项。因为 full 是 /debug 标志的默认行为,所以上述所有选项在功能上是等效的:
csc /debug *.cs csc /debug+ *.cs csc /debug:full *.cs
另一方面,指定 /debug:pdbonly 可以生成一个 *.pdb 文件,以及一个只能在程序由调试工具直接启动时进行调试的程序集:
csc /debug:pdbonly *.cs
在任何情况下,既然您具有必需的 *.pdb 文件,那么您就可以使用许多调试工具(cordbg.exe、dbgclr.exe 或 Visual Studio)调试应用程序。为了不偏离本文的重点介绍命令行这一特征,我们将使用 cordbg.exe 实用工具调试该程序:
cordbg finalEx.exe
在调试会话开始以后,您就可以使用 so(单步执行)命令单步执行每个代码行了。当您单击出错的代码行时,您可以找到如图 13 所示的代码转储。
要终止 cordbg.exe 实用工具,请键入 exit 并按 Return 键。
注 本文的重点不是解释 .NET 调试工具的用法。如果您希望了解有关在命令行进行调试的过程的更多信息,请在 Visual Studio 帮助系统内查找“cordbg.exe”。
杂项
至此,您已经了解了 C# 命令行编译器的核心选项背后的详细信息。为了使本文的内容更加完整,表 7 简要描述了我尚未谈论到的其余标志。
表 7. csc.exe 的其余选项
csc.exe 的其余选项 | 定义 |
---|---|
/baseaddress | 该选项使您可以指定加载 *.dll 的预期基址。默认情况下,该基址由 CLR 选择。 |
/checked | 指定溢出数据类型界限的整数运算是否会在运行时导致异常。 |
/codepage | 指定要用于编译中的所有源代码文件的代码页。 |
/filealign | 该选项控制输出程序集内部的节大小调整(512、1024、2048、4096 或 8192 字节)。如果目标设备是手持型设备(例如,Pocket PC),则可以使用 /filealign 指定可能存在的最小节。 |
/langversion | 该选项指示编译器只使用 ISO-1 C# 语言功能,它基本上可以归结为 C# 1.0 语言功能。 |
/main | 如果当前项目定义了多个 Main() 方法(这在单元测试期间可能有所帮助),则可以使用该标志指定在程序集加载时执行哪个 Main() 方法。 |
/nostdlib | 默认情况下,程序集清单自动引用 mscorlib.dll。指定该选项可以禁止这一行为。 |
/optimize | 当被启用 (/optimize+) 时,可指示编译器尽可能生成最小且最快的程序集。该选项会发出还指示 CLR 在运行时优化代码的元数据。 |
/platform | 该标志告诉编译器针对 32 位或 64 位处理器优化程序集。一般来说,该选项只在 C# 代码基使用 P/Invoke 和/或不安全的代码结构时有用。默认值是“anycpu”。 |
/unsafe | 当被启用时,该选项使 C# 文件可以声明不安全的作用范围,这通常用于操纵 C++ 样式指针。 |
/utf8output | 该选项告诉编译器使用 UTF-8 编码输出数据。 |
需要了解的是,对于绝大多数 .NET 项目而言,表 7 中列出的选项只能提供非常少的好处。鉴于此,如果您需要进一步的详细信息,请参阅 MSDN。
小结
本文向您介绍了使用 C# 命令行编译器生成程序集的过程。就像您已经了解的那样,大多数工作可以使用两个标志 — /target 和 /reference 完成。除了分析 csc.exe 的核心标志以外,本文还解释了响应文件的好处以及多文件程序集的结构。
尽管本文没有提供有关 csc.exe 的每个选项的全部详细信息,但我希望您能够方便地使用 Visual Studio 2005 文档了解其余标志。
祝您编码愉快!
Andrew Troelsen 是一位 Microsoft MVP,他在 Intertech Training 担任顾问和培训讲师。Andrew 创作了许多著作,其中包括获奖的 C# and the .NET Platform Second Edition (Apress 2002)。他每月都为(真巧)MacTech 撰写专栏文章,他在这些文章中研究了如何使用 SSCLI、Portible.NET 和 Mono CLI 分发在基于 Unix 的系统上进行 .NET 开发。