摘“.NET Framework FAQ”

.NET Framework FAQ
作者Andy McMullan 译者 荣耀
更新日期: 2002-09-10
这篇FAQ试图回答关于.NET Framework基本原理的一些常见问题。说到基本原理,我是指.NET Framework底层运作机制的具体细节,讨论主题包括配件(assemblies)、垃圾收集机制、安全性、COM互操作以及远程化(remoting)等,并涵盖类库最常用的部分。至于.NET Framework的其它方面,比如ASP.NET、ADO.NET和WinForms,不在本FAQ讨论范围。
这篇FAQ的灵感来自于DOTNET邮件列表。假如你还没有成为这个邮件列表的成员,可以从此加入: http://discuss.develop.com/dotnet.html 。在这儿还可以查阅邮件列表存档文件。
假如你有任何评注、建议、更正或批评,请给我写信(andy@andymcm.com)。
假如你对C#感兴趣,为什么不看看我的“C# FAQ for C++ programmers”呢?(英文:http://www.andymcm.com/csharpfaq.htm,中文:http://www.royaloo.com/articles/articles_2002/CSharpFAQ.htm
声明1: 这篇FAQ不过是我对从形形色色的渠道搜集到的信息的个人阐述而已,来源包括DOTNET邮件列表以及五花八门的微软文档。这些答案并非多么得完备,甚至可能不完全正确。即使这些答案今天是正确的,说不定明天就不再如此。
声明2: 这篇FAQ和DOTNET邮件列表或Developmentor(把持这个列表的公司)或微软没有任何正式联系。
修订历史
2002-07-09 加入序列化一节
2002-06-07 修整“资源”部分,加入新的书籍推荐Weblogs主题
2002-01-17 很小的更新,以反映版本1的来临
2001-08-20 更新所有代码样例,使其完全兼容于Beta 2。
2002-06-28 很小的更新,以反映Beta2的到来。
2000-11-18 向“类库”一节,加入“跟踪”子小节
2000-11-13 为Beta1的到来而更新
2000-11-09 着手编写“类库”一节
2000-11-03 加入“IL”一节
2000-10-14 加入“代码访问安全”一节
2000-08-29 开始维护“修订历史”
2000-07-29 至 2000-08-28 杂七杂八的更新(没作记录)
2000-07-29 公布本FAQ第一版
内容
很难用一句话说清楚。按照微软的说法,.NET是一个“革命性的新平台,构建于开放的Internet协议和标准之上,并提供工具和服务,从而可以新的方式融合计算和通讯。”
更实在一些的定义应该是这个样子:.NET是一个用于开发和运行软件应用的新环境。特色有:开发基于Web的服务更加容易;具有丰富的标准运行时服务,可以被不同程序语言编写的组件所使用;具有跨语言和跨机器互操作能力。
注意,当术语“.NET”被用于此FAQ时,它只指新的.NET运行时以及与之相关的技术,有时称之为“.NET Framework”。这篇FAQ不涉及微软正往上贴.NET名头的任何其它五花八门的现有的和新的产品与技术(比方“SQL Server.NET”)。
非也。倘若你编写任何Windows软件(使用ATL/COM、MFC、VB甚至是原生的Win32 API),.NET可以为你目前行事方式提供一个可行的替代方案(或者说是一种额外的办法)。当然了,假如你在开发Web站点的话,.NET有许多可以勾起你兴趣的东西 — 不单单是ASP.NET。
比尔·盖茨在2000年6月22日举行的“论坛2000”上发表了一个演讲,描绘了.NET美景。在2000年7月召开的职业开发者大会期间,举行了许多关于.NET技术的会议。与会代表们还领到了包含有.NET framework/SDK和Visual Studio.NET预览版的光盘。
SDK 1.0和运行时的最终版公开发行于太平洋标准时间2002年1月15日下午6点左右,与此同时,Visual Studio.NET最终版也向MSDN订户开始发售。
有很多可用工具,下面以价格递增顺序加以描述:
.NET Framework SDK:SDK是免费的,包括C++、C#和VB.NET命令行编译器,以及许多其它辅助开发工具(译注:印象中并不包含C++命令行编译器)。
ASP.NET Web Matrix:这是一个来自微软的免费ASP.NET开发环境。除了GUI开发环境外,下载文件中还包括一个简单的Web服务器,可以用来代替IIS作为ASP.NET应用的宿主(host)。这为Windows XP家庭版用户打开了ASP.NET开发之门 — 这个版本的Windows不能运行IIS。
微软Visual C# .NET标准版:这是一个廉价的(大约100美元左右)Visual Studio.NET版本,仅限于一种语言,并且只有有限的向导支持,比方说,没有提供对类库或自定义UI控件的向导支持。它对于初学者学习是有用的,对于有本事在缺乏向导的情况下进行开发的机灵鬼来说,也是有意义的。除了C#版,还有VB.NETC++版。
微软Visual Studio.NET专业版:假如你有一个Visual Studio 6.0授权,你就可以获得升级版。在写这些文字的时候(2002年6月),微软正在对升级版提供300美元的邮寄折扣,因此,这是一个相当划算的买卖。Visual Studio.NET包括了对所有微软语言(C#、C++、VB.NET)的支持,它还包含广泛的向导支持。
位于“价格光谱”最高端的是 Visual Studio.NET企业版企业架构师版。它们提供了额外的功能,例如Visual Sourcesafe(用于版本控制)和性能与分析工具。可到这儿察看Visual Studio.NET功能比较:http://msdn.microsoft.com/vstudio/howtobuy/choosing.asp
运行时支持Windows XP、Windows 2000、NT4 SP6a和Windows ME/98,不支持Windows 95。Framework的某些组成部分,并不是在所有平台上都能工作,比方说,ASP.NET只能运行于Windows XP和Windows 2000上,Windows 98/ME不能用作开发环境。
Windows XP家庭版不支持IIS,因此它不能用来作为ASP.NET的宿主。不过,ASP.NET Web Matrix Web服务器,可以运行于XP家庭版上。
Mono项目试图在Linux上实现 .NET Framework。
微软提供了C#、C++、VB和JScript编译器,其它厂商已经宣称它们有意开发针对COBOL、Eiffel、Perl、 Smalltalk 和 Python等语言的.NET编译器。
以下是来自于http://msdn.microsoft.com/net/ecma/的一段话:“2001年12月13日,ECMA会员大会批准C#和通用语言基础设施(CLI)规范成为国际标准。在ECMA标准中,它们的名字将会是ECMA-334 (C#)和ECMA-335 (CLI)。”
CLR = Common Language Runtime(通用语言运行时)。CLR是一套标准资源,(理论上)可以被任何.NET程序所利用,而不管使用哪种程序语言。Robert Schmidt(微软)在他的MSDN PDC#文章里,列举了如下CLR资源:
  • 面向对象的编程模型(继承、多态、异常处理和垃圾收集等) 
  •   安全模型
  •   类型系统
  •   所有.NET基类
  •   许多.NET framework类
  • 开发、调试和评测(profiling)工具
  •   执行和代码管理
  •   IL到本地代码(IL-to-native)转换器和优化器
这对于.NET世界来说意味着什么?不同程序语言的能力将比过去任何时候更加趋同,尽管显然并非所有语言都能支持所有CLR服务。
CTS = Common Type System(通用类型系统)。这是.NET运行时能够理解的一大套类型,因此,.NET应用程序就可以使用它们。不过要注意,并不是所有.NET语言都支持CTS中所有类型的。CTS是CLS的一个超集。
CLS = Common Language Specification(通用语言规范)。它是CTS的一个子集,所有.NET语言都将支持它。任何使用CLS兼容的类型的程序,都可以和以任何语言编写的.NET程序进行互操作。
理论上,这允许在不同的.NET语言之间进行非常紧密地互操作。比方说,允许一个C#类从一个VB类继承下来。
IL = Intermediate Language(中间语言)。也称为MSIL(Microsoft Intermediate Language,微软中间语言)或CIL(Common Intermediate Language,通用中间语言)。所有.NET源代码(不管用哪种语言编写)都被编译成IL。当软件被安装时,IL转换为机器码,或在运行时被即时(Just-In-Time,JIT)编译器所处理。
C#是微软设计的一门新语言,用于和.NET framework协作。在它的白皮书《C#介绍》(“Introduction to C#”)中,微软是这么描述C#的:
“C#是一门派生于C和C++的简单、现代、面向对象和类型安全的程序语言。C#(发音C sharp)坚实地植身于C和C++语言家族树中,并将很快为C和C++程序员所熟悉。C#目标是将Visual Basic的高生产力和C++的原生威力联合起来。”
把上面一段话中的“C#”用“Java”取而代之,你会发现这个说法依然挺好 J
假如你是一名C++程序员,你可能乐意看看我的C# FAQ(英文:http://www.andymcm.com/csharpfaq.htm,中文:http://www.royaloo.com/articles/articles_2002/CSharpFAQ.htm)。
术语“托管(managed)”是许多混乱产生的起因,在.NET中,它被用于许多不同的地方,所表达的东西有着细微的差别。
托管代码(Managed code):.NET framewor为运行在它里面的程序提供了数个核心运行时服务,例如异常处理和安全。为了使这些服务能够运作起来,代码必须向运行时提供最起码的信息,这样的代码被称为托管代码。默认来说,所有C#和Visual Basic.NET代码都是托管的。VS7 C++代码缺省来说不是托管的,但可以通过指定命令行开关(/com+),使编译器生成托管代码。
托管数据(Managed data):这些数据由.NET运行时垃圾收集器进行配置和清除。C#和VB.NET数据总是托管的,默认来说,VS7 C++数据是非托管的,即使使用/com+开关也是如此,但可以用__gc关键字将其标记为托管的。
托管类(Managed classes):通常在C++托管扩展(Managed Extensions for C++)背景下谈及这个术语。当使用ME C++时,类别可标以__gc关键字。正如名字所暗示的,这意味着这个类的实体所占用的内存由垃圾收集器托管,但意思还不仅于此。这个类变成了一个完全交付给.NET大家庭的成员,它可享受.NET所带来的好处,同时也受.NET施加的有关制约。关于好处的一个例子是可以和其它语言编写的类很好地互操作,例如,一个managed C++类可以继承自一个VB类。关于约束的一个例子是,托管类只能继承自一个基类。
所有.NET编译器都为它们生成的模块(modules)中所定义的类型产生元数据(metadata),这些元数据和模块打包在一起(模块被打包于配件(assemblies)之中),并可通过反射机制(reflection)存取。System.Reflection名字空间中包含的类,可以用于“查询”模块/配件的类型。
使用反射来存取.NET元数据,非常类似于使用ITypeLib/ITypeInfo存取COM中的类型库数据,并且,它被用于类似的用途,比方说,决定数据类型尺寸,以用于跨越上下文/进程/机器边界列集(marshaling)数据。
反射还可以用于动态调用方法(methods)(参见System.Type.InvokeMember),或在运行时刻动态创建类型(参见System.Reflection.Emit.TypeBuilder)。
配件有时候被描述为一个逻辑.EXE或.DLL,可以是一个应用(application)(具有一个主入口点),也可以是一个库(library)。一个配件由一个或多个文件(dlls、exes以及html 文件等)构成,代表一组资源、类型定义以及对这些类型的实现。一个配件还可以包含对其它配件的引用。这些资源、类型和引用,描述于一个称为清单(manifest)的数据块之中。清单是配件的一个组成部分,从而使得配件具有自描述能力。
关于配件一个重要方面是,它们是类型身份(identity)的一个组成部分。类型的身份是由类型所处的配件和类型名字联合而成,这意味着,假如配件A导出一个叫T的类型,配件B也导出一个叫T的类型,.NET运行时会将它们视作两个完全不同的类型。此外,不要把配件和名字空间混为一谈。名字空间不过是一种用来按层次组织类型名字的方式,对于运行时来说,类型名字就是类型名字,不管用没用名字空间来组织名字。配件加上类型名字(不管这个类型名字是否属于一个名字空间),可向运行时标记一个唯一的类型。
对.NET安全来说,配件也非常重要,许多安全约束被强加于配件边界。
最后,在.NET中,配件是版本协调的单元。下面对此有更多描述。
生成配件最简单的办法是直接利用.NET编译器。例如下面C#程序:
public class CTest
{
     public CTest()
     {
          System.Console.WriteLine( "Hello from CTest" );
     }
}
可以这样将它编译成一个库配件(library assembly, dll):
csc /t:library ctest.cs
然后,可以运行“IL反汇编器(IL Disassembler)”来查看配件内容,这个工具是跟随.NET SDK一起分发的。
你也可以将源代码编译成模块(modules),然后使用配件连接器(assembly linker,al.exe)将模块连结成一个配件。对于C#编译器来说,可以使用/target:module开关生成一个模块,而不是生成一个配件。
位置和可见性: 一个私有配件通常为单个应用所使用,并且存储于这个应用所在的目录之中,或此目录下面的一个子目录中。共享配件通常存储在全局配件缓存(global assembly cache)之中,这是一个由.NET运行时所维护的配件仓库。共享配件通常是对许多应用都有用的代码库,比如.NET framework类。 
版本协调:运行时只对共享配件强加版本约束,对私有配件不起作用。
通过搜索目录路径来彼此发现。有几个因素会影响这个路径(比如应用域宿主(AppDomain host)和应用配置文件)。但对于私有配件来说,搜索路径通常是应用所在目录及其子目录,而对于共享配件来说,搜索路径通常和私有配件路径一样,外加共享配件缓存。
每一个配件都有一个称为兼容性版本的版本号,并且,对一个配件的每一个引用(从另外一个配件中进行引用),既包括被引用配件的名字,也包括版本号。
版本号由四个数字部分组成(例如:5.5.2.33)。前两个数字中,有任意一个不一样的配件,通常就被看作是不兼容的。假如前两个相同,但第三个不同,配件就被认为“可能兼容”。假如只有第四个数字是不一样的,配件就被认为是兼容的。不过,这只是默认的指导方针,是版本策略(version policy)决定了这些规则的强加范围。版本策略可经由应用配置文件来指定。
切记:版本协调只针对共享配件,对私有配件不起作用。
可将应用域想象为轻量级进程。多个应用域可以共存于一个Win32进程之中。应用域的首要用途是为了将应用程序彼此隔离开。
Win32进程通过占有截然不同的内存地址空间而提供隔离机制,这是很管用的,但它代价昂贵而且伸缩性不好。.NET运行时通过保持对内存使用的控制,来强制执行应用域隔离。应用域中的所有内存都由.NET运行时来托管,因此,运行时可以确保一个应用域里的东西不会存取别的应用域的内存。
应用域通常由宿主(hosts)创建。Windows外壳(Windows Shell)、ASP.NET和IE都是宿主的例子。当从命令行运行.NET应用时,宿主就是外壳。外壳为每一个应用创建一个新的应用域。
应用域还可以由.NET应用显式创建。这儿有一个C#例子,它创建了一个AppDomain,后者又创建了一个对象类的实体,然后执行这个对象的一个方法。注意,你必须将这段代码产生的可执行文件命名为appdomaintest.exe,以使其能够如期工作。
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", "CAppDomainInfo" );
          CAppDomainInfo adInfo = (CAppDomainInfo)(oh.Unwrap());
         string info = adInfo.GetAppDomainInfo();
        
          Console.WriteLine( "AppDomain info: " + info );
         return 0;
    }
}
4.3 我能编写自己的.NET宿主(.NET host)吗?
可以。关于做法的例子,可以参考Jason Whittington和Don Box开发的dm.net moniker的源代码(http://staff.develop.com/jasonw/clr/readme.htm)。在.NET SDK中,也有一个叫CorHost的代码例子。
垃圾收集是一种由运行时组件负责管理对象生命期以及它们所占用的堆内存(heap memory)的系统。对于.NET来说,这并不是什么新东西,Java以及很多其它语言/运行时使用垃圾收集机制已经有那么一段时间了。
没错。对于对象的销毁以及它所占用的内存的回收时机,垃圾收集器没有提供任何担保。
在档案库里有一个有意思的线索,由Chris Sells发起,是关于C#中对象的非确定性销毁的:
2000年十月,微软Brian Harry投递了对这个问题的长篇分析:
可在这儿看到Chris Sells对Brian贴子的应答:
这要归因于垃圾收集算法。.NET垃圾收集器的工作机理是,周期性地遍历被应用当前引用的所有对象的列表。在这个搜索过程中,凡是没有发现的对象,都将准备予以销毁,它们占用的内存也将被收回。这种算法暗示当对象的最后一个引用也被解除时,运行时并不会立即接到通知,只有下一次对堆(heap)进行清扫时,才能发现这个情况。
进一步而言,执行垃圾收集清扫次数越少,这类算法工作得越好。通常来说,堆的耗尽是收集清扫的触发条件。
当然是个问题,它影响了组件的设计。假如你的对象持有代价昂贵或稀缺的资源(比如数据库锁),你应该提供某种办法,使得客户程序可以在完事时,告诉对象释放掉这些资源。微软建议你提供一个叫Dispose()的方法来达到这个目的。然而,对于分布式对象来说,这将会导致问题。在一个分布式的系统中,谁来调用Dispose()方法?需要有某种形式的引用计数机制或所有权管理机制,来操纵分布式对象,不幸的是,运行时对此没有提供任何帮助。
是的。当在托管代码中使用COM对象时,你大大依赖于垃圾收集器来调用终结方法,以释放你的对象。假如你的COM对象持有一个代价高昂的资源,而它只能在最终释放时得以清除的话,你可能要为你的对象提供一个新的接口,从而支持显式的Dispose()方法。
一个具有Finalize方法的对象比没有这个方法的对象更适合于垃圾收集器。不存在关于对象终结顺序的保证,因此,从Finalize方法中去存取别的对象是有问题的。最后,不存在对一个对象调用Finalize方法的任何担保,因此,永远都不应该依赖它来清除对象所占用的资源。
微软推荐如下解决模式:
public class CTest : IDisposable
{
     public void Dispose()
     {
         ... // Cleanup activities
          GC.SuppressFinalize(this);
     }
 
     ~CTest() // C# syntax hiding the Finalize() method
     {
          Dispose();
     }
}
通常情况下,客户调用Dispose(),对象的资源得到了释放,垃圾收集器通过调用SuppressFinalize(),以免除履行终结义务。在最坏的情况下,也就是说,在客户忘记调用Dispose()的情况下,就有一个合情合理的机会,通过垃圾收集器对Finalize()的调用,对象的资源将最终得到释放。鉴于垃圾收集算法的局限性,这看起来象是一个相当合理的办法。
一点点。举例来说,System.GC类暴露了一个Collect方法,它强迫垃圾收集器立即去收集所有不再被引用的对象。

许多有意思的统计结果经由“.NET CLR xxx”性能计数器从.NET运行时输出,可以使用性能监视器来查看它们。
序列化是将对象转换为字节流的过程。反序列化是从字节流构建对象的反向过程。序列化/反序列化通常用于对象的传输(比方说,在远程化(remoting)的过程中),或者将对象持久化(比方说,存到文件中或数据库里)。
.NET类库提供了两类独立的机制:XmlSerializer和SoapFormatter/BinaryFormatter。微软将XmlSerializer 用于 Web Services,而将SoapFormatter/BinaryFormatter用于远程化。在你自己的代码里,两个随便你用。
要视具体情况而定。XmlSerializer有些严格的限制,比方说,要求目标类有一个无参数的构造器,并且,只有公开的读/写属性和字段才可被序列化。不过,从好的一面来说,XmlSerializer对定制XML文档提供了良好支持。XmlSerializer的特性意味着,它最适合于用在跨平台的情况下,或者用于从现有的XML文档构建对象。
SoapFormatter和BinaryFormatter比XmlSerializer的限制要少,比如说,它们可以序列化私有字段。然而,它们都需要目标类标以[Serializable]特性,因此,象XmlSerializer一样,需要小心以序列化方式来编写类。有些诡异的东西也是要小心提防的,比方说,在反序列化时,新对象的构造器并不被调用。
对SoapFormatter和BinaryFormatter的选择,取决于应用。BinaryFormatter对于序列化和反序列化都是发生在.NET平台上并且执行性能很重要的情况大有用处。通常来说,在所有其它情况下,SoapFormatter更有意义,假如没出什么岔子的话,它更易于调试。
可以。XmlSerializer支持很多特性(attributes),它们可以用于为特定的类设定序列化方式。例如,可将某个字段或属性标以[XmlIgnore]特性,从而将其排除在序列化机制之外。另外一个例子是[XmlElement]特性,它可用于指定XML元素名称 — 这个元素名称可被用于特定的属性或字段。
对于经由SoapFormatter/BinaryFormatter的序列化,也可以使用特性进行某种程度的控制。例如,[NonSerialized]特性等价于XmlSerializer的[XmlIgnore]特性。对序列化过程的终极控制,可以通过在其实体打算被序列化的类上,实现ISerializable接口。
XmlSerializer有一个once-per-process-per-type负担。因此,当你在应用中第一次序列化或反序列化一个给定类型的对象时,会感觉到明显的延迟。通常来说,这不是个问题,但这或许意味对于GUI应用启动期间装载配置信息来说,XmlSerializer不是一个好的选择。
XmlSerializer不能用于序列化任何实现了IDictionary接口的类的实体,比方说Hashtable。但SoapFormatter和BinaryFormatter没有这个限制。
可查看抛出异常的InnerException 属性,以获得更详尽的出错消息。
至少有两类.NET特性。第一类我称之为元数据特性(metadata attribute),它允许将一些数据附在类或方法上,这些数据成为类的元数据的一部分,并(象类的其它元数据一样)可以反射(reflection)方式访问。关于元数据特性的例子之一是[serializable],它可以附加在某个类上,表示这个类的实体可被序列化。
[serializable] public class CTest {}
另外一类是上下文特性(context attribute)。上下文特性和元数据特性使用类似的语法,但它们之间有着根本不同。上下文特性提供了一种截取机制,藉此,实体激活和方法调用就可被早一些或晚一些处理。假如你对Keith Brown的universal delegator印象深刻的话,你应该对这个思想感到熟悉。(译注:可访问http://www.develop.com/kbrown/,以了解关于universal delegator更多信息)
可以。只需从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(true);
 
          foreach( object att in atts )
              if( att is InspiredByAttribute )
                   Console.WriteLine( "Class CTest was inspired by {0}", ((InspiredByAttribute)att).InspiredBy );
     }
}
可以。看看Don Box的例子(名叫CallThreshold): http://www.develop.com/dbox/dotnet/threshold/,以及Peter Drayton 的 Tracehook.NET: http://www.razorsoft.net/
CAS是.NET安全模型的一个组成部分,它决定某段代码是否允许执行,以及它在运行时可以使用什么样的资源。例如,CAS可以防止.NET Web小程序(applet)格式化你的硬盘。
CAS安全策略(policy)围绕两个关键概念:代码组(code groups)和权限(permissions)。每一个.NET配件都是一个特定代码组的成员,每一个代码组都被授予在某个命名权限集(named permission set)里设定的权限。
例如,假如使用默认的安全策略,从某个Web站点上下载的控件属于“Zone – Internet”代码组,这个代码组就具有由“Internet”命名权限集所定义的权限。(自然而然,“Internet”命名权限集代表着一个非常有限的范围的权限)
微软定义了一些默认的代码组,但你可以修改它们,甚至创建你自己的代码组。要想看看机器上定义的代码组,可以命令行方式运行“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”),然后被分成几个子组,每一个子组还可以再细分下去。还要注意的是(这有点违反直觉),子组可以关联有比其父组更放得开的权限集。
使用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自己生成的东西,目的是为了易于以命令行方式查看代码组,底层的运行时永远都看不到这些东西。
使用caspol。假如你是机器的系统管理员的话,你可以在“机器”层进行操作,这意味着你做的更改不但会成为那台机器的默认设置,而且用户也不能够修改放大这些权限。假如你是一个普通用户((不是系统管理员)),你仍然可以修改权限,但只能将权限缩小。例如,为了让Intranet代码能够做它想做的事,你可以这么做。
caspol -cg 1.2 FullTrust
注意,因为这比默认策略(在一个标准系统之中)的权限来得大,你只能在机器层做这个修改,在用户层做这样的修改没什么效果。
可以。使用caspol –ap,定义一个XML文件,它包含有权限集中所要包含的权限。为了节省你一点时间,这儿(http://www.eponymous.eclipse.co.uk/samplepermset.xml)有一个相应于“Everything”权限集的样例文件,你可以修改它以满足你的需要。改好以后,可以这样将它加入到可用的权限集之中:
caspol -ap samplepermset.xml
接下来,为了将这个权限集施加于某个代码组上,可以这么做:
caspol -cg 1.3 SamplePermSet
(默认来说,1.3是“Internet”代码组)
Caspol有一些选项可能会派上用场。首先,你可以使用caspol –rsg,叫caspol告诉你配件所属的代码组是什么。同样地,你也可以使用caspol –rsg,来查询是什么样的权限施加于指定的配件之上了。
可以,只要你是一名系统管理员的话。运行caspol -s off 即可。
可以。微软提供了一个叫作Ildasm的工具,可以用于查看配件的IL和元数据。
可以。通常来说,从IL重新生成高级源码(比如C#)是相当容易的。
目前还没有什么简单的办法来阻止从IL“反向工程”出代码。将来或许可以利用一些IL混淆工具(IL obfuscation tools),它们可能来自微软,也可能来自第三方厂商。这些工具通过对IL进行特殊处理,使得“反向工程”变得大大困难。
当然,假如你编写web services的话,“反向工程”就不是个问题了,因为客户无法直接访问你的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形式的配件。
可以。举两个简单的例子:在IL中,你可以抛出不是从System.Exception派生出来的异常,你还可以使用下标不是从0开始的数组。
这个话题招致了大量的争论,假如你阅读了邮件列表档案资料你就知道了。看看下面这两条线索:
我是这么看的:COM内容丰富,对于不同的人它代表着不同的东西,但对我而言,COM从根本上来说,是小块代码之间如何彼此发现并相互通讯的机制。COM精确规定了如何定位和通讯。在一个纯粹的.NET世界里,一切全由.NET对象构成,小块代码仍然需要彼此发现和交流,但它们并非使用COM手段。它们使用一种从某些方面来说类似于COM的模型,比如以表格形式同组件一起打包存储的类型信息,非常类似于和COM组件一起打包存储的类型库信息,但它并非COM。
那么,这有什么要紧的吗?唔,我真的不关心大多数COM东西渐渐离去。我不会在意不需要跑到注册表里转一圈的组件查找方式,我也不在乎不使用IDL来定义我的接口。但是,有一件事情我不会放弃:我不想丢掉基于接口的开发思想。在我看来,COM最伟大的力量,就是它对接口和实现之间进行分离的铸铁般地坚持。不幸的是,.NET framework看来并没有如此坚持,它让你能够做基于接口的开发,但它并未坚持非这样不可。有人可能会争论多一种选择从来都不是一件坏事,或许他们是对的,但我无法不想像这可能是一个退步。
对于.NET开发人员来说,它已经濒临死亡了。.NET Framework 提供了一个不是基于DCOM的新的远程化模型,当然了,在互操作的情形下,还会用到DCOM的。
不然。第一版的.NET提供了对现有COM+服务的访问(通过一个互操作层),而不是使用一个本地.NET服务来取代它。有一些五花八门的工具和功能以试图使这种访问尽可能少一些苦痛。职业开发人员大会上发送的.NET SDK,包括对COM+核心服务的互操作支持(比如JIT激活、事务等),但尚未提供对某些更高层服务(例如COM+事件、队列组件等)的支持。
随着时间的推移,我们期望互操作能够变得更加无缝,这或许意味着某些服务将成为CLR的核心组成部分,并且(或者)这意味着某些服务将会以托管代码重写,以运行于CLR之上。
关于这个主题的更多讨论,可以到档案中心搜索Joe Long的帖子。Joe是微软COM+团队项目经理。可从下面这条消息开始:
可以。在.NET运行时中,可以经由运行时可调用包装器(Runtime Callable Wrapper,RCW)来访问COM组件。这个包装器将COM组件暴露的COM接口转换为.NET兼容的接口。对于OLE自动化接口而言,RCW可以从类型库自动生成。对于非OLE自动化接口来说,可能需要开发一个定制的RCW,以将COM接口暴露的类型以手工方式映射到.NET兼容的类型。
对于那些熟悉ATL的开发人员来说,这儿有一个简单例子。首先,创建一个实现了如下IDL的ATL组件:
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;
     };
};
一旦创建好组件,你应该能够得到一个类型库,对这个类型库运行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() );
     }
}
注意,我们使用类型库的名字作为名字空间,并以COM类的名字作为类的名字。作为代替,我们也可以使用CPPCOMSERVERLib.CppName作为类的名字,并且不使用using CPPCOMSERVERLib语句。
可以这样编译这些C#代码:
csc /r:cppcomserverlib.dll csharpcomclient.cs
注意,编译器被告知去引用我们先前使用TLBIMP从类型库生成的DLL。
现在应该可以运行csharpcomclient.exe,并在控制台上得到如下输出:
Name is bob
可以。在COM中,可以经由COM可调用包装器(COM Callable Wrapper,CCW)来访问.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 /codebase
现在,需要创建一个客户程序来测试你的.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”。
一个替代办法是使用由Jason Whittington 和 Don Box开发的dm.net moniker,可以到这儿看看:http://staff.develop.com/jasonw/clr/readme.htm
没错,假如你在编写运行于.NET framework里的应用的话。当然,有很多开发人员可能希望继续使用ATL来编写运行于framework之外的C++ COM组件,但是,假如你在framework之内的话,你将几乎可以肯定想使用C#。原生的C++(ATL的基础)在.NET世界中并没有太多的位置,它太接近于底层并且提供了太多的弹性,.NET运行时难以管制它。
.NET remoting涉及沿着通道(channels)发送消息。有HTTP和TCP两个标准通道。TCP通道目的仅仅是为了用于局域网(LANs),而HTTP通道既可以用于局域网,也可以用于广域网(WANs,Internet)。
提供了多种消息序列化格式支持,例如SOAP格式(基于XML)和二进制格式。默认来说,HTTP通道使用SOAP格式(经由.NET运行时序列化SOAP格式器(Serialization SOAP Formatter)),TCP通道使用二进制方式(经由.NET运行时序列化二进制格式器(Serialization Binary Formatter)),其实两种通道都可以使用任一种序列化格式。
有如下一些远程存取风格:
SingleCall :来自客户的每一个请求都由一个新的对象提供服务,当请求完成时,对象也就被抛弃了。这种(本质上是无状态的)模型,在ASP.NET环境中,可以利用ASP.NET状态服务来存储应用或会话状态,从而可被做成有状态的。
Singleton :来自任何客户的任何请求都由单个服务器对象处理。
Client-activated object :这是一种老的有状态的COM/DCOM模型。客户获得远程对象的引用,然后保持那个引用(从而保持远程对象处于存活状态),直至不再使用。
对象的分布式垃圾收集由一个称为“基于租约的生命期(leased based lifetime)”的系统所管理。每一个对象都有一个租约期,当到期时,对象从.NET 运行时远程化基础设施分离。对象有一个默认的续约时间,当某个客户对这个对象进行了成功调用时,租约重新起算,客户也可以显式重置租约时间。
假如你对使用XML-RPC作为SOAP的替代品感兴趣,可看看Charles Cook的XML-RPC.Net站点:http://www.cookcomputing.com/xmlrpc/xmlrpc.shtml
使用平台调用服务(P/Invoke)。它使用了和COM互操作类似的技术,但它是用来访问静态DLL入口点而不是COM对象的。这儿有一个C#调用Win32 MessageBox函数的例子:
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 );
     }
首先,使用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()是不必要的。
类似于读的例子,除了使用StreamWriter代替 StreamReader外。
类似于读/写文本文件,除了使用BinaryReader/BinaryWriter对象而不是StreamReader/StreamWriter对象来包装FileStream对象外。
使用System.IO.File的静态Delete()方法:
File.Delete( @"c:/test.txt" );
支持。可以使用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 );
}
首先使用System.Net.WebRequestFactory类获得一个WebRequest对象:
WebRequest request = WebRequest.Create( "http://localhost" );
然后索取对request(请求)的response(响应):
WebResponse response = request.GetResponse();
GetResponse方法一直阻塞,直到下载完成。然后,你就可以象这样来访问应答流(response stream):
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相关功能。
有两种方式。针对所有Web请求,可以这么做:
System.Net.GlobalProxySelection.Select = new WebProxy( "proxyname", 80 );
倘若为指定的Web请求设置代理,可以这么做:
HttpWebRequest request = (HttpWebRequest)WebRequest.Create( "http://localhost" );
request.Proxy = new WebProxy( "proxyname", 80 );
支持。看看这个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
不支持。取而代之的是,提供了一个新的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 );
     }
}
支持。使用XPathXXX类:
XPathDocument xpdoc = new XPathDocument("test.xml");
XPathNavigator nav = xpdoc.CreateNavigator();
XPathExpression expr = nav.Compile("descendant::PEOPLE/PERSON");  
XPathNodeIterator iterator = nav.Select(expr);
while (iterator.MoveNext())
     Console.WriteLine(iterator.Current);
支持。对多线程提供了广泛支持。可以产生新线程,系统还提供了一个线程池(thread pool)供应用程序使用。
创建一个System.Threading.Thread实体,传入一个将在这个新线程里执行的ThreadStart委托(delegate)的实体。例如:
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();
有几种办法。你可以使用你自己的通讯机制去告诉ThreadStart方法停止执行。Thread类也提供了通知线程终止的内建支持。两种首要的方法是Thread.Interrupt() 和 Thread.Abort(),前者将会导致对线程抛出一个ThreadInterruptedException异常 — 当它下一次进入WaitJoinSleep状态时。换句话说,Thread.Interrupt是当线程不再做任何有意义的工作时,叫它终止的礼貌方式。与之形成对照的是,Thread.Abort()抛出一个ThreadAbortException异常,而不管线程正在做些什么。此外,通常无法捕获ThreadAbortException异常(尽管ThreadStart的finally方法将会执行)。Thread.Abort()是一种严厉的机制,一般情况下,不应该用它。
通过向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 );
     }
}
没有什么办法可以用来向线程池查询这种信息。你必须将代码放入WaitCallback方法之中以发出信号,表示它已经结束了。在这种情况下,事件(Events)就派上了用场。
每一个对象都有一个与之关联的并发锁(临界区段(critical section))。可以使用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();
}
有,在System.Diagnostics名字空间中。有两个主要的类用于处理跟踪:Debug和Trace。它们工作方式类似,区别在于,只有当定义了DEBUG符号时,Debug类才能进行跟踪,而只有当定义了TRACE符号时,Trace类才能进行跟踪。通常来说,这意味着你应该在进行调试版和发行版的编连(build)时,采用System.Diagnostics.Trace.WriteLine来进行跟踪,而在进行调试版的编连时,采用System.Diagnostics.Debug.WriteLine来进行跟踪。
可以。Debug和Trace类都有一个Listeners属性,它是一个接收器(sinks)集合,接收分别经由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.Flush();
注意,要使用Trace.Listeners.Clear()移去默认的监听器。假如你不这么做的话,结果将会同时输出到文件和OutputDebugString()中去。通常来说,这不是你想要的,因为OutputDebugString()对性能造成了很大的冲击。
可以。你可以编写你自己的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;
}
(注意,这个实现并不完整。比方说,没有覆写(overridden)TraceListener.Write方法)
这个方法的好处是,当MyListener的一个实体被加入到Trace.Listeners集合中时,对Trace.WriteLine()的所有调用,都要经过MyListener,包括对MyListener类一无所知的被引用的配件所做的调用,也不例外。
我推荐如下书籍,要么是因为我个人喜欢,要么是因为我认为它们为其他.NET开发人员所看重。(注意,假如你根据下面链接之一购书,我将从Amazon那儿得到一些佣金。)
l          Understanding .NET: A Tutorial and Analysis》David Chappell 侯捷/荣耀 译
作为一名企业级软件资深开发人员,对于这本书,我个人给予极高评价。任何一位需要全面了解.NET的架构师、程序员和技术管理人员,这本书,都应该人手一本。(荣耀推荐)
l          Applied Microsoft .NET Framework Programming》Jeffrey Richter
已经有太多的期待。这主要归因于Richter精彩的Win32书籍,绝大多数人认为它不负众望。“applied”一词有点误导,这本书主要是关于.NET Framework底层工作机理的。例子是用C#编写的,不过,本书也有单独的VB版本。
l          Programming Windows with C#》Charles Petzold
又一个有点误导的标题。这本书仅限于GUI编程 — Windows Forms和GDI+。写得不错,内容全面。我对它唯一的(小的)批评是,这本书过于紧贴事实,很少提及编写真实应用所需要的“技巧和窍门”。
l          Developing Applications with Visual Studio.NET》Richard Grimes
包含了其它书籍所不包含的大量有趣的主题,包括ATL7、Managed C++、国际化(internationalization)、远程化(remoting),连同更多常见的CLR和C#的东西,还包含大量关于Visual Studio IDE的内容。这本书尤其适合具有相当经验的C++程序员阅读。
l          C# and the .NET Platform》Andrew Troelsen
受到很多人的推崇,被认为可能是最好的C#/.NET书籍。内容广泛,涵盖Windows Forms、COM互操作、ADO.NET、ASP.NET等等。Troelsen还写了一本倍受尊重的VB.NET书籍:《Visual Basic .NET and the .NET Platform: An Advanced Guide》。
l          Programming Microsoft Visual Basic .NET》Francesco Balena
Balena是一位VB专家,他的VB.NET书籍好评如潮。
l          .NET and COM - The Complete Interoperability Guide》Adam Nathan
被视作.NET/COM互操作圣经,受到广泛尊重。
l          Advanced .NET Remoting》Ingo Rammer
被广泛推荐。
l          微软.NET主页位于http://www.microsoft.com/net/,微软还把持GOTDOTNET(http://www.gotdotnet.com/)。
l          DevX 的.NET Zonehttp://www.devx.com/dotnet/)。
l          http://www.cetus-links.org/oo_dotnet.html是一组极好的.NET资源链接。
l          Chris Sells 有一些精彩的.NET链接,见http://www.sellsbrothers.com/links/#manlinks
l          CSharp.org (http://www.csharp.org/)。
l          microsoft.public.dotnet.* 新闻组。
l          我的C# FAQ for C++ Programmers位于http://www.andymcm.com/csharpfaq.htm,中文版请访问http://www.royaloo.com/articles/articles_2002/dotNetFAQ.htm
以下Weblogs有一些正规的.NET内容:
l          The .NET Guy (Brad Wilson) (http://www.quality.nu/dotnetguy/
l          Charles Cook: XML-RPC.NET开发者(http://www.cookcomputing.com/
l          John Lam (http://www.iunknown.com/
l          Peter Drayton:《C# Essentials》和《C# in a Nutshell》合著者 (http://www.cookcomputing.com/
l          Ingo Rammer:《Advanced .NET remoting》作者 (http://www.dotnetremoting.cc/DotNetCentric/
l          Drew Marsh (http://radio.weblogs.com/0104813/
l          Tomas Restrepo (http://www.winterdom.com/weblog/
l          Justin Rudd (http://www.pinetree-tech.com/weblog/
l          Simon Fell:PocketSOAP开发者 (http://www.pocketsoap.com/weblog/
l          Richard Caetano (http://www.stronglytyped.com/
l          sellsbrothers.com:Windows Developer News:并非真正的blog,但包括一些正规的.NET相关新闻 (http://www.sellsbrothers.com/#news
l          Lutz Roeder有一些精彩的工具和库,位于http://www.aisto.com/roeder/dotnet/
l          Peter Drayton的.NET Goodies页面位于http://www.razorsoft.net/
l          Don Box & Jason Whittington的dm.net COM moniker位于http://staff.develop.com/jasonw/clr/readme.htm
l          Mike Woodring有一些 .NET例子,见http://staff.develop.com/woodring/dotnet/
  l          Charles Cook的XML-RPC.Net库,可在http://www.cookcomputing.com/找到。
-完-

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值