要唯一确定一个类型,就需要类型的完全限定名:
命名空间.类型名,程序集名(这里分强命名还是弱命名文件名,语言文化,PublickeyToken,版本号)。这种完全限定名在.NET的config配置文件里用的比较多:
这是Web程序里,典型的用来配置HttpHandler的,用了这个完全限定名,ASP.NET就可以轻松的将请求路由到那个类型。
对于一个类型,实际上只有三种成员:字段、方法、嵌套类型。
那可能有人会说:事件、属性、索引呢。实际上这些都是编译器提供的语法支持,封装字段和方法的“包装成员”。
比如属性,实际上就是一个get_xxx方法和set_xxx方法(如果该属性是可读可写的),这其中xxx就是该属性的名字。不过我们通常将属性与一个私有字段配合,不过属性本身和这个私有字段没有半点关系。也就是说属性就是两个方法加一些元数据。
一个类包含的成员按照所有者又可分为两种:类型成员和实例成员。
在C#里,类型成员使用static来修饰(vb里用Shared,个人看来Shared更能表明类型成员真实的含义)。虽然C#里也沿用了C里的static关键字,可千万不要混淆了这两个概念。
对于字段而言,就代表着内存如何分配。用static修饰的字段,只会分配一次内存,而默认情况下,字段是实例字段,每次实例化一个类型时都会分配内存。这样一看,好像这个static好像是共享的,所以前面说VB.NET的Shared关键字更能表明其含义。对于方法也是一样,static的方法属于类型的,必须通过类型名来访问(这点和Java不同)。
对于static的字段,CLR会根据该字段的类型初始化该字段的值,如果该字段是数字类型的则会初始化为0,如果是布尔类型的则会初始化为false,而对于是引用类型的则初始化为null。CLR还会按照这个原则初始化应该分配在堆上的成员。
也许受到某类书的迫害,很多人在骨子里一直认为:引用类型分配在堆上,值类型分配在栈上。但是又想不通,如果一个类,它的实例应该分配在堆上,那它的实例字段是在栈上还是堆上呢。对于一个值类型,如果它是类型的字段成员(我不认为一个方法的参数和方法内的局部变量是它所在类型的成员)则分配在堆上。而如果该值类型是一个方法的参数或局部变量则分配在栈上。
在我们声明一个实例字段,然后马上初始化时,类似于下面:
然后编译,再用ILDasm反编译,我们会奇怪的发现,现在只有字段的声明,而字段的初始化(也就是赋初值5)的代码跑到该类的实例构造器里面了,更奇怪的是,如果你给该类提供了几个构造函数,则每个构造函数里都会拷贝一份这个初始化的代码。
如下这样的代码:
{
private int _count = 5;
private int _perCount = 1;
private int _timeOut = 5;
public Foo()
{
}
public Foo(int count)
{
}
public Foo(int count, int perCount)
{
}
}
实际上与下面的代码是等同的:
{
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;
}
}
如此一来,代码迅速膨胀。那有什么好的办法呢?
对于这种给许多字段赋初值,而且有具有多个构造器,我们应该在一个构造器里对这些字段进行赋初值,其他构造调用该构造器。
根据这个最佳实践,则上面的代码应该修改为:
{
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()
{
}
}
默认构造器
当你定义一个类型时,如果没有给该类型提供任何构造器,则编译器会自动的给该类型定义一个无参的构造器。比如下面这个类型:
{
}
虽然从表象上看,该类型没有任何成员,但实际上它具有一个编译器自动生成的构造器。不过如果你给该类型只要“手写”了一个构造器,那个默认的构造器就不会有了。
构造器链
有的时候我们经常需要提供下面这样一串的构造器
{
}
public Foo(int count)
{
}
public Foo(int count, int perCount)
{
}
public Foo(int count, int perCount,int timeOut)
{
}
这往往是为了给类型的实例化工作带来更多的灵活性。但是这些构造器里的代码我们要怎么写呢?每个构造器里都写上初始化的代码么?答案是否定的,有个大名鼎鼎的原则DRY,Don’t Repeat Yourself。说的就是不要重复,如果每个构造器里都写着初始化的代码,如果有一天初始化的方式变了(比如需要用一个小算法初始化),那所有的构造器里的代码我们都得一个个的修改,做的事情太多,就容易出错,所以我们应该这样:
{
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特性显式的控制字段的偏移)。
好了,就说这么多了。下一篇文章我会谈谈静态成员的一些事情。