在方法内部使用const代替变量的优点


77

每当方法中有局部变量时,ReSharper建议将其转换为常量:

// instead of this:
var s = "some string";
var flags = BindingFlags.Public | BindingFlags.Instance;

// ReSharper suggest to use this:
const string s = "some string";
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;

鉴于这些实际上是常量值(而不是变量),我知道ReSharper建议将其更改为const。

但是除此之外,使用const(例如,更好的性能)时是否还有其他优点可以证明使用合理的关键字const BindingFlags代替了方便和可读的var关键字?

顺便说一句:我刚刚在这里发现了一个类似的问题:Resharper总是建议我使用const string而不是string,但是我认为这更多地是关于类的字段,而我的问题是关于局部变量/ consts的。

Answers:


87

如果尝试为常量分配值,编译器将引发错误,从而可能防止您意外更改它。

同样,使用常量与变量通常会带来较小的性能优势。根据此MSDN杂志的问答与它们被编译为MSIL的方式有关:

现在,无论代码中引用了myInt的什么地方,MSIL都不必加载“ ldloc.0”来从变量中获取值,而是只需加载将其硬编码到MSIL中的常数值即可。因此,使用常量通常在性能和内存上具有较小的优势。但是,要使用它们,您必须在编译时获取变量的值,并且在编译时对此常量的任何引用(即使它们位于不同的程序集中)也将进行此替换。

如果您知道编译时的值,则常量无疑是有用的工具。如果不这样做,但要确保仅将变量设置一次,则可以在C#中使用readonly关键字(映射到MSIL中的initonly)来指示只能在构造函数中设置变量的值;在那之后,更改它是一个错误。当字段有助于确定类的标识时,通常使用此方法,并且通常将其设置为等于构造函数参数。


6
对于局部变量,使用时没有性能优势const。它不会为编译器提供任何其他信息。检查此答案以获取更多详细信息。
Drew Noakes '18

至少对于单声道而言,这并不总是正确的。已通过Mono 5.14.0.177验证。当使用const计算某些值(fe const int a = 10; const int b = a + 20;)时,局部const将导致不同的语句。ldc.r4 110代替ldc.i4.s和其他操作来实际计算值。(以Debug为目标进行测试,仍然发现它是相关的,尤其是对于高性能代码;对于低性能,差异可能不那么相关)
DanielBişar18年

2
@SACO如果您担心性能问题,应该检查发布版本。即使Mono编译器发出加法运算,JIT也可能会将基本常量折叠应用于机器代码。C#7.3中的Roslyn做正确的事
Drew Noakes

@Drew Noakes真好!与您分享了一个很棒的链接。
DanielBişar19年

@SACO一定也要签出JIT Asm视图
Drew Noakes

28

tl; dr对于具有文字值的局部变量,const完全没有区别。


您对“内部方法”的区分非常重要。让我们看一下,然后将其与const字段进行比较。

常量局部变量

局部变量的唯一好处const是无法重新分配该值。

但是const仅限于基本类型(intdouble...)和string,这限制了其适用性。

题外话:C#编译器提出了一些建议,以允许使用“只读”局部变量(此处)的更一般概念,从而将这种优势扩展到其他场景。他们可能不会被认为是const,虽然,很可能有这样的声明不同的关键字(即letreadonly var或类似的东西)。

考虑以下两种方法:

private static string LocalVarString()
{
    var s = "hello";
    return s;
}

private static string LocalConstString()
{
    const string s = "hello";
    return s;
}

在内置Release模式下,我们看到以下(删节的)IL:

.method private hidebysig static string LocalVarString() cil managed 
{
    ldstr        "hello"
    ret          
}

.method private hidebysig static string LocalConstString() cil managed 
{
    ldstr        "hello"
    ret          
}

如您所见,它们都产生完全相同的IL。本地s是否const没有影响。

原始类型也是如此。这是一个使用示例int

private static int LocalVarInt()
{
    var i = 1234;
    return i;
}

private static int LocalConstInt()
{
    const int i = 1234;
    return i;
}

再次,IL:

.method private hidebysig static int32 LocalVarInt() cil managed
{
    ldc.i4       1234
    ret          
}

.method private hidebysig static int32 LocalConstInt() cil managed
{
    ldc.i4       1234
    ret     
}

因此,我们再次看到没有区别。这里不能有性能或内存差异。唯一的区别是开发人员无法重新分配该符号。

常量字段

const字段与可变字段进行比较不同的。非常量字段必须在运行时读取。因此,您最终会遇到这样的IL:

// Load a const field
ldc.i4       1234

// Load a non-const field
ldsfld       int32 MyProject.MyClass::_myInt

显然,假设JIT本身不能内联常量值,那么这将如何导致性能差异。

这里的另一个重要区别是在程序集之间共享的公共const字段。如果一个程序集公开了一个const字段,而另一个程序集则使用了它,则在编译时将复制该字段的实际值。这意味着,如果包含const字段的程序集已更新,但使用的程序集未重新编译,则将使用旧的(可能不正确的)值。

常量表达式

考虑以下两个声明:

const int i = 1 + 2;
int i = 1 + 2;

对于const表单,加法必须在编译时计算,这意味着数字3保留在IL中。

对于非const格式,编译器可以自由地在IL中发出加法运算,尽管JIT几乎可以肯定会应用基本的常量折叠优化,因此生成的机器代码将是相同的。

C#7.3的编译器发出ldc.i4.3操作码为两个以上表达式。


不能同意您的说法:“对于具有文字值的局部变量,const完全没有区别。” 如果在具有早期返回值和条件语句的方法中使用它们,则是有意义的。例如,如果您在方法的开头声明一个变量,并且仅在if()中使用它,则const不会从内存中获取任何字节,直到您输入if语句(而常规变量仍然会占用一些字节)。
尤里·科兹洛夫

1
假设您进行了微优化,那么将在局部方法中使用的常量定义为类中的字段,然后在方法中引用它们是否有意义?我猜想这将以可读性为代价,并且还会使这些方法变得“独立”(封装)。
Dan Diplo

1
@YuryKozlov完全没有区别。看看IL。
Drew Noakes

@DanDiplo如果它的const无关紧要,无论您在哪里定义它。const值最终直接在IL中表示。
Drew Noakes

15

根据我的理解,Const值在运行时不存在(即,以存储在某些内存位置的变量形式),它们在编译时嵌入在MSIL代码中。因此会对性能产生影响。由于变量需要进行这些检查,因此也不需要更多的运行时来执行任何内部管理(转换检查/垃圾收集等)。


3
加1指出转换检查/垃圾回收。
克里斯蒂安·马克

在原来的问题给出的代码没有差别所产生的IL,因此没有向存储器,检查,垃圾回收等差
德鲁诺克斯

在运行时不存在常量值会引起误解。它们确实存在,否则您将无法使用它们。
利亚姆

@Liam-同意,我试图在下一句话中解释它。“常量值在运行时不存在-即以存储在某个内存位置的变量形式存在”
YetAnotherUser

4

const是一个编译时间常量-这意味着所有使用const变量的代码都将编译为包含const变量包含的常量表达式-发出的IL本身将包含该常量值。

这意味着您的方法的内存占用空间较小,因为常量不需要在运行时分配任何内存。


1
我不相信关于内存减少的说法在这里是正确的。const仅适用于原语(导致IL将常量加载到评估堆栈上的IL,如您所说)和字符串(它们分配相同数量的内存,而不管它们是否为const)。
德鲁·诺阿克斯

1
进行一些测试,在运行时与内存或其他任何东西都没有区别。生成的代码是相同的。
Drew Noakes '18

4

除了性能方面的细微改进外,声明常量时,您还将对自己和将使用您的代码的其他开发人员强制执行两个规则

  1. 我现在必须用一个值初始化它,而我在其他任何地方都无法做。
  2. 我无法在任何地方更改其值。

在代码中,所有内容都与可读性和通信性有关。


2

常量值也在对象的所有实例之间“共享”。这也可能导致内存使用率降低。

举个例子:

public class NonStatic
{
    int one = 1;
    int two = 2;
    int three = 3;
    int four = 4;
    int five = 5;
    int six = 6;
    int seven = 7;
    int eight = 8;
    int nine = 9;
    int ten = 10;        
}

public class Static
{
    static int one = 1;
    static int two = 2;
    static int three = 3;
    static int four = 4;
    static int five = 5;
    static int six = 6;
    static int seven = 7;
    static int eight = 8;
    static int nine = 9;
    static int ten = 10;
}

.Net中的内存消耗是棘手的,我不会假装理解它的详细信息,但是如果实例化具有一百万个“静态”值的列表,则使用的内存可能会比不使用它少得多。

    static void Main(string[] args)
    {
        var maxSize = 1000000;
        var items = new List<NonStatic>();
        //var items = new List<Static>();

        for (var i=0;i<maxSize;i++)
        {
            items.Add(new NonStatic());
            //items.Add(new Static());
        }

        Console.WriteLine(System.Diagnostics.Process.GetCurrentProcess().WorkingSet64);
        Console.Read();
    }

使用“非静态”时,工作集为69,398,528,而使用静态时,仅为32,423,936。


您可以使用的唯一参考类型conststring。const字符串(const string s = "foo";)和字符串文字(var s = "foo";)都将被插入。因此,这里的内存消耗没有区别。所有其他const值均按值传递。
德鲁·诺阿克斯

@DrewNoakes-我不确定我是否完全理解,但是我扩大了答案。如果我犯了一个错误或错过了你的意思,请告诉我。
罗伯P.18年

静态不同于const,与原始问题无关,对不起。我认为您的修改无济于事。您实际上无法使用const对N个元素进行测试,因为您最终会在内存中获得一个实例,就像使用编译时字符串一样。例如,"hello"将截断一个字符串文字,因此定义"hello"为局部变量的两个方法最终将共享相同的引用(基于字符串是不可变的)。但是,它"hello".ToUpper()是一个运行时值,因此多个调用将产生多个实例。
德鲁·诺阿克斯

我在此答案中提供了更多信息。
德鲁·诺阿克斯


1

C#中的常量在内存中提供了一个命名位置来存储数据值。这意味着变量的值将在编译时已知,并将存储在单个位置。

声明它时,它在Microsoft中间语言(MSIL)中是一种“硬编码”。

尽管有一点点,但它可以提高代码的性能。如果我声明一个变量,并且可以将其设为const,那么我总是这样做。不仅因为它可以提高性能,而且因为这就是常量的概念。否则,它们为什么存在?

Reflector在这种情况下非常有用。尝试声明一个变量,然后使其成为常量,并查看在IL中生成了什么代码。然后,您所需要做的就是查看说明中的差异,并查看这些说明的含义。


您的第二句话不是@BrokenGlass在回答中写的相反吗?
M4N
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.