Effective C#笔记(4)

来源:互联网 发布:单片机 蓝牙 编辑:程序博客网 时间:2024/06/05 10:21

这章主要讲如何创建二进制组件(Component),组件的Assembly是为了更容易共享组件里面的逻辑,利用夸语言编程的功能,使得发布更容易。减少两个组件之间的的耦合度可以使得组件的发布变得更容易。下面主要就介绍如何创建易用,易发布,易更新的Assembly(Assmeblies)。
CLR加载Assembly是根据需要来的,只有使用到的Assembly才会加载进内存里面。首先,CLR会决定什么文件需要加载,在Assembly的无数据中记录了这个Assembly所依赖的其它Assembly,对于强命名的Assembly来说,会包括名字,版本,Culture,Key Token在里面,而对于弱命名的Assembly,只有名字存在这个记录里面。使用强命名的Assembly可以防止恶意的软件替换你的Assembly。对于强命名的Assembly,首先会在GAC(Global Assembly Cache)里面查找是否已经存在,如果在配置文件里面存在Codebase目录,则会对这个目录查找,如果该目录里面找不到,则加载这个Assembly失败。如果没有Codebase目录,则会从当前应用程序目录,Cluture子目录,Assembly子目录(Culture和Assembly子目录是组合的,比如可以是culture下的Assembly子目录,也可以是Assembly下的Culture子目录)里面查找。
由上面的描述可以知道,只有强命名的的Assembly可以存储在GAC里面,可能通过配置文件去修改Assembly更新的默认行为,强命名的Assembly通过防止恶意的篡改(Malicious Tampering)具有更高的安全性。
因此,我们创建Assembly的时候,要尽量创建强命名的Assembly,并把元数据里面的所有信息都填写完整。
对于AssemblyCulture,只有当Assembly具有本地化的资源的时候,才需要填写。
对于AssemblyVersion版本,可以采用默认的1.0.*,这样编译器会根据编译的时间来生成后面的编译版本(Build Version)和修改版本(Revision Version),这样保证版本号是永远向上增长的。但要注意对于COM的Assembly来说,不要采用编译器自动生成的版本号,因为每一个版本的COM的Assembly都会在注册表里面注册一个记录,这样如果每个都修改版本号,会很快把注册表塞满。
对于强命名的Assembly来说,也有一个例外,就是对于ASP.NET的程序,强命名的Assembly加载得不正确(不知3.0修正了没有),而且强命名的Assembly必须添加AllowParticiallyTrustedCallers属性,不然没法在非强命名的Assembly中访问。
我们通过配置文件来修改引用的Assembly,可能通过应用程序配置文件,GAC的Publisher Policy File(什么来的?)和机器的配置文件来更新,但通常情况下,不要去修改机器配置文件,对于单个应用程序的更新,通过应用程序配置文件来更新,而对于多个应用程序之间共用Assembly的更新,通过Publisher Policy File来更新。

(1) 创建CLS-Compliance的Assembly

CLS是Common Language Subsystem,遵循CLS的Assembly能够被其它不同语言的程序所使用。顺从CLS的Assembly必须限制Assembly的公共接口在CLS的规定里面。具体来说,可以分成以下两点:(a) Pulbic或者Protected的函数的方法的返回值,参数必须是在CLS规定里面的;(b) 所有非CLS-Compliant的public或者Protected变量必须包含一个CLS-Compliant等价的方法。
在Assemply加上CLSCompliant(true)就能够保证编译器检查出所有不遵循CLS规定的public或者Protected方法。比如:public UInt32 Foo(){}就不能通过编译,因为UInt32不是CLS-Compliant的。
对于Assembly是供其它程序使用的,如果这个Assembly不是CLS-Compliant的,则使用它的Assembly要想达到CLS-Compliant就会比较困难。
对于操作符重载,不是所有的语言都支持操作符重载的,CLS标准也没有规定不能采用。对于重载了的操作符,在支持操作符重载的函数中使用时,就一样可以使用,而在不支持操作符重载的函数中,则需要通过op_XXX函数来调用。比如op_equals就对应于重载的=号操作符。所以如果你希望你重载的操作符被其它语言所使用,最好提供一个等价的函数,这样,别的语言在使用的时候就可以直接调用那个函数。
最后就要特别注意多态的参数通过接口暴露出非CLS-Compliant的类。最常见的就是事件的参数,比如:
Internal class BadEventArgs : EventArgs { internal UInt32 ErrorCode; }
public delete void MyEventHandler(object sender, EventArgs args);
public event MyEventHandler OnStuffHappers;
BadEventArgs arg = new BadEventArg();
OnStuffHappens(this, arg);
非CLS-Compliant的BadEventArgs通过事件传递给其它语言,但其它语言并不能处理这个参数,而引起错误。
对于接口来说,如果被声明在CLS-Compliant的Assembly里面,那么他就是CLS-Compliant的,否则就不是,即使他的参数那些都顺应了CLS的规定。
对于一个非CLS-Compliant的接口,你可以在CLS-Compliant的类中实现这个接口,但必须用的接口实现,例如:
public inter IFoo { void DoStuff (UInt32 arg); }
public class MyType : IFoo
{
    void IFoo.DoStuff(UInt32 arg) {}
}
IFoo并不是CLS-Compliant的,但在CLS-Compliant的MyType里面,仍然可以实现这个接口,只不过对这个方法的引用不能通过MyType.DoStuff,而只能通过IFoo.DoStuff。这样也没有破坏MyType暴露出来的公共接口必须是CLS-Compliant的规则。因为DoStuff是通过IFoo暴露出来的,本身IFoo就不是CLS-Compliant的。

(2) 尽量使用简短的函数

一些有经验的程序员经常会自己手工去优化代码的效率,但实际上有时候会弄巧成拙。比如如果为了避免函数的调用而写一个很长很长的函数,反而会降低代码执行的效率。
首先要知道编译器首先生成中间语言存储在Assembly里面,然后由JIT编译器将中间语言翻译成机器代码去执行。而JIT调用是由函数级别来调用的,只有调用到的函数才会被翻译成机器语言去执行。如果一个很长的函数里面存在很复杂的逻辑,而且有些子句并不需要执行的,放在一个函数里面会使得JIT必须将整个函数里面的语句,包括不需要执行的语句,也翻译成机器语言,然后执行。如果再把这个大的函数分成几个小的函数,那么只有使用到的函数才会被JIT加载进来。特别是在if-else子句或者switch子句的时候,更是如此。
另一个原因是简短的函数使得JIT的Enregistrations(把一些变量存储在寄存器以加快运行的速度)变得更容易。简短的使得JI更容易分辨哪些变量可以被放到寄存器里面。
另外,JIT同时也决定了哪些函数要被内联(Inline)。越是简短的函数,越容易被内联进执行代码中。
在机器层次上的优化并不是程序员的责任,可以放心地把这些交给C#编译器和JIT编译器来做。他们所采用的算法并不是固定的,但肯定是最优或者比较优的,而且这些算法也随着时间的发展而改进,我们不需要自己手工去优化这些效率。我们所要做的就是尽量使得函数简单,短小。

(3) 创建小的,粘性高的Assembly

粘性(Cohesion) is the degree to which the responsibility of a single component form a meaningful unit.
相对于一个大的Assembly来说,使用多个小的Assembly可以使Assembly的更新更容易。更小的assembly也使得程序的启动更快,虽然只有被调用的方法才被JIT转换成机器代码,但整个Assembly都会被加载进放在,并且CLR会为每个方法都生成一个Stub。
当然,这也不是鼓励你把一个类作为一个Assembly,过多的assbmbly会降低性能。加载更多的assembly需要更多的工作;转换成机器代码也需要更多的时间,特别是解决函数的地址问题的时候,跨assembly需要更长的时间;跨assembly之间的安全检查也会浪费时间,同一个assembly里面的代码是在同一个信任层次的,不同的assembly之间的访问,CLR会检查安全问题。
然后C#和.NET在设计的时候就支持很多的组件,因此更好的灵活性通常情况下也会值得这些代价,只要这些assembly之间的划分是合理的。
另外,小也没有一个绝对的概念,比如Microlib.dll就有24M,然而System.Web.RegularExpression就只有56K。

(4) 限制你的类的可见度

你应该给你的类最低的可见度(Visibility)。通常情况下,你都会高估了你的类所需的可见度。Private和Protected的类型可以实现public的接口,然后所有的用户都能够通过这个public的接口来访问这个private的类型。类库里面比较经典的就是ArrayList里面的ArrayListEnumerator,这个类实现了IEnumerator接口,但这个类是private的,所有的用户都能够访问这个Private类的方法。
public class ArrayList : IEnumerable
{
    private class ArrayListEnumerator : IEnumerator {}
    public IEnumerator GetEnumerator()
    { return new ArrayListEnumerator(); }
}
通过降低你的类的可见度,你就降低了你以后更新类的时候需要修改的代码。一旦你的类声明为public方法并且发布之后,你就必须永远维护这个接口,这样就限制了以后你更新你代码的灵活性。而对于private或者internal的方法,你可以更灵活地进行你所希望的所有修改。

(5) 创建高粒度的Web API

对于web程序来说,高粒度的API意味着客户端和服务器端的传输次数变得更少。Web程序中最昂贵的操作在于服务器与客户端的传输。客户端应该一次性把所有服务器端需要的数据传到服务器,服务器也应该一次性把客户端所有需要的数据传给客户端。低粒度的API会使得客户端和服务器端不断地进行数据的交互。
当然,也要考虑到 Transaction的问题,而不是越多数据一次性传输越好,所以这里面就存在着如何平衡的问题了。

原创粉丝点击