一 应用程序——编程系统的产品
什么是应用程序?简单的说就是编程系统的产品。我非常喜欢《人月神话》中描述的编程产品的演进,通过下图我们来看看自己在开发什么。
程序 程序是代码最直接的产物,它本身是完整的,可以在我们的开发环境上运行,最常见的就是可执行文件。我们最容易得到的就是程序,程序对我们来说,只是开发的第一步,它仅仅代表我们的代码可以运行,但并不是我们的目标,我们还要进一步升级我们的成果。
编程产品 编程产品把程序上升为可以在不同平台上稳定运行,提供使用者一定功能的通用的编程产品。我们可以通过对程序的各种严格测试和不断的改进,撰写相应文档,提供技术支持,使我们的程序转换为编程产品。编程产品的例子是:串口调试工具。
编程系统 程序也可以转变成编程系统中的一个构件单元。它遵守一定的规范,与其它程序协作,共同组成系统。公司内部的框架可以看成一种编程系统。
编程系统产品 这是程序进化的究级形态,是将编程系统升华到产品的级别。比如Word等商用软件。这是我们大多数软件项目努力的目标,同时也是软件产品的价值所在。
我们必须清楚,软件开发不是一个人的事情,我们很难独自将我们的程序升级到产品级别,所以我们程序员大多数时间还是在生产程序也就是编程系统的某个构件单元。(这也是为什么我们工资低的原因,呵呵。)
二 第一个.NET程序
我们完全可以使用记事本开发C#程序,但一般没人这么干,记住这个名字吧“Visual Studio”,这个强大的IDE令无数同行(开发非.NET程序的)羡慕不已。VS太过强大了,将VS与微软的TFS结合可以迅速帮助一家企业达到CMMI3的标准。VS将是我们的开发利器,我们还可以通过安装各种插件来强化我们手中的工具。 (VS功能十分全面,我不会在文章中讲述其具体用法,请读者自行学习。)
第一个.NET程序“hello world”:
using System; namespace CLRTest { class Program { static void Main(string[] args) { Console.WriteLine("Hello world."); Console.ReadKey(); } } }
(一) 引用程序集
我们在用VS创建项目时,首先要选择.Net Framework版本,高版本的.Net Framework会支持更多的开发模板、更新的C#语法和一些高级别的FCL库。虽然高版本的.Net Framework为我们提供的功能更强大,但同样也要求了程序的运行环境,可能会提高系统的运营成本,所以要根据实际情况进行取舍,最新的并不一定是最合理的。
选择某个开发模板后,VS会为我们自动引用相应的程序集,并创建一些结构化的文件,以减少我们的工作量。如果要使用其它程序集,我们还需要在VS中手动添加引用。
除了在开发期间,添加对程序集的引用外,还可以在运行期间动态加载和创建程序集。动态加载和创建程序集,是比较高级的用法,会在以后的文章中描述,这里就不再说明了。
(二) 命名空间
为了确保基类库中的所有类型能良好地组织在一起,.NET平台提出了命名空间的概念。命名空间就是一个程序集内相关类型的一个分组。任何基于.NET运行库的语言都可以使用相同的命名空间和相同的数据类型。上例中,命名空间“CLRTest”成为类型“Program”概念上的容器,只要类型处在一个相同的命名空间,我们就可以认为它们在概念上是一个分组的。所以,我们可以定义两个名称相同的类型,通过命名空间的不同加以区别。需要注意的是任何Microsoft的嵌套命名空间包含的类型都用于和那些只属于微软操作系统的服务进行交互,这些类型不能在其它OS上运行。
我们完全可以不使用命名空间,也可以不引入命名空间。不显式声明命名空间,我们定义的类型就属于默认的命名空间,有时也称为全局命名空间(global),也就是System所在的命名空间。如果不引人命名空间,可以使用完全限定名来避免歧义,这两种方法得到的IL代码是相同的,对程序集的大小和性能没有任何影响。实际上,在CLR代码中,类型总是以完全限定名进行定义的。
命名空间可以创建别名。C# using关键字可以用于创建类型完全限定名的别名和命名空间的别名,例如:“using MyNamespace=xxxx.yyyy.zzzz.Some;”之后,我们就可以直接使用MyNamespace来取代xxxx.yyyy.zzzz.Some了,对于过长的命名空间特别好用。(类型的别名和命名空间的类似)
命名空间的命名一般规则:<公司名称>.(<产品名称>|<相关技术>)[.<用途>] [.<子命名空间>](注意:不要使用太一般化的类型名,引起不必要的类型名冲突。)
此外,C#还提供了一种机制来解决命名空间的命名冲突,通过使用命名空间别名限定符(::)和global标识。如果我们将“Hello world”代码改成如下所示,会引起歧义:
class Program { public class System { }; static void Main(string[] args) { System.Console.WriteLine("Hello world."); System.Console.ReadKey(); } }
在 :: 运算符前面使用的 global 关键字引用全局命名空间以解决歧义,例如:
global::System.Console.WriteLine("Hello world.");
global::System.Console.ReadKey();
命名空间嵌套 命名空间可以嵌套有两种写法:
(1) 层次式
namespace City { namespace Town { ... } }
(2) 紧凑式(大多数情况下,我们都是使用紧凑式)
namespace City.Town
{
...
}
(三) AssemblyInfo.cs
AssemblyInfo.cs是VS自动创建的,用以描述程序集的常规信息即配置程序集清单。我们可以通过VS中的“程序集信息”对话框来修改AssemblyInfo.cs的内容,例如:
我们会得到如下代码(注意我注释中描述的两个版本号的区别):
using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // 有关程序集的常规信息通过下列属性集 // 控制。更改这些属性值可修改 // 与程序集关联的信息。 //标题 [assembly: AssemblyTitle("测试“标题”")] //备注 [assembly: AssemblyDescription("测试“说明”")] //配置文件 [assembly: AssemblyConfiguration("")] //公司 [assembly: AssemblyCompany("测试“公司”")] //产品 [assembly: AssemblyProduct("测试“产品”")] //版权 [assembly: AssemblyCopyright("测试“版权”")] //商标 [assembly: AssemblyTrademark("测试“商标”")] //语言文化,这个对辅助程序集很重要 [assembly: AssemblyCulture("")] // 将 ComVisible 设置为 false 使此程序集中的类型 // 对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型, // 则将该类型上的 ComVisible 属性设置为 true。 [assembly: ComVisible(false)] // 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID [assembly: Guid("c7ea708d-9c07-4b4d-b234-69db65bec2b7")] // 程序集的版本信息由下面四个值组成: // // 主版本 // 次版本 // 内部版本号 // 修订号 // // 可以指定所有这些值,也可以使用“内部版本号”和“修订号”的默认值, // 方法是按如下所示使用“*”: // [assembly: AssemblyVersion("1.0.*")] //这个版本号非常重要,它是程序集版本的唯一标识,元数据中存的就是它。 [assembly: AssemblyVersion("1.2.333.4")] //仅供参考的版本号,存储在Win32版本资源中,使用Windows资源管理器可以看到它,但CLR不会使用它。 [assembly: AssemblyFileVersion("4.3.222.1")]
程序集将语言文化作为其身份标识的一部分,可以使程序集只面向一种语言。语言文化用一个字符串来标识,该字符串包含一个主标记和一个副标记,例如 en(主标记)-US(副标记)表示美国英语。一般包含具体代码实现的程序集不会指定语言文化,因为代码实现不应该涉及语言文化。没有指定语言文化的程序集称为语言文化中性。
程序集版本号格式如下:
主版本号和次版本号构成了公众对一个版本的理解,不会经常变化,但后两个版本号却经常在变。如果不显示设置版本号,程序集会默认设置为0.0.0.0。
我们可以通过编程方式获取程序集信息,例如:Assembly.GetExecutingAssembly().FullName,可以返回包括类名、版本、公钥等信息。
对于私有程序集(下文会详细分析私有程序集)来说,版本问题并不重要,因为引用的程序集会和主程序一起复制,但是,版本对应软件项目管理来说非常重要。版本控制是项目配置管理的一部分,很多项目实施时都不会去刻意维护这个版本号,这是不对的,难以想象一个连版本都不做管理的项目,其配置管理是如何进行的。虽然现今提倡可工作的代码胜过华丽的文档,但还是请读者养成习惯,在自己的项目中采用版本控制,不要图一时省事埋下灾难的种子。
(四) 应用程序对象和可执行程序入口点Main
在C#中创建全局函数或全局数据是不可能的,其所有的成员和方法都必须包含在一个类型定义中。
程序中定义了只有一个Main()方法的类类型。默认情况下,VS会把定义Main()的类命名为“Program”。每个可执行程序必须包含一个定义了Main()的类,这个方法用来表示应用程序入口点,这个类叫做“应用程序对象”。
Main()方法有static 关键字修饰(这允许C#不必创建实例对象即可运行程序),同时可以接受参数,使用void返回值(也可以返回int)。如果不显示使用访问修饰符,Main()默认为隐式私有,以确保其他应用程序不能直接调用另一个应用程序的入口点。
大多数程序会使用void作为Main()的返回值,这其实是在隐式返回0作为错误代码,返回值0表示程序结束,而其它值表示有错误发生。该值在程序结束时传递给系统,保存在“%ERRORLEVEL%”(环境变量)中,可以使用System.Diagnostics.Process.ExitCode属性来获取%ERRORLEVEL%的值。
(五) 调试与发布
VS中的调试(Debug)默认设置为/optimize-和/debug:full开关,生成的程序集为调试版本,生成的IL代码和最终的本地代码不会被优化,生成时除了主要文件外,还包括调试信息(*.pdb,文件中就记录了代码中的断点等调试信息);发布(Release)默认设置为/optimize+和/debug:pdbonly开关,不包含调试信息,生成的IL代码和最终的本地代码均会被优化。最终,我们需要的将是“发布”版本的程序。
(六) 部署与配置
可以使用VS或其它打包软件,将程序打包为安装文件,但打包程序集最简单的方式就是直接复制所有文件(我自己开发的简单应用程序就是将程序集做成“自解压”文件——免安装绿色版)。卸载程序集时,删除文件即可。之所以能实现这份简单的安装/移动/卸载,是因为每个程序集都用元数据指明了自己引用的程序集,不需要依靠注册表设置。
多数.NET应用程序,默认拥有配置文件,配置文件包含XML代码,其节点与某个应用关联。
可以把配置分为如下几类:
配置会受3个配置文件影响,他们是:
(1) 应用程序配置文件(上面介绍的那个),在可执行程序所在的目录下。
(2) 机器配置文件,应用全系统配置,在安装路径的config目录下,叫“Machine.config”。轻易不要修改,一旦该文件设置错误,很可能导致.NET平台的崩溃。
(3) 发布策略文件(以后介绍),用于指定共享程序集可以与旧版本兼容,它存储在GAC中。
此外,可以通过代码自定义配置文件。对于大型项目来说,自定义配置文件有其灵活性。而对于小型项目来说,默认的配置文件就足够了。
(在配置管理中,我们非常关注配置文件的参数设置,必须形成一套标准的配置管理流程。)
三 程序集初探
程序集,简单说就是一个以CLR为宿主的、版本化的、自描述的二进制文件。一个.NET应用程序可以由多个程序集拼装而成。程序集具有如下优点:
程序集分为DLL库程序集和EXE应用程序集,它们的界线非常模糊,你能用DLL库程序集做什么,也就能用EXE应用程序集来做什么。然而,反之则不成立,例如:
(一) 程序集的组成
程序集由几部分组成:
1 PE32(PE32+)头、CLR头、程序集清单
PE32(PE32+)头 PE32(PE32+)头使程序集被OS加载和操作,它标识了应用程序将以什么类型(控制台、GUI、*.dll)驻留于OS中。
CLR头 CLR头定义了多个标记,它们使得CLR可以了解到托管文件的布局。
程序集清单 清单是一组元数据表的集合,清单描述程序集自己,提供在程序集中所有的模块和组件共享的逻辑属性。清单由以下部分组成:
每个清单都包含程序集中不同模块的一个密码哈希,当加载程序集时,.NET运行时重新计算这个模块的密码哈希,如果与清单中的不同,就会拒绝加载程序集,并引发异常。同时,清单也是.NET采集其它引用程序集信息的方式。要确保版本的兼容性和程序集之间的交互,清单的信息尤为重要。
使用ildasm打开 Hello word 的程序集,可以查看清单的详细信息:
.assembly extern 块,由.publickeytoken 和.ver 组成。.publickeytoken 信息仅当程序集被设置为强名称是时候才使用。.ver 表示数字版本标识
.assembly 标记用于标识程序集的友好名称(我的是CLRTest)。
.custom 标识了程序集的特性,即我们上面设置的公司名称等信息。
.module 标识了模块自身的名称。
.ver 标识了程序集的版本号,该版本号是 AssemblyVersion 而不是 AssemblyFileVersion。
2 元数据
元数据是一个二进制数据块,由几个表构成。这些表分为三个类别:定义表、引用表、清单表(一组元数据的集合,包含程序集中一部分文件名称、还描述了程序集的版本、语言、发布者、共有打出类型、以及程序所需的所有文件)。常用的元数据表及关系如下:
使用 ildasm 查看 Hello world 程序的元数据如下:
.NET使用元数据跨越执行边界进行远程调用封送处理,元数据就是对对象类型和程序的准确、正式的描述。
3 IL、资源
IL 使用 ildasm 查看 Hello world 程序的 Main 方法IL如下:
资源 程序集可以内嵌资源,如图标、图片、字符串表等。(.NET把只包含语言文化特有资源,不包含任何代码实现的程序集称为附属程序集,这些程序集为“本地化”提供支持。CLR在查找附属程序集时,会忽略版本号,只使用语言文化信息。)
(二) 生成程序集
在程序集所有文件中,必然有一个文件容纳了清单。CLR总是首先加载包含“清单”元数据表的文件,在根据这个“清单”来获取程序集中的其它文件的名称。类型为了顺利地进行打包、版本控制、安全保护以及使用,必须放在作为程序集一部分的模块中。为了生成程序集,必须选择自己的一个PE文件作为清单的宿主,而且程序集中的所有文件都必须存在。然后,可以使用编译器 CSC.exe 或者 连接器 AL.exe 来创建程序集。