[转]再谈C#中的Const、ReadOnly和Static变量
常量的定义,其关键字就是const。在定义常量时,必须赋予其初始值。一旦赋予了初始值后,就不能修改其值。也就是所谓的常量值不能更改的含义。由于C#是一门纯粹的面向对象语言,并不存在一个常量或者变量游离于对象之外,因此,这些定义,必然都是在一个类型内完成的。
关于常量的使用,除了会用作一些算法的临时常量值以外,最重要的是定义一些全局的常量,被其他对象直接调用。而集中这些常量最好的类型是struct(结构)。关于struct我会在后面的章节详细讲解,在这里仅举一例说明常量的这种运用。例如,我们需要在.Net下使用FTP,那么一些特定的FTP代码就可以通过这种方式完成定义,如下所示:
1: public struct FtpCode
2: {
3: public const string ConnectOk = "220";
4: public const string RequiredPassword = "331";
5: public const string LoginOk = "230";
6: public const string PasvOk = "227";
7: public const string CwdOk = "250";
8: public const string PwdOk = "257";
9: public const string TransferOk = "226";
10: public const string ListOk = "150";
11: public const string PortOK = "200";
12: public const string NoFile = "550";
13: }
要使用这些常量,可以直接调用,例如FtpCode.ConnectOk。如果结构FtpCode仅用于本程序集内部,也可以把结构类型和内部的常量设置为internal。采用这种方式有三个好处:
1、集中管理全局常量,便于调用;
2、便于修改,一旦Ftp的特定代码发生变化,仅需要修改FtpCode中的常量值即可,其余代码均不受影响;
3、便于扩展。要增加新的Ftp代码,可以直接修改结构FtpCode,其余代码不受影响。
虽然说变量的值可以修改,但我们也可以定义只读的变量,方法就是在定义的时候加上关键字readonly。如下定义:
1: public readonly int number = 20;
变量number的值此时是只读的,不能再对其进行重新赋值的操作。在定义只读变量的时候,建议必须为变量赋予初值。如果不赋予初值,.Net会给与警告,同时根据其类型不同,赋予不同的初值。例如int类型赋初值为0,string类型赋初值为null。由于定义的只读变量其值不可修改,因此不赋初值的只读变量定义,没有任何意义,反而容易造成空引用对象的异常。
static的意义与const和readonly迥然不同。const仅用于常量定义,readonly仅用于变量定义,而static则和常量、变量无关,它是指所定义的值与类型有关,而与对象的状态无关。
前面我已介绍,所谓“对象”,可以称为一个类型的实例,以class类型为例,当定义了一个类类型之后,要创建该类型的对象,必须进行实例化,方可以调用其属性或者方法。例如User类型的Name、Password属性,SignIn和SignOut方法,就都是与对象相关的,要调用这些属性和方法,只能通过实例化对象来调用,如下所示:
1: User user = new User();
2: user.Name = "bruce zhang";
3: user.Password = "password";
4: user.SignIn();
5: user.SignOut();
然而,我们在定义类的成员时,也可以利用static关键字,定义一些与对象状态无关的类成员,例如下面的代码:
1: public class LogManager
2: {
3: public static void Logging(string logFile,string log)
4: {
5: using (StreamWriter logWriter = new StreamWriter(logFile,true))
6: {
7: logWriter.WriteLine(log);
8: }
9: }
10: }
方法Logging为static方法(静态方法),它们与类LogManager的对象状态是无关的,因此调用这个方法时,并不需要创建LogManager的实例:
1: LogManager.Logging ("log.txt","test.");
所谓“与对象状态无关”,还需要从实例化谈起。在对一个类类型进行实例化操作的时候,实际上就是在内存中分配一段空间,用以创建该对象,并储存对象的一些值,如Name和Password等。对同一个类类型,如果没有特殊的限制,是可以同时创建多个对象的,这些对象被分配到不同的内存空间中,它们的类型虽然一样,却具有不同的对象状态,如内存地址、对象名、以及对象中各个成员的值等等。例如,我们可以同时创建两个User对象:
1: User user1 = new User();
2: User user2 = new User();
由于Name和Password属性是和对象紧密相关的,方法SignIn和SignOut的实现也调用了内部的Name和Password属性值,因此也和对象紧密相关,所以这些成员就不能被定义为静态成员。试想一下,如果把Name和Password属性均设置为静态属性,则设置其值时,只能采用如下形式:
1: User.Name = "bruce zhang";
2: User.Password = "password";
显然,此时设置的Name和Password就与实例user无关,也就是说无论创建了多少个User实例,Name和Password都不属于这些实例,这显然和User类的意义相悖。对于方法SignIn和SignOut,也是同样的道理。当然我们也可以更改方法的定义,使得该方法可以被定义为static,如下所示:
1: public class User
2: {
3: public static void SignIn(string userName, string password)
4: {
5: //代码略
6: }
7: public static void SignOut(string userName, string password)
8: {
9: //代码略
10: }
11: }
由于SignIn和SignOut方法需要调用的Name和Password值改为从方法参数中传入,此时这两个方法就与对象的状态没有任何关系。定义好的静态方法的调用方式略有不同:
1: User user = new User();
2: user.Name = "bruce zhang";
3: user.Password = "password";
4: User.SignIn(user.Name, user.Password);
5: User.SignIn(user.Name, user.Password);
两相比较,这样的修改反而导致了使用的不方便。因此,当一个方法与对象的状态有较紧密的联系时,最好不要定义为静态方法。
那么为什么在LogManager类中,我将Logging方法均定义为静态方法呢?这是因为该方法与对象状态没有太大的关系,如果将方法的参数logFile和log定义为LogManager类的属性,从实际运用上也不合理,同时也会导致使用的不方便。最重要的是,一旦要调用非静态方法,不可避免的就需要创建实例对象。这会导致不必要的内存空间浪费。毕竟LogManager类型对于调用者而言,仅在于其Logging方法,而和对象的状态没有太大的关系,因此并不需要为调用这个方法专门去创建一个实例。这一点是和User类型是完全不同的。
在一个类类型的定义中,既可以允许静态成员,也可以允许非静态成员。然而在一个静态方法中,是不允许直接调用同一类型的非静态方法的,如下所示:
1: public class Test
2: {
3: private void Foo1()
4: {
5: //代码略;
6: }
7: public static void Foo2()
8: {
9: Foo1(); //错误;
10: }
11: public void Foo3()
12: {
13: Foo1(); //正确;
14: }
15: }
在静态方法Foo2中,直接调用了同一类型Test下的私有非静态方法Foo1,将会发生错误;而非静态方法Foo3对Foo1的调用则正确。如要在静态方法Foo2中正确调用Foo1方法,必须创建Test类的实例,通过它来调用Foo1方法,修改如下:
1: public static void Foo2()
2: {
3: Test test = new Test();
4: testFoo1(); //正确;
5: }
在Foo2方法中,创建了Test的实例,通过实例对象test来调用Foo1方法。需要注意的是虽然Foo1方法是private方法,但由于Foo2方法本身就在Test对象中,所以此时的私有方法Foo1是可以被调用的,因为对象的封装仅针对外部的调用者而言,对于类型内部,即使是private,也是可以被调用的。
对于类型的静态属性成员而言,具有和静态方法一样的限制。毕竟,从根本上说,类型的属性,其实就是两个get和set方法。
如果在类中定义了static的字段,有两种方式对其初始化。一是在定义时初始化字段,或者是在类型的构造器中为这些静态字段赋予初始值。例如:
1: class ExplicitConstructor
2: {
3: private static string message;
4: public ExplicitConstructor()
5: {
6: message = "Hello World";
7: }
8: public static string Message
9: {
10: get { return message; }
11: }
12: }
13: class ImplicitConstructor
14: {
15: private static string message = "Hello World";
16: public static string Message
17: {
18: get { return message; }
19: }
20: }
在类ExplicitConstructor中,是利用构造器为静态字段message初始化值,而在类ImplicitConstructor中,则是直接在定义时初始化message静态字段。虽然这两种方式均可达至初始化的目的,但后者在性能上有明显的优势(有兴趣者,可以阅读我博客上的一篇文章http://wayfarer.cnblogs.com/archive/2004/12/20/78817.html)。因此,我建议当需要初始化静态字段时,应直接初始化。
如果对于静态字段未设置值,.Net会给出警告,并根据类型的不同赋予不同的初始值。此外,static还可以和readonly结合起来使用,定义一个只读的静态变量。但是static不能应用到常量的定义中。
在C# 1.x中,static并不能用来修饰类类型,也就是说,我们不能定义一个静态类。然而对于一个类类型,如果其成员均为静态成员,则此时实例化该类是没有意义的。此时,我们常常将构造器设置为private,同时将其类设置为sealed(sealed表明该类不可继承,关于sealed会在后面介绍)。这样就可以避免对类的实例化操作,如前面定义的LogManager,即可以修改定义:
1: public sealed class LogManager
2: {
3: private LogManager()
4: {}
5: public static void Logging(string logFile,string log)
6: {
7: using (StreamWriter logWriter = new StreamWriter(logFile,true))
8: {
9: logWriter.WriteLine(log);
10: }
11: }
12: }
1: public static class LogManager{}