C#静态类和静态类成员

17 篇文章 11 订阅

一、引言

写代码的时候遇到了一个问题,定义了一个数据连接相关的静态类,

public static class SQLConnect
{
		// 数据库名
		public static string DBName = "dmanager";
        // 数据库服务器Ip
        public static string DBIp = "localhost";
        // 数据库服务器端口
        public static string DBPort = "3306";
        // 连接参数
        public static string ConnectionStatement = string.Format("server={0};port={1};user=root;password=666;database={2};"
        , DBIp, DBPort, DBName);
}

由于数据库的服务器可能发生变化,所以我在登录的时候需要添加一个配置服务器连接的功能。主要就是对服务器Ip端口进行配置,可以去连接不同的服务器。我想通过在一个静态类中定义一个静态字符串变量来实现这个功能(因为在我认知中,C#的静态变量有全局变量的意味,我在登录时修改静态变量的值,就可以在后续连接的时候连接修改后的值对应的服务器)。

但是,实验后发现Ip和端口的值变了,但ConnectionStatement的值并没有发生变化。

	_setIpCommand = new RelayCommand(() =>
    {
        MessageBox.Show(DBIp);
        DBIp = this.ServerIp;
        MessageBox.Show(DBIp);
        DBPort = this.ServerPort;
        MessageBox.Show(ConnectionStatement);
    });

当然,从结果来看,可以隐隐约约猜到哪里不对(比如静态成员变量是否只初始化了一次之类的)但是确切原因还是得学习一下后再下定论。


二、静态类与静态类的成员

静态类与非静态类基本上是相同的,但有一点明显不同:静态类无法被实例化。换言之,你无法使用new操作符来创建一个静态类的变量。因为它根本没有实例变量,所以你需要使用静态类的类名来访问静态类的成员。例如,如果你有一个名为UtilityClass的静态类,它有一个名为MethodA公有静态方法,你可以通过以下方式来调用该方法:

UtilityClass.MethodA();

静态类可用作操作一组方法的便捷容器,这些方法只需关心操作输入的参数,不需要获取或设置任何内部实例字段。例如,在.NET类库中,静态的System.Math类包含了各种数学运算的方法,而不需要存储或检索Math类的特定实例所独有的数据。就是说,你可以通过指定类名和方法名来引用类的成员,就像下面示例一样:

double dub = -3.14;  
Console.WriteLine(Math.Abs(dub));  
Console.WriteLine(Math.Floor(dub));  
Console.WriteLine(Math.Round(Math.Abs(dub)));  
  
// Output:  
// 3.14  
// -4  
// 3

与所有类一样,当引用该类的程序被加载时,.NET运行环境将加载静态类的类型信息。程序无法确切地指定何时加载类。但是,在程序中第一次引用类之前,得保证加载它、初始化它的字段并且调用它的静态构造函数。静态构造函数仅调用一次,静态类在程序所在的应用程序域(Application domain)的生命周期内一直保存在内存中

注意:
要创建一个只允许一个实例存在的非静态的类,请参阅C#中单例(singleton)实现的相关资料。

下面列出了一些静态类的主要特征:

  • 只包含静态成员。
  • 无法被实例化。
  • 是密封的(无法被继承)。
  • 不能包含实例构造器。

因此,创建静态类与创建只包含静态成员和私有构造函数的类基本相同。私有构造函数也会阻止类被实例化。使用静态类的好处是,编译器可以检查,以确保没有意外添加实例成员。编译器还能保证类的实例无法被创建。

静态类是密封的,因此不能被继承。同时也不能继承除Object以外的任何类。静态类无法包含实例构造函数。不过,可以包含静态构造函数。如果非静态类包含需要非平凡初始化(non-trivial initialization,大概指不是在类中写死的)的静态成员,则还应定义静态构造函数(详细信息看静态构造函数相关内容)。

1. 示例

下面示例,有一个包含两个方法的静态类,两个方法分别将温度从摄氏度转为华氏度和从华氏度转为摄氏度:

public static class TemperatureConverter
{
    public static double CelsiusToFahrenheit(string temperatureCelsius)
    {
        // Convert argument to double for calculations.
        double celsius = Double.Parse(temperatureCelsius);

        // Convert Celsius to Fahrenheit.
        double fahrenheit = (celsius * 9 / 5) + 32;

        return fahrenheit;
    }

    public static double FahrenheitToCelsius(string temperatureFahrenheit)
    {
        // Convert argument to double for calculations.
        double fahrenheit = Double.Parse(temperatureFahrenheit);

        // Convert Fahrenheit to Celsius.
        double celsius = (fahrenheit - 32) * 5 / 9;

        return celsius;
    }
}

class TestTemperatureConverter
{
    static void Main()
    {
        Console.WriteLine("Please select the convertor direction");
        Console.WriteLine("1. From Celsius to Fahrenheit.");
        Console.WriteLine("2. From Fahrenheit to Celsius.");
        Console.Write(":");

        string? selection = Console.ReadLine();
        double F, C = 0;

        switch (selection)
        {
            case "1":
                Console.Write("Please enter the Celsius temperature: ");
                F = TemperatureConverter.CelsiusToFahrenheit(Console.ReadLine() ?? "0");
                Console.WriteLine("Temperature in Fahrenheit: {0:F2}", F);
                break;

            case "2":
                Console.Write("Please enter the Fahrenheit temperature: ");
                C = TemperatureConverter.FahrenheitToCelsius(Console.ReadLine() ?? "0");
                Console.WriteLine("Temperature in Celsius: {0:F2}", C);
                break;

            default:
                Console.WriteLine("Please select a convertor.");
                break;
        }

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}
/* Example Output:
    Please select the convertor direction
    1. From Celsius to Fahrenheit.
    2. From Fahrenheit to Celsius.
    :2
    Please enter the Fahrenheit temperature: 20
    Temperature in Celsius: -6.67
    Press any key to exit.
 */

2. 静态成员

一个非静态类是可以包含静态的成员、字段、属性和事件的。即使没有创建类的实例,也可以直接在类上调用静态成员。静态成员始终是通过类名而不是实例名来访问的。并且静态成员只存在一个副本,无论你创建了多少实例。静态方法和属性不能访问非静态字段和事件,并且它们也不能访问任何对象的实例变量,除非显式地在方法参数中传递。

静态成员就是类里面静态的各种东西(属性、字段、事件等等)。

与将整个类声明为静态类相比,声明带有一些静态成员的非静态类更典型(常见)。静态字段的两个常见用途是保存已实例化对象的数量计数器,或存储必须在所有实例之间共享的值。

静态方法可以重载但不能重写,因为它们属于类,而不属于类的任何实例。

尽管不能将字段声明为static const(静态常量?),但const字段的行为本质上就是静态的。它是属于类的,而不是类的实例的。因此,const字段可以通过使用与静态字段相同的 类名.成员名 来访问,不需要对象实例。

C#不支持静态局部变量(即在方法域中声明的静态变量)。

如下所示,你可以声明静态类成员,通过在成员返回类之前使用static关键字:

public class Automobile
{
    public static int NumberOfWheels = 4;

    public static int SizeOfGasTank
    {
        get
        {
            return 15;
        }
    }

    public static void Drive() { }

    public static event EventType? RunOutOfGas;

    // Other non-static fields and properties...
}

静态成员会在第一次访问静态成员之前被初始化,如果有静态构造函数,则在调用静态构造函数之前初始化。要访问静态类成员,使用类名而不是变量名来指定成员的位置,如下所示:

Automobile.Drive();
int i = Automobile.NumberOfWheels;

如果你的类包含了静态字段,你可以提供一个静态构造函数来在类被加载时初始化它们。

静态方法的调用会在MSIL(Microsoft Intermediate Language,微软中间语言)中生成调用指令,而对实例方法的调用会生成callvirt指令,该指令还会检查空对象引用。大多数情况下,两者之间的性能差异并不显著。

3. 静态成员和实例成员

类的成员可以是静态成员或实例成员。

注意:一般来说,静态成员看作是属于类的,而实例成员看作是属于对象(类的实例)的,这点很有用。

当字段、方法、属性、事件、操作符或构造器的声明包含了static修饰符,就声明为了静态成员了。此外,常量或类型声明隐式声明为静态成员。静态成员具有以下特点:

  • 当静态成员ME.M的形式引用时,E应该是一个拥有成员M的类。如果E是一个实例,那就会报编译错误。
  • 非泛型类中的静态字段准确地标识一个存储位置。无论创建了多少非泛型的实例,静态字段都只有一个副本(内存中只有一份)。每个类型都有自己的一组静态字段,与它的实例数无关。
  • 静态函数成员(方法、属性、事件、操作符或构造器)不在实例上进行作用,并且在函数成员中引用实例也会引起编译时的错误。

当字段、方法、属性、事件、索引器、构造器或终结器不含static修饰符时,它就被声明为了实例成员。(实例成员有时也叫非静态成员)实例成员有以下特性:

  • 当实例成员ME.M的方式引用时,E应是一个含有成员M的类的实例。如果E是一个类,则会出现绑定时错误。
  • 类的每个实例都包含类的所有实例字段的一个单独集合。(就是说每个实例的这些实例字段,都是该实例所特有的)
  • 实例函数成员(方法、属性、索引器、实例构造器或终结器)作用于类的一个给定实例,这个实例能够使用this去访问。

下面例子演示并说明了静态成员和实例成员的访问规则:

class Test
{
    int x;
    static int y;
    void F()
    {
        x = 1;               // Ok, same as this.x = 1
        y = 1;               // Ok, same as Test.y = 1
    }

    static void G()
    {
        x = 1;               // Error, cannot access this.x
        y = 1;               // Ok, same as Test.y = 1
    }

    static void Main()
    {
        Test t = new Test();
        t.x = 1;       // Ok
        t.y = 1;       // Error, cannot access static member through instance
        Test.x = 1;    // Error, cannot access instance member through type
        Test.y = 1;    // Ok
    }
}

F方法展示了一个实例函数成员,simple_name可以用来访问实例成员和静态成员。G方法是一个静态函数成员,通过simple_name来访问实例成员时,它会报编译时错误。Main方法表明了实例成员应该通过实例去访问,静态成员应该通过类来访问。(simple_name在这里可以暂时理解为类中的成员标识名)


三、结尾

  • 静态类无法被实例化
  • 静态类可用作操作一组方法的便捷容器(像Math那样,方便地将一大类的方法统一管理起来,而不是像C语言那样分散开来)
  • 静态构造函数仅调用一次,静态类在程序所在的应用程序域的生命周期内一直保存在内存中
  • 静态成员始终是通过类名而不是实例名来访问的
  • 无论你创建了多少实例,静态成员只存在一个副本
  • 声明时不含static修饰符时(const这种隐式静态的除外),它就被声明为了实例成员(实例成员有时也叫非静态成员)

看下来似乎文章开头的问题没有明显的答案,但有一句话已经很接近实际现象了,

静态成员会在第一次访问之前被初始化,如果有静态构造函数,则在调用静态构造函数之前初始化。

想象一下,如果你new了一个对象,内部触发了构造函数,构造函数中用了一些字段属性来做运算进行该对象的初始化。后来你修改了其中一些做运算用到的属性值,那么那些在构造函数代码中受影响的字段或属性会自动发生变化吗?当然是不会。这边也是一样的道理,如果把string.format()看作是该静态字段的构造函数,那也就初始化的时候运行了一次,后面你修改相关变量,它才不会自动变化了。

所以我以为这就是答案。那如果我想修改,应该怎么做呢?
如果还是沿用这种思路来实现,应该定义一个修改ConnectionStatement的静态方法,每次更改相关变量时,去调用它。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值