65.9K
CodeProject 正在变化。 阅读更多。
Home

必须记住:关键字“Static”的 9 个关键概念

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (97投票s)

2015年4月1日

CPOL

12分钟阅读

viewsIcon

113251

downloadIcon

1035

本文总结了每个开发人员都必须记住的关键字“Static”的一些关键概念。

前言

当我们谈论面向对象编程 (OOP) 方法论时,脑海中会立刻闪现抽象、封装、多态和继承。随着咖啡在桌上混合了更多人,讨论往往会进一步延伸到数据隐藏、重载、重写、抽象类和密封类。根据我的经验,每次我回顾这些术语时,我都会遇到一些未知的角落,这让我感到惊讶。其中一个关键字就是“Static”。它始于一个简单的“共享”概念,但深入研究,它会导致一些令人惊讶的结果。今天,我们将探讨这些概念。

免责声明:本文涉及 C# 编程语言中 Static 关键字的概念。其中一些概念可能不适用于其他高级编程语言。

在我们开始之前…

对于那些对 static 关键字了解不多或了解很少的人,我想快速回顾一下概念。

例如,我们有以下 DistanceConverter 类,其中有一个名为 GetDistanceInKM() 的方法,该方法接受以英里为单位的距离并将其转换为 KM(公里)。

    /// <summary>
    /// Converts distance from one unit to another
    /// </summary>
    public class DistanceConverter
    {
        float _multiplier = 1.60934F;

        /// <summary>
        /// Return the distance in KM
        /// </summary>
        /// <param name="distanceInMiles">Distance in Miles</param>
        /// <returns></returns>
        public float GetDistanceInKM(float distanceInMiles)
        {
            return (_multiplier * distanceInMiles);
        }
    }

Main() 函数

    class Program
    {
        static void Main()
        {
            DistanceConverter objD1 = new DistanceConverter();
            Console.WriteLine("D1:" + objD1.GetDistanceInKM(2));

            DistanceConverter objD2 = new DistanceConverter();
            Console.WriteLine("D2:" + objD2.GetDistanceInKM(3));

            DistanceConverter objD3 = new DistanceConverter();
            Console.WriteLine("D3:" + objD3.GetDistanceInKM(4));

            Console.ReadLine();
        }
    }

Main() 中可以看到,我们创建了 DistanceConverter 类的多个对象。以下是后台的内存分配。

可以看到,对象 objD1objD2objD3 都使用了相同的字段和相同的方法。它们只是碰巧传递了不同的以英里为单位的距离值来获得 KM 距离。仔细观察,字段 _multiplier 的值是相同的。如果我们创建另外 100、200 或 1000 个对象,字段 _multiplier 的值将会在所有对象中重复出现。但是等等,这是内存使用不当!真正的意义上,最好能有 1 个 _multiplier 副本在类的所有实例之间共享。就像下图所示一样

因此,就有了这个概念,即称为staticshared(在 VB.NET 世界中)字段或变量的共享概念。同样,我们可以拥有共享的构造函数、方法、属性甚至整个类。如果我们想以编程方式实现这一点,我们将使用 static 关键字进行标识,如下所示

    /// <summary>
    /// Converts distance from one unit to another
    /// </summary>
    public class DistanceConverter
    {
        static float _multiplier = 1.60934F;

        /// <summary>
        /// Return the distance in KM
        /// </summary>
        /// <param name="distanceInMiles">Distance in Miles</param>
        /// <returns></returns>
        public float GetDistanceInKM(float distanceInMiles)
        {
            return (_multiplier * distanceInMiles);
        }
    }

在上面的示例中,_multiplierStatic,因此被称为类的Static 成员,而 GetDistanceInKM() 方法是非静态的,因此被称为类的实例成员。通过这个例子,我们试图了解 static 关键字的重要性。让我们回到主要讨论,并讨论值得记住的关于 static 关键字的关键概念。

1. 适用性

类可以是静态的,但结构和接口不能。同样,构造函数、字段、属性和方法可以是静态的,但析构函数不能。

我们可以拥有静态类、字段、属性、构造函数和方法

    /// Static class
    static class StaticClass
    {
        //Some class members
    }

    public class SomeClass
    {
        //Static Field
        static int intstatic = 0;

        //Static Property
        public static string StaticProperty { get; set; }

        //Static Constructor
        static SomeClass()
        { }

        //Static Method
        public static void DoSomething()
        {
            Console.WriteLine(intstatic);
        }
    }

但我们不能拥有静态接口、结构或析构函数

    /// Trying to declare static Interface
    static interface IStaticInterface
    {
        //some interface members
    }

    /// Trying to declare static structure
    static struct StaticStruct
    {
        //some structure member
    }

    /// Some Random class
    class SomeClass1
    {
        /// Trying to declare static Destructor
        static ~SomeClass1()
        { }
    }

如果我们尝试创建一个,将得到以下编译错误

"该项的修饰符‘static’无效"

稍后,我们还将讨论为什么这样的实现是有意义的。

2. 访问修饰符

静态构造函数没有访问修饰符。其他静态元素可以。

我们可以拥有带有访问修饰符的静态类、字段、属性和方法。但如果您对静态构造函数这样做

    //Static class with Access Modifier
    public class SomeClass
    {
        //Static Field with Access Modifier
        public static int intstatic = 0;

        //Static Property with Access Modifier
        public static string StaticProperty { get; set; }

        //Trying to declare static Constructor with Access Modifier
        public static SomeClass()
        { }

        //Static Method with Access Modifier
        public static void DoSomething()
        {
            Console.WriteLine(intstatic);
        }
    }

这将导致编译错误。

"不允许对静态构造函数使用访问修饰符"

另外,如果您注意到,到目前为止我一直使用“构造函数”一词,而不是“构造函数(s)”。为什么?下一节将看到。

3. 唯一的静态构造函数

一个类只能有一个静态构造函数,而且该构造函数必须是无参数的。

如果我们遵循重载,我们可以拥有多个具有不同签名的构造函数。如果我们尝试将它们标记为静态,

    public class SomeClass
    {
        /// Static Constructor without parameters
        static SomeClass()
        {
            //Do something
        }

        /// Trying to declare Static Constructor with parameters
        static SomeClass(int input)
        {
            //Do something
        }
    }

我们将收到编译错误

"静态构造函数必须是无参数的"

因此,一个类不能有带参数的静态构造函数,因此它只能有一个静态构造函数,而且是无参数的。

4. 优先级 - 静态 vs 实例构造函数

静态构造函数在实例构造函数之前执行,并且只执行一次。

让我们创建一个简单的程序来证明这一点

    public class SampleClass
    {
        //Static Constructor
        static SampleClass()
        {
            Console.WriteLine("This is SampleClass Static Constructor.");
        }

        //Instance Constructor
        public SampleClass()
        {
            Console.WriteLine("This is SampleClass Instance Constructor.");
        }
    }

    class Program
    {
        static void Main()
        {
            SampleClass objC1 = new SampleClass();
            SampleClass objC2 = new SampleClass();
            SampleClass objC3 = new SampleClass();

            Console.ReadLine();
        }
    }

此程序的输出如下

正如您所看到的,静态构造函数是第一个执行的,并且只执行一次。我们将再次回顾这一点。尤其是“之前”这个词。

5. 可访问性

实例成员在类定义外部使用类对象访问,在类定义内部使用'this'关键字访问,而静态成员可以直接使用类名称在定义外部和内部进行访问。

这是一个要点,而且很有意义,因为静态成员独立于实例对象。

    public class SampleClass {
        public string instanceMsg = "This is instance Field.";
        public static string staticMsg = "This is static Field";

        public SampleClass()
        {
            // Access within class definition
            Console.WriteLine("Within class instanceMsg:{0}", 
                                this.instanceMsg);
            Console.WriteLine("Within class staticMsg:{0}", 
                                SampleClass.staticMsg);
        }
    }
    
    class Program
    {
        static void Main()
        {
            SampleClass ObjS1 = new SampleClass();

            // Access outside class definition
            Console.WriteLine("From Main Program instanceMsg: {0}", 
                                ObjS1.instanceMsg);
            Console.WriteLine("From Main Program staticMsg: {0}", 
                                SampleClass.staticMsg);

            Console.ReadLine();
        }
    }

我们将在接下来的部分中再次回顾这一点。

6. 兼容性 – 静态 vs 实例成员/容器

实例成员不能从静态容器创建或访问,而静态成员可以从实例容器创建或访问。

副标题不清楚!别担心。让我们通过一些例子来理解这一点。

在场景 1 中,我们尝试在实例方法 DoSomething() 中访问静态字段 iInc

   // This compiles successfully   
   public class InstanceContainerStaticMember
    {
        static int iInc = 0;
        
        // Instance Container Method
        public void DoSomething()
        {
            // Static Member
            iInc++;
            Console.WriteLine("iInc:{0}", iInc);
        }
    }

另一方面,在场景 2 中,我们尝试在静态方法 DoSomething() 中访问实例成员 iInc

   // This will result in compilation Error
   public class StaticContainerInstanceMember
    {
        int iInc = 0;

        /// Static Container Method
        public static void DoSomething()
        {
            //Instance Member
            iInc++;
            Console.WriteLine("iInc:{0}", iInc);
        }
    } 

但是当我们编译时,场景 2 抛出了编译错误。

"非静态字段、方法或属性需要对象引用"

因此,编译错误很清楚,我们不能从静态容器访问实例成员。为什么?我们将通过一个例子来探讨。回顾第 5 点。根据第 5 点,静态成员是用类名引用的。所以在这个例子中

    public class SampleClass {
        private int _id;

        public SampleClass(int ID)
        {
            _id = ID;
        }

        public static void StaticMethod()
        { 
            //I want to access _id but who will provide me
        }
    }
    class Program
    {
        static void Main()
        {
            SampleClass s1 = new SampleClass(1);
            SampleClass s2 = new SampleClass(2);
            SampleClass s3 = new SampleClass(3);

            //What will be the value for this s1, s2 or s3
            SampleClass.StaticMethod();
        }
    }

我们有一个 SampleClass,它有一个非静态字段 _id 和一个静态方法 StaticMethod(),该方法想要访问 _id。在 Main() 程序中,创建了 3 个 SampleClass 类的对象 s1s2s3。因此,对于对象 s1 s2s3_id 的值将分别是123。现在,当程序调用 SampleClass.StaticMethod() 时。由于它是用类名调用的,如果静态方法被允许访问 _id它应该返回哪个 _id 值,s1s2 还是 s3?不,它无法返回任何值,因为 StaticMethod() 与类对象无关,因此也与实例成员无关,所以编译器会显示错误。这对于静态类也是如此。静态类不能有实例成员,如字段、属性、构造函数或方法。自己试试吧!

7. 对象创建和实例化

静态类的对象既不能被创建也不能被实例化。

由于静态类不能有实例成员,因此它不能被实例化。在下面的示例中,我们尝试创建 SampleClass 类的实例。

    public static class SampleClass { 
        //static class members
    }

    class Program
    {
        static void Main()
        {
            //This statement will result compilation error.
            SampleClass s1 = new SampleClass();
        }
    }

我们将收到编译错误

"不能声明‘SampleClass’的静态类型变量"
"不能创建静态类‘SampleClass’的实例"

重要

在构建应用程序时,您会遇到在应用程序中重复使用但又不属于特定代码层级的代码块。开发人员通常称它们为实用函数。它们在应用程序中提供通用实用功能。您肯定希望将这些代码模块化。同时,您希望在不创建对象的情况下访问这些函数。静态类非常适合组织这些函数。在 Microsoft.NET 框架中,Math 类就是一个很好的实用类示例。请看下面的截图

8. 继承

静态类不能成为继承的一部分。它不能作为基类、派生类或实现接口。

是的,这是真的。一如既往,让我们通过以下示例来看。

情况 1:静态类不能作为基类

    public static class BaseClass 
    { 
        //Class member goes here
    }
    public class DeriveClass : BaseClass
    { 
        //Class member goes here
    }

编译错误

"不能从静态类‘BaseClass’派生"

情况 2:静态类不能作为派生类

    public class BaseClass
    {
        //Class member goes here
    }
    public static class DeriveClass : BaseClass
    {
        //Class member goes here
    }

编译错误

"静态类‘DeriveClass’不能派生自类型‘BaseClass’。静态类必须派生自 object。"

情况 3:静态类不能实现接口

    public interface IInterface
    {
        //Interface member goes here
    }
    public static class DeriveClass : IInterface
    {
        //Class member goes here
    }

编译错误

"‘DeriveClass’:静态类不能实现接口"

这很清楚,但有一个问题。我们已经看到静态类不能成为继承层次结构的一部分,那么静态类中的 protected 成员会怎么样?答案是静态类不能有 protected 成员。如果您声明一个,

    public static class StaticClass 
    {
        //Protected is not allowed here
        protected static int iNum;
        
        static StaticClass()
        { }
    }

它将抛出编译错误

"静态类不能包含受保护的成员"

事实上,非静态类中的静态成员不能被重写。 

    public class BaseClass
    {
        public static virtual void StaticMethod()
        { }
    }

    public class DerivedClass : BaseClass
    { 
        public override void StaticMethod()
        { }
    }

编译错误

"静态成员不能标记为 override、virtual 或 abstract"

9. 生命周期

只要静态元素第一次在程序中被引用,它们就会处于作用域内,并在整个 AppDomain 的生命周期内保持作用域内。

这是理解静态元素作用域的一个重要概念,但我将讨论更多。请看下面的示例

    public class SampleClass
    {
        static int _iCount = 0;

        static SampleClass()
        {
            Console.WriteLine("This is Static Ctor");
        }

        public void SetValue(int Count)
        {
            _iCount = Count;
        }

        public static void Print()
        {
            Console.WriteLine("The value of Count:{0}",
                                SampleClass._iCount);
        }
    }

在上面的类中,我们有一个静态字段 _iCount 和一个静态方法 Print()。在下面的 Main() 程序中,我们正在调用函数 function1()function2()。在这两个函数中,我们都在创建 SampleClass 类的对象。

    class Program
    {
        static void Main()
        {
            Console.WriteLine("First line in Main().");
            SampleClass.Print();
            function1();
            function2();
            SampleClass.Print();
            Console.WriteLine("Last line in Main().");
            Console.ReadLine();
        }

        public static void function1()
        {
            Console.WriteLine("First line in function1().");
            SampleClass objS2 = new SampleClass();
            objS2.SetValue(1);
            Console.WriteLine("Last line in function1().");
        }

        public static void function2()
        {
            Console.WriteLine("First line in function2().");
            SampleClass objS3 = new SampleClass();
            objS3.SetValue(2);
            Console.WriteLine("Last line in function2().");
        }
    }

最后是这个程序的输出

一些有趣的观察

  • 输出的第二行是对静态构造函数的调用(参见“这是静态 Ctor”)。但是,如果您参考 Main() 函数,我们既没有创建对象,也没有实例化。甚至 SampleClass 也未声明为 static。但是,程序会调用静态构造函数。请记住,在第 4 点中,我们讨论过静态构造函数在实例构造函数之前调用。“之前”这个词很重要,因为它表明,一旦静态元素被访问(无论是否创建对象),构造函数就会初始化,然后静态成员就会处于作用域内,但不是在程序开始执行时。因为输出的第一行(即“Main() 中的第一行。”)是 Main() 函数的 Console.writeline() 语句,然后调用 SampleClass 的构造函数。总结来说,只要静态元素以任何方式被程序引用,它们就处于作用域内。
  • 另外,在 function1()function2() 中分别创建了对象 ObjS2ObjS3 。理想情况下,它们的作用域仅限于各自的函数体。但是当我们打印 Main()_iCount 的值时,打印的值是 function2() 设置的“2”(即 objS3.SetValue(2))。

所以从上一点的收获是,静态元素在程序执行完最后一行之前一直处于活动状态。对于这个程序,我们的假设是绝对正确的。原因是,我们程序的范围仅限于一个AppDomain。但是,如果我们引入不同的AppDomain,情况将会改变。让我们看另一个例子。

假设我们有一个类库项目 

namespace RemoteClassLibrary
{
    public class RemoteClass
    {
        static int _myID = 0;

        public RemoteClass()
        {
            Console.WriteLine("My ID is {0}", ++_myID);
        }
    }
}

我们将类库 dll 保存到本地驱动器 c:\app\。我们还创建了另一个使用反射来执行此代码的程序

using System.Reflection;

namespace TestingProgram
{
    class Program
    {
        static void Main()
        {
            const string ClassLibraryPath = @"c:\app\RemoteClassLibrary.dll";

            //Load assembly in current App Domain
            Assembly Assembly1 = Assembly.LoadFrom(ClassLibraryPath);
            Assembly1.CreateInstance("RemoteClassLibrary.RemoteClass");

            //Load assembly in current App Domain
            Assembly Assembly2 = Assembly.LoadFrom(ClassLibraryPath);
            Assembly2.CreateInstance("RemoteClassLibrary.RemoteClass");

            Console.ReadKey();
        }
    }
}

此程序的明显输出是

请注意,即使加载了该程序集,静态成员仍然保留其值,或者可以说只创建了静态类的一个副本。现在,让我们修改主程序以引入多个 AppDomain,它们将分别加载此程序集

    class Program
    {
        static void Main()
        {
            const string ClassLibrary = "RemoteClassLibrary.RemoteClass";
            const string ClassLibraryPath = @"c:\app\RemoteClassLibrary.dll";

            //Load class Library in AppDomain1
            AppDomain AppDomain1 = AppDomain.CreateDomain("AppDomain1");
            AppDomain1.CreateInstanceFrom(ClassLibraryPath, ClassLibrary);

            //Load class Library in AppDomain2
            AppDomain AppDomain2 = AppDomain.CreateDomain("AppDomain2");
            AppDomain2.CreateInstanceFrom(ClassLibraryPath, ClassLibrary);

            Console.ReadKey();
        }
    }

令人惊讶的输出是,

因此,即使单个程序正在执行,但由于不同的 AppDomain,程序会为每个 AppDomain 维护 2 个静态成员的副本。简而言之,它们的作用域限制在 AppDomain 内。

最后,我们已经讨论过,静态元素一旦被访问就会处于作用域内,并且作用域限制在特定的 AppDomain 内。

为什么接口、结构和析构函数不能是静态的?

我在第 1 点中提到,接口、结构和析构函数不能是静态的,我故意没有详细讨论,因为我们需要有足够的基础才能证明这一点。现在我们已经回顾了静态成员的一些关键概念,现在是时候详细讨论了。

接口设置了指导方针和期望,供实现类了解其功能。如果类是对象的蓝图,那么接口就提供了类的轮廓/契约。为了证明,接口描述了对象的功能,但不提供对象表示。

为了更清楚,让我们举个例子

    public interface IVehicle
    {
        void Drive();
    }

    public class Car : IVehicle
    {
        public void Drive()
        {
            Console.WriteLine("I am Driving Car");
        }
    }

    public class Bike : IVehicle
    {
        public void Drive()
        {
            Console.WriteLine("I am Riding Bike");
        }
    }

如上例所示,接口 IVehicle CarBike 类提供了契约。而 CarBike 提供了它们的原生表示。IVehicle 没有自己的表示,但可以要求 CarBike 提供自己的表示。

    class Program
    {
        static void Main()
        {
            IVehicle vehicle1 = new Car();
            vehicle1.Drive();
            //I am Driving Car

            IVehicle vehicle2 = new Bike();
            vehicle2.Drive();
            //I am Riding Bike

            Console.Read();
        }
    }

为了理解,即使我们假设 IVehicle 被允许拥有静态,它也不会有对象表示,因为静态类型不能被实例化(记住第 7 点),并且 IVehicle.Drive() 将没有表示。因此,在语义上,静态接口是没有意义的。

至于结构,结构在内存表示方式上与类不同。结构是值类型,类是引用类型。由于我们不能拥有静态类型的实例,因此 static classstatic struct 的结果将是相同的表示。这会导致不必要的重复和混淆。

最后是析构函数,它们用于对象内存清理(释放资源)。静态类型没有对象表示,因此我们不需要静态析构函数,就像结构(值类型)没有析构函数一样。

结束语

我们从跨类所有对象共享变量的简单概念开始,然后深入研究了这个简单概念所代表的各种场景,这肯定会让我们感到惊讶。为了给您一个更精确的例子,这就像一个客户提出实现一个简单的程序,但随着时间的推移,当您构建、测试、审查和交付时,您会意识到完成这个简单的程序需要多少细节。在我们整个讨论中,我试图通过例子来演示一切,使其简短、简单但有效。如果您有任何可以改变文章标题从“9 个关键概念”到 10、11、12 甚至更多的内容,请告诉我。我将非常乐意添加您的建议、输入和评论。

快乐学习!

版本历史

版本 1.0 - 提交的初始文章

版本 1.1 - 以下更改

  • 添加了“为什么接口、结构和析构函数不能是静态的?”
  • 第 8 点:添加了关于“类的受保护成员”以及“虚拟”和“重写”的解释
  • 第 9 点:将“静态元素作用域”详细说明为“AppDomain”

作者的其他文章

© . All rights reserved.