1.1 什么是 .NET?
很难用一句话来讲清楚。根据 Microsoft 的说法,.NET 是一个“革命性的新平台,建立在开放的 Internet 协议和标准之上,通过工具和服务将计算和通讯以崭新的方式融合到一起” 。
更为实际的定义是:.NET 是一个开发和运行软件的新环境,便于开发基于 Web 的服务,拥有丰富的运行库服务以支持用多种编程语言编写的组件,具有跨语言和跨平台的互操作能力。
注意,本文中使用术语“.NET”时,它仅指新的 .NET 运行库及其相关技术。有时我们也称其为“.NET 框架”。本文不包括其它 Microsoft 正在往上添加 .NET 名字的任何现有的产品和技术 (例如 SQL Server.NET)。
1.2 .NET 只是 Windows DNA 的一个新名字吗?
不。在很多地方,Windows DNA 仅仅是指使用现有技术的一种途径(即所谓的三阶式途径)的市场术语。.NET 更为急进,并且包括一个完整的软件开发和运行库框架。
1.3 .NET 只适用于建立 Web 网站吗?
不。如果你编写任何 Windows 软件 (使用 ATL/COM、MFC、VB 甚至 Win32 裸接口),.NET 都可能为你正在做的事情提供可行的选择 (或补充)。当然,如果你就是在开发 Web 网站,.NET 有很多令你感兴趣的东西—不仅仅是 ASP+。
1.4 .NET 是在什么时候宣布的?
在 2000 年 6 月 22 日举行的 Forum 2000 论坛上,Bill Gates 做了一次 演说,勾画了 .NET 的“前景”。2000 年 7 月的 PDC 会议上针对 .NET 技术做了很多会谈,会谈代表得到了包含 .NET Framework/SDK 和 Visual Studio 7 预览版的光盘。
1.5 .NET 将在何时发布?
预计是在 2001 年的下半年。
1.6 如何开发 .NET 应用程序?
.NET Framework SDK 包含可用于建立 .NET 应用程序的命令行编译器和实用程序。Visual Studio 的下一版本 (称为 Visual Studio 7 或 Visual Studio.NET) 将完全集成对 .NET 开发的支持。
1.7 可以从哪里下载 .NET SDK 和 Visual Studio 7?
从 http://msdn.microsoft.com/net 可以下载 SDK 的 Beta 1 版。如果你是 MSDN Universal 订户,你还可以下载 Visual Studio 7 的 Beta 1 版。
1.8 .NET 中的关键技术是什么?
ASP.NET、CLR (Common Language Runtime—通用语言运行库)、C# (新一代的类-Java 语言)、SOAP、XML、ADO.NET、多语言支持 (Eiffel、COBOL 等等)
1.9 .NET 框架将运行在什么平台上?
Beta 1 支持在 Windows 2000、NT4 SP6a, Windows Me 和 Windows 98 上进行开发。Windows 95 支持运行库。
Microsoft 将按照和 .NET 运行库相似的时间表发布一个新版本 Windows。它的代号是“Whistler”,在很大程度上是对 Windows 2000 的扩充性更新,对 GUI 有重要的改变。Microsoft 将以“.NET-enabled”作为新操作系统的卖点,但看起来它没有和 .NET 运行库绑在一起。如果 .NET 运行库能及时完成,它将包含在 Whistler 之内;否则,Whistler 将单独发货。
1.10 .NET 框架支持什么语言?
开始 Microsoft 将提供 C#、C++、VB 和 JScript 编译器。其它供应商宣布他们有意开发像 COBOL、Eiffel、Perl、Smalltalk 和 Python 等语言的 .NET 编译器。
1.11 .NET 框架符合标准化趋势吗?
C# 以及称为“通用语言基础结构”的一些东西的推荐标准草案已经提交给了 ECMA。参见 http://msdn.microsoft.com/net/ecma/
2. 基本术语
2.1 什么是 CLR?
CLR = Common Language Runtime—通用语言运行库。CLR 是一组标准资源集合,无论编程语言是什么,所有 (理论上) .NET 程序都能从中获益。Robert Schmidt (Microsoft) 在他的 MSDN PDC# 文章 中列出了以下 CLR 资源:
面向对象的编程模型 (继承、多态、异常处理、垃圾收集)
安全模型
类型系统
所有的 .NET 基础类
许多 .NET 框架类
开发、调试和测评工具
运行和代码管理
IL-机器语言 转换器和优化器
这些的含义是,在 .NET 世界里,不同的编程语言将在能力上比过去任何时候都更平等,虽然显然不是所有语言都支持所有 CLR 服务。
2.2 什么是 CTS?
CTS = Common Type System—通用类型系统。它是指 .NET 运行库所理解、并且随后 .NET 应用程序可以使用的一系列类型。然而,注意不是所有的 .NET 语言都将支持 CTS 中的所有类型。CTS 是 CLS 的超集。
2.3 什么是 CLS?
CLS = Common Language Specification—通用语言规范。它是预计所有 .NET 语言都支持的一个 CTS 的子集。这一思想是让使用 CLS-相容类型的任何程序和以任何语言编写的 .NET 程序可以互相操作。
理论上它能允许在不同的 .NET 语言之间有紧密的互操作性—例如允许从一个 VB 类里继承一个 C# 类。
2.4 什么是 IL?
IL = Intermediate Language—中间语言。又称为 MSIL。所有 .NET 源代码 (使用任何语言) 被编译为 IL。然后在软件的安装点上或者运行时,IL 由即时 (JIT) 编译器转换为机器码。
2.5 什么是 C#?
C# 是在 .NET 框架中运行的一种新语言。在他们的“C# 简介”白皮书中,Microsoft 这样描述 C#:
“C# 是从 C 和 C++ 派生出来的一种简单的、面向对象的、并且是类型安全的现代编程语言。C# (发音为‘C sharp’) 牢固地根植于在 C 和 C++ 家族之树,将很快为 C 和 C++ 程序员所熟悉。C# 帮助开发者将 Visual Basic 的高生产率和 C++ 的直接控制能力结合起来。”
将以上引言中的“C#”换成“Java”,你会发现这句陈述依然很正确 :)。
假如你是一位 C++ 程序员,你可能想看看我的 C# FAQ。
2.6 在 .NET 范畴里,“被管理”是什么含义?
术语“被管理”导致了很多误解。在 .NET 里的不同地方都使用了它,分别指相互差别不大的不同东西。
被管理代码:.NET 框架为运行在其上的程序提供了几个核心的运行服务—例如异常处理和安全性。为使这些服务能工作,代码必须提供运行时的最低程度的一些信息。这样的代码被称为被管理代码。默认情况下,所有 C#、Visual Basic.NET 和 JScript.NET 代码都是被管理代码。如不指明,VS7 C++ 代码不是被管理代码,但能通过一个命令行开关 (/com+) 使编译器产生被管理代码。
被管理数据:是指由 .NET 运行库的垃圾收集器分配和回收的数据。C#、VB.NET 和 JScript.NET 数据总是被管理的。即使使用了 /com+ 开关,默认情况下 VS7 C++ 数据也不是被管理的,但可以使用 __gc 关键字将其指定为被管理数据。
被管理类:通常在 C++ 的 Managed Extensions (ME) 范畴中涉及。使用 ME C++ 时,可以用 __gc 关键字将其指定为被管理的。名副其实,该类的实例所占用的内存由垃圾收集器管理,但还不止如此。该类还成为了完全的 .NET 团体的成员,同时带来了好处和限制。好处之一是获得了与其它语言编写的类之间的互操作性—例如,一个被管理 C++ 类可以继承 VB 类。限制之一是被管理类只能继承一个基类。
2.7 什么是映像?
所有的 .NET 编译器都产生关于它们所产生的模块中的类型定义的特殊数据。这些特殊数据同模块封装在一起 (随后模块被封装到元件中),可以通过称为映像 的机制来访问。System.Reflection 命名空间中包含向模块或元件询问其类型的类。
使用映像来访问 .NET 的特殊数据同使用 ITypeLib/ITypeInfo 来访问 COM 中的类型库数据非常相似,而且使用的目的也很相似—例如确定数据类型大小,以便在上下文、进程、机器的边界间调度它们。
映像还可以被用来动态调用方法 (参见 System.Type.InvokeMember),甚至在运行时动态创建类型 (参见 System.Reflection.Emit.TypeBuilder )。
3. 元件
3.1 什么是元件?
元件有时被描述为一个逻辑上的 .EXE 或 .DLL,它可以是任何一个应用程序 (有一个主入口点) 或库。一个元件由一个或多个文件组成 (dll、exe、html 文件等等),表示一组资源、类型定义以及这些类型的实现。一个元件也可以包含对其它元件的引用。这些资源、类型和引用在称为清单的一个数据块中描述。清单是元件的一部分,这样一来元件就是自描述的。
元件的一个重要方面使他们是一个类型的唯一标志的一部分。类型的唯一标志是将它所在的元件和类型名组合在一起得到的。这就是说,例如,如果元件 A 输出了一个称为 T 的类型,同时元件 B 输出了一个也称为 T 的类型,.NET 运行库将它们视为完全不同的两个类型。此外,不要混淆元件和命名空间—命名空间仅仅是组织类型名字的一种层次化方法。对于运行库,不论使用哪一个命名空间来组织名字,类型名就是类型名。从运行库来看,是元件加上类型名 (无论类型名属于哪个命名空间) 唯一地标识出一个类型。
元件在 .NET 的安全方面也很重要—许多安全限制是在元件的边界上实施的。
最后,元件是 .NET 中版本控制的单元—详情见下文。
3.2 怎样创建元件?
创建元件最简单的方法是直接使用 .NET 编译器。例如,以下 C# 程序:
public class CTest
{
public CTest()
{
System.Console.WriteLine( "Hello from CTest" );
}
}
能用以下方法编译为一个库元件 (dll):
csc /t:library ctest.cs
通过运行 .NET SDK 所带的“IL 反汇编”工具,你能看到元件的内容。
另外你也能把你的源代码编译成模块,然后使用元件连接器 (al.exe) 将模块组合成一个元件。对 C# 编译器,/target:module 开关可以指定产生模块而不是元件。
3.3 私有元件和共享元件有什么不同?
空间分配和可见性:私有元件通常由一个应用程序使用,被存储到这个应用程序的目录或其下的子目录之下。共享元件通常存储到全局的元件缓冲区中,这里是 .NET 运行库维护的元件的储藏所。共享元件通常是许多应用程序都要用到的代码库,例如 .NET 框架类。
版本控制:运行库只对共享元件实施版本约束,而不对私有元件实施。
3.4 元件如何相互找到?
通过寻找目录路径。有几个因素会影响路径 (比如 AppDomain 宿主、应用程序配置文件等),但对于私有元件,搜索路径通常是应用程序的目录及其子目录。对于共享元件,搜索路径通常和私有元件的一样,再加上共享元件缓冲区。
3.5 元件版本如何起作用?
每个元件由一个称为兼容性版本的版本号。同样,对元件的引用 (从另一个元件) 包括被引用元件的名称和版本。
版本号有四个数字部分 (例如 5.5.2.33)。前两部分不相同的元件被视为不兼容的。如果前两部分相同,但第三部分不同,元件被认为“可能兼容”。如果仅仅第四部分不同,则元件被视为是兼容的。然而,这只是默认的指导方针—是 版本策略决定施用这些规则的范围。版本策略可以在应用程序配置文件中指定。
记住:版本控制仅仅针对于共享元件,而不对私有元件。
4. 应用程序域
4.1 什么是应用程序域?
应用程序域 (AppDomain) 可以被看作一个轻型的进程。在一个 Win32 进程中可以存在多个 AppDomain。AppDomain 的主要目的是将应用程序和其它应用程序隔离开来。
通过使用独立的地址空间,Win32 进程提供隔离性。这种方法很有效,但开销很大并且伸缩性不好。.NET 运行库通过控制对内存的是用来施加 AppDomain 隔离—AppDomain 中的所有内存是由 .NET 运行库来管理的,所以运行库可以确保 AppDomain 之间不能访问彼此的内存。
4.2 如何创建 AppDomain?
AppDomains 通常有宿主创建。宿主包括 Windows Shell、ASP+ 和 IE。当你从命令行运行一个 .NET 应用程序时,宿主是 Shell。Shell 为每个应用程序创建一个新的 AppDomain。
AppDomains 也可以由 .NET 应用程序来显式创建。这里是一个创建 AppDomain 的一个 C# 例子,它创建对象的一个实例,并随后执行对象的一个方法:
using System;
using System.Runtime.Remoting;
public class CAppDomainInfo : MarshalByRefObject
{
public string GetAppDomainInfo()
{
return "AppDomain = " + AppDomain.CurrentDomain.FriendlyName;
}
}
public class App
{
public static int Main()
{
AppDomain ad = AppDomain.CreateDomain( "Andy's new domain", null, null );
ObjectHandle oh = ad.CreateInstance( "appdomaintest.exe", "CAppDomainInfo" );
CAppDomainInfo adInfo = (CAppDomainInfo)(oh.Unwrap());
string info = adInfo.GetAppDomainInfo();
Console.WriteLine( "AppDomain info: " + info );
return 0;
}
}
4.3 我能编写自己的 .NET 宿主吗?
能。关于怎样来做的例子,看看 Jason Whittington 和 Don Box 开发的 dm.net moniker 的源代码 (http://staff.develop.com/jasonw/clr/readme.htm)。在 .NET SDK 中也有一个叫作 CorHost 的代码示例。
5. 垃圾收集
5.1 什么是垃圾收集?
垃圾收集是一个系统,运行库组件通过它来管理对象的生存周期和它们占用的堆内存。对 .NET 而言它并不是一个新概念—Java 和许多其它语言/运行库使用垃圾收集已经有一段时间了。
5.2 对对象的最后一个引用撤销后,它并不一定立即被破坏,对吗?
是的。垃圾收集器并不提供销毁对象并是放其内存的时间保证。
关于 C# 中隐含的非确定化对象析构,Chris Sells 有一个令人感兴趣的线索:http://discuss.develop.com/archives/wa.exe?A2=ind0007&L=DOTNET&P=R24819
2000 年 10 月,Microsoft 的 Brian Harry 贴出了一个针对这个问题的很长的分析:http://discuss.develop.com/archives/wa.exe?A2=ind0010A&L=DOTNET&P=R28572
Chris Sells 对 Brian 贴子的答复在这里:http://discuss.develop.com/archives/wa.exe?A2=ind0010C&L=DOTNET&P=R983
5.3 .NET 为什么不提供确定化的析构?
因为垃圾收集算法。.NET 的垃圾收集器通过周期地扫描应用程序正在使用的所有对象的列表来工作。扫描过程中所有未被发现的对象就可以被销毁并释放内存。当对对象的最后一个引用撤销后,算法的这种实现使运行库不能立即得到通知—它只能在下一次清理堆时发现。
而且,这种算法尽可能少地进行垃圾收集,以便工作得最有效率。通常,堆容量的消耗会触发收集过程。
5.4 在 .NET 中缺少确定化的析构有问题吗?
这确实会影响组件的设计。如果你的对象需要昂贵或紧缺的资源 (例如对数据库的锁定),你需要提供某种方法让客户端在工作完成后能告诉对象以释放资源。Microsoft 建议,为此目的你应提供一个称为 Dispose () 的方法。然而,这样会在分布式对象中引起问题—在一个分布式系统中由谁来调用 Dispose () 方法?需要有某种形式的引用-计数机制或所有者管理机制来处理分布式对象—不幸的是运行库对此爱莫能助。
5.5 确定化的析构是否影响在被管理代码中使用 COM 对象?
是的。从被管理代码中使用 COM 对象时,你实际上是依赖垃圾收集器来最终释放你的对象。如果你的 COM 对象占有昂贵的资源且只能在最终释放对象后才能释放,你可能需要在你的对象上提供一个新接口以支持显式的 Dispose () 方法。
5.6 我听说应该避免使用 Finalize 方法,那么是否应该在我的类理实现 Finalize?
对垃圾收集器而言,拥有 Finalize 方法的对象比没有此方法的对象需要做更多的工作。同时也不保证对象 Finalized 的次序,所以对于从 Finalized 方法访问其它对象有不同的看法。最后,不能保证 Finalized 方法一定能被调用。所以,永远不应该依赖它来清理对象的资源。
Microsoft 建议使用以下方式:
public class CTest
{
public override void Dispose()
{
... // Cleanup activities
GC.SuppressFinalize(this);
}
protected override void Finalize()
{
Dispose();
}
}
一般情况下客户端调用 Dispose (),对象的资源被释放,并且通过调用 SuppressFinalize (),垃圾收集器被免除了对它进行 Finalize 的义务。在最不利的情况下,即客户端忘记了调用 Dispose (),有很大的机会通过垃圾收集器调用 Finalize () 来最终释放对象的资源。由于垃圾收集算法的缺陷,这看起来像是相当合理的处理办法了。
5.7 我有控制垃圾收集算法的手段吗?
有一点。System.GC 类提供了一对有趣的方法。第一个是 Collect 方法—它强制垃圾收集器立即收集所有未被引用的对象。另一个是 RequestFinalizeOnShutdown (),它告诉垃圾收集器在应用程序关闭时一定要对每个对象运行 Finalize () 方法。在应用程序关闭时,垃圾收集器一般优先选择快速的推出方式而不是调用 Finzlize (),所以这个方法能手工强制运行库多负一点责任。
如果你想验证这不仅仅是理论上的说法,是一十下面的测试程序:
using System;
class CTest
{
protected override void Finalize()
{
Console.WriteLine( "This is the Finalizer." );
}
}
class CApplication
{
public static void Main()
{
Console.WriteLine( "This is Main." );
CTest test = new CTest();
// GC.RequestFinalizeOnShutdown();
}
}
运行此程序,然后再去掉 GC.RequestFinalizeOnShutdown() 这一行前面的注释标记并重新运行,注意有什么不同……
5.8 我怎么知道垃圾收集器在做什么?
.NET 运行库中很多令人感兴趣的统计通过 'COM+ Memory' 性能对象输出。使用 Performance Monitor 查看它们。
6. 属性
6.1 什么是属性?
最少有两种类型的 .NET 属性。第一类我称其为 metadata 属性—它允许将某些数据附加到类或方法上。这些数据称为类的 metadata 的一部分,并且可以像类的其它 metadata 一样通过映射来访问。metadata 的另一种属性是 [serializable],将它附加到类上表示类的实例可以被串行化。
[serializable] public class CTest {}
另一种类型的属性是上下文属性。上下文类型的属性使用和 metadata 相似的语法,但实际上它们是不同的。上下文类型属性提供一种解释机制,通过这种机制,实例的活动和方法调用可以是预先处理和/或随后处理的。如果你了解 Keith Brown 的通用委托器你可能熟悉这种思想。
6.2 我能创建自己的 metadata 属性吗?
是的。简单地从 System.Attribute 导出一个类并将其标记为 AttributeUsage 属性。例如:
[AttributeUsage(AttributeTargets.Class)]
public class InspiredByAttribute : System.Attribute
{
public string InspiredBy;
public InspiredByAttribute( string inspiredBy )
{
InspiredBy = inspiredBy;
}
}
[InspiredBy("Andy Mc's brilliant .NET FAQ")]
class CTest
{
}
class CApp
{
public static void Main()
{
object[] atts = typeof(CTest).GetCustomAttributes();
foreach( object att in atts )
if( att is InspiredByAttribute )
Console.WriteLine( "Class CTest was inspired by {0}", _
((InspiredByAttribute)att).InspiredBy );
}
}
6.3 我能创建自己的 context 属性吗?
是的。看看 http://www.develop.com/dbox/dotnet/threshold/ 处的 Don Box 的例子 (叫作 CallThreshold) 和 http://www.razorsoft.net/ 处的 Perter Drayton 的 Tracehook.NET
7. 代码访问安全性
7.1 什么是代码访问安全性 (CAS)?
CAS 是 .NET 安全性模型的一部分,它确定一段代码是否允许被运行,以及当它运行是可以使用什么资源。例如,CAS 可以防止一个 .NET 的 Web applet 将你的硬盘格式化。
7.2 CAS 如何起作用?
CAS 安全策略设计两个关键概念—代码组和权限。每个 .NET 元件是特定 代码组的成员,并且每个代码组被授予由有名权限集所指定的权限。
例如,使用默认的安全策略时,一个从 Web 站点下载的控件属于“Zone - Internet”代码组,它保持由有名权限集“Internet”所定义的权限。(自然,有名权限集“Internet”表示一组受到严格限制的权限。)
7.3 谁定义 CAS 代码组?
Microsoft 定义了一些默认代码组,但你可以改变这些甚至创建你自己的代码组。要想看到你的系统中定义的代码组,可以从命令横行运行“caspol -lg”命令。再我的系统里它看起来像这些:
Level = Machine
Code Groups:
1. All code: Nothing
1.1. Zone - MyComputer: FullTrust
1.1.1. Honor SkipVerification requests: SkipVerification
1.2. Zone - Intranet: LocalIntranet
1.3. Zone - Internet: Internet
1.4. Zone - Untrusted: Nothing
1.5. Zone - Trusted: Internet
1.6. StrongName - 0024000004800000940000000602000000240000525341310004000003
000000CFCB3291AA715FE99D40D49040336F9056D7886FED46775BC7BB5430BA4444FEF8348EBD06
F962F39776AE4DC3B7B04A7FE6F49F25F740423EBF2C0B89698D8D08AC48D69CED0FC8F83B465E08
07AC11EC1DCC7D054E807A43336DDE408A5393A48556123272CEEEE72F1660B71927D38561AABF5C
AC1DF1734633C602F8F2D5: Everything
注意代码组的层次—顶层 ('All code') 是最通用的,它随后分为几个组,每个还可以再分。同时注意,和一般的想象不同,子组可以被赋予比它的上级更宽的权限集。
7.4 如何定义自己的代码组?
使用 caspol。例如,假定你信任来自 www.mydomain.com 的代码,并且希望它对你的系统拥有完全的访问权,但是希望对其它 Internet 站点保持默认的限制。要实现这些,你可以在“Zone - Internet”组中增加一个子组,就像下面那样:
caspol -ag 1.3 -site www.mydomain.com FullTrust
现在如果你运行 caspol -lg 就可以看到新的代码组被增加为 1.3.1 组:
...
1.3. Zone - Internet: Internet
1.3.1. Site - www.mydomain.com: FullTrust
...
注意数字标号 (1.3.1) 只是 caspol 编出来以便能从命令行方便地操纵代码组的。底层的运行库永远看不到它。
7.5 如何改变代码组的权限集?
使用 caspol。如果你是机器的管理员,你能在 'machine' 层次上操作—这不仅意味着你所做的改变将成为机器的默认设置,而且用户不能把权限改得更宽。如果你是一个普通用户 (不是管理员) 你仍然可以修改权限,但只能使它们变得更严格。例如,为使 intranet 代码能做它们想做的事,你可能需要这样:
caspol -cg 1.2 FullTrust
注意,因为 (在标准的系统里) 这比默认的安全策略权限更大,你应该在 machine 层次上做这些—在 user 层次上这样做不起作用。
7.6 能否创建自己的权限集?
是的。使用 caspol -ap,指定一个包含权限集中所有的权限的 XML 文件。这里 是一个指定 'Everything' 权限集的示例文件—修改它以适应你的需要,这样可以节省一些时间。修改完成后,用以下方法将它添加到可用的权限集中:
caspol -ap samplepermset.xml
然后,用以下方法将此权限集施加到一个代码组上:
caspol -cg 1.3 SamplePermSet
(默认情况下,1.3 是 'Internet' 代码组)
7.7 CAS 有问题时,如何诊断自己的程序?
caspol 有一组可能有用的选项。首先,使用 caspol -rsg,你能让 caspol 告诉你一个元件属于哪一个代码组。类似地,使用 caspol -rsp,你能询问在特定元件上施加了什么权限。
7.8 我受不了 CAS 带来的麻烦,能否关掉它?
是的,只要你是系统管理员。只要运行:
caspol -s off
8. 中间语言 (IL)
8.1 我能看到元件的中间语言吗?
是的。Microsoft 提供了一个称为 Ildasm 的工具,它可以用来查看元件的 metadata 和 IL。
8.2 能否通过反向工程从 IL 中获得源代码?
是的。相对而言,从 IL 来重新生成高级语言源代码 (例如 C#) 通常是很简单的。
8.3 如何防止别人通过反向工程获得我的代码?
目前唯一的办法是运行带有 /owner 选项的 ilasm。这样生成的元件的 IL 不能通过 ildasm 来查看。然而,意志坚定的代码破译者能够破解 ildasm 或者编写自己的 ildasm 版本,所以这种方法只能吓唬那些业余的破译者。
不幸的事,目前的 .NET 编译器没有 /owner 选项,所以要想保护你的 C# 或 VB.NET 元件,你需要像下面那样做:
csc helloworld.cs
ildasm /out=temp.il helloworld.exe
ilasm /owner temp.il
(这个建议是 Hany Ramadan 贴到 DOTNET 上的。)
看起来过一段时间能有 IL 加密工具 (无论来自 Microsoft 或第三方)。这些工具会以这样的方式来“优化” IL:使反向工程变得更困难。
当然,如果你是在编写 Web 服务,反向工程看起来就不再是一个问题,因为客户不能访问你的 IL。
8.4 我能直接用 IL 编程吗?
是的。Peter Drayton 在 DOTNET 邮件列表里贴出了这个简单的例子:
.assembly MyAssembly {}
.class MyApp {
.method static void Main() {
.entrypoint
ldstr "Hello, IL!"
call void System.Console::WriteLine(class System.Object)
ret
}
}
将其放入名为 hello.il 的文件中,然后运行 ilasm hello.il,将产生一个 exe 元件。
8.5 IL 能做到 C# 中做不到的事吗?
是的。一些简单的例子是:你能抛出不是从 SystemException 导出的异常,另外你能使用非以零起始的数组。
9. 关于 COM
9.1 COM 消亡了吗?
就像你在邮件列表中看到的那样,这个主题导致了激烈的争论。看看以下两个地方:
http://discuss.develop.com/archives/wa.exe?A2=ind0007&L=DOTNET&D=0&P=68241
http://discuss.develop.com/archives/wa.exe?A2=ind0007&L=DOTNET&P=R60761
我的理解是:COM 包含很多内容,并且对于不同的人而言它是不同的东西。但是对我来说,COM 基本上是关于一小段代码如何找到另一小段代码,以及当它们相互找到后该如何相互通讯。COM 准确地指明了这种定位和通讯该如何进行。在完全由 .NET 对象构成的“纯” .NET 世界里,小段代码依然相互寻找并相互交谈,但它们不使用 COM 来做这些。它们使用在某些地方和 COM 很相像的一种模型—例如,类型信息保存在和组件封装在一起的表单中,这和在 COM 组件中封装一个类型库十分相似。但它不是 COM。
所以,这里有什么问题吗?好吧,我确实不关心大多数 COM 消失了—我不关心寻找组件不再和注册表有关,我也不使用 IDL 来定义我的借口。但有一件东西我不希望它消失—我不希望失去基于接口的开发这种思想。照我看来,COM 最强大的力量是它坚持在接口和实现之间竖起铸铁般的隔墙。不幸的是,看来 .NET 不再那样坚持—它允许你做基于接口的开发,但它并不坚持。一些人可能会辩解说有一个选择总不会是坏事,可能他们是对的,但我不能不觉得这可能是一个退步。
9.2 DCOM 消亡了吗?
差不多是,尤其是对于 .NET 开发者。.NET 框架有一个不基于 DCOM 的新的远程模型。当然 DCOM 还会在互操作场合下使用。
9.3 MTS/COM+ 消亡了吗?
不。第一个 .NET 版本考虑的是提供对现有 COM+ 服务 (通过一个互操作层) 而不是使用 .NET 自己的服务来取代它们。很多工具和属性被用以实现尽可能平滑的过渡。.NET SDK 的 PDC 版本包括对核心服务 (JIT 活动、事务) 的支持,但不包括一些高层服务 (例如 COM+ 事件、队列化组件)。
在一段时间内看来,互操作性可以预期是无缝集成的—这意味着一些服务将成为 CLR 的一部分,并且/或者意味着一些服务将以可管理代码的形式重写并运行在 CLR 的顶层。
关于这个主题,参见 Joe Long 的贴子—Joe 是 Microsoft 的 COM+ 组的经理。从这里开始:
http://discuss.develop.com/archives/wa.exe?A2=ind0007&L=DOTNET&P=R68370
9.4 能在 .NET 中使用 COM 组件吗?
可以。可以通过 Runtime Callable Wrapper (RCW) 从 .NET 中访问 COM 组件。它通过将 COM 组件映射为与 .NET 兼容的接口来使 COM 接口可以被访问。对于 oldautomation 接口,可以自动地从一个类型库中产生。对于非 oleautomation 接口,可以开发一个定制的 RCW,以便手工地将 COM 接口的类型映射为与 .NET 兼容的类型。
对于熟悉 ATL 的读者,这里有一个简单的示例。首先,创建一个 ATL 组件以实现以下 IDL:
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(EA013F93-487A-4403-86EC-FD9FEE5E6206),
helpstring("ICppName Interface"),
pointer_default(unique),
oleautomation
]
interface ICppName : IUnknown
{
[helpstring("method SetName")] HRESULT SetName([in] BSTR name);
[helpstring("method GetName")] HRESULT GetName([out,retval] BSTR *pName );
};
[
uuid(F5E4C61D-D93A-4295-A4B4-2453D4A4484D),
version(1.0),
helpstring("cppcomserver 1.0 Type Library")
]
library CPPCOMSERVERLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(600CE6D9-5ED7-4B4D-BB49-E8D5D5096F70),
helpstring("CppName Class")
]
coclass CppName
{
[default] interface ICppName;
};
};
建立了组件以后,你会得到一个 typelibrary。在 typelibrary 上运行 TLBIMP 实用程序,就像这样:
tlbimp cppcomserver.tlb
如果成功,你会得到像这样的信息:
Typelib imported successfully to CPPCOMSERVERLib.dll
现在你需要一个 .NET 客户端—我们用 C# 创建一个包含以下代码的 .cs 文件:
using System;
using CPPCOMSERVERLib;
public class MainApp
{
static public void Main()
{
CppName cppname = new CppName();
cppname.SetName( "bob" );
Console.WriteLine( "Name is " + cppname.GetName() );
}
}
注意我们使用 typelibrary 的名字作为命名空间,COM 类的名字作为类名。我们也可以选择使用 CPPCOMSERVERLib.CppName 作为类名而且不需要语句 using CPPCOMSERVERLib。
像这样编译以上 C# 代码:
csc /r:cppcomserverlib.dll csharpcomclient.cs
注意,编译被告知,引用我们刚才用 TLBIMP 从 typelibrary 产生的 DLL。
现在你应该可以运行 csharpcomclient.exe,并从控制台得到如下输出:
Name is bob
9.5 能在 COM 中使用 .NET 组件吗?
可以。可以通过一个 COM Callable Wraper (CCW) 从 COM 中访问 .NET 组件。这和 RCW 很相似 (参见上一个问题),但以相反的方向工作。同样,如果它不能由 .NET 开发工具自动产生,或不想要自动产生的行为逻辑,可以开发一个定制的 CCW。为使 COM 可以“看见” .NET 组件,.NET 组件必须在注册表里注册。
这里是一个简单的例子。创建一个名为 testcomserver.cs 的 C# 文件并输入下面的代码:
using System;
namespace AndyMc
{
public class CSharpCOMServer
{
public CSharpCOMServer() {}
public void SetName( string name ) { m_name = name; }
public string GetName() { return m_name; }
private string m_name;
}
}
然后编译 .cs 文件:
csc /target:library testcomserver.cs
你会得到一个 dll,这样将它注册:
regasm testcomserver.dll /tlb:testcomserver.tlb
现在你需要创建一个客户端程序来测试你的 .NET COM 组件。VBScript 可以—将以下内容放到一个名为 comclient.vbs 的文件中:
Dim dotNetObj
Set dotNetObj = CreateObject("AndyMc.CSharpCOMServer")
dotNetObj.SetName ("bob")
MsgBox "Name is " & dotNetObj.GetName()
运行此脚本:
wscript comclient.vbs
嘿!你得到一个显示文本“Name is bob”的消息框。
(注意,编写此程序时,看起来可以通过几种路径将 .NET 类作为 COM 组件访问—为了避免问题,在 testcomserver.dll 相同的目录下运行 comclient.vbs。
一种替代的方法是使用 Jason Whittington 和 Don Box 开发的 dm.net moniker。到这里 http://staff.develop.com/jasonw/clr/readme.htm 查看。
9.6 在 .NET 的世界中 ATL 是多余的吗?
是的。如果你在编写 .NET 框架内的应用程序。当然许多开发者希望继续使用 ATL 来编写 .NET 框架以外的 C++ COM 组件,但当你在 .NET 框架内时你差不多总是希望使用 C#。在 .NET 世界里,原始的 C++ (以及基于它的 ATL) 并没有太多的地位—它太直接了,并且提供了太多的适应性,以至于运行库不能管理它。
10. 杂项
10.1 .NET 的远程计算如何工作?
.NET 的远程计算涉及通过通道发送消息。两种标准的通道是 HTTP 和 TCP。仅仅在局域网上才倾向于使用 TCP—HTTP 能在局域网和广域网 (internet) 上使用。
现在提供了对多种消息串行化格式的支持,例如 SOAP (基于 XML) 和二进制格式。默认情况下,HTTP 通道使用 SOAP (通过 .NET 运行库的 Serialization SOAP Formatter),而 TCP 通道使用二进制格式 (通过 .NET 运行库的 Serialization Binary Formatter)。但每个通道可以使用任一串行化格式。
这里是远程访问的一些方式:
SingleCall。每个来自客户端的请求由一个新对象服务。当请求完成后对象被丢弃。可以在 ASP+ 环境中使用 ASP+ 国家服务来保存应用程序或会话的国家,从而使这种模型 (无国家之分的) 变成有国家支持的。
Singleton。所有来在客户端的请求由单一的服务器对象处理。
Client-activated object。这是老的有国家支持的 (D)COM 模型,这里客户端受到一个远端对象的引用并保留此引用 (以保持远端对象的生存),直到对它的访问完成。
对象的分布式垃圾收集由称为“基于租用的生命周期”管理。每个对象拥有一个租用时间,这个时间到达时,从 .NET 运行库的远程子结构断开对象。对象有默认的更新时间—从客户端发起的成功调用会更新租用时间。客户端也可以显示地更新租用时间。
如果你对使用 XML-RPC 来代替 SOAP,可以看看 Charles Cook 在 http://www.cookcomputing.com/xmlrpc/xmlrpc.shtml 的 XML-RPC.Net 站点。
10.2 如何在 .NET 程序中获得 Win32 API?
使用 P/Invoke。它使用了和 COM 互操作性相似的技术,但被用来访问静态 DLL 入口点而不是 COM 对象。以下是一个调用 Win32 MessageBox 函数的 C# 程序示例:
using System;
using System.Runtime.InteropServices;
class MainApp
{
[dllimport("user32.dll", EntryPoint="MessageBox", SetLastError=true, CharSet=CharSet.Auto)]
public static extern int MessageBox(int hWnd, String strMessage, String strCaption, uint uiType);
public static void Main()
{
MessageBox( 0, "Hello, this is PInvoke in operation!", ".NET", 0 );
}
}
11. 类库
11.1 文件 I/O
11.1.1 如何读文本文件?
首先,使用 System.IO.FileStream 对象打开文件:
FileStream fs = new FileStream( @"c:\test.txt", FileMode.Open, FileAccess.Read );
FileStream 继承于 Stream,所以你可以用一个 StreamReader 对象把 FileStream 对象包装起来。这样为一行一行地进行流处理提供了一个良好的界面:
StreamReader sr = new StreamReader( fs );
string curLine;
while( (curLine = sr.ReadLine()) != null )
Console.WriteLine( curLine );
最后关闭 StreamReader 对象:
sr.Close();
注意这样将自动地在底层 Stream 对象上调用 Close (),所以不必显示地执行 fs.Close()。
11.1.2 如何写文本文件?
和读文件的例子相似,只是把 StreamReader 换成 StreamWriter。
11.1.3 如何读写二进制文件?
和文本文件类似,只是要用 BinaryReader/Writer 对象而不是 StreamReader/Writer 来包装 FileStream 对象。
11.1.4 如何删除文件?
在 System.IO.File 对象上使用静态方法 Delete ():
File.Delete( @"c:\test.txt" );
11.2 文本处理
11.2.1 是否支持正规表达式?
是的。使用 System.Text.RegularExpressions.Regex 类。例如,以下代码更新 HTML 文件的标题:
FileStream fs = new FileStream( "test.htm", FileMode.Open, FileAccess.Read );
StreamReader sr = new StreamReader( fs );
Regex r = new Regex( "<TITLE>(.*)</TITLE>" );
string s;
while( (s = sr.ReadLine()) != null )
{
if( r.IsMatch( s ) )
s = r.Replace( s, "<TITLE>New and improved ${1}</TITLE>" );
Console.WriteLine( s );
}
11.3 Internet
11.3.1 如何下载网页?
首先使用 System.Net.WebRequestFactory 类来获得一个 WebRequest 对象:
WebRequest request = WebRequestFactory.Create( "http://localhost" );
然后请求应答:
WebResponse response = request.GetResponse();
GetResponse 方法被阻塞直到下载完成。然后你能像下面那样访问应答流:
Stream s = response.GetResponseStream();
// Output the downloaded stream to the console
StreamReader sr = new StreamReader( s );
string line;
while( (line = sr.ReadLine()) != null )
Console.WriteLine( line );
注意 WebRequest 和 WebReponse 对象分别向下兼容 HttpWebRequest 和 HttpWebReponse 对象,它们被用来访问和 http 相关的功能。
11.3.2 如何使用代理服务器 (proxy)?
两种—这样做以便影响所有 Web 请求:
System.Net.GlobalProxySelection.Select = new DefaultControlObject( "proxyname", 80 );
另外一种,要想对特定的 Web 请求设置代理服务,这样做:
ProxyData proxyData = new ProxyData();
proxyData.HostName = "proxyname";
proxyData.Port = 80;
proxyData.OverrideSelectProxy = true;
HttpWebRequest request = (HttpWebRequest)WebRequestFactory.Create( "http://localhost" );
request.Proxy = proxyData;
11.4 XML
11.4.1 是否支持 DOM?
是的。看看以下示例 XML文档:
<PEOPLE>
<PERSON>Fred</PERSON>
<PERSON>Bill</PERSON>
</PEOPLE>
可以这样处理此文档:
XmlDocument doc = new XmlDocument();
doc.Load( "test.xml" );
XmlNode root = doc.DocumentElement;
foreach( XmlNode personElement in root.ChildNodes )
Console.WriteLine( personElement.FirstChild.Value.ToString() );
输出为:
Fred
Bill
11.4.2 是否支持 SAX?
不。作为替换,提供了一个新的 XmlReader/XmlWriter API。像 SAX 一样,它是基于流的,但它使用“pull”模型而不是 SAX 的“push”模型。这是一个例子:
XmlTextReader reader = new XmlTextReader( "test.xml" );
while( reader.Read() )
{
if( reader.NodeType == XmlNodeType.Element && reader.Name == "PERSON" )
{
reader.Read(); // Skip to the child text
Console.WriteLine( reader.Value );
}
}
11.4.3 是否支持 XPath?
是的,通过 XmlNavigator 类 (DocumentNavigator 是从 XmlNavigator 导出的):
XmlDocument doc = new XmlDocument();
doc.Load( "test.xml" );
DocumentNavigator nav = new DocumentNavigator(doc);
nav.MoveToDocument();
nav.Select( "descendant::PEOPLE/PERSON" );
while( nav.MoveToNextSelected() )
{
nav.MoveToFirstChild();
Console.WriteLine( "{0}", nav.Value );
}
11.5 线程
11.5.1 是否支持多线程?
是的,对多线程有广泛的支持。系统能产生新线程,并提供应用程序可以使用的线程池。
11.5.2 如何产生一个线程?
创建 System.Threading.Thread 对象的一个实例,把将要在新线程中执行的 ThreadStart 示例传递给它。例如:
class MyThread
{
public MyThread( string initData )
{
m_data = initData;
m_thread = new Thread( new ThreadStart(ThreadMain) );
m_thread.Start();
}
// ThreadMain() is executed on the new thread.
private void ThreadMain()
{
Console.WriteLine( m_data );
}
public void WaitUntilFinished()
{
m_thread.Join();
}
private Thread m_thread;
private string m_data;
}
这里创建 MyThread 的一个实例就足以产生线程并执行 MyThread.ThreadMain () 方法:
MyThread t = new MyThread( "Hello, world." );
t.WaitUntilFinished();
11.5.3 如何停止一个线程?
有好几个办法。首先,你能使用自己的通讯机制告诉 ThreadStart 方法结束。另外 Thread 类有内置的支持来命令线程停止。基本的两个方法是 Thread.Interrupt () 和 Thread.Abort ()。前者导致抛出一个 ThreadInterruptedException 并随后进入 WaitJoinSleep 状态。换句话说,Thread.Interrupt 是一种礼貌的方式,它请求线程在不再进行任何有用的工作时自行停止的。与此相对应,Thread.Abort () 抛出一个 ThreadAbortException 而不管线程正在做什么。而且,ThreadAbortException 不能像通常的异常那样被捕获 (即使最终将执行 ThreadStart 的终止方法)。Thread.Abort () 是一般情况下不需要的非常手段。
11.5.4 怎样使用线程池?
通过向 ThreadPool.QueueUserWorkItem () 方法传递 WaitCallback 的一个实例:
class CApp
{
static void Main()
{
string s = "Hello, World";
ThreadPool.QueueUserWorkItem( new WaitCallback( DoWork ), s );
Thread.Sleep( 1000 ); // Give time for work item to be executed
}
// DoWork is executed on a thread from the thread pool.
static void DoWork( object state )
{
Console.WriteLine( state );
}
}
11.5.5 怎样知道我的线程池工作项目是在何时完成的?
没有方法询问线程池这类信息。你必须在 WaitCallback 方法中放置代码来发出信号以表明它已经完成。这里事件也很有用处。
11.5.6 怎样防止对数据的并发访问?
每个对象有一个与之相联的并发锁 (受批评的部分)。System.Threading.Monitor.Enter/Exit 方法用来获得和释放锁。例如,下面类的实例只允许一个线程同时进入方法 f ():
class C
{
public void f()
{
try
{
Monitor.Enter(this);
...
}
finally
{
Monitor.Exit(this);
}
}
}
C# 有一个关键字‘lock’提供了以上代码的简单形式:
class C
{
public void f()
{
lock(this)
{
...
}
}
}
注意,调用 Monitor.Enter (myObject) 并不意味着对 myObject 的所有访问都被串行化了。它意味着请求同 myObject 相联的同步锁,并且在调用 Monitor.Exit(o) 之前,没有任何其它线程可以请求该锁。换句话说,下面的类和以上给出的类在功能上是等同的:
class C
{
public void f()
{
lock( m_object )
{
...
}
}
private m_object = new object();
}
11.6 跟踪
11.6.1 有内置的跟踪/日志支持吗?
是的,在 System.Diagnostics 命名空间中。有两个处理跟踪的主要的类—Debug 和 Trace。它们以相似的方式工作—不同之处是 Debug 类中的跟踪只能在用 DEBUG 标志生成的代码中工作,而 Trace 类中的跟踪只能在指明了 TRACE 标记生成的代码中工作。典型地,这意味着你应该在你希望能在 debug 和 release 版本中都能跟踪时使用 System.Diagnostics.Trace.WriteLine,而在你希望只能在 debug 版本中能跟踪时使用 System.Diagnostics.Debug.WriteLine。
11.6.2 能否将跟踪输出重定向到一个文件?
是的。Debug 类和 Trace 类都有一个 Listeners 属性,它们分别收集你用 Debug.WriteLine 或 Trace.WriteLine 产生的输出。默认情况下 Listeners 只有一个收集槽,它是 DefaultTraceListener 类的一个实例。它将输出发送到 Win32 的 OutputDebugString () 函数和 System.Diagnostics.Debugger.Log () 方法。调试时这很有用,但如果你试图从客户站点跟踪一个问题,将输出重定向到一个文件中就更为恰当。幸运的是,为此目的提供了 TextWriterTraceListener 类。
这里是 TextWriterTraceListener 如何将 Trace 输出重定向到一个文件:
Trace.Listeners.Clear();
FileStream fs = new FileStream( @"c:\log.txt", FileMode.Create, FileAccess.Write );
Trace.Listeners.Add( new TextWriterTraceListener( fs ) );
Trace.WriteLine( @"This will be writen to c:\log.txt!" );
注意使用 Trace.Listeners.Clear () 去掉了默认的 listener。如果不这样做,输出将在文件和 OutputDebugString () 中同时产生。一般情况下你不希望如此,因为 OutputDebugString () 导致很大的性能开销。
11.6.3 能否定制跟踪的输出?
是的。你能编写你自己的 TraceListener 导出类,并把所有的输出重定向到它上面。这里有一个简单的例子,它从 TextWriterTraceListener 导出 (并随后内建了对写文件的支持) 并在每个输出行上添加时间信息和线程 ID:
class MyListener : TextWriterTraceListener
{
public MyListener( Stream s ) : base(s)
{
}
public override void WriteLine( string s )
{
Writer.WriteLine( "{0:D8} [{1:D4}] {2}",
Environment.TickCount - m_startTickCount,
AppDomain.GetCurrentThreadId(),
s );
}
protected int m_startTickCount = Environment.TickCount;
}
(注意这个实现并不完整—例如没有覆盖 TraceListener.Write 方法。)
这种方法的美妙之处在于,向 Trace.Listener 添加 MyListener 之后,所有对 Trace.WriteLine () 的调用都转向了 MyListener,包括从对 MyListener 一无所知的被引用元件发出的调用。
12. 资源
12.1 从哪里可以获得关于 .NET 的详情?
Microsoft .NET 主页位于 http://www.microsoft.com/net/。Microsoft 同时将它发布在 GOTDOTNET。
Microsoft 还发布了 .NET Framework FAQ,和本文很相似。可以在那里找到这里许多问题更“权威”的解答。
Robert Scoble 编辑了一个很容易理解的在线列表 http://www.devx.com/dotnet/resources/,这里还有一个 http://www.singularidad.com.ar/dotnet.asp。
在 http://www.devx.com/free/press/2000/vs-qalist.asp Robert 还有一个 .NET“著名问题与解答”主页。
Richard Grimes 和 Richard Anderson 有一个叫作 Managed World.COM.的站点。
http://www.ibuyspy.com/ 是一个以展示 .NET 平台为目的创建的示例站点。
还有我的 C# FAQ for C++ Programmers。
12.2 示例代码和实用程序
Peter Drayton 的 .NET Goodies 主页位于 http://www.razorsoft.net/
Don Box 的 CallThreshold 示例位于 http://www.develop.com/dbox/dotnet/threshold
Don 的 UnwindScope Service 位于 http://www.develop.com/dbox/dotnet/unwind
Don 的 CLR scripting host 位于 http://www.develop.com/dbox/dotnet/clrscript
Don 和 Jason 的 dm.net COM moniker 位于 http://staff.develop.com/jasonw/clr/readme.htm 在 http://www.bearcanyon.com/dotnet/ 有 Mike Woodring 的一些 .NET 例子。
在 http://www.cookcomputing.com/xmlrpc/xmlrpc.shtml 可以找到 Charles Cook 的 XML-RPC.Net library。
很难用一句话来讲清楚。根据 Microsoft 的说法,.NET 是一个“革命性的新平台,建立在开放的 Internet 协议和标准之上,通过工具和服务将计算和通讯以崭新的方式融合到一起” 。
更为实际的定义是:.NET 是一个开发和运行软件的新环境,便于开发基于 Web 的服务,拥有丰富的运行库服务以支持用多种编程语言编写的组件,具有跨语言和跨平台的互操作能力。
注意,本文中使用术语“.NET”时,它仅指新的 .NET 运行库及其相关技术。有时我们也称其为“.NET 框架”。本文不包括其它 Microsoft 正在往上添加 .NET 名字的任何现有的产品和技术 (例如 SQL Server.NET)。
1.2 .NET 只是 Windows DNA 的一个新名字吗?
不。在很多地方,Windows DNA 仅仅是指使用现有技术的一种途径(即所谓的三阶式途径)的市场术语。.NET 更为急进,并且包括一个完整的软件开发和运行库框架。
1.3 .NET 只适用于建立 Web 网站吗?
不。如果你编写任何 Windows 软件 (使用 ATL/COM、MFC、VB 甚至 Win32 裸接口),.NET 都可能为你正在做的事情提供可行的选择 (或补充)。当然,如果你就是在开发 Web 网站,.NET 有很多令你感兴趣的东西—不仅仅是 ASP+。
1.4 .NET 是在什么时候宣布的?
在 2000 年 6 月 22 日举行的 Forum 2000 论坛上,Bill Gates 做了一次 演说,勾画了 .NET 的“前景”。2000 年 7 月的 PDC 会议上针对 .NET 技术做了很多会谈,会谈代表得到了包含 .NET Framework/SDK 和 Visual Studio 7 预览版的光盘。
1.5 .NET 将在何时发布?
预计是在 2001 年的下半年。
1.6 如何开发 .NET 应用程序?
.NET Framework SDK 包含可用于建立 .NET 应用程序的命令行编译器和实用程序。Visual Studio 的下一版本 (称为 Visual Studio 7 或 Visual Studio.NET) 将完全集成对 .NET 开发的支持。
1.7 可以从哪里下载 .NET SDK 和 Visual Studio 7?
从 http://msdn.microsoft.com/net 可以下载 SDK 的 Beta 1 版。如果你是 MSDN Universal 订户,你还可以下载 Visual Studio 7 的 Beta 1 版。
1.8 .NET 中的关键技术是什么?
ASP.NET、CLR (Common Language Runtime—通用语言运行库)、C# (新一代的类-Java 语言)、SOAP、XML、ADO.NET、多语言支持 (Eiffel、COBOL 等等)
1.9 .NET 框架将运行在什么平台上?
Beta 1 支持在 Windows 2000、NT4 SP6a, Windows Me 和 Windows 98 上进行开发。Windows 95 支持运行库。
Microsoft 将按照和 .NET 运行库相似的时间表发布一个新版本 Windows。它的代号是“Whistler”,在很大程度上是对 Windows 2000 的扩充性更新,对 GUI 有重要的改变。Microsoft 将以“.NET-enabled”作为新操作系统的卖点,但看起来它没有和 .NET 运行库绑在一起。如果 .NET 运行库能及时完成,它将包含在 Whistler 之内;否则,Whistler 将单独发货。
1.10 .NET 框架支持什么语言?
开始 Microsoft 将提供 C#、C++、VB 和 JScript 编译器。其它供应商宣布他们有意开发像 COBOL、Eiffel、Perl、Smalltalk 和 Python 等语言的 .NET 编译器。
1.11 .NET 框架符合标准化趋势吗?
C# 以及称为“通用语言基础结构”的一些东西的推荐标准草案已经提交给了 ECMA。参见 http://msdn.microsoft.com/net/ecma/
2. 基本术语
2.1 什么是 CLR?
CLR = Common Language Runtime—通用语言运行库。CLR 是一组标准资源集合,无论编程语言是什么,所有 (理论上) .NET 程序都能从中获益。Robert Schmidt (Microsoft) 在他的 MSDN PDC# 文章 中列出了以下 CLR 资源:
面向对象的编程模型 (继承、多态、异常处理、垃圾收集)
安全模型
类型系统
所有的 .NET 基础类
许多 .NET 框架类
开发、调试和测评工具
运行和代码管理
IL-机器语言 转换器和优化器
这些的含义是,在 .NET 世界里,不同的编程语言将在能力上比过去任何时候都更平等,虽然显然不是所有语言都支持所有 CLR 服务。
2.2 什么是 CTS?
CTS = Common Type System—通用类型系统。它是指 .NET 运行库所理解、并且随后 .NET 应用程序可以使用的一系列类型。然而,注意不是所有的 .NET 语言都将支持 CTS 中的所有类型。CTS 是 CLS 的超集。
2.3 什么是 CLS?
CLS = Common Language Specification—通用语言规范。它是预计所有 .NET 语言都支持的一个 CTS 的子集。这一思想是让使用 CLS-相容类型的任何程序和以任何语言编写的 .NET 程序可以互相操作。
理论上它能允许在不同的 .NET 语言之间有紧密的互操作性—例如允许从一个 VB 类里继承一个 C# 类。
2.4 什么是 IL?
IL = Intermediate Language—中间语言。又称为 MSIL。所有 .NET 源代码 (使用任何语言) 被编译为 IL。然后在软件的安装点上或者运行时,IL 由即时 (JIT) 编译器转换为机器码。
2.5 什么是 C#?
C# 是在 .NET 框架中运行的一种新语言。在他们的“C# 简介”白皮书中,Microsoft 这样描述 C#:
“C# 是从 C 和 C++ 派生出来的一种简单的、面向对象的、并且是类型安全的现代编程语言。C# (发音为‘C sharp’) 牢固地根植于在 C 和 C++ 家族之树,将很快为 C 和 C++ 程序员所熟悉。C# 帮助开发者将 Visual Basic 的高生产率和 C++ 的直接控制能力结合起来。”
将以上引言中的“C#”换成“Java”,你会发现这句陈述依然很正确 :)。
假如你是一位 C++ 程序员,你可能想看看我的 C# FAQ。
2.6 在 .NET 范畴里,“被管理”是什么含义?
术语“被管理”导致了很多误解。在 .NET 里的不同地方都使用了它,分别指相互差别不大的不同东西。
被管理代码:.NET 框架为运行在其上的程序提供了几个核心的运行服务—例如异常处理和安全性。为使这些服务能工作,代码必须提供运行时的最低程度的一些信息。这样的代码被称为被管理代码。默认情况下,所有 C#、Visual Basic.NET 和 JScript.NET 代码都是被管理代码。如不指明,VS7 C++ 代码不是被管理代码,但能通过一个命令行开关 (/com+) 使编译器产生被管理代码。
被管理数据:是指由 .NET 运行库的垃圾收集器分配和回收的数据。C#、VB.NET 和 JScript.NET 数据总是被管理的。即使使用了 /com+ 开关,默认情况下 VS7 C++ 数据也不是被管理的,但可以使用 __gc 关键字将其指定为被管理数据。
被管理类:通常在 C++ 的 Managed Extensions (ME) 范畴中涉及。使用 ME C++ 时,可以用 __gc 关键字将其指定为被管理的。名副其实,该类的实例所占用的内存由垃圾收集器管理,但还不止如此。该类还成为了完全的 .NET 团体的成员,同时带来了好处和限制。好处之一是获得了与其它语言编写的类之间的互操作性—例如,一个被管理 C++ 类可以继承 VB 类。限制之一是被管理类只能继承一个基类。
2.7 什么是映像?
所有的 .NET 编译器都产生关于它们所产生的模块中的类型定义的特殊数据。这些特殊数据同模块封装在一起 (随后模块被封装到元件中),可以通过称为映像 的机制来访问。System.Reflection 命名空间中包含向模块或元件询问其类型的类。
使用映像来访问 .NET 的特殊数据同使用 ITypeLib/ITypeInfo 来访问 COM 中的类型库数据非常相似,而且使用的目的也很相似—例如确定数据类型大小,以便在上下文、进程、机器的边界间调度它们。
映像还可以被用来动态调用方法 (参见 System.Type.InvokeMember),甚至在运行时动态创建类型 (参见 System.Reflection.Emit.TypeBuilder )。
3. 元件
3.1 什么是元件?
元件有时被描述为一个逻辑上的 .EXE 或 .DLL,它可以是任何一个应用程序 (有一个主入口点) 或库。一个元件由一个或多个文件组成 (dll、exe、html 文件等等),表示一组资源、类型定义以及这些类型的实现。一个元件也可以包含对其它元件的引用。这些资源、类型和引用在称为清单的一个数据块中描述。清单是元件的一部分,这样一来元件就是自描述的。
元件的一个重要方面使他们是一个类型的唯一标志的一部分。类型的唯一标志是将它所在的元件和类型名组合在一起得到的。这就是说,例如,如果元件 A 输出了一个称为 T 的类型,同时元件 B 输出了一个也称为 T 的类型,.NET 运行库将它们视为完全不同的两个类型。此外,不要混淆元件和命名空间—命名空间仅仅是组织类型名字的一种层次化方法。对于运行库,不论使用哪一个命名空间来组织名字,类型名就是类型名。从运行库来看,是元件加上类型名 (无论类型名属于哪个命名空间) 唯一地标识出一个类型。
元件在 .NET 的安全方面也很重要—许多安全限制是在元件的边界上实施的。
最后,元件是 .NET 中版本控制的单元—详情见下文。
3.2 怎样创建元件?
创建元件最简单的方法是直接使用 .NET 编译器。例如,以下 C# 程序:
public class CTest
{
public CTest()
{
System.Console.WriteLine( "Hello from CTest" );
}
}
能用以下方法编译为一个库元件 (dll):
csc /t:library ctest.cs
通过运行 .NET SDK 所带的“IL 反汇编”工具,你能看到元件的内容。
另外你也能把你的源代码编译成模块,然后使用元件连接器 (al.exe) 将模块组合成一个元件。对 C# 编译器,/target:module 开关可以指定产生模块而不是元件。
3.3 私有元件和共享元件有什么不同?
空间分配和可见性:私有元件通常由一个应用程序使用,被存储到这个应用程序的目录或其下的子目录之下。共享元件通常存储到全局的元件缓冲区中,这里是 .NET 运行库维护的元件的储藏所。共享元件通常是许多应用程序都要用到的代码库,例如 .NET 框架类。
版本控制:运行库只对共享元件实施版本约束,而不对私有元件实施。
3.4 元件如何相互找到?
通过寻找目录路径。有几个因素会影响路径 (比如 AppDomain 宿主、应用程序配置文件等),但对于私有元件,搜索路径通常是应用程序的目录及其子目录。对于共享元件,搜索路径通常和私有元件的一样,再加上共享元件缓冲区。
3.5 元件版本如何起作用?
每个元件由一个称为兼容性版本的版本号。同样,对元件的引用 (从另一个元件) 包括被引用元件的名称和版本。
版本号有四个数字部分 (例如 5.5.2.33)。前两部分不相同的元件被视为不兼容的。如果前两部分相同,但第三部分不同,元件被认为“可能兼容”。如果仅仅第四部分不同,则元件被视为是兼容的。然而,这只是默认的指导方针—是 版本策略决定施用这些规则的范围。版本策略可以在应用程序配置文件中指定。
记住:版本控制仅仅针对于共享元件,而不对私有元件。
4. 应用程序域
4.1 什么是应用程序域?
应用程序域 (AppDomain) 可以被看作一个轻型的进程。在一个 Win32 进程中可以存在多个 AppDomain。AppDomain 的主要目的是将应用程序和其它应用程序隔离开来。
通过使用独立的地址空间,Win32 进程提供隔离性。这种方法很有效,但开销很大并且伸缩性不好。.NET 运行库通过控制对内存的是用来施加 AppDomain 隔离—AppDomain 中的所有内存是由 .NET 运行库来管理的,所以运行库可以确保 AppDomain 之间不能访问彼此的内存。
4.2 如何创建 AppDomain?
AppDomains 通常有宿主创建。宿主包括 Windows Shell、ASP+ 和 IE。当你从命令行运行一个 .NET 应用程序时,宿主是 Shell。Shell 为每个应用程序创建一个新的 AppDomain。
AppDomains 也可以由 .NET 应用程序来显式创建。这里是一个创建 AppDomain 的一个 C# 例子,它创建对象的一个实例,并随后执行对象的一个方法:
using System;
using System.Runtime.Remoting;
public class CAppDomainInfo : MarshalByRefObject
{
public string GetAppDomainInfo()
{
return "AppDomain = " + AppDomain.CurrentDomain.FriendlyName;
}
}
public class App
{
public static int Main()
{
AppDomain ad = AppDomain.CreateDomain( "Andy's new domain", null, null );
ObjectHandle oh = ad.CreateInstance( "appdomaintest.exe", "CAppDomainInfo" );
CAppDomainInfo adInfo = (CAppDomainInfo)(oh.Unwrap());
string info = adInfo.GetAppDomainInfo();
Console.WriteLine( "AppDomain info: " + info );
return 0;
}
}
4.3 我能编写自己的 .NET 宿主吗?
能。关于怎样来做的例子,看看 Jason Whittington 和 Don Box 开发的 dm.net moniker 的源代码 (http://staff.develop.com/jasonw/clr/readme.htm)。在 .NET SDK 中也有一个叫作 CorHost 的代码示例。
5. 垃圾收集
5.1 什么是垃圾收集?
垃圾收集是一个系统,运行库组件通过它来管理对象的生存周期和它们占用的堆内存。对 .NET 而言它并不是一个新概念—Java 和许多其它语言/运行库使用垃圾收集已经有一段时间了。
5.2 对对象的最后一个引用撤销后,它并不一定立即被破坏,对吗?
是的。垃圾收集器并不提供销毁对象并是放其内存的时间保证。
关于 C# 中隐含的非确定化对象析构,Chris Sells 有一个令人感兴趣的线索:http://discuss.develop.com/archives/wa.exe?A2=ind0007&L=DOTNET&P=R24819
2000 年 10 月,Microsoft 的 Brian Harry 贴出了一个针对这个问题的很长的分析:http://discuss.develop.com/archives/wa.exe?A2=ind0010A&L=DOTNET&P=R28572
Chris Sells 对 Brian 贴子的答复在这里:http://discuss.develop.com/archives/wa.exe?A2=ind0010C&L=DOTNET&P=R983
5.3 .NET 为什么不提供确定化的析构?
因为垃圾收集算法。.NET 的垃圾收集器通过周期地扫描应用程序正在使用的所有对象的列表来工作。扫描过程中所有未被发现的对象就可以被销毁并释放内存。当对对象的最后一个引用撤销后,算法的这种实现使运行库不能立即得到通知—它只能在下一次清理堆时发现。
而且,这种算法尽可能少地进行垃圾收集,以便工作得最有效率。通常,堆容量的消耗会触发收集过程。
5.4 在 .NET 中缺少确定化的析构有问题吗?
这确实会影响组件的设计。如果你的对象需要昂贵或紧缺的资源 (例如对数据库的锁定),你需要提供某种方法让客户端在工作完成后能告诉对象以释放资源。Microsoft 建议,为此目的你应提供一个称为 Dispose () 的方法。然而,这样会在分布式对象中引起问题—在一个分布式系统中由谁来调用 Dispose () 方法?需要有某种形式的引用-计数机制或所有者管理机制来处理分布式对象—不幸的是运行库对此爱莫能助。
5.5 确定化的析构是否影响在被管理代码中使用 COM 对象?
是的。从被管理代码中使用 COM 对象时,你实际上是依赖垃圾收集器来最终释放你的对象。如果你的 COM 对象占有昂贵的资源且只能在最终释放对象后才能释放,你可能需要在你的对象上提供一个新接口以支持显式的 Dispose () 方法。
5.6 我听说应该避免使用 Finalize 方法,那么是否应该在我的类理实现 Finalize?
对垃圾收集器而言,拥有 Finalize 方法的对象比没有此方法的对象需要做更多的工作。同时也不保证对象 Finalized 的次序,所以对于从 Finalized 方法访问其它对象有不同的看法。最后,不能保证 Finalized 方法一定能被调用。所以,永远不应该依赖它来清理对象的资源。
Microsoft 建议使用以下方式:
public class CTest
{
public override void Dispose()
{
... // Cleanup activities
GC.SuppressFinalize(this);
}
protected override void Finalize()
{
Dispose();
}
}
一般情况下客户端调用 Dispose (),对象的资源被释放,并且通过调用 SuppressFinalize (),垃圾收集器被免除了对它进行 Finalize 的义务。在最不利的情况下,即客户端忘记了调用 Dispose (),有很大的机会通过垃圾收集器调用 Finalize () 来最终释放对象的资源。由于垃圾收集算法的缺陷,这看起来像是相当合理的处理办法了。
5.7 我有控制垃圾收集算法的手段吗?
有一点。System.GC 类提供了一对有趣的方法。第一个是 Collect 方法—它强制垃圾收集器立即收集所有未被引用的对象。另一个是 RequestFinalizeOnShutdown (),它告诉垃圾收集器在应用程序关闭时一定要对每个对象运行 Finalize () 方法。在应用程序关闭时,垃圾收集器一般优先选择快速的推出方式而不是调用 Finzlize (),所以这个方法能手工强制运行库多负一点责任。
如果你想验证这不仅仅是理论上的说法,是一十下面的测试程序:
using System;
class CTest
{
protected override void Finalize()
{
Console.WriteLine( "This is the Finalizer." );
}
}
class CApplication
{
public static void Main()
{
Console.WriteLine( "This is Main." );
CTest test = new CTest();
// GC.RequestFinalizeOnShutdown();
}
}
运行此程序,然后再去掉 GC.RequestFinalizeOnShutdown() 这一行前面的注释标记并重新运行,注意有什么不同……
5.8 我怎么知道垃圾收集器在做什么?
.NET 运行库中很多令人感兴趣的统计通过 'COM+ Memory' 性能对象输出。使用 Performance Monitor 查看它们。
6. 属性
6.1 什么是属性?
最少有两种类型的 .NET 属性。第一类我称其为 metadata 属性—它允许将某些数据附加到类或方法上。这些数据称为类的 metadata 的一部分,并且可以像类的其它 metadata 一样通过映射来访问。metadata 的另一种属性是 [serializable],将它附加到类上表示类的实例可以被串行化。
[serializable] public class CTest {}
另一种类型的属性是上下文属性。上下文类型的属性使用和 metadata 相似的语法,但实际上它们是不同的。上下文类型属性提供一种解释机制,通过这种机制,实例的活动和方法调用可以是预先处理和/或随后处理的。如果你了解 Keith Brown 的通用委托器你可能熟悉这种思想。
6.2 我能创建自己的 metadata 属性吗?
是的。简单地从 System.Attribute 导出一个类并将其标记为 AttributeUsage 属性。例如:
[AttributeUsage(AttributeTargets.Class)]
public class InspiredByAttribute : System.Attribute
{
public string InspiredBy;
public InspiredByAttribute( string inspiredBy )
{
InspiredBy = inspiredBy;
}
}
[InspiredBy("Andy Mc's brilliant .NET FAQ")]
class CTest
{
}
class CApp
{
public static void Main()
{
object[] atts = typeof(CTest).GetCustomAttributes();
foreach( object att in atts )
if( att is InspiredByAttribute )
Console.WriteLine( "Class CTest was inspired by {0}", _
((InspiredByAttribute)att).InspiredBy );
}
}
6.3 我能创建自己的 context 属性吗?
是的。看看 http://www.develop.com/dbox/dotnet/threshold/ 处的 Don Box 的例子 (叫作 CallThreshold) 和 http://www.razorsoft.net/ 处的 Perter Drayton 的 Tracehook.NET
7. 代码访问安全性
7.1 什么是代码访问安全性 (CAS)?
CAS 是 .NET 安全性模型的一部分,它确定一段代码是否允许被运行,以及当它运行是可以使用什么资源。例如,CAS 可以防止一个 .NET 的 Web applet 将你的硬盘格式化。
7.2 CAS 如何起作用?
CAS 安全策略设计两个关键概念—代码组和权限。每个 .NET 元件是特定 代码组的成员,并且每个代码组被授予由有名权限集所指定的权限。
例如,使用默认的安全策略时,一个从 Web 站点下载的控件属于“Zone - Internet”代码组,它保持由有名权限集“Internet”所定义的权限。(自然,有名权限集“Internet”表示一组受到严格限制的权限。)
7.3 谁定义 CAS 代码组?
Microsoft 定义了一些默认代码组,但你可以改变这些甚至创建你自己的代码组。要想看到你的系统中定义的代码组,可以从命令横行运行“caspol -lg”命令。再我的系统里它看起来像这些:
Level = Machine
Code Groups:
1. All code: Nothing
1.1. Zone - MyComputer: FullTrust
1.1.1. Honor SkipVerification requests: SkipVerification
1.2. Zone - Intranet: LocalIntranet
1.3. Zone - Internet: Internet
1.4. Zone - Untrusted: Nothing
1.5. Zone - Trusted: Internet
1.6. StrongName - 0024000004800000940000000602000000240000525341310004000003
000000CFCB3291AA715FE99D40D49040336F9056D7886FED46775BC7BB5430BA4444FEF8348EBD06
F962F39776AE4DC3B7B04A7FE6F49F25F740423EBF2C0B89698D8D08AC48D69CED0FC8F83B465E08
07AC11EC1DCC7D054E807A43336DDE408A5393A48556123272CEEEE72F1660B71927D38561AABF5C
AC1DF1734633C602F8F2D5: Everything
注意代码组的层次—顶层 ('All code') 是最通用的,它随后分为几个组,每个还可以再分。同时注意,和一般的想象不同,子组可以被赋予比它的上级更宽的权限集。
7.4 如何定义自己的代码组?
使用 caspol。例如,假定你信任来自 www.mydomain.com 的代码,并且希望它对你的系统拥有完全的访问权,但是希望对其它 Internet 站点保持默认的限制。要实现这些,你可以在“Zone - Internet”组中增加一个子组,就像下面那样:
caspol -ag 1.3 -site www.mydomain.com FullTrust
现在如果你运行 caspol -lg 就可以看到新的代码组被增加为 1.3.1 组:
...
1.3. Zone - Internet: Internet
1.3.1. Site - www.mydomain.com: FullTrust
...
注意数字标号 (1.3.1) 只是 caspol 编出来以便能从命令行方便地操纵代码组的。底层的运行库永远看不到它。
7.5 如何改变代码组的权限集?
使用 caspol。如果你是机器的管理员,你能在 'machine' 层次上操作—这不仅意味着你所做的改变将成为机器的默认设置,而且用户不能把权限改得更宽。如果你是一个普通用户 (不是管理员) 你仍然可以修改权限,但只能使它们变得更严格。例如,为使 intranet 代码能做它们想做的事,你可能需要这样:
caspol -cg 1.2 FullTrust
注意,因为 (在标准的系统里) 这比默认的安全策略权限更大,你应该在 machine 层次上做这些—在 user 层次上这样做不起作用。
7.6 能否创建自己的权限集?
是的。使用 caspol -ap,指定一个包含权限集中所有的权限的 XML 文件。这里 是一个指定 'Everything' 权限集的示例文件—修改它以适应你的需要,这样可以节省一些时间。修改完成后,用以下方法将它添加到可用的权限集中:
caspol -ap samplepermset.xml
然后,用以下方法将此权限集施加到一个代码组上:
caspol -cg 1.3 SamplePermSet
(默认情况下,1.3 是 'Internet' 代码组)
7.7 CAS 有问题时,如何诊断自己的程序?
caspol 有一组可能有用的选项。首先,使用 caspol -rsg,你能让 caspol 告诉你一个元件属于哪一个代码组。类似地,使用 caspol -rsp,你能询问在特定元件上施加了什么权限。
7.8 我受不了 CAS 带来的麻烦,能否关掉它?
是的,只要你是系统管理员。只要运行:
caspol -s off
8. 中间语言 (IL)
8.1 我能看到元件的中间语言吗?
是的。Microsoft 提供了一个称为 Ildasm 的工具,它可以用来查看元件的 metadata 和 IL。
8.2 能否通过反向工程从 IL 中获得源代码?
是的。相对而言,从 IL 来重新生成高级语言源代码 (例如 C#) 通常是很简单的。
8.3 如何防止别人通过反向工程获得我的代码?
目前唯一的办法是运行带有 /owner 选项的 ilasm。这样生成的元件的 IL 不能通过 ildasm 来查看。然而,意志坚定的代码破译者能够破解 ildasm 或者编写自己的 ildasm 版本,所以这种方法只能吓唬那些业余的破译者。
不幸的事,目前的 .NET 编译器没有 /owner 选项,所以要想保护你的 C# 或 VB.NET 元件,你需要像下面那样做:
csc helloworld.cs
ildasm /out=temp.il helloworld.exe
ilasm /owner temp.il
(这个建议是 Hany Ramadan 贴到 DOTNET 上的。)
看起来过一段时间能有 IL 加密工具 (无论来自 Microsoft 或第三方)。这些工具会以这样的方式来“优化” IL:使反向工程变得更困难。
当然,如果你是在编写 Web 服务,反向工程看起来就不再是一个问题,因为客户不能访问你的 IL。
8.4 我能直接用 IL 编程吗?
是的。Peter Drayton 在 DOTNET 邮件列表里贴出了这个简单的例子:
.assembly MyAssembly {}
.class MyApp {
.method static void Main() {
.entrypoint
ldstr "Hello, IL!"
call void System.Console::WriteLine(class System.Object)
ret
}
}
将其放入名为 hello.il 的文件中,然后运行 ilasm hello.il,将产生一个 exe 元件。
8.5 IL 能做到 C# 中做不到的事吗?
是的。一些简单的例子是:你能抛出不是从 SystemException 导出的异常,另外你能使用非以零起始的数组。
9. 关于 COM
9.1 COM 消亡了吗?
就像你在邮件列表中看到的那样,这个主题导致了激烈的争论。看看以下两个地方:
http://discuss.develop.com/archives/wa.exe?A2=ind0007&L=DOTNET&D=0&P=68241
http://discuss.develop.com/archives/wa.exe?A2=ind0007&L=DOTNET&P=R60761
我的理解是:COM 包含很多内容,并且对于不同的人而言它是不同的东西。但是对我来说,COM 基本上是关于一小段代码如何找到另一小段代码,以及当它们相互找到后该如何相互通讯。COM 准确地指明了这种定位和通讯该如何进行。在完全由 .NET 对象构成的“纯” .NET 世界里,小段代码依然相互寻找并相互交谈,但它们不使用 COM 来做这些。它们使用在某些地方和 COM 很相像的一种模型—例如,类型信息保存在和组件封装在一起的表单中,这和在 COM 组件中封装一个类型库十分相似。但它不是 COM。
所以,这里有什么问题吗?好吧,我确实不关心大多数 COM 消失了—我不关心寻找组件不再和注册表有关,我也不使用 IDL 来定义我的借口。但有一件东西我不希望它消失—我不希望失去基于接口的开发这种思想。照我看来,COM 最强大的力量是它坚持在接口和实现之间竖起铸铁般的隔墙。不幸的是,看来 .NET 不再那样坚持—它允许你做基于接口的开发,但它并不坚持。一些人可能会辩解说有一个选择总不会是坏事,可能他们是对的,但我不能不觉得这可能是一个退步。
9.2 DCOM 消亡了吗?
差不多是,尤其是对于 .NET 开发者。.NET 框架有一个不基于 DCOM 的新的远程模型。当然 DCOM 还会在互操作场合下使用。
9.3 MTS/COM+ 消亡了吗?
不。第一个 .NET 版本考虑的是提供对现有 COM+ 服务 (通过一个互操作层) 而不是使用 .NET 自己的服务来取代它们。很多工具和属性被用以实现尽可能平滑的过渡。.NET SDK 的 PDC 版本包括对核心服务 (JIT 活动、事务) 的支持,但不包括一些高层服务 (例如 COM+ 事件、队列化组件)。
在一段时间内看来,互操作性可以预期是无缝集成的—这意味着一些服务将成为 CLR 的一部分,并且/或者意味着一些服务将以可管理代码的形式重写并运行在 CLR 的顶层。
关于这个主题,参见 Joe Long 的贴子—Joe 是 Microsoft 的 COM+ 组的经理。从这里开始:
http://discuss.develop.com/archives/wa.exe?A2=ind0007&L=DOTNET&P=R68370
9.4 能在 .NET 中使用 COM 组件吗?
可以。可以通过 Runtime Callable Wrapper (RCW) 从 .NET 中访问 COM 组件。它通过将 COM 组件映射为与 .NET 兼容的接口来使 COM 接口可以被访问。对于 oldautomation 接口,可以自动地从一个类型库中产生。对于非 oleautomation 接口,可以开发一个定制的 RCW,以便手工地将 COM 接口的类型映射为与 .NET 兼容的类型。
对于熟悉 ATL 的读者,这里有一个简单的示例。首先,创建一个 ATL 组件以实现以下 IDL:
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(EA013F93-487A-4403-86EC-FD9FEE5E6206),
helpstring("ICppName Interface"),
pointer_default(unique),
oleautomation
]
interface ICppName : IUnknown
{
[helpstring("method SetName")] HRESULT SetName([in] BSTR name);
[helpstring("method GetName")] HRESULT GetName([out,retval] BSTR *pName );
};
[
uuid(F5E4C61D-D93A-4295-A4B4-2453D4A4484D),
version(1.0),
helpstring("cppcomserver 1.0 Type Library")
]
library CPPCOMSERVERLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(600CE6D9-5ED7-4B4D-BB49-E8D5D5096F70),
helpstring("CppName Class")
]
coclass CppName
{
[default] interface ICppName;
};
};
建立了组件以后,你会得到一个 typelibrary。在 typelibrary 上运行 TLBIMP 实用程序,就像这样:
tlbimp cppcomserver.tlb
如果成功,你会得到像这样的信息:
Typelib imported successfully to CPPCOMSERVERLib.dll
现在你需要一个 .NET 客户端—我们用 C# 创建一个包含以下代码的 .cs 文件:
using System;
using CPPCOMSERVERLib;
public class MainApp
{
static public void Main()
{
CppName cppname = new CppName();
cppname.SetName( "bob" );
Console.WriteLine( "Name is " + cppname.GetName() );
}
}
注意我们使用 typelibrary 的名字作为命名空间,COM 类的名字作为类名。我们也可以选择使用 CPPCOMSERVERLib.CppName 作为类名而且不需要语句 using CPPCOMSERVERLib。
像这样编译以上 C# 代码:
csc /r:cppcomserverlib.dll csharpcomclient.cs
注意,编译被告知,引用我们刚才用 TLBIMP 从 typelibrary 产生的 DLL。
现在你应该可以运行 csharpcomclient.exe,并从控制台得到如下输出:
Name is bob
9.5 能在 COM 中使用 .NET 组件吗?
可以。可以通过一个 COM Callable Wraper (CCW) 从 COM 中访问 .NET 组件。这和 RCW 很相似 (参见上一个问题),但以相反的方向工作。同样,如果它不能由 .NET 开发工具自动产生,或不想要自动产生的行为逻辑,可以开发一个定制的 CCW。为使 COM 可以“看见” .NET 组件,.NET 组件必须在注册表里注册。
这里是一个简单的例子。创建一个名为 testcomserver.cs 的 C# 文件并输入下面的代码:
using System;
namespace AndyMc
{
public class CSharpCOMServer
{
public CSharpCOMServer() {}
public void SetName( string name ) { m_name = name; }
public string GetName() { return m_name; }
private string m_name;
}
}
然后编译 .cs 文件:
csc /target:library testcomserver.cs
你会得到一个 dll,这样将它注册:
regasm testcomserver.dll /tlb:testcomserver.tlb
现在你需要创建一个客户端程序来测试你的 .NET COM 组件。VBScript 可以—将以下内容放到一个名为 comclient.vbs 的文件中:
Dim dotNetObj
Set dotNetObj = CreateObject("AndyMc.CSharpCOMServer")
dotNetObj.SetName ("bob")
MsgBox "Name is " & dotNetObj.GetName()
运行此脚本:
wscript comclient.vbs
嘿!你得到一个显示文本“Name is bob”的消息框。
(注意,编写此程序时,看起来可以通过几种路径将 .NET 类作为 COM 组件访问—为了避免问题,在 testcomserver.dll 相同的目录下运行 comclient.vbs。
一种替代的方法是使用 Jason Whittington 和 Don Box 开发的 dm.net moniker。到这里 http://staff.develop.com/jasonw/clr/readme.htm 查看。
9.6 在 .NET 的世界中 ATL 是多余的吗?
是的。如果你在编写 .NET 框架内的应用程序。当然许多开发者希望继续使用 ATL 来编写 .NET 框架以外的 C++ COM 组件,但当你在 .NET 框架内时你差不多总是希望使用 C#。在 .NET 世界里,原始的 C++ (以及基于它的 ATL) 并没有太多的地位—它太直接了,并且提供了太多的适应性,以至于运行库不能管理它。
10. 杂项
10.1 .NET 的远程计算如何工作?
.NET 的远程计算涉及通过通道发送消息。两种标准的通道是 HTTP 和 TCP。仅仅在局域网上才倾向于使用 TCP—HTTP 能在局域网和广域网 (internet) 上使用。
现在提供了对多种消息串行化格式的支持,例如 SOAP (基于 XML) 和二进制格式。默认情况下,HTTP 通道使用 SOAP (通过 .NET 运行库的 Serialization SOAP Formatter),而 TCP 通道使用二进制格式 (通过 .NET 运行库的 Serialization Binary Formatter)。但每个通道可以使用任一串行化格式。
这里是远程访问的一些方式:
SingleCall。每个来自客户端的请求由一个新对象服务。当请求完成后对象被丢弃。可以在 ASP+ 环境中使用 ASP+ 国家服务来保存应用程序或会话的国家,从而使这种模型 (无国家之分的) 变成有国家支持的。
Singleton。所有来在客户端的请求由单一的服务器对象处理。
Client-activated object。这是老的有国家支持的 (D)COM 模型,这里客户端受到一个远端对象的引用并保留此引用 (以保持远端对象的生存),直到对它的访问完成。
对象的分布式垃圾收集由称为“基于租用的生命周期”管理。每个对象拥有一个租用时间,这个时间到达时,从 .NET 运行库的远程子结构断开对象。对象有默认的更新时间—从客户端发起的成功调用会更新租用时间。客户端也可以显示地更新租用时间。
如果你对使用 XML-RPC 来代替 SOAP,可以看看 Charles Cook 在 http://www.cookcomputing.com/xmlrpc/xmlrpc.shtml 的 XML-RPC.Net 站点。
10.2 如何在 .NET 程序中获得 Win32 API?
使用 P/Invoke。它使用了和 COM 互操作性相似的技术,但被用来访问静态 DLL 入口点而不是 COM 对象。以下是一个调用 Win32 MessageBox 函数的 C# 程序示例:
using System;
using System.Runtime.InteropServices;
class MainApp
{
[dllimport("user32.dll", EntryPoint="MessageBox", SetLastError=true, CharSet=CharSet.Auto)]
public static extern int MessageBox(int hWnd, String strMessage, String strCaption, uint uiType);
public static void Main()
{
MessageBox( 0, "Hello, this is PInvoke in operation!", ".NET", 0 );
}
}
11. 类库
11.1 文件 I/O
11.1.1 如何读文本文件?
首先,使用 System.IO.FileStream 对象打开文件:
FileStream fs = new FileStream( @"c:\test.txt", FileMode.Open, FileAccess.Read );
FileStream 继承于 Stream,所以你可以用一个 StreamReader 对象把 FileStream 对象包装起来。这样为一行一行地进行流处理提供了一个良好的界面:
StreamReader sr = new StreamReader( fs );
string curLine;
while( (curLine = sr.ReadLine()) != null )
Console.WriteLine( curLine );
最后关闭 StreamReader 对象:
sr.Close();
注意这样将自动地在底层 Stream 对象上调用 Close (),所以不必显示地执行 fs.Close()。
11.1.2 如何写文本文件?
和读文件的例子相似,只是把 StreamReader 换成 StreamWriter。
11.1.3 如何读写二进制文件?
和文本文件类似,只是要用 BinaryReader/Writer 对象而不是 StreamReader/Writer 来包装 FileStream 对象。
11.1.4 如何删除文件?
在 System.IO.File 对象上使用静态方法 Delete ():
File.Delete( @"c:\test.txt" );
11.2 文本处理
11.2.1 是否支持正规表达式?
是的。使用 System.Text.RegularExpressions.Regex 类。例如,以下代码更新 HTML 文件的标题:
FileStream fs = new FileStream( "test.htm", FileMode.Open, FileAccess.Read );
StreamReader sr = new StreamReader( fs );
Regex r = new Regex( "<TITLE>(.*)</TITLE>" );
string s;
while( (s = sr.ReadLine()) != null )
{
if( r.IsMatch( s ) )
s = r.Replace( s, "<TITLE>New and improved ${1}</TITLE>" );
Console.WriteLine( s );
}
11.3 Internet
11.3.1 如何下载网页?
首先使用 System.Net.WebRequestFactory 类来获得一个 WebRequest 对象:
WebRequest request = WebRequestFactory.Create( "http://localhost" );
然后请求应答:
WebResponse response = request.GetResponse();
GetResponse 方法被阻塞直到下载完成。然后你能像下面那样访问应答流:
Stream s = response.GetResponseStream();
// Output the downloaded stream to the console
StreamReader sr = new StreamReader( s );
string line;
while( (line = sr.ReadLine()) != null )
Console.WriteLine( line );
注意 WebRequest 和 WebReponse 对象分别向下兼容 HttpWebRequest 和 HttpWebReponse 对象,它们被用来访问和 http 相关的功能。
11.3.2 如何使用代理服务器 (proxy)?
两种—这样做以便影响所有 Web 请求:
System.Net.GlobalProxySelection.Select = new DefaultControlObject( "proxyname", 80 );
另外一种,要想对特定的 Web 请求设置代理服务,这样做:
ProxyData proxyData = new ProxyData();
proxyData.HostName = "proxyname";
proxyData.Port = 80;
proxyData.OverrideSelectProxy = true;
HttpWebRequest request = (HttpWebRequest)WebRequestFactory.Create( "http://localhost" );
request.Proxy = proxyData;
11.4 XML
11.4.1 是否支持 DOM?
是的。看看以下示例 XML文档:
<PEOPLE>
<PERSON>Fred</PERSON>
<PERSON>Bill</PERSON>
</PEOPLE>
可以这样处理此文档:
XmlDocument doc = new XmlDocument();
doc.Load( "test.xml" );
XmlNode root = doc.DocumentElement;
foreach( XmlNode personElement in root.ChildNodes )
Console.WriteLine( personElement.FirstChild.Value.ToString() );
输出为:
Fred
Bill
11.4.2 是否支持 SAX?
不。作为替换,提供了一个新的 XmlReader/XmlWriter API。像 SAX 一样,它是基于流的,但它使用“pull”模型而不是 SAX 的“push”模型。这是一个例子:
XmlTextReader reader = new XmlTextReader( "test.xml" );
while( reader.Read() )
{
if( reader.NodeType == XmlNodeType.Element && reader.Name == "PERSON" )
{
reader.Read(); // Skip to the child text
Console.WriteLine( reader.Value );
}
}
11.4.3 是否支持 XPath?
是的,通过 XmlNavigator 类 (DocumentNavigator 是从 XmlNavigator 导出的):
XmlDocument doc = new XmlDocument();
doc.Load( "test.xml" );
DocumentNavigator nav = new DocumentNavigator(doc);
nav.MoveToDocument();
nav.Select( "descendant::PEOPLE/PERSON" );
while( nav.MoveToNextSelected() )
{
nav.MoveToFirstChild();
Console.WriteLine( "{0}", nav.Value );
}
11.5 线程
11.5.1 是否支持多线程?
是的,对多线程有广泛的支持。系统能产生新线程,并提供应用程序可以使用的线程池。
11.5.2 如何产生一个线程?
创建 System.Threading.Thread 对象的一个实例,把将要在新线程中执行的 ThreadStart 示例传递给它。例如:
class MyThread
{
public MyThread( string initData )
{
m_data = initData;
m_thread = new Thread( new ThreadStart(ThreadMain) );
m_thread.Start();
}
// ThreadMain() is executed on the new thread.
private void ThreadMain()
{
Console.WriteLine( m_data );
}
public void WaitUntilFinished()
{
m_thread.Join();
}
private Thread m_thread;
private string m_data;
}
这里创建 MyThread 的一个实例就足以产生线程并执行 MyThread.ThreadMain () 方法:
MyThread t = new MyThread( "Hello, world." );
t.WaitUntilFinished();
11.5.3 如何停止一个线程?
有好几个办法。首先,你能使用自己的通讯机制告诉 ThreadStart 方法结束。另外 Thread 类有内置的支持来命令线程停止。基本的两个方法是 Thread.Interrupt () 和 Thread.Abort ()。前者导致抛出一个 ThreadInterruptedException 并随后进入 WaitJoinSleep 状态。换句话说,Thread.Interrupt 是一种礼貌的方式,它请求线程在不再进行任何有用的工作时自行停止的。与此相对应,Thread.Abort () 抛出一个 ThreadAbortException 而不管线程正在做什么。而且,ThreadAbortException 不能像通常的异常那样被捕获 (即使最终将执行 ThreadStart 的终止方法)。Thread.Abort () 是一般情况下不需要的非常手段。
11.5.4 怎样使用线程池?
通过向 ThreadPool.QueueUserWorkItem () 方法传递 WaitCallback 的一个实例:
class CApp
{
static void Main()
{
string s = "Hello, World";
ThreadPool.QueueUserWorkItem( new WaitCallback( DoWork ), s );
Thread.Sleep( 1000 ); // Give time for work item to be executed
}
// DoWork is executed on a thread from the thread pool.
static void DoWork( object state )
{
Console.WriteLine( state );
}
}
11.5.5 怎样知道我的线程池工作项目是在何时完成的?
没有方法询问线程池这类信息。你必须在 WaitCallback 方法中放置代码来发出信号以表明它已经完成。这里事件也很有用处。
11.5.6 怎样防止对数据的并发访问?
每个对象有一个与之相联的并发锁 (受批评的部分)。System.Threading.Monitor.Enter/Exit 方法用来获得和释放锁。例如,下面类的实例只允许一个线程同时进入方法 f ():
class C
{
public void f()
{
try
{
Monitor.Enter(this);
...
}
finally
{
Monitor.Exit(this);
}
}
}
C# 有一个关键字‘lock’提供了以上代码的简单形式:
class C
{
public void f()
{
lock(this)
{
...
}
}
}
注意,调用 Monitor.Enter (myObject) 并不意味着对 myObject 的所有访问都被串行化了。它意味着请求同 myObject 相联的同步锁,并且在调用 Monitor.Exit(o) 之前,没有任何其它线程可以请求该锁。换句话说,下面的类和以上给出的类在功能上是等同的:
class C
{
public void f()
{
lock( m_object )
{
...
}
}
private m_object = new object();
}
11.6 跟踪
11.6.1 有内置的跟踪/日志支持吗?
是的,在 System.Diagnostics 命名空间中。有两个处理跟踪的主要的类—Debug 和 Trace。它们以相似的方式工作—不同之处是 Debug 类中的跟踪只能在用 DEBUG 标志生成的代码中工作,而 Trace 类中的跟踪只能在指明了 TRACE 标记生成的代码中工作。典型地,这意味着你应该在你希望能在 debug 和 release 版本中都能跟踪时使用 System.Diagnostics.Trace.WriteLine,而在你希望只能在 debug 版本中能跟踪时使用 System.Diagnostics.Debug.WriteLine。
11.6.2 能否将跟踪输出重定向到一个文件?
是的。Debug 类和 Trace 类都有一个 Listeners 属性,它们分别收集你用 Debug.WriteLine 或 Trace.WriteLine 产生的输出。默认情况下 Listeners 只有一个收集槽,它是 DefaultTraceListener 类的一个实例。它将输出发送到 Win32 的 OutputDebugString () 函数和 System.Diagnostics.Debugger.Log () 方法。调试时这很有用,但如果你试图从客户站点跟踪一个问题,将输出重定向到一个文件中就更为恰当。幸运的是,为此目的提供了 TextWriterTraceListener 类。
这里是 TextWriterTraceListener 如何将 Trace 输出重定向到一个文件:
Trace.Listeners.Clear();
FileStream fs = new FileStream( @"c:\log.txt", FileMode.Create, FileAccess.Write );
Trace.Listeners.Add( new TextWriterTraceListener( fs ) );
Trace.WriteLine( @"This will be writen to c:\log.txt!" );
注意使用 Trace.Listeners.Clear () 去掉了默认的 listener。如果不这样做,输出将在文件和 OutputDebugString () 中同时产生。一般情况下你不希望如此,因为 OutputDebugString () 导致很大的性能开销。
11.6.3 能否定制跟踪的输出?
是的。你能编写你自己的 TraceListener 导出类,并把所有的输出重定向到它上面。这里有一个简单的例子,它从 TextWriterTraceListener 导出 (并随后内建了对写文件的支持) 并在每个输出行上添加时间信息和线程 ID:
class MyListener : TextWriterTraceListener
{
public MyListener( Stream s ) : base(s)
{
}
public override void WriteLine( string s )
{
Writer.WriteLine( "{0:D8} [{1:D4}] {2}",
Environment.TickCount - m_startTickCount,
AppDomain.GetCurrentThreadId(),
s );
}
protected int m_startTickCount = Environment.TickCount;
}
(注意这个实现并不完整—例如没有覆盖 TraceListener.Write 方法。)
这种方法的美妙之处在于,向 Trace.Listener 添加 MyListener 之后,所有对 Trace.WriteLine () 的调用都转向了 MyListener,包括从对 MyListener 一无所知的被引用元件发出的调用。
12. 资源
12.1 从哪里可以获得关于 .NET 的详情?
Microsoft .NET 主页位于 http://www.microsoft.com/net/。Microsoft 同时将它发布在 GOTDOTNET。
Microsoft 还发布了 .NET Framework FAQ,和本文很相似。可以在那里找到这里许多问题更“权威”的解答。
Robert Scoble 编辑了一个很容易理解的在线列表 http://www.devx.com/dotnet/resources/,这里还有一个 http://www.singularidad.com.ar/dotnet.asp。
在 http://www.devx.com/free/press/2000/vs-qalist.asp Robert 还有一个 .NET“著名问题与解答”主页。
Richard Grimes 和 Richard Anderson 有一个叫作 Managed World.COM.的站点。
http://www.ibuyspy.com/ 是一个以展示 .NET 平台为目的创建的示例站点。
还有我的 C# FAQ for C++ Programmers。
12.2 示例代码和实用程序
Peter Drayton 的 .NET Goodies 主页位于 http://www.razorsoft.net/
Don Box 的 CallThreshold 示例位于 http://www.develop.com/dbox/dotnet/threshold
Don 的 UnwindScope Service 位于 http://www.develop.com/dbox/dotnet/unwind
Don 的 CLR scripting host 位于 http://www.develop.com/dbox/dotnet/clrscript
Don 和 Jason 的 dm.net COM moniker 位于 http://staff.develop.com/jasonw/clr/readme.htm 在 http://www.bearcanyon.com/dotnet/ 有 Mike Woodring 的一些 .NET 例子。
在 http://www.cookcomputing.com/xmlrpc/xmlrpc.shtml 可以找到 Charles Cook 的 XML-RPC.Net library。