静态变量和静态字段的实现区别是什么

本文关键字:静态 区别 是什么 实现 字段 变量 | 更新日期: 2023-09-27 18:25:38

这个问题来自编译器实现的角度

我想知道C#中的静态变量,我找到了为什么它们没有实现的解释(这里:http://blogs.msdn.com/b/csharpfaq/archive/2004/05/11/why-doesn-t-c-support-static-method-variables.aspx)。

引用"通过类级静态可以获得几乎相同的效果"——这让我很好奇,有什么区别?假设C#将具有静态变量语法——实现可以是"将其作为静态字段静默地推送,并保留条件初始化(如果必要)"。完成。

我唯一能发现的是给定初始化的值类型的问题。还有什么适合"差不多"的吗?

我重新表述了这个问题——如何在C#编译器中仅使用现有功能来实现静态变量(因此静态变量必须在内部以当前状态生成)。

静态变量和静态字段的实现区别是什么

实际上,检查编译器在C#中实现静态变量时需要做什么非常容易。

C#被设计为编译成CIL(公共中间语言)。支持静态变量的C++也可以编译为CIL。

让我们看看当我们这样做时会发生什么。首先,让我们考虑以下简单的类:

public ref class Class1
{
private:
    static int i = 0;
public:
    int M() {
        static int i = 0;
        i++;
        return i;
    }
    int M2() {
        i++;
        return i;
    }
};

}

两个方法,行为相同-i初始化为0,每次调用方法时递增并返回。让我们比较一下IL。

.method public hidebysig instance int32  M() cil managed
{
  // Code size       20 (0x14)
  .maxstack  2
  .locals ([0] int32 V_0)
  IL_0000:  ldsfld     int32 '?i@?1??M@Class1@CppClassLibrary@@Q$AAMHXZ@4HA'
  IL_0005:  ldc.i4.1
  IL_0006:  add
  IL_0007:  stsfld     int32 '?i@?1??M@Class1@CppClassLibrary@@Q$AAMHXZ@4HA'
  IL_000c:  ldsfld     int32 '?i@?1??M@Class1@CppClassLibrary@@Q$AAMHXZ@4HA'
  IL_0011:  stloc.0
  IL_0012:  ldloc.0
  IL_0013:  ret
} // end of method Class1::M
.method public hidebysig instance int32  M2() cil managed
{
  // Code size       20 (0x14)
  .maxstack  2
  .locals ([0] int32 V_0)
  IL_0000:  ldsfld     int32 CppClassLibrary.Class1::i
  IL_0005:  ldc.i4.1
  IL_0006:  add
  IL_0007:  stsfld     int32 CppClassLibrary.Class1::i
  IL_000c:  ldsfld     int32 CppClassLibrary.Class1::i
  IL_0011:  stloc.0
  IL_0012:  ldloc.0
  IL_0013:  ret
} // end of method Class1::M2

同样。唯一的区别是字段名称。它使用的字符在CIL中是合法的,但在C++中是非法的,因此在C++代码中不能使用相同的名称。对于自动生成的字段,C#编译器经常使用这种技巧。唯一的区别是静态变量不能通过反射访问——我不知道是怎么做到的。

让我们来看一个更有趣的例子。

int M3(int a) {
    static int i = a;
    i++;
    return i;
}

现在,乐趣开始了。无法再在编译时初始化静态变量。它必须在运行时完成。编译器必须确保它只初始化过一次,所以它必须是线程安全的。

由此产生的CIL是

.method public hidebysig instance int32  M3(int32 a) cil managed
{
  // Code size       73 (0x49)
  .maxstack  2
  .locals ([0] int32 V_0)
  IL_0000:  ldsflda    int32 '?$TSS0@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'                                              
  IL_0005:  call       void _Init_thread_header_m(int32 modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)*)
  IL_000a:  ldsfld     int32 '?$TSS0@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'
  IL_000f:  ldc.i4.m1
  IL_0010:  bne.un.s   IL_0035
  .try
  {
    IL_0012:  ldarg.1
    IL_0013:  stsfld     int32 '?i@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'
    IL_0018:  leave.s    IL_002b
  }  // end .try
  fault
  {
    IL_001a:  ldftn      void _Init_thread_abort_m(int32 modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)*)
    IL_0020:  ldsflda    int32 '?$TSS0@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'
    IL_0025:  call       void ___CxxCallUnwindDtor(method void *(void*),
                                                   void*)
    IL_002a:  endfinally
  }  // end handler
  IL_002b:  ldsflda    int32 '?$TSS0@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'
  IL_0030:  call       void _Init_thread_footer_m(int32 modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)*)
  IL_0035:  ldsfld     int32 '?i@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'
  IL_003a:  ldc.i4.1
  IL_003b:  add
  IL_003c:  stsfld     int32 '?i@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'
  IL_0041:  ldsfld     int32 '?i@?1??M3@Class1@CppClassLibrary@@Q$AAMHH@Z@4HA'
  IL_0046:  stloc.0
  IL_0047:  ldloc.0
  IL_0048:  ret
} // end of method Class1::M3

看起来要复杂得多。第二个静态字段,看起来像是一个关键部分(尽管我找不到任何关于_Init_thread_*方法的信息)。

它看起来不再那么琐碎了。性能也受到影响。IMHO,不在C#中实现静态变量是一个很好的决定。

总而言之,

为了支持静态变量,C#编译器必须:

  1. 为变量创建一个专用静态字段,确保名称是唯一的,并且不能直接在C#代码中使用
  2. 通过反射使此字段不可见
  3. 如果无法在编译时完成初始化,请确保其线程安全

这看起来不算多,但如果你把几个像这样的功能结合起来,复杂性就会呈指数级上升。

您得到的唯一回报是一个简单的、编译器提供的、线程安全的初始化。

仅仅因为其他语言支持而向一种语言添加功能不是一个好主意。在真正需要的时候添加该功能。C#设计团队已经在数组协方差方面犯了这个错误

我的想法是,您需要开始在初始化程序上放置"不可见"锁。

考虑两个线程同时类Foo.UseStatic;

class Foo
{
    static int counter = 0;
    void UsesStatic()
    {
        static int bar = (counter++) + (counter++);
    }
}

基于counter++bar初始化可能是一场线程噩梦。(看一下联锁类。)

如果同时有十个线程调用此代码,那么bar可能会使用任何旧的vale。锁可以稳定东西,但在没有用户同意的情况下,你就插入了这个大而钝的性能障碍。

编辑:添加了新场景

@greenoldman的一条评论表明,这个简单的例子是可以处理的。但是C#充满了句法糖,它被转换成不同的"基本"结构。例如,闭包被转换为带有字段的类,using语句变成try/finaly块,等待的调用变成传递的回调,迭代器方法变成状态机。

那么,当静态变量初始化发生时,编译器是否必须处理任何特殊情况?我们有信心做到这一点吗?

async Task<int> UsesStatic(int defaultValue) 
{
    static int bar;
    try
    {
        throw new Exception("Boom!");
    }
    catch
    {
        using(var errorLogger = Log.NewLogger("init failed")
        {
            // here's the awaited call;
            bar = await service.LongRunningCall(() => Math.Abs(defaultValue));
            // that'll fail; 
            throw new Exception("Oh FFS!");
        }
    }
    finally
    {
        bar = 0;
    }
    return bar;
}

我的猜测是,C#团队看到了这一点,认为"这是一个纯粹的错误来源",并没有理会。