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