代码改变世界

【读书笔记】.NET本质论第三章-Type Basics(Part 1)

2009-06-28 22:16  横刀天笑  阅读(1158)  评论(0编辑  收藏  举报
第二章讨论的是程序集等内容。实际上作为开发者,我们更多的时候是在考虑如何组织一个类型。一个类型该放哪些东西呢?这个类型该具有哪些职责呢?所以,类型才是我们常常思考的单元。

要唯一确定一个类型,就需要类型的完全限定名:

命名空间.类型名,程序集名(这里分强命名还是弱命名文件名,语言文化,PublickeyToken,版本号)。这种完全限定名在.NETconfig配置文件里用的比较多:

<add verb="*" path="get-nodes.aspx" type="WebHandler.GetNodesHandler,WebHandler"/>

这是Web程序里,典型的用来配置HttpHandler的,用了这个完全限定名,ASP.NET就可以轻松的将请求路由到那个类型。

对于一个类型,实际上只有三种成员:字段、方法、嵌套类型。

那可能有人会说:事件、属性、索引呢。实际上这些都是编译器提供的语法支持,封装字段和方法的“包装成员”。

比如属性,实际上就是一个get_xxx方法和set_xxx方法(如果该属性是可读可写的),这其中xxx就是该属性的名字。不过我们通常将属性与一个私有字段配合,不过属性本身和这个私有字段没有半点关系。也就是说属性就是两个方法加一些元数据。

一个类包含的成员按照所有者又可分为两种:类型成员和实例成员。

C#里,类型成员使用static来修饰(vb里用Shared,个人看来Shared更能表明类型成员真实的含义)。虽然C#里也沿用了C里的static关键字,可千万不要混淆了这两个概念。

对于字段而言,就代表着内存如何分配。用static修饰的字段,只会分配一次内存,而默认情况下,字段是实例字段,每次实例化一个类型时都会分配内存。这样一看,好像这个static好像是共享的,所以前面说VB.NETShared关键字更能表明其含义。对于方法也是一样,static的方法属于类型的,必须通过类型名来访问(这点和Java不同)。

NOTE常常在网络上看到有这样的提问:何时使用一个静态方法呢?对于这个也许每个人的看法都不同,可能涉及设计理念的问题。不过我个人觉得,一个静态方法,属于它的类型,所以静态方法应该是跟这个类型的领域职责联系不紧密的,属于一个中性的方法。比如有很多辅助方法、管理者类里就有很多静态方法,而这样的类一般都是一些与领域不相关的。

 

对于static的字段,CLR会根据该字段的类型初始化该字段的值,如果该字段是数字类型的则会初始化为0,如果是布尔类型的则会初始化为false,而对于是引用类型的则初始化为nullCLR还会按照这个原则初始化应该分配在堆上的成员。

值类型一定分配在栈上么?
也许受到某类书的迫害,很多人在骨子里一直认为:引用类型分配在堆上,值类型分配在栈上。但是又想不通,如果一个类,它的实例应该分配在堆上,那它的实例字段是在栈上还是堆上呢。对于一个值类型,如果它是类型的字段成员(我不认为一个方法的参数和方法内的局部变量是它所在类型的成员)则分配在堆上。而如果该值类型是一个方法的参数或局部变量则分配在栈上。

在我们声明一个实例字段,然后马上初始化时,类似于下面:

 

private int _count = 5;

然后编译,再用ILDasm反编译,我们会奇怪的发现,现在只有字段的声明,而字段的初始化(也就是赋初值5)的代码跑到该类的实例构造器里面了,更奇怪的是,如果你给该类提供了几个构造函数,则每个构造函数里都会拷贝一份这个初始化的代码。

如下这样的代码:

 

class Foo
{
        
private int _count = 5;
        
private int _perCount = 1;
        
private int _timeOut = 5;
        
public Foo()
        { 
        }
        
public Foo(int count)
        { 
        
        }
        
public Foo(int count, int perCount)
        { 
        
        }
}

 

实际上与下面的代码是等同的:

 

class Foo
{
        
private int _count;
        
private int _perCount;
        
private int _timeOut;
        
public Foo()
        {
            _count 
= 5;
            _perCount 
= 1;
            _timeOut 
= 5;
        }
        
public Foo(int count)
        {
            _count 
= 5;
            _perCount 
= 1;
            _timeOut 
= 5;
        }
        
public Foo(int count, int perCount)
        {
            _count 
= 5;
            _perCount 
= 1;
            _timeOut 
= 5;
        }
}

如此一来,代码迅速膨胀。那有什么好的办法呢?

最佳实践
对于这种给许多字段赋初值,而且有具有多个构造器,我们应该在一个构造器里对这些字段进行赋初值,其他构造调用该构造器。

根据这个最佳实践,则上面的代码应该修改为:

class Foo
{
        
private int _count;
        
private int _perCount;
        
private int _timeOut;
        
public Foo()
        {
            _count 
= 5;
            _perCount 
= 1;
            _timeOut 
= 5;
        }
        
public Foo(int count):this()
        {
        }
        
public Foo(int count, int perCount):this()
        {
        }
}

默认构造器

当你定义一个类型时,如果没有给该类型提供任何构造器,则编译器会自动的给该类型定义一个无参的构造器。比如下面这个类型:

class Foo
{

}

虽然从表象上看,该类型没有任何成员,但实际上它具有一个编译器自动生成的构造器。不过如果你给该类型只要“手写”了一个构造器,那个默认的构造器就不会有了。

构造器链
有的时候我们经常需要提供下面这样一串的构造器

public Foo()
{
}
public Foo(int count)
{
}
public Foo(int count, int perCount)
{
}
public Foo(int count, int perCount,int timeOut)
{
}

这往往是为了给类型的实例化工作带来更多的灵活性。但是这些构造器里的代码我们要怎么写呢?每个构造器里都写上初始化的代码么?答案是否定的,有个大名鼎鼎的原则DRYDon’t Repeat Yourself。说的就是不要重复,如果每个构造器里都写着初始化的代码,如果有一天初始化的方式变了(比如需要用一个小算法初始化),那所有的构造器里的代码我们都得一个个的修改,做的事情太多,就容易出错,所以我们应该这样:

class Foo
{
        
private int _count;
        
private int _perCount;
        
private int _timeOut;
        
public Foo():this(0,0,0)
        {

        }
        
public Foo(int count):this(count,0,0)
        {
        }
        
public Foo(int count, int perCount):this(count,perCount,0)
        {
        }
        
public Foo(int count, int perCount,int timeOut)
        {
            _count 
= count;
            _perCount 
= perCount;
            _timeOut 
= timeOut;
        }
}

在一个比较“全”的构造器里提供实现,而其他的构造器调用这个构造器,就像一个链条一样,这就是构造器链。

默认情况下,一个实例里的字段的内存分配是不透明的,CLR会经常调整这些字段的分配顺序。这点C#就不像它的前辈C++。因为需要“数据对齐”,在C++里定义字段的顺序会带来空间或时间上的一些微妙的问题,但是在.NET里,字段定义的顺序确实无关紧要的,因为CLR会动态的调整这些字段的顺序,使得它们在时间和空间上都做的更好。(不过我们可以使用StructLayout特性显式的控制字段的偏移)。

好了,就说这么多了。下一篇文章我会谈谈静态成员的一些事情。