MONO:.NET 框架的替代方案
本文介绍了使用 MONO 平台在 Windows 以外的操作系统上开发 .NET 应用程序的可能性。文章将阐述其优点和挑战,并介绍使用 .NET 技术开发应用程序时遇到的一些常见问题。
本文由 Arkadiusz Merta 撰写,最初发表于 2005 年 2 月的 Software 2.0 杂志。您可以在 SDJ 网站找到更多文章。
引言
“.NET 技术为高度分布式应用程序开启了一个新的可能性世界。.NET Remoting 和 Web services 使它们能够遍布全球进行通信。语言无关性、与数据库的无缝集成、易于构建的 Web 界面,就像构建常规 Windows 应用程序一样——一个新的可能性世界等待着开发者去发现。”
本文介绍了使用 MONO 平台在 Windows 以外的操作系统上开发 .NET 应用程序的可能性。文章将阐述其优点和挑战。本文还将介绍使用 .NET 技术开发应用程序时遇到的一些常见问题。主要目的是展示相同的 C# 代码示例是否可以使用 Microsoft 的 C# 编译器 (csc) 或 MONO 编译器 (mcs) 在 .NET 和 MONO 中编译,在 .NET 或 MONO 下执行,并在 Windows 或 Linux 下运行。
萌芽
在 .NET Framework 中,核心理念之一是将应用程序(程序集)与操作系统隔离。这是通过将 .NET 源代码编译成包含Microsoft Intermediate Language (MSIL) 的可执行文件来实现的,MSIL 不依赖于操作系统或目标处理器。在执行时,MSIL 会被翻译成特定处理器的本地代码,并与相应的操作系统库链接,然后作为“常规”应用程序执行。因此,在这种情况下,.NET Framework 为 .NET 应用程序构建了一个功能性的抽象层。例如,Windows Forms 是 .NET 应用程序的标准图形界面,它通过 .NET Framework 转换为一系列对 .NET Framework通用库 (CL) 的调用。这与之前 Visual C++Microsoft Foundation Classes (MFC) 使用的方法截然不同,MFC 实际上执行的是对 Win32 API 的直接调用。.NET 程序集是针对 .NET Framework 执行的,而 Win32 应用程序直接调用操作系统。此功能有望实现操作系统无关性。诀窍在于 .NET Framework 是单一供应商的解决方案;尽管有一些针对移动设备的端口(SmartPhone),但它们确实绑定到了微软的产品。
.NET 的最大竞争对手——Java 虚拟机——有一个使其具有优势的特性:真正的系统无关性。Java 的问世比 Microsoft .NET 早得多,从一开始就旨在成为一个开源产品。难怪社区接受了它,并为 Java 虚拟机开发了许多不同平台的端口。当 .NET 发布时,Java 已经相当流行,移植到了许多操作系统,并且已经有丰富的免费开发工具可用(例如,IBM Sun ONE Studio 和 Eclipse)。.NET Framework 是免费提供的,但 .NET 开发工具相当昂贵——Visual Studio .NET 的售价超过 2000 美元。
为了能够与 Sun 的 Java 竞争,微软(与惠普和英特尔一起)发布了 C# 和通用语言基础结构 (CLI) 规范,并将其提交给国际标准化组织 ECMA。结果,ECMA-334 (C#) 和 ECMA-335 (CLI) 标准于 2001 年 12 月下旬获得批准。这样,.NET Framework 就走上了真正可移植的道路。
诞生
MONO 项目始于 2001 年,但第一个稳定版本 1.0 于 2004 年第二季度发布。该工作由 Ximiana 发起(基于 ROTOR 研究项目),随后被 Novell 收购。
目标非常明确:将 .NET Framework 从单一供应商解决方案转变为一个广泛且可移植的标准,适用于多个平台和操作系统。已提供 x86、SPARC 和 PowerPC 平台的版本。支持 Unix、Linux、FreeBSD 和 Windows。C#(稳定版)和最终的 Basic(不稳定版)可用于开发。System、Web、Remoting、security、Web services 和 XML 包已准备就绪。部分数据组件已准备就绪(Oracle),部分则不稳定(包括 Microsoft SQL 客户端;完整的包列表可在“MONO 组件”部分找到)。
本文参考的是 MONO 1.0.1。
Windows 版 Mono
在本文中,“.NET Framework” 指的是微软产品,“MONO Framework” 指的是 MONO 对 CLI 的实现。
请将 Windows 版 MONO 安装到:c:\mono\Mono-1.0,这将为您节省大量编译包的时间(包管理器配置中存在一些错误)。此外,还必须将 PATH
变量更新为 MONO bin 目录的路径(此处:c:\mono\Mono-1.0\bin)。
在 Windows 上安装 MONO 和 .NET Framework 实际上意味着两个执行引擎将并存。请注意,简单地双击程序集(或从命令行运行它)将使用 .NET Framework 执行它,这是系统的默认设置。为了在 MONO 下执行程序集,必须使用一个命令。
mono <assembly_name>
最简单的方法是使用 MONO 命令提示符。
学校
让我们尝试编写一个简单的 .NET 控制台应用程序,在 Windows 上使用 Microsoft 和 MONO 工具进行编译,并在 Windows 上执行。
图 1 所示的 C# 代码在标准控制台输出 Hello World!。
图 1. C# 中的 Hello World 应用程序
using System;
namespace FirstHomeWork{
class First{
[STAThread]
static void Main(string[] args){
System.Console.WriteLine(“Hello World!”);
System.Console.ReadLine();
}
}
}
我们将它保存为 first.cs,并使用 csc(Microsoft 的 .NET C# 编译器)编译成 first_ms.exe。
>csc /out:first_ms.exe first.cs
>first_ms.exe
Hello World!
正如预期的那样,执行会在标准控制台上输出程序员们喜爱的字符串,并等待按下“Enter”键。
我们使用 msc(MONO C# 编译器)对 MONO 执行相同的操作。
>msc -out:first_mono.exe first.cs
结果是生成了一个 first_mono.exe 文件。尝试运行它,我们将得到与 first_ms.exe 文件相同的结果。
>first_mono.exe
Hello World!
但这里到底发生了什么?我们只是在... Microsoft .NET Framework 下执行了使用 MONO msc 编译器编译的 C# 源代码!
要在 MONO Framework 下执行 first_mono.exe,我们需要使用:
>mono first_mono.exe
Hello World!
结果(正如预期的那样)是相同的。mcs 和 csc 生成的程序集文件大小相同。但是,当我们逐字节比较它们时,会发现一些字节码不同。
这里一个重要的注意事项是 MONO 程序集的执行方式。对于后一个示例,我们使用了 mono 命令来运行它们。此命令使用即时 (JIT) 编译器将代码从中间语言转换为目标平台本地代码,将结果存储在内存中,然后从那里执行。JIT 编译的代码副本会保留在内存中以供后续使用。
此处应介绍一项 MONO 功能:mint。Mint 是一个解释器。为了使用 mint 执行 MONO 应用程序,我们可以从命令行调用以下命令:
mint first_mono.exe
输出将大致相同。但在底层,执行方式不同。Mint 作为解释器,根本不使用 JIT,它只是遍历编译后的代码,解释每个 ECMA-CLI 字节,并针对 x86 指令集执行它们。由于解释是在每一步完成的,因此,应用程序通过 mint 运行的总体速度通常比通过 mono 运行的慢。但是,JIT 编译在开始时会花费一些时间,对于小型应用程序,使用 mint 可能比使用 mono 运行更快。
但是,由于 mint 会解释代码,因此 C# 编译器可用于调试目的。此外,通过避免 JIT,编译允许在 JIT 尚不可用的平台上执行(OS X)。
现在我们了解了一些 MONO 的基本原理;现在让我们尝试编写一个简单的对话框应用程序。该应用程序将使用 Visual Studio .NET 创建(参见图 1 和图 2)。
图 1. 简单的对话框应用程序——一个标签和一个“Say Hello”按钮
图 2. 单击“Say Hello”后的简单对话框应用程序
我们有了源代码,让我们尝试在 MONO 下编译它。
mcs –out:form_mono.exe form.cs
form.cs(13) error CS0246: Cannot find type ‘System.Windows.Forms.Form’
Compilation failed: 1 error(s), 0 warnings
不成功?失败的原因很简单:MONO Framework 实现了 CLI 功能,但并非完全实现。不幸的是,一些包仍被标记为不稳定或未实现( MONO 1.0 中包含的包列表可在“MONO 组件”部分找到)。此外,一些包可能根本不会被实现。
让我们专注于 GUI。如引言所述,MONO 项目的目的是创建基于 CLI 规范的框架,用于 x86 和 MS Windows 以外的平台。.NET Framework 用于构建 Windows 应用程序,它使用 Windows Forms (System.Windows.Forms
命名空间)。但它包含一组专门为 Microsoft Windows 设计的控件。这是否意味着无法使用 MONO 构建 GUI?当然不是。区别在于基于 Unix/Linux 的系统拥有自己广泛接受的图形包:Gtk。
Gtk 从一开始就是为了支持 GIMP 而创建的,GIMP 是一个用于 X-Windows 系统的图形工具。之后,它成为构建在 Unix/Linux 下运行的图形界面的重要构建器。现在,其托管版本称为 Gtk#,也随 MONO 项目一起分发,位于 Gtk
和 GtkSharp
命名空间下(gtk-sharp.dll)。
使用 MONO 编译的程序集只要使用了 MONO 中实现的 .NET Framework 命名空间,就可以在 .NET Framework(注意:不是 MONO Framework)下执行。
因此,要创建可以在 Windows 或 Linux 下运行的 MONO GUI 应用程序,必须使用 Gtk#。再说一遍,目前无法在 MONO 下编译稳定的 .NET Windows Forms 应用程序(并在跨平台运行时)。
好消息是 Gtk# 相对容易学习,并且许多功能与 Windows Forms 相同。让我们尝试测试这个结论(图 2)。将其保存到 form_mono.cs 文件并使用 mcs 编译。
mcs –out:form_mono.exe –pkg:gtk-sharp form_mono.cs
请注意,必须添加对 gtk-sharp 包的引用(-pkg:gtk-sharp)。让我们通过调用:mono form_mono.exe 在 MONO Framework 上执行应用程序。结果可以在图 3 中看到。
图 2. 使用 Gtk# 构建图形界面的对话框应用程序
using System;
using Gtk;
using GtkSharp;
namespace FirstDialogMONOApp{
public class form_mono{
private static Label lbl;
static void onWindowDelete(
object obj, DeleteEventArgs args){
Application.Quit();
}
static void onBtnClick(object obj, EventArgs args){
lbl.Text = "Hello to you!";
}
static void Main() {
Application.Init();
Window win = new Window("First Form - mono");
win.DeleteEvent+=
new DeleteEventHandler(onWindowDelete);
VBox vbox = new VBox(false,1);
lbl = new Label("???");
vbox.PackStart(lbl,false,false,1);
Button btn = new Button ("Say Hello");
btn.Clicked+=new EventHandler(onBtnClick);
vbox.PackStart(btn,true,true,1);
win.Add(vbox);
win.ShowAll();
Application.Run();
}
}
}
图 3. 简单的对话框应用程序——MONO 版本
如果我们比较 Windows 和 MONO 版本的源代码,会发现很多相似之处。一些控件名称非常相似(Label
、Button
),一些方法和属性相同(例如 Label
构造函数、Label.Text
属性),事件也匹配(DeleteEvent – DeleteEventHandler,Clicked – EventHandler)。
好的,现在我们知道要构建 MONO 中的 GUI 应用程序,必须使用 Gtk#,因为 MONO 1.0 不包含 System.Windows.Forms
的实现。这就是为什么 .NET 应用程序无法直接使用 MONO 编译的原因。出现了一个问题:是否可以使用 .NET Framework 执行使用 Gtk# 的 MONO GUI 应用程序?让我们通过调用:form_mono.exe 来检查。结果,我们将收到一个即时调试对话框,其中包含一个异常(图 4)。仔细查看图 4,我们将发现异常类型是 System.IO.FileNotFoundException
。允许调试器运行,扩展消息将为我们提供更多详细信息(图 5)。
图 4. JIT 异常对话框
图 5. 调试器异常窗口
glib-sharp 对我们来说现在是熟悉的——我们在编译 MONO 对话框应用程序时引用了它。然而,在应用程序执行过程中,似乎找不到它了。由于此库将用于多个应用程序——让我们尝试使其可访问。
将 .NET(或 .NET 兼容)程序集共享给多个应用程序的最佳位置是全局程序集缓存 (GAC)。GAC 是一个特殊的、隔离的存储区域,应用程序可以在其中存储并以分配给它们的策略执行。可以通过管理工具/Microsoft .NET Framework 配置(图 6)列出 .NET Framework GAC 的内容。
通过单击查看程序集列表,我们将收到驻留在 GAC 中的程序集列表。由于我们的任务是向 GAC 添加 gtk-sharp,我们选择向程序集缓存添加程序集,找到 gtk-sharp.dll(应位于 Mono-1.0/lib/mono/gtk-sharp),然后添加它。让我们通过选择查看程序集列表选项(图 7)来检查它是否已添加。
GAC 的内容也可以从命令行列出,使用 gacutil 工具。
gacutil /lr
其他命令行开关允许 GAC 操作。
图 6. .NET 配置
图 7. 包含“gtk-sharp”程序集的 .NET GAC
让我们再次尝试通过调用:form_mono.exe 来执行我们的 MONO 应用程序。
仍然不成功?我们将收到相同的错误消息。但是,异常消息包含以下陈述:未找到 gtk-sharp 或其依赖项之一。由于我们已经将 gtk-sharp 放入 GAC,因此我们可以假设第一个条件已满足。我们需要检查 Gtk# 需要哪种依赖项。
清单
.NET 可执行文件与常规 Windows 可执行文件大不相同。除了包含中间代码而不是系统本地代码外,它们的组织方式略有不同。目前最重要的是它们包含一个清单。清单数据包含有关程序集及其依赖程序集列表的信息。它还包含所有公开的类型和资源。研究它是一个收集模块如何使用的知识的好方法。
让我们使用 Microsoft 的 ildasm 工具(图 8)检查依赖项。ILDSAM 工具解析任何 .NET Framework EXE/DLL 模块,并以易于理解的格式显示信息。它允许用户浏览一个清单,该清单包含使用的 .NET 命名空间、类型和依赖项。MONO 提供了一个命令行反汇编器:monodis。
图 8. Ildasm 工具列出了 gtk-sharp.dll 清单的内容
正如我们所见,gtk-sharp 程序集依赖于几个其他程序集:
- mscorlib,这是一个 .NET 引擎。
- glib-sharp——通用实用程序。
- atk-sharp——可访问性工具包。
- gdk-sharp——一个中间层,将 Gtk 与窗口系统细节隔离开。
- pango-sharp——用于 GDK 和 Gtk 的高级字体和文本处理。
- System,一个默认命名空间。
就像上面对 gtk-sharp 所做的那样——让我们将 atk-sharp、gdk-sharp、glib-sharp 和 pango-sharp 添加到 GAC(图 9)。
图 9. GAC 已更新,包含 Gtk# 及其依赖项。
最后一步——再次运行应用程序——现在应该成功了。这证明了 Gtk# 及其依赖项完全符合 Microsoft .NET Framework。
学习
我相信到目前为止我们已经了解了足够多的知识,可以尝试使我们的应用程序可分发。如MONO 组件中所见,MONO Framework 实现 System.Web
和 System.Web.Services
命名空间——我们将尝试使用它们来构建一个从 Web 服务器获取数据并使用 Web 服务的应用程序。此处将扩展Hello 对话框应用程序以使用 Web 服务获取欢迎字符串值。我们将测试 MONO 是否可以创建一个 Web 服务,该服务可供 .NET 或 MONO 编写的应用程序使用。
MONO Web 服务器
MONO 配备了自己的小型 Web 服务器,称为 XSP。它可以使用以下方式批量运行:
c:\mono\Mono-1.0\bin\startXSP.bat
默认情况下,它位于端口 8088。要查看其起始页,应在 Web 浏览器中输入类似以下地址:
https://:8088/
应该在 Web 浏览器中输入。
首先,让我们创建 hello.asmx 并将其放在 Web 服务器根目录下。
<%@ WebService Language="c#"
Class="helloServices.hello"
Codebehind="hello.asmx.cs" %>
头部指定了:
- 代码隐藏类是用 C# 实现的。
- 命名空间定义为
helloServices
,Web 服务类名为hello
。 - 类在 hello.asmx.cs 文件中实现。
现在,让我们实现 Web 服务(图 3)。
图 3. 实现 Web 服务的类
using System;
using System.Web.Services;
namespace helloServices{
public class hello:System.Web.Services.WebService {
[WebMethod]
public string getHello(){
return "Hello (from the Web Service)";
}
}
}
Web 服务代码-behind 文件(如图 3 所示)包含一个名为 getHello
的 Web 方法,该方法返回一个欢迎字符串。现在我们将编译代码-behind 文件,添加对 System.Web.Services
的引用(/r: 开关)并指定目标为库(/t: 开关)。
>mcs /r:System.Web.Services.dll /t:library hello.asmx.cs
输出文件(hello.asmx.dll)应存储在 Web 服务器的 /bin 目录中。让我们尝试使用 Web 浏览器查看 Web 服务是否正常工作(图 10)。
图 10. 从 Web 浏览器调用的 Hello Web 服务
它成功了!现在我们可以调用方法,单击 getHello 链接,然后单击调用。
好的——我们编写了一个 Web 服务,使用 mcs 在 MONO 下编译了它,即使从 IIS 调用它也能正常工作。但如何在我们的应用程序中使用它?
让我们从更简单的解决方案开始:在 .NET 应用程序中使用 MONO 的 Web 服务。为此,必须在 Visual Studio .NET 解决方案中添加对 hello 服务的 Web 引用,并修改按钮点击处理程序,如
private void btn_Click(object sender, System.EventArgs e){
localhost.hello helloService = new localhost.hello();
lbl.Text = helloService.getHello();
}
由于我是在单台机器上操作的,因此添加了一个对 localhost 的引用。通过这样做,Visual Studio 会为服务生成一个代理(localhost.hello
类),然后实例化该代理并调用 getHello
Web 方法。结果符合预期(图 11)。
图 11. 从使用 MONO 创建的 Web 服务获取欢迎字符串的对话框应用程序
我们的 MONO 应用程序怎么样?如前所述,Visual Studio 会自动创建代理。由于 Windows 上还没有专门用于 MONO 的开发工具——我们必须自己使用一个名为 wsdl 的工具(包含在 MONO 包中)。
>wsdl.exe –n:helloWS https:///hello.asmx?wsdl
这将生成包含代理代码的 hello.cs 文件。请始终使用 -n 开关指定代理的命名空间(此处:命名空间设置为 helloWS
)。让我们修改 Say Hello 按钮处理程序。
static void onBtnClick(object obj, EventArgs args){
helloWS.hello helloService = new helloWS.hello();
lbl.Text = helloService.getHello();
}
MONO 分发版中包含的 Web 服务器 XSP 也支持代理生成。让我们编译所有内容,添加对 Web 服务的引用。
> mcs -out:form_mono.exe -pkg:gtk-sharp
-r:System.Web.Services.dll form_mono.cs hello.cs
...并在 MONO 下使用:mono form_mono.exe 运行,得到的结果如图 12 所示。
图 12. 调用 Web 服务的 MONO 应用程序
由于已将 Gtk# 添加到 GAC,因此在 .NET Framework 下执行的应用程序也能正常工作!因此,现在我们可以使用 MONO 构建 Web 服务和适当的客户端。我们已经看到代码可以由 .NET 或 MONO 编译,并在两个框架下执行。当然,这是因为 MONO Framework 实现了 System.Web
命名空间。
整合
使用 MONO 而不是 Microsoft .NET 的一个关键原因是 MONO 能够跨不同平台运行。对于 Linux 平台,我们将使用带有 GNOME 桌面的 Red Hat 9 服务器作为测试机器。
在 Linux 下安装 MONO
要在 Red Hat Linux 下安装 MONO:
从 此处下载 mono_all.zip。
- 解压——它应该包含几个 rpm 包。
- 执行 shell:>rpm –Uvh –nodeps *.rpm
- 运行 Web 服务器:>mono /usr/bin/xsp.exe
- 从菜单运行 MonoDevelop。
我在 SuSE Linux 9.1 Personal 上成功安装了 MONO。但是,MonoDevelop 工具未能运行,因为缺少软件包依赖项(SuSE 默认使用 KDE 而不是 GNOME)。
图 13. Red Hat 9 下的 MonoDevelop
MonoDevelop
为 MONO 平台创建了一个基于 GUI 的开发工具。MonoDevelop 不如 Visual Studio .NET 精细。它包含一个带有语法高亮、上下文帮助、代码自动补全的简单编辑器,并且支持编译应用程序。它将文件组织成项目和解决方案,并提供多个项目模板。
为了测试 MONO 的可移植性,我们将尝试使用 MONO mcs 在 Windows 下编译的应用程序在 RedHat 上使用 MONO 1.0 执行。为此,只需将程序集复制到 Linux 机器并在那里执行(图 14)。
图 14. 在 Linux 下执行的对话框应用程序
它又成功了!仔细查看图 14,我们可以看到应用程序的外观与在 Windows 上执行的应用程序略有不同。这是因为 Windows 版 Gtk# 使用了 Windows 的外观和感觉——Linux 桌面小部件略有不同。
知情的朋友
企业级应用程序的一个特定问题是它们执行大量处理,并与数据库紧密协作。Microsoft 的 .NET 在强大的 Microsoft SQL 客户端的基础上进行了改进,通过 DataSet
s 显著提高了数据库工作效率。DataSet
是 ADO.NET 的主要组件之一。DataSet
是根据视图、表或行(使用存储过程或 SQL 查询)查询数据库的结果。它可以被视为查询数据的快照。它不仅提供数据,还集成了与数据源同步(插入、删除和更新)的功能。
MONO 可以使用 ADO.NET(和一些第三方工具)与各种商业和开源数据库进行交互,如表 1 所示。
表 1. MONO 支持的数据库或数据库引擎
数据库/引擎 |
命名空间 |
需要用“‘-r’”选项引用的程序集 |
注释 |
.NET |
MySQL |
|
System.Data.dll ByteFX.Data.dll |
与 MONO 一起分发的第三方包 |
|
ODBC
|
|
System.Data.dll |
|
是 |
Microsoft SQL |
|
System.Data.dll |
支持 ver. 7 和 2000
|
是 |
Oracle |
|
System.Data.dll System.Data.OracleClient.dll
|
|
是 |
PostgreSQL |
|
System.Data.dll Npgsql.dll
|
|
|
Firebird/ Interbase |
|
System.Data.dll FirebirdSql.Data.Firebird.dll |
与 MONO 一起分发的第三方包 |
|
IBM DB2 |
|
System.Data.dll IBM.Data.DB2.dll
|
|
|
OLE DB |
|
System.Data.dll |
使用 Gnome DB,即 Access |
是 |
SQL Lite |
|
System.Data.dll Mono.Data.SqliteClient.dll |
可嵌入的 SQL 数据库引擎
|
|
Sybase |
|
System.Data.dll Mono.Data.SybaseClient.dll
|
|
|
可以看出,MONO Framework 支持的数据库数量远多于 Microsoft 提供的数据库。当然,也存在 .NET 中缺少的数据库库——但它们没有与 Visual Studio .NET 一起分发。
图 4 给出了 Microsoft SQL 数据库引擎的示例用法。
图 4. SQL 客户端源代码
using System;
using System.Data;
using System.Data.SqlClient;
namespace SQLClient{
class sqlclient{
[STAThread]
static void Main(string[] args){
SqlConnection conn;
conn = new SqlConnection();
conn.ConnectionString =
"user id=sa;data source=192.168.89.176;" +
"persist security info=False;
initial catalog=MONOTest";
SqlCommand sel = new SqlCommand();
sel.Connection = conn;
sel.CommandText =
"SELECT id, Name, Surname FROM people";
SqlDataAdapter dataAdapter = new SqlDataAdapter();
dataAdapter.SelectCommand = sel;
DataSet ds = new DataSet();
dataAdapter.Fill(ds, "people");
DataTable table = ds.Tables[0];
for(int row = 0; row < table.Rows.Count; row++){
System.Console.Write("{0}: ", row+1);
for(int col = 1; col < table.Columns.Count; col++)
System.Console.Write("{1}:{2}, ",
row+1,
table.Columns[col].ColumnName,
table.Rows[row].ItemArray[col].ToString());
System.Console.WriteLine("");
}
System.Console.ReadLine();
}
}
它可以在 .NET(使用 csc)或 MONO(使用 mcs;添加 -r:System.Data 开关)下编译。它可以在 .NET 或 MONO(调用 mono)框架下执行,并且在 Windows 或 Red Hat 下都能正常工作(图 15)。
图 15. 在 Red Hat 中执行的 SQL 客户端
一场追逐
任何两个平台的比较都不能缺少性能测试。作为示例测试,我选择了上一节中的一个应用程序,并为其添加了插入和删除操作。测试数据库包含 2500 行用于选择。'Insert' 添加了另外 500 行。'Delete' 则删除了它们。作为测试机器,使用了一台 Pentium III 500,配备 64 MB RAM。结果显示在表 2 中。
表中显示,针对 MONO 执行的应用程序通常比针对 .NET Framework 执行的应用程序慢得多。实际上,微软的框架连接到微软的数据库比 MONO 更快也就不足为奇了。真正令人惊讶的是,在 .NET Framework 下执行的 MONO 应用程序和微软的原生应用程序一样快。
表 2. 在 Windows 2000 下运行的 MONO 和 .NET 编译应用程序的性能比较
|
MS Windows 2000 | |||
平台 MONO |
平台 .NET | |||
MONO 应用* [毫秒] |
.NET 应用** [毫秒] |
MONO 应用*** [毫秒] |
.NET 应用**** [毫秒] | |
连接创建 |
1071 |
891 |
111 |
111 |
选择 2500 行 |
2667 |
2353 |
781 |
781 |
插入 500 行 |
2072 |
2125 |
719 |
713 |
删除 500 行 |
2720 |
2754 |
663 |
662 |
* 使用 mono 在 MONO 平台下编译(使用 mcs)并执行的应用程序。
** 使用 mono 在 MONO 平台下编译(使用 csc)并执行的应用程序。
*** 在 .NET 平台下编译(使用 mcs)并执行的应用程序。
**** 在 .NET 平台下编译(使用 csc)并执行的应用程序。
这里一个重要的注意事项是,MONO 和 .NET 程序集以几乎相同的速度执行。
为了准确起见,让我们快速查看一下使用 mint 解释器执行的测试(表 4)。比较表明,对于某些操作,解释器可能更快;但对于其他操作,它们甚至可能慢四倍!
表 3. 在 Red Hat 9 下运行的 MONO 和 .NET 编译应用程序的性能比较
|
Red Hat 9 | ||
MONO/Windows 应用* [毫秒] |
MONO/Linux 应用** [毫秒] |
.NET/Windows 应用*** [毫秒] | |
连接创建 |
510 |
510 |
509 |
选择 2500 行 |
664 |
626 |
652 |
插入 500 行 |
2072 |
2039 |
2061 |
删除 500 行 |
2770 |
2726 |
2859 |
* 在 Windows MONO 下编译(使用 mcs)并在 RedHat 9 下的 MONO 平台(使用 mono)上执行的应用程序。
** 在 RedHat MONO 下编译(使用 mcs)并在 RedHat 9 下的 MONO 平台(使用 mono)上执行的应用程序。
*** 在 Windows .NET 下编译(使用 csc)并在 RedHat 9 下的 MONO 平台(使用 mono)上执行的应用程序。
表 4. 使用“mono”命令(即使用 JIT)和“mint”命令(即解释执行)运行的 MONO 应用程序的比较
|
Windows 2000 |
Red Hat 9 | |
mono <>* [毫秒] |
mint <>** [毫秒] |
mint <>*** [毫秒] | |
连接创建 |
1071 |
1106 |
807 |
选择 2500 行 |
2667 |
3620 |
1419 |
插入 500 行 |
2072 |
5491 |
5932 |
删除 500 行 |
2720 |
9720 |
10889 |
* 在 MONO 平台(使用 mono)下编译(使用 mcs)并执行的应用程序,由 Windows 托管(用于比较)。
** 在 MONO 平台(使用 mint)下编译(使用 mcs)并执行的应用程序,由 Windows 托管。
*** 在 MONO 平台(使用 mint)下编译(使用 mcs)并执行的应用程序,由 RedHat 托管。
短暂休息
MONO 包还包含并支持一组第三方工具,其中之一称为 IKVM.NET。IKVM.NET 是 .NET 或 MONO 运行时平台的 Java 虚拟机实现(参见图 13——其中一个项目模板节点称为Java)。IKVM.NET 旨在连接被认为是相互排斥的 Sun 和 Microsoft 技术,以允许在单个运行时框架下无缝执行。
该项目仍处于积极开发阶段,许多功能需要实现(例如,AWT 和 Swing 组件!)。然而,未来的一些关键点值得一提:
- 用 .NET 实现的 Java 虚拟机。
- .NET 对 Java 类库的实现。
- ikvmc 工具,可以将 Java jar 转换为 .NET 兼容程序集。
利用用 Java 编写的遗留系统以及 .NET 的互操作性的承诺相当有趣。不过,这属于未来。
周末:进入 Glade
MONO 的一个有趣部分称为glade。Glade 类使应用程序能够在运行时从 XML 文件加载用户界面。因此,通常可以在不重新编译的情况下更改应用程序的外观和感觉!
让我们使用 Glade 实现我们的对话框应用程序。首先,我们将创建一个 GUI 轮廓。如上所述,它在 XML 文本文件中定义。当然,它可以手动编写,但有几个免费构建器可用。这里将使用Glade for Windows(图 16)。Glade for Windows 可以从 SourceForge 下载。Linux 版本可以在 这里找到。
图 16. “Glade for Windows”在构建示例应用程序时
生成的 gui.glade XML 文件定义了一个 GUI,其外观如列表 5 所示(为清晰起见,删除了一些行)。
图 5. 包含 GUI 定义的 XML 文件
<?xml version="1.0" standalone="no"?>
<!--*- mode: xml -*-->
<!DOCTYPE glade-interface
SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
<glade-interface>
<widget class="GtkWindow" id="window1">
<property name="title" translatable="yes">
First Form</property>
...
<property name="resizable">False</property>
...
<signal name="delete_event"
handler="OnWindowDeleteEvent" />
<child>
<widget class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="homogeneous">False</property>
<property name="spacing">0</property>
<child>
<widget class="GtkLabel" id="lbl">
<property name="label" translatable="yes">???</property>
...
</widget>
</child>
...
<child>
<widget class="GtkButton" id="btn">
<property name="label" translatable="yes">
Say Hello</property>
...
<signal name="clicked"
handler="onBtnClick"/>
</widget>
</child>
...
</widget>
</child>
</widget>
</glade-interface>
请查看以下行:signal 和 property。它们设置基本属性(例如,name),或定义与其关联的信号。Glade 信号实际上是一个事件,它将由指定的处理程序进一步处理。
我们将创建一个 Glade 客户端应用程序(glade.cs;列表 6)。客户端代码显示了触发应用程序的 main 类以及在各自 signal XML 语句中指定的两个事件处理程序(用于按钮点击和窗口关闭)。此外,Glade.XML.GetWidget
用于获取对 label 元素的引用。XML 文件作为 Glade.XML
构造函数参数给出。
图 6. 使用 XML 文件中 GUI 定义的 C# 应用程序
using System;
using Gtk;
using Glade;
public class GladeApp {
private Glade.XML gxml;
public static void Main (string[] args) {
new GladeApp (args);
}
public GladeApp (string[] args) {
Application.Init();
gxml =
new Glade.XML (null, "gui.glade", "window1", null);
gxml.Autoconnect (this);
Application.Run();
}
public void onBtnClick(object obj, EventArgs args){
helloWS.hello helloService = new helloWS.hello();
Gtk.Widget wg = gxml.GetWidget("lbl");
if(wg != null )
((Label)wg).Text = helloService.getHello();
}
public void OnWindowDeleteEvent (object o, DeleteEventArgs args) {
Application.Quit ();
args.RetVal = true;
}
}
现在,让我们编译所有内容,如列表 6 所示。
>mcs –pkg:gtk-sharp –pkg:glade-sharp –r:System.Web.Services
–linkresource:gui.glade glade.cs hello.cs
请注意:
- 与之前一样,添加了对 Gtk# 的引用(-pkg:gtk-sharp 开关)。
- 添加了对 Glade 的引用(-pkg:glade-sharp 开关)。
- 添加了对
System.Web.Services
的引用(-r: System.Web.Services 开关)。 - 添加了对资源文件 gui.glade 的引用(-linkresource:gui.glade 开关)。
- 将 glade.cs 和 hello.cs 添加到构建中。
并调用运行:
mono glade.exe
应用程序与之前示例中显示的应用程序没有区别。这是因为 Glade 使用 Gtk# 小部件。
这个解决方案的美妙之处在于,我们可以编辑 gui.glade(例如,使用记事本)。例如——标签名称可以从 ??? 更改为 Waiting...;再次运行 glade.exe 将显示这些更改——而无需重新编译任何内容。
当然,这种解决方案也有一些缺点。显然,事件(signal)处理程序必须包含在编译后的程序集中,因此,如果功能发生变化,应用程序仍然需要重建。此外,XML 尽管是文本格式,但很难阅读,并且需要一个好的工具来创建和编辑它。
内在生活
MONO 的另一个有趣特性是能够将其嵌入到其他程序中:您可以编写 C 代码(非托管),它调用 MONO 程序集(托管)。尽管这似乎是一个非常学术性的讨论——让我们尝试一下(只是为了了解可能性)。
C 明显缺乏互操作性例程。能够访问 Web 服务不是很好吗?
作为 Web 服务,将使用已知的 hello。我们将创建一个 C 客户端和一个调用 Web 服务的托管程序集。让我们首先创建一个 exposeWS.cs 文件,它将调用 Web 服务(图 7)。
图 7. 调用 Web 服务的 C# 应用程序
using System;
class helloStub{
static void Main(){
Console.WriteLine("Contacting WebService...");
helloWS.hello helloService = new helloWS.hello();
String answer = helloService.getHello();
Console.WriteLine("Web service answer: {0}", answer);
}
}
并在 MONO 下编译,调用:
mcs –r:System.Web.Services exposeWS.cs hello.cs.
结果,将创建 exposeWS.exe 程序集。现在,我们将编写一个包含 C 客户端的 client.c 文件(图 8)。我们将使用 gcc 和从 pkg-config 工具获得的链接器参数来编译我们的 C 客户端。
gcc client.c -o:client .exe
`pkg-config –cflags –libs mono`
图 8. 使用 MONO 应用程序的 C 应用程序
#include <mono/jit/jit.h> int main(int argc, char* argv[]){ //Create domain for assembly MonoDomain *domain; domain = mono_jit_init ("exposeWS.exe"); if(!domain)error(); //Load assembly into domain MonoAssembly *assembly; assembly = mono_domain_assembly_open(domain, "exposeWS.exe"); if( !assembly )error(); //Execute assembly mono_jit_exec(domain,assembly,0,argv); //Clean up mono_jit_cleanup(domain); return 0; }
最后一件事是运行客户端:
>client.exe
Contacting WebService...
Web service answer: Hello (from the Web service)
我们刚刚运行了一个连接到 Web 服务的 C 客户端!
麻烦
每个编程框架的基本功能之一是能够调试在其上运行的程序集。正常情况下,将检查两个功能:跟踪和调试。
跟踪
跟踪会在程序集到达执行的特定点时输出日志。可以使用 -trace 选项在运行时跟踪 MONO 程序集。让我们尝试跟踪上一个 exposeWS.exe 程序集使用的 hello.cs 代理中对 getHello
方法的调用。
> mono –trace=’M:helloWS.hello:getHello’ exposeWS.exe
Contacting WebService...
EXCEPTION handling: FormatException
EXCEPTION handling: FormatException
EXCEPTION handling: FormatException
EXCEPTION handling: FormatException
Web service answer: Hello (from the Web Service)
ENTER: helloWS.hello:getHello ()
(this:0x80d8f00[helloWS.hello exposeWS.exe], )
LEAVE: helloWS.hello:getHello ()
[STRING:0x83bf9b0:Hello (from the Web Service)]
请注意,在 mono 命令行中添加了 -trace=’M:helloWS.hello:getHello’ 开关。该开关指定将跟踪位于 helloWS
命名空间中的 hello
类中的 getHello
方法。已记录一些异常——输出了四行,如 EXCEPTION handling: FormatException。让我们尝试检查这些异常是在何处抛出的。为此,我们将跟踪对 helloWS
命名空间的所有调用。
> mono –trace=’N:helloWS’ exposeWS.exe
Contacting WebService...
ENTER: (wrapper remoting-invoke-with-check)
helloWS.hello:.ctor ()
(this:0x80d8f00[helloWS.hello exposeWS.exe], )
. ENTER: helloWS.hello:.ctor ()
(this:0x80d8f00[helloWS.hello exposeWS.exe], )
EXCEPTION handling: FormatException
EXCEPTION handling: FormatException
EXCEPTION handling: FormatException
EXCEPTION handling: FormatException
Web service answer: Hello (from the Web Service)
. LEAVE: helloWS.hello:.ctor ()
LEAVE: (wrapper remoting-invoke-with-check)
helloWS.hello:.ctor ()
ENTER: helloWS.hello:getHello ()
(this:0x80d8f00[helloWS.hello exposeWS.exe], )
LEAVE: helloWS.hello:getHello ()
[STRING:0x83c1a00:Hello (from the Web Service)]
Contacting WebService...
...
LEAVE: helloWS.hello:getHello ()
[STRING:0x83bf9b0:Hello (from the Web Service)]
请注意,在 mcs 命令行中添加了 -trace=’N:helloWS’ 开关。正如所见,异常是在 hello
类构造函数中抛出的。由于它不做任何特别的事情(图 9),我们可以怀疑异常是在超类构造函数中的某个地方抛出的。让我们首先尝试跟踪整个命名空间。
图 9. hello 类构造函数
public class hello :
System.Web.Services.Protocols.SoapHttpClientProtocol {
public hello () {
this.Url = "https://:8080/hello.asmx";
}
}
> mono –trace=’N: System.Web.Services.Protocols’ exposeWS.exe
Contacting WebService...
ENTER: System.Web.Services.Protocols.SoapHttpClientProtocol:
.ctor ()(this:0x80d8f00[helloWS.hello exposeWS.exe], )
. ENTER: System.Web.Services.Protocols.HttpWebClientProtocol:
.ctor ()(this:0x80d8f00[helloWS.hello exposeWS.exe], )
...
. . . . . . ENTER: System.Web.Services.Protocols.SoapExtension:
InitializeGlobalExtensions ()()
EXCEPTION handling: FormatException
EXCEPTION handling: FormatException
EXCEPTION handling: FormatException
EXCEPTION handling: FormatException
. . . . . . LEAVE: System.Web.Services.Protocols.SoapExtension:
InitializeGlobalEx
可以看出,异常发生在 System.Web.Services.Protocols.SoapExtension
类的方法 InitializeGlobalExtensions()
中。那么——这是一个 bug 吗?实际上不是——正如日志记录器输出的那样——发生的是异常处理,而不是异常本身。抛出了异常,但也捕获(并处理)了。
通过调用:mono –help-trace 可以访问跟踪选项的完整列表。
还有一点值得在此指出,那就是所呈现的代码根本不包含任何 TRACE
方法。输出是基于操作堆栈生成的。
跟踪开关
另一个值得提及的特性,在 MONO 和 .NET 中都常见,是跟踪开关。这个想法很清楚:通过在外部配置文件中设置适当的标志,可以打开或关闭输出跟踪信息的生成。这可以用于已部署的应用程序在未来某个时候需要调查执行过程中实际发生的情况。可以设置一个外部标志,收集跟踪日志,然后——再次取消设置(并且不再输出任何日志)。
让我们更新 exposeWS.cs,如列表 10 所示。首先,创建了一个 BooleanSwitch
并命名为 infoWS
。因为必须指定跟踪的输出,所以设置了一个跟踪侦听器来使用默认控制台。此外,添加了几条 Trace.Writeif
指令来阐明执行过程。仅当开关 sw
启用时,它们才会将值输出到标准控制台。
图 10. 带有跟踪开关的 C# 应用程序
using System;
using System.Diagnostics;
class helloStub{
private static BooleanSwitch sw =
new BooleanSwitch("infoSW", "Info trace:");
static void Main(){
Trace.Listeners.Add(
new TextWriterTraceListener(Console.Out));
Console.WriteLine("Contacting WebService...",);
Trace.WriteIf(
sw.Enabled, "Instantializing Web service proxy...");
helloWS.hello helloService = new helloWS.hello();
Trace.WriteIf(
sw.Enabled, "OK\r\nQuerying for hello string...");
String answer = helloService.getHello();
Trace.WriteIf(
sw.Enabled, "OK\r\nDisplaying hello string...\r\n");
Console.WriteLine("Web service answer: {0}", answer);
Trace.WriteIf(infoSW.Enabled, "OK\r\nLeaving...");
}
如前所述,此处将需要一个外部配置文件(exposeWS.exe.config;列表 11)。
配置文件定义了一个名为 infoWS
的开关,并将其值设置为 0(请注意 BooleanSwitch
构造函数参数与配置文件中的 add
元素 name
属性之间的相似性)。配置文件必须与程序集存储在同一目录中。此外,配置文件必须命名为:<程序集名称和扩展名><.config>。 例如,对于编译为 exposeWS.exe 的 exposeWS.cs,配置文件必须命名为 exposeWS.exe.config。
让我们尝试编译:
>mcs –r:System.Web.Services.dll –d:TRACE exposeWS.cs hello.cs
请注意,-d:TRACE 选项已添加到 mcs 命令行。这是因为默认情况下,跟踪指令会从代码中删除。-d 选项将阻止此操作。
通过调用运行:mono exposeWS.exe
有什么变化吗?当然没有——我们将 infoSW
的值指定为 0(false)。让我们尝试将其值设置为 1。
通过使用 mono exposeWS.exe 执行应用程序,我们将看到 Trace.WriteIf()
调用添加的几行额外输出。
图 11. 包含跟踪开关定义的应用程序配置文件
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.diagnostics>
<switches>
<add name="infoSW" value="0"/>
</switches>
</system.diagnostics>
</configuration>
跟踪开关
跟踪开关可以有两种类型:
BooleanSwitch
,可以设置为true
或false
。TraceSwitch
,可以设置为以下之一:0
——关闭,不输出任何跟踪信息。TraceError
(1)TraceWarning
(2)TraceInfo
(3)TraceVerbose
(4)——输出所有跟踪信息。
调试
好的,现在我们对跟踪和跟踪开关有了足够的了解,但是关于调试呢?此时,我恐怕不得不让所有开始成为潜在 MONO 狂热者的人失望。根据 MONO 手册,为了在调试模式下运行程序集,必须先用--g 选项进行编译,该选项生成包含调试器符号的 *.gdb 文件。在 Windows 上,这种尝试会导致找不到 Mono.CSharp.Debugger.dll 的错误。这被记录为已知 bug 之一,并有望在后续版本中修复。
MONO 1.0(也包括 MONO 1.0.1 或 MONO 1.0.2)的 Linux 版 mcs 也无法生成调试器符号。输出的程序集比没有符号编译的程序集大,但这实际上是唯一发生的情况。
MONO 团队承诺在 2004 年初提供调试器。我将随时向您通报这项工作的进展。
MONO 调试器
对于这些问题有一些变通方法(例如,在 Linux 下使用 gdb),但我只关注现有软件包。
Martin Baulig 发起了一项有趣的倡议。他编写了一个调试器,可以从 这里下载。当前版本是 0.9。发布的最后一个 IDE 版本是 0.4;下一个版本将与 MonoDevelop 集成。
生命的意义
基于本文列出的考虑,可以得出几个结论:
- MONO 提供了一个可用于开发应用程序的框架。它支持应用程序的编译和执行。
- MONO Framework 已在多个 Unix/Linux 平台上实现,这使得程序集真正可移植。MONO Framework 包适用于 Windows、SuSe、RedHat、Debian、Mac OS 和 OS/2。
- 不幸的是,MONO 没有完全实现 CLI 功能,这使得选择 MONO 作为开发平台成为一个问题。
- Windows Forms 尚未完全实现(不稳定)——应改用 GTK#。
- 缺少代码访问安全性。
- 如果使用,.NET 和 MONO 应用程序(在 MONO 下编译)的通用命名空间可以在 .NET 或 MONO Framework(仍然在 Linux 下运行)下执行。
System.Data
也未完全移植,特别是用于与 Microsoft SQL Server 数据库通信的SQLClient
。- 对程序集调试的支持非常糟糕。实际上——目前——仅限于跟踪。我们必须等待后续版本。
- C# 完全受 MONO Framework 支持。VB.NET 被标记为不稳定。
- Glade 可用于构建由外部 XML 文件定义的 GUI。
- 支持大量的商业和开源数据库引擎。
尽管如此,我们仍需考虑到 MONO 尚在开发中。在需要可移植性的地方,它是一个有前途的替代方案。然而,当前版本仅适用于较小的项目。
MONO 组件
以下列表显示了各个模块的状态:
稳定
Commons.RelaxNG
Cscompmgd
Mono.Data
Mono.Data.Tds
Mono.Posix
Mono.Security
Mono.Security.Win32
System.Web
System.Configuration.Install
System.Data
System.Data.OracleClient
System.DirectoryServices
系统
System.Drawing
System.Runtime.Remoting
System.Security
System.Web.Services
System.XML
不稳定
- Accessibility
Mono.Cairo
Mono.CSharp.Debugger
Mono.Data.DB2Client
Mono.Data.SqlLite
Mono.Data.SybaseClient
Mono.GetOptions
System.Web.Mobile
System.Design
System.Drawing.Design
System.Windows.Forms
Formatters.Soap
Mono.Data.TdsClient
(旧的 Sybase 和 MS SQL)
缺失
System.EnterpriseServices
System.Management
System.Messaging
System.ServiceProcess
System.Web.RegularExpressions
包含的第三方程序集
- ByteFX.Data
- Npgsql
- PEAPI
- SharpZipLib。
- IKVM.NET 的 Java 集成
语言
稳定
- C#
不稳定
- VB.NET
尚未准备好
- JScript