• C#静态类和静态类成员


    一、引言

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

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

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

    	_setIpCommand = new RelayCommand(() =>
        {
            MessageBox.Show(DBIp);
            DBIp = this.ServerIp;
            MessageBox.Show(DBIp);
            DBPort = this.ServerPort;
            MessageBox.Show(ConnectionStatement);
        });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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


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

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

    UtilityClass.MethodA();
    
    • 1

    静态类可用作操作一组方法的便捷容器,这些方法只需关心操作输入的参数,不需要获取或设置任何内部实例字段。例如,在.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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    与所有类一样,当引用该类的程序被加载时,.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.
     */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    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...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

    Automobile.Drive();
    int i = Automobile.NumberOfWheels;
    
    • 1
    • 2

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

    静态方法的调用会在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
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

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


    三、结尾

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

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

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

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

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

  • 相关阅读:
    企业信息化改革怎么做?
    怒刷LeetCode的第3天(Java版)
    IvorySQL3.0:基于PG16.0最新内核,实现兼容Oracle数据库再升级
    Opencv项目实战:01 文字检测OCR(2)
    网页制作课作业基于HTML+CSS+JavaScript+jquery仿慕课网教学培训网站设计实例 企业网站制作
    OpenHarmony后代组件双向同步,跨层级传递:@Provide装饰器和@Consume装饰器
    Spring Boot 3.0正式发布及新特性解读
    地震勘探——相关概念(二)
    Qtday3
    MPLS VPN跨域C1方案 RR反射器
  • 原文地址:https://blog.csdn.net/BadAyase/article/details/125909018