这段时间工作有点忙,系统刚上线,也有点累。这段时间主要看到了书中.net类型设计这一部分,这一部分主要介绍的是如何用不同的成员来设计一个类型。
一:类型成员介绍
面向对象的语言中,最重要的一个特点就是封装,说到最多的一个词就是类。什么是类,类是定义同一类所有对象的变量和方法的蓝图或原型。让数据同操作分离开。人类是一个类,而我们每个人就是这个类的一个实例,也就这个类的一个对象。 我们这里说的类型,并不是程序中的类Class,因为我们知道了类型有,值类型,引用类型。这里用类Class主要是帮助大家理解,毕竟它是用的最多。
一个类型,他可以包括以下一个或多个成员:
常数:常数是一个表示恒定不变的数值符号。常数 总是和类型而非他们实例相关联,所以他们可以说总是静态的。
字段:字段表示一个数据的值,他可以是只读的,也可以是可读写的。字段分为动态字段和实例字段。一般字段声名为私有,这也是面向对象中推荐的作法,防止字段被随意操作。也就是我们类中定义的数据成员。
方法:方法是一个函数,用来改变或查询一个类型,或者一个对象的状态。也就是我们类中定义的成员函数。
属性:属性也是一种方法,当他比较特殊,他主要是用来设置和保护类中的数据成员。
事件:用通俗的话来说,就是你操作某个对象时,执行的某一个方法。他只在特定的时间,由指定的对象来执行指定的方法。
除了上面介绍的这些,一个类型中还包括,构造器(类型初始化用),重载操作符,转换操作符等。对于一个类型,内部可以嵌套定义其他类型。这可以使得复杂的类型划分为小的代码块,简化实现。
下面是类型成员的限定修符的解释:
图1
图2
当需要限制一个类型(比如Console和Math),不能被继承也不能被实例化的时候希望可以使用Abstract和Sealed,但是CLR并不支持,所以
如果想实在C#中现这样一个类型,可以把类型限制为Sealed,然后定义一个私有(Private)无参数构造器。这样可以防止编译器自动产生一
个公有的构造器。没有构造器可访问,也就无法实例化。
当源代码被编译时,编译器会检查他们是否正确的引用了类型和成员。如果源代码中非法引用了一些类型或成员,编译器会产生相应的错误
信息。在JIT对IL代码进行编译时也会进行检查,如有非法的引用则会抛出异常,这样就保证了代码能安全的执行。
二:数据成员
一个类型中的数据成员主要包括常数和字段两种。
对于一个常数,我们必须在编译的时候就确定它的值。编译后他的枝保存在元数据中,也就意味着常数的类型只能是那些被编译器认为是基
元类型的(只有基元类型才能利用文本常数初始化,而其他类型需要用构造器初始化)。使用常数的好处是他内嵌在代码中不需要在为它分
配内存空间,但同时也带来了版本问题。字段是静态的,直接通过类型名来访问。
{
public const Int32 MaxLixt = 20 ; // C#不允许为常数指定static关键字,因为常数隐藏为static
}
class App
{
static void Main()
{
Console.WriteLine(Component.MaxList);
}
}
看上面的代码,在编译时,在App的元数据中,已经有了MaxList的值20,所以App虽然引用了Component,但实际运行时可以完全不需要它。
即便重新编译了Component也改变不了App输出的值。只有一起重新编译。所以如果要求一个模块在运行时获得另一个模块的数据,最好用不
要用常数,而是用只读字段。对于那些不会更改的,使用常数可以减少内存空间。
对于字段他有静态和实例两种。字段会在该类型被加载进一个应用程序域时为其分配内存,这通常发生在引用该类型的方法第一次被JIT编
译时。对于实例字段,系统在该类型的实例被构建时动态分配。
如果把上面Component程序中的定义改为 public static readonly Int32 MaxList =20;就解决了版本问题。这里定义静态字段需要显式的
定义。而readonly试其不可修改。这时只需要重新编译Component就可以改变App的输出了(前提是编译后的DLL程序集是非强命名程序集)
,虽然性能可能会有一点影响。
可以Component中的定义,是使用内联方式(声明字段的同时进行初始化赋值)进行初始化的,这是C#提供的一种简便的方法,而实际上他
们是初始化都是在构造器类完成的。
三:方法
方法决定了一个类型所具有的功能,其中除了我们自己定义的方法外,用的最多的就是类型的构造方法、操作符重载方法、转换操作符方
法。
谈到构造器,他的作用是非常大的。他负责把类型成员初始化到一个良好的状态。构造器包括实例构造器和类型构造器,从名字也看的出他
们的不同。前者主要是针对实例成员,而后者主要针对类型成员。
对于可验证代码,CLR要求每个类至少定义一个实例构造器。在创建一个引用类型的实例时系统执行3个步骤:
1:为该实例分配内存
2:初始化对象的附加成员(方法表指针和一个ScBlockIndex)
3:调用类型的实例构造器设置对象的初始状态(在调用构造器之前系统为该对象分配的内存总是被设置为0)
PS:关于类型的内存分配问题,之前的学习笔记中已经介绍过了,但介绍的比较粗略。特别是方法表这块,目前还不是太清楚,比如装箱类
型通过方法表指针来访问自己的方法,但方法表存放在那呢?是什么形式呢?非装箱类型有没有方法表呢?又是如何访问的呢?在加上虚方
法,继承,多态等,非常复杂,暂时就不深究了。以后在专门研究了。懂的朋友也可以告诉我。在网上找了相关的资料大家可以看下:
http://www.cnblogs.com/blusehuang/archive/2007/10/23/833593.html
具体的构造方法就看一些例子把
1:使用默认构造器。
... {
int a;
static int b;
}
// 对应默认构造器的IL代码
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
... {
// 代码大小 7 (0x7)
.maxstack 1
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method example::.ctor
2:显式定义构造器:
... {
int a;
static int b;
public example()
...{
a = 0;
}
public example(int a)
...{
this.a = a;
}
}
// 系统产生了两个构造器的IL代码
// 无参数的构造器
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
... {
// 代码大小 14 (0xe)
.maxstack 2
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ldarg.0
IL_0007: ldc.i4.0
IL_0008: stfld int32 example::a
IL_000d: ret
} // end of method example::.ctor
// 带参数的构造器
.method public hidebysig specialname rtspecialname
instance void .ctor(int32 a) cil managed
... {
// 代码大小 14 (0xe)
.maxstack 2
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ldarg.0
IL_0007: ldarg.1
IL_0008: stfld int32 example::a
IL_000d: ret
} // end of method example::.ctor
可见对字段a进行的初始化,之前仍旧调用了基类的构造方法。两个构造器的IL基本一样,但实际还是有点小区别。这里没有写出一个public ex(){}的构造器,这样一个构造器是默认的构造器是完全一样的。
3:以内联方式初始化变量:
... {
int a = 1;
static int b;
}
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
... {
// 代码大小 14 (0xe)
.maxstack 2
IL_0000: ldarg.0
IL_0001: ldc.i4.1
IL_0002: stfld int32 example::a
IL_0007: ldarg.0
IL_0008: call instance void [mscorlib]System.Object::.ctor()
IL_000d: ret
} // end of method example::.ctor
我们在程序中直接给a进行了赋值,但如前面提到过的,实际这只是一种简化方式,实际上,他也是在构造器中进行初始化的。首先给a赋值,然后调用了基类的构造器。如果我定义了多个构造器,每个构造器都会先把a初始化。这个和第一个例子基本一样,但可以发现这种方式使得代码大小大了很多。所以尽量避免这种方式。在看下面
... {
int a = 1;
static int b;
public example()
...{
a = 0;
}
}
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
... {
// 代码大小 21 (0x15)
.maxstack 2
IL_0000: ldarg.0
IL_0001: ldc.i4.1
IL_0002: stfld int32 example::a
IL_0007: ldarg.0
IL_0008: call instance void [mscorlib]System.Object::.ctor()
IL_000d: ldarg.0
IL_000e: ldc.i4.0
IL_000f: stfld int32 example::a
IL_0014: ret
} // end of method example::.ctor
这个使用了内联定义的,然后又显式的使用了构造器,最后a的值应该是0。可见这样让代码变的更多了。
4:
class example
... {
int a = 0;
int b = 1;
string s;
public example(string s)
...{
this.s = s;
}
}
.method public hidebysig specialname rtspecialname
instance void .ctor( string s) cil managed
... {
// 代码大小 28 (0x1c)
.maxstack 2
IL_0000: ldarg.0
IL_0001: ldc.i4.0
IL_0002: stfld int32 example::a
IL_0007: ldarg.0
IL_0008: ldc.i4.1
IL_0009: stfld int32 example::b
IL_000e: ldarg.0
IL_000f: call instance void [mscorlib]System.Object::.ctor()
IL_0014: ldarg.0
IL_0015: ldarg.1
IL_0016: stfld string example::s
IL_001b: ret
} // end of method example::.ctor
// 使用一个构造器来初始化a,b
class example2
... {
int a ;
int b;
string s;
//定义一个默认构造器
public example2()
...{
a = 0;
b = 1;
}
//调用默认构造器初始化a,b否则系统自动调用基类的构造函数初始化a,b
public example2(string s):this()
...{
this.s = s;
}
}
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
... {
// 代码大小 21 (0x15)
.maxstack 2
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ldarg.0
IL_0007: ldc.i4.0
IL_0008: stfld int32 example2::a
IL_000d: ldarg.0
IL_000e: ldc.i4.1
IL_000f: stfld int32 example2::b
IL_0014: ret
} // end of method example2::.ctor
.method public hidebysig specialname rtspecialname
instance void .ctor( string s) cil managed
... {
// 代码大小 14 (0xe)
.maxstack 2
IL_0000: ldarg.0
IL_0001: call instance void example2::.ctor()
IL_0006: ldarg.0
IL_0007: ldarg.1
IL_0008: stfld string example2::s
IL_000d: ret
} // end of method example2::.ctor
通过上面我们看到,可以发现上面2个对S字段初始化的IL代码,采用内联方式的代码大小为28,而后面一种方法为14。小了很多。可能有人会说,那时因为对a,b初始化在默认构造器内,但你想想当我多几个字段,多几个重载的构造器会怎么样?这里用到了 public example2(string s):this(),用this()表示在初始化s的时候调用默认的构造函数初始化a,b,否则系统会自动调用基类的默认构造器。
所以如果我么有一些需要初始化的字段和许多重载的构造器方法,最好显式的在默认构造器内初始化,然后在初始化其他字段的构造器中调用这个默认构造器。这也相当于一个重用。
5:值类型构造器
CLR并不要求值类型必须定义构造器方法,实际上C#编译器也不会为值类型产生默认的无参构造器。但CLR允许我们为值类型定义构造器。当我们使用构造器,只有我们显式调用构造器才会被执行,用new来创建一个值类型时,只是调用的他的构造器(而引用类型却有3个步骤,前面介绍过)。如果不使用new来创建,那么值类型的字段都会保持为0,因为前面介绍过,在调用构造器之前系统为该对象分配的内存总是被设置为0。
需要注意的时,不能给值类型定义一个无参构造器。严格的说只有内嵌于引用类型的值类型字段,CLR才会保证字段被初始化为0或null,基于堆栈的值类型字段无法保证。但因为CLR的代码可验证机制,要求所有基于堆栈的值类型字段在使用前都必须被赋值,而编译器可以保证这一点,这就保证了运行时不会出现异常。
... {
int a ;
int b ;
public example(int a ,int b)
...{
this.a = a;
this.b = b;
}
}
.method public hidebysig specialname rtspecialname
instance void .ctor(int32 a,
int32 b) cil managed
... {
// 代码大小 15 (0xf)
.maxstack 2
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: stfld int32 example::a
IL_0007: ldarg.0
IL_0008: ldarg.2
IL_0009: stfld int32 example::b
IL_000e: ret
} // end of method example::.ctor
6:类型构造器
除了上面介绍的,用的最多的实例构造器外,还有类型构造器。他主要左右是初始化类型中的静态字段。默认的类型中没有定义类型构造器,要定义的话,也只能定义一个,并且不能有任何参数。
... {
static int a ;
static int b = 1 ;
static example()
...{
a = 2;
b = 1;
}
}
.method private hidebysig specialname rtspecialname static
void .cctor() cil managed
... {
// 代码大小 19 (0x13)
.maxstack 1
IL_0000: ldc.i4.1
IL_0001: stsfld int32 example::b
IL_0006: ldc.i4.2
IL_0007: stsfld int32 example::a
IL_000c: ldc.i4.1
IL_000d: stsfld int32 example::b
IL_0012: ret
} // end of method example::.cctor
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
... {
// 代码大小 7 (0x7)
.maxstack 1
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method example::.ctor
可以看到和实例类型差不多,只是他构造器的方法是cctor(),而且不调用任何基类的类型构造器。实际上也不能这么做,因为静态字段并不从基类继承。还可以发现后面还有个ctor的实例构造器,这是系统自动为引用类型产生的,而如果是值类型就不会有这个实例构造器。
通过上面,应该是对构造器有了个大概的了解,通过IL代码也了解了构造的过程,下面列出要注意的地方
a:对于引用类型系统会自动产生实例构造器。实例构造器在访问基类字段之前都必须调用基类的构造器。编译器会自动产生对基类默认构造器的访问代码。每一个构造器最后总会调用到system.object的公有无参构造器。
b:构造器可以采用内联的方式来初始化,但这样会导致代码量的增加。
c:对于值类型,系统不会自动产生一个默认的构造器。我们可以自己定义,但不允许定义一个无参的值类型实d例构造器。而且对于手动定义的构造器必须初始化值类型中所有的字段。
d:对于类型构造器,类型中默认是没有定义的。我们只能定义一个没有参数的类型构造器。而且构造器类型必须为static。默认为私有,并且不允许我们显式定义。类型构造器不应该调用其基类的类型构造器。
四:重载符和转换操作符
1:重载操作符
操作符号重载是用的比较多的,比如对于string类型来说,为什么可以用'+' 来连接2个字符串,用=,!=来判断字符串是否相等?实际上CLR对于这些符号一无所知。但他去归法了语言怎样提供操作符重载。
... {
public static String operator+(String s1,String s2)...{...........}
}
比如String类型中定义了2个操作符的表现行为。在编译的时候,系统会产生名为op_Addition的方法,并且该方法有一个specialname标记。当编译器在代码中碰到+操作符时,它们会去看其中的操作数类型中有那一个定义了参数类型和操作数类型兼容、名为op_Addition的specialname方法。如果存在就调用此方法,否则出现编译错误。
对于一些核心类型(Int32,Int64等),他们没有定义任何操作符重载方法,因为CLR提供了直接操作这些类型实例的IL指令(如Add)。如果为他们提供的重载方法,编译器产生的代码实际也是调用了这些IL指令,那么性能就会有所损失。所以说如果某个编程语言不支持某个FCL核心类型,那么我们不能在其实例上进行任何操作。比如VB就不支持无符号整数。
不是所有的语言都支持操作符重载,所以如果我们在VB上对一个非基元类型使用+操作符时,编译器将产生一个错误。所以在VB上,我们只能定义一个与+功能相同的方法来实现。下面看看C#和VB操作符的互操作。
public class CSharpex
... {
public static CSharpex operator+(example e1,example e2)
...{return null;}
}
// VB中使用
Public Class VBex
Public Shared Sub Main()
Dim cs as new CSharpex()
’需要注销掉这句,因为VB不支持,他不知道如何把 + 翻译成op_Addition方法
‘cs = cs + cs
‘用这种方式可以显式使用 + 操作符的方法,只是没有用 + 好看
cs = CSharpex.op_Addition(cs,cs)
End Sub
End Class
// 在VB中只能这样定义操作符号,而不能用+
Public Class VBex
Public Shared Function op_Addition(a as VBex, b as VBex)
Return Nothing
End Sub
End Class
// 在C#中调用时,也只能使用op_Addition,而不能使用+,因为VB编译器不会给此方法加上specailname标记。
2:转换操作符
前面几次我们讲过了类型转换,那么他们是怎么实现的呢?如果想把一个自己定义的类型转换为Int型要怎么做呢?通过定义下面的转换操作符的方法,我们可以把一个类型转换为另一个类型。
public class Rational
... {
//由Int32构造
public Rational(Int32 num)...{}
//由Single构造
public Rational(Single value)...{}
//转化为Int32
public Int32 ToInt32()...{}
//转换为Single
public Singal ToSignal()...{}
//由Int32隐式构造一个Rational并返回
public static implicit operator Rational(Int32 num)
...{return new Rational(num);}
//由Single隐式构造一个Rational并返回
public static implicit operator Rational(Single value)
...{return new Rational(value);}
//由Rational显式返回一个Int32
public static implicit operator Int32(Rational r)
...{return r.ToInt32();}
//由Rational显式返回一个Single
public static implicit operator Single(Rational r)
...{return r.ToSingle();}
}
和重载操作符一样,转换操作符方法也必须为public 和static。不同的是,我们还要告诉编译器是显式还是隐式的进行操作。其中implicit告诉编译器需要隐式的进行转换,而explicit为显式的进行转换。后面的operator告诉编译器该方法是一个转换操作符。对于转换过程中不可能丢失精度或数量及的转换,应该定为隐式的;而对于会丢失精度或数量及的应该定义为显式转换。
编译器在编译时,会产生如下的方法:
public static Rational op_Implicit(Single value)
public static Int32 op_Explicit(Rational r)
public static Single op_Explicit(Rational r)
可以看到最后两个方法的IL代码只有返回类型不同,CLR是支持只有返回类型不同的方法的,但C#,VB这些语言都不支持的。C#中不能为一种转换操作同时定义隐式转换操作和显式转换操作。
3:关于类型转换:
类型转换确实是个很复杂的地方。昨天后来大概看了下.C#里的类型转换,主要是使用强制转换,使用Convert,使用类型的ToXXX()方法。下面就讲下我的理解。
1:对于基元类型间进行转换时,CLR会产生支持的IL指令,而使用Convert时是调用相应的方法
public class example
... {
public static void Main()
...{
float a = 10.0f;
int b = (int)a;
int c = Convert.ToInt32(a);
}
}
.method public hidebysig static void Main() cil managed
... {
// 代码大小 17 (0x11)
.maxstack 1
.locals init (float32 V_0,
int32 V_1,
int32 V_2)
IL_0000: ldc.r4 10.
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: conv.i4
IL_0008: stloc.1
IL_0009: ldloc.0
IL_000a: call int32 [mscorlib]System.Convert::ToInt32(float32)
IL_000f: stloc.2
IL_0010: ret
} // end of method example::Main
对于强制类型转换,使用了conv.i4的指令,使用Convert时是调用Convert::ToInt32(float32)。可见对基元类型之间使用强制转换的效率是最高的。我想使用Convert在内部应该也是调用此指令。
2:对于其他一些非基元类型的转换
... {
public static void Main()
...{
float a = 10.0f;
decimal d = (decimal)a;
decimal e = Convert.ToDecimal(a);
}
}
.method public hidebysig static void Main() cil managed
... {
// 代码大小 22 (0x16)
.maxstack 1
.locals init (float32 V_0,
valuetype [mscorlib]System.Decimal V_1,
valuetype [mscorlib]System.Decimal V_2)
IL_0000: ldc.r4 10.
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: conv.r4
IL_0008: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Explicit(float32)
IL_000d: stloc.1
IL_000e: ldloc.0
IL_000f: call valuetype [mscorlib]System.Decimal [mscorlib]System.Convert::ToDecimal(float32)
IL_0014: stloc.2
IL_0015: ret
} // end of method example::Main
Decimal是个比较特殊的类型,他不同于其他基元类型,可以看到Decimal类型中定义了转换操符号,在强制转换时,调用了此方法,而没有对应的IL指令。可见非基元类型之间的转换的效率会低一些。在看看使用Convet进行转换的,他使用的是Convert类型中的ToDecimal方法。
3:关于Convert类
这个类将一个基本数据类型转换为另一个基本数据类型。我们知道,基元类型基本都继承了IConvertible 接口。此接口提供特定的方法,用以将实现类型的实例值转换为具有等效值的公共语言运行库类型。
通常,公共语言运行库通过 Convert 类公开 IConvertible 接口。基元使用 Convert 类进行转换,而不是使用此类型的 IConvertible 显式接口成员实现。也就是说,调用Convert 类的静态方法,和类型本身实现的 IConvertible 接口方法是一样的。只是基元类型没有公开自己实现的IConvertible 接口。所以我们不能使用Int32.ToSingle(),这样的方法,而要使用Convert.ToSingle(Int32).
看看下面的例子:
... {
public static void Main()
...{
float a = 10.0f;
decimal d = (decimal)a;
decimal e = Convert.ToDecimal(a);
int f =decimal.ToInt32(d);
int g = Convert.ToInt32(d);
int h = (int)d;
}
}
.method public hidebysig static void Main() cil managed
... {
// 代码大小 45 (0x2d)
.maxstack 1
.locals init (float32 V_0,
valuetype [mscorlib]System.Decimal V_1,
valuetype [mscorlib]System.Decimal V_2,
int32 V_3,
int32 V_4,
int32 V_5)
IL_0000: ldc.r4 10.
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: conv.r4
IL_0008: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Explicit(float32)
IL_000d: stloc.1
IL_000e: ldloc.0
IL_000f: call valuetype [mscorlib]System.Decimal [mscorlib]System.Convert::ToDecimal(float32)
IL_0014: stloc.2
IL_0015: ldloc.1
IL_0016: call int32 [mscorlib]System.Decimal::ToInt32(valuetype [mscorlib]System.Decimal)
IL_001b: stloc.3
IL_001c: ldloc.1
IL_001d: call int32 [mscorlib]System.Convert::ToInt32(valuetype [mscorlib]System.Decimal)
IL_0022: stloc.s V_4
IL_0024: ldloc.1
IL_0025: call int32 [mscorlib]System.Decimal::op_Explicit(valuetype [mscorlib]System.Decimal)
IL_002a: stloc.s V_5
IL_002c: ret
} // end of method example::Main
看下这段代码就明白了,最后三个转换,使用了三种方法:
一是直接调用Decimal类型继承IConvertible 接口的静态实现方法。
二是通过Convert来实现,从IL代码来看也看的出区别。为什么他可以用,而Int32这些不行呢,因为他公开了接口实现,所以可以使用decimal.ToInt32(d)。
三是使用强制转换,用到了转换操作符。
总结:
总的说类型转换(.net1.1 C#)主要是上面3种方法:
1:使用强制类型转换,对于基元类型,系统会自动产生它对应的IL指令,而其他类型会去检查是否定义了转换操作符。
2:使用类型继承IConvertible 接口的实现的转换方法。这只对非基元有效,并且开放了这些方法的访问。
3:使用Convert类来实现。主要是对基元来使用,因为他们都不开放IConvertible 接口的实现,所以只能使用Convert来实现。
4:Decimal是个列外,他又开放了IConvertible 接口的实现,也可以用Convert来实现,所以它是不是基元类型很难说。在C#中是的,但在其他语言中就不一定了。
5:关于值类型和String类型的转换,比较复杂,但我们可以看到,基元类型都继承并重写了ToString()的实例方法。所以我们可以直接使用int32.ToString()来实现。而类型中的Parse的静态方法则提供了把String型转换为本类型的功能。
疑问:基元类型为什么不和Decimal一样提供ToXXX()方法呢?他内部是否实现了这些方法?
(下一篇解答)