静态变量和静态字段之间的实现差异是什么?

3
这个问题是从编译器实现的角度提出的。
我对C#中的静态变量产生了疑问,通过这篇文章(链接在此:http://blogs.msdn.com/b/csharpfaq/archive/2004/05/11/why-doesn-t-c-support-static-method-variables.aspx),找到了为什么它们没有被实现的解释。
引用 “通过具有类级别的静态方法几乎可以获得相同的效果”,这让我很好奇,它们之间有什么区别?假设C#有静态变量语法--实现可以是“将它作为静态字段静默推送并保留条件初始化(如果必要)”。 完成。
我唯一能看出的问题是给定初始化值的值类型。还有其他与 "几乎" 相符的事情吗?
重新表达问题 - 如何使用现有功能在C#编译器中实现静态变量(因此静态变量必须在当前状态术语下进行内部制作)。

我认为区别在于方法级别的静态只能从该方法中访问,而类级别的静态可以从类中的任何地方访问,并且如果它是公共的,则可以从类外部访问,尽管我猜想,如果您试图获得几乎相同的效果,您会将其声明为私有。 - juharr
@juharr,请注意“编译器实现角度”,创建仅由知道其存在的一方访问的隐藏类成员是微不足道的。 - greenoldman
顺便说一句 - 你的问题不够明确。或许可以澄清一下你真正想要问什么。 - Yuval Itzchakov
尝试充分理解问题...改述让我感到困惑。据我理解,您正在询问阻止将提升为私有静态变量到类范围的确切情况是什么。这正确吗? - abluejelly
2个回答

3

其实,检查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_*方法的任何信息)。

它看起来并不那么简单了。性能也受到影响。在我看来,不实现C#中的静态变量是一个明智的决定。

总结一下,

为了支持静态变量,C#编译器需要:

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

这似乎并不多,但如果将几个类似于此类的功能组合在一起,复杂性就会呈指数级增长。

而你得到的唯一回报就是一个由编译器提供的易于使用、线程安全的初始化。

仅仅因为其他语言支持某个功能,就向语言添加该功能并不是一个好主意。只有真正需要时才能添加该功能。C#设计团队已经在数组协变 上犯过这个错误。


唯一的区别是静态变量无法通过反射访问。如果我执行这段C#代码会发生什么:Array.ConvertAll(typeof(Class1).GetFields(BindingFlags.Static|BindingFlags.NonPublic|BindingFlags.Public),fi=>fi.GetValue(null))GetFields方法是否不返回该静态字段的FieldInfo对象,或者GetValue方法会抛出异常? - user4003407
@PetSerAl GetFields 只返回一个元素,即 private static int i。它不会返回静态变量的 FieldInfo - Jakub Lortz
非常感谢,看到你聪明的方法真是让人耳目一新。还要感谢你解释CIL,否则我可能会有点困难去理解它。 - greenoldman
看起来是模块中托管的静态变量,而不是类中的。但是您可以使用这种代码访问它们:typeof(Class1).Assembly.GetModules().SelectMany(mi=>mi.GetFields(BindingFlags.Static|BindingFlags.NonPublic|BindingFlags.Public)).Where(fi=>fi.Name.Contains("Class1")) - user4003407

1
我的想法是,您需要开始在初始化程序上放置“隐形”锁定。

考虑两个线程同时使用类Foo.UseStatic的情况。
class Foo
{
    static int counter = 0;

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

基于 counter++bar 的初始化可能会导致线程问题。(请查看 interlocked 类。)
如果十个同时运行的线程调用此代码,则 bar 可能会得到任何旧值。使用锁定将稳定事务,但这样就插入了一个大而笨重的性能障碍,而用户没有发言权。 编辑:添加了新场景。 @ greenoldman 的评论表明可以处理此简单示例。但是,C#充满了被转换为不同“基本”结构的语法糖。例如,闭包被转换为具有字段的类,using 语句变成 try / finally 块,等待调用变成传递的回调,迭代器方法变成状态机。
因此,编译器在静态变量初始化发生时是否必须处理任何特殊情况?我们对此有信心吗?
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#团队看到这个问题后认为“这是一个纯粹的错误源”,所以没有去碰它。


1
非常好的、简明扼要地解释了静态变量的问题。如果这个答案排在前面,许多滚轮的使用寿命将会增加。 - Jakub Lortz
谢谢,但静态变量本身没有问题。在您的情况下,静态变量未使用本地数据进行初始化,因此可以将其安全地移动为静态字段,并面对完全相同的问题,正如您所描述的那样。换句话说,您所描述的问题不是由于静态变量,而是由于您使用的“扭曲”表达式引起的。 - greenoldman
我在想静态构造函数保证只运行一次,所以在静态构造函数中分配bar只是一个普通的“赋值字段”语句,并且很容易理解,因为它只调用一次。在静态构造函数中,当分配bar时,没有任何东西可以修改counter,所以它是可预测的。我还附加了更多的“编译器噩梦”场景,展示了尝试证明其他语言特性相互作用可能是疯狂的。虽然不是完整的论点,但可以作为讨论的内容。 - Steve Cooper

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接