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






4.71/5 (97投票s)
本文总结了每个开发人员都必须记住的关键字“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
类的多个对象。以下是后台的内存分配。
可以看到,对象 objD1
、objD2
和 objD3
都使用了相同的字段和相同的方法。它们只是碰巧传递了不同的以英里为单位的距离值来获得 KM 距离。仔细观察,字段 _multiplier
的值是相同的。如果我们创建另外 100、200 或 1000 个对象,字段 _multiplier
的值将会在所有对象中重复出现。但是等等,这是内存使用不当!真正的意义上,最好能有 1 个 _multiplier
副本在类的所有实例之间共享。就像下图所示一样
因此,就有了这个概念,即称为static 或shared(在 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);
}
}
在上面的示例中,_multiplier
是Static,因此被称为类的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()
{ }
}
如果我们尝试创建一个,将得到以下编译错误
稍后,我们还将讨论为什么这样的实现是有意义的。
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
类的对象 s1
、s2
和 s3
。因此,对于对象 s1
、 s2
和 s3
,_id
的值将分别是1、2 和 3。现在,当程序调用 SampleClass.StaticMethod()
时。由于它是用类名调用的,如果静态方法被允许访问 _id
,它应该返回哪个 _id
值,s1
、s2
还是 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();
}
}
我们将收到编译错误
重要
在构建应用程序时,您会遇到在应用程序中重复使用但又不属于特定代码层级的代码块。开发人员通常称它们为实用函数。它们在应用程序中提供通用实用功能。您肯定希望将这些代码模块化。同时,您希望在不创建对象的情况下访问这些函数。静态类非常适合组织这些函数。在 Microsoft.NET 框架中,Math
类就是一个很好的实用类示例。请看下面的截图
8. 继承
静态类不能成为继承的一部分。它不能作为基类、派生类或实现接口。
是的,这是真的。一如既往,让我们通过以下示例来看。
情况 1:静态类不能作为基类
public static class BaseClass
{
//Class member goes here
}
public class DeriveClass : BaseClass
{
//Class member goes here
}
编译错误
情况 2:静态类不能作为派生类
public class BaseClass
{
//Class member goes here
}
public static class DeriveClass : BaseClass
{
//Class member goes here
}
编译错误
情况 3:静态类不能实现接口
public interface IInterface
{
//Interface member goes here
}
public static class DeriveClass : IInterface
{
//Class member goes here
}
编译错误
这很清楚,但有一个问题。我们已经看到静态类不能成为继承层次结构的一部分,那么静态类中的 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() { } }
编译错误
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()
中分别创建了对象ObjS2
和ObjS3
。理想情况下,它们的作用域仅限于各自的函数体。但是当我们打印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
为 Car
和 Bike
类提供了契约。而 Car
和 Bike
提供了它们的原生表示。IVehicle
没有自己的表示,但可以要求 Car
和 Bike
提供自己的表示。
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 class
和 static struct
的结果将是相同的表示。这会导致不必要的重复和混淆。
最后是析构函数,它们用于对象内存清理(释放资源)。静态类型没有对象表示,因此我们不需要静态析构函数,就像结构(值类型)没有析构函数一样。
结束语
我们从跨类所有对象共享变量的简单概念开始,然后深入研究了这个简单概念所代表的各种场景,这肯定会让我们感到惊讶。为了给您一个更精确的例子,这就像一个客户提出实现一个简单的程序,但随着时间的推移,当您构建、测试、审查和交付时,您会意识到完成这个简单的程序需要多少细节。在我们整个讨论中,我试图通过例子来演示一切,使其简短、简单但有效。如果您有任何可以改变文章标题从“9 个关键概念”到 10、11、12 甚至更多的内容,请告诉我。我将非常乐意添加您的建议、输入和评论。
快乐学习!
版本历史
版本 1.0 - 提交的初始文章
版本 1.1 - 以下更改
- 添加了“为什么接口、结构和析构函数不能是静态的?”
- 第 8 点:添加了关于“类的受保护成员”以及“虚拟”和“重写”的解释
- 第 9 点:将“静态元素作用域”详细说明为“AppDomain”