关于c#:如果我们只为它们分配null,为什么我们允许使用带引用类型的const?

Why are we allowed to use const with reference types if we may only assign null to them?

这个问题实际上非常简单。以下代码在其正下方引发异常:

1
2
3
4
5
6
7
class Foo
{
    public const StringBuilder BarBuilder = new StringBuilder();
    public Foo(){

    }
}

错误:

Foo.BarBuilder' is of type 'System.Text.StringBuilder'. A const field
of a reference type other than string can only be initialized with
null.

我理解,从const的角度来看,这是有意义的:

A constant expression is an expression that can be fully evaluated at
compile time. Therefore, the only possible values for constants of
reference types are string and a null reference.

然而,我不明白为什么或在哪里使用null常数。那么,为什么一开始可以用const定义一个引用类型(字符串除外),如果它只能设置为null,如果它是一个经过深思熟虑的决定(我相信它是),那么在哪里可以使用带有空值的常量呢?

更新:

当我们想到一个答案时,请让我们以不同于"我们有这个,为什么不……"的方式思考。


来自MSDN

when the compiler encounters a constant identifier in C# source code (for example, months), it substitutes the literal value directly into the intermediate language (IL) code that it produces. Because there is no variable address associated with a constant at run time, const fields cannot be passed by reference and cannot appear as an l-value in an expression.

因为在运行时需要构造引用类型(而不是空值和特殊的字符串),所以对于引用类型来说,上述方法是不可能的。

对于引用类型,最接近的是静态只读:

1
2
3
4
5
6
7
class Foo
{
    // This is not a good idea to expose a public non-pure field
    public static readonly StringBuilder BarBuilder = new StringBuilder();
    public Foo(){
    }
}

与const替换(在调用代码中)不同,static readonly创建引用类型的单个共享实例,如果程序集版本发生更改,则会有细微的差异。

尽管不能(通常)重新分配引用,但它并不排除调用StringBuilder上的非纯方法(如Append等)。这与consts不同,后者的值类型和字符串是不可变的(可以说应该是"永恒的")。


However, I don't see the reason why or where we would use null constant.

空常量用作sentinel值。

例如,这:

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
public class MyClass
{
    private const Action AlreadyInvoked = null;

    private Action _action;

    public MyClass(Action action) {
        _action = action;
    }

    public void SomeMethod()
    {
        _action();

        _action = AlreadyInvoked;
    }

    public void SomeOtherMethod()
    {
        if(action == AlreadyInvoked)
        {
            //...
        }
    }
}

比这更具表现力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyClass
{
    //...

    public void SomeMethod()
    {
        _action();

        _action = null;
    }

    public void SomeOtherMethod()
    {
        if(action == null)
        {
            //...
        }
    }
}

Lazy类的源代码显示微软使用了类似的策略。尽管它们使用了一个永远不能作为sentinel值调用的静态只读委托,但它们可能只是使用了一个空常量:

1
2
3
4
5
static readonly Func<T> ALREADY_INVOKED_SENTINEL = delegate
{
    Contract.Assert(false,"ALREADY_INVOKED_SENTINEL should never be invoked.");
    return default(T);
};


我认为您在问,为什么具有空值的引用类型允许作为常量。

我认为你是对的,它没有多大意义,但如果你设计了自己的库,如果你想与空值进行比较,但想给出特殊的意义(比如只与库值进行比较,而不是直接与空值进行比较),它是有用的。

1
2
3
4
5
6
7
public class MyClass
    {
        public const MyClass MyClassNull = null;
        public MyClass()
        {
        }
    }

它的用法是这样的。

1
2
3
4
object obj = GetMyClass();
if(obj == MyClass.MyClassNull) // This going to convert to actual null in MSIL.
{    
}

正如您在问题中所述,有一个引用类型可以放入const引用字符串中。编译器的特殊情况是这样的,将字符串放入已编译的输出中,并允许它们在运行时被读取到引用类型中。

当然,这就引出了一个问题——为什么不让字符串作为唯一可以是const的引用类型,只要我们对它们进行特殊的大小写?为此,我只能推测在编译器中添加特殊情况比在语言中添加特殊情况更简单,问题也更少。从语言的角度来看,字符串只是一种引用类型,即使编译器具有从字符串文本和编译资源创建其实例的特殊处理。