1.2 - C#语言习惯 - 用运行时常量readonly而不是编译期常量const

简介:   C#中有两种类型的常量:编译期常量和运行时常量。二者有着截然不同的行为,使用不当将会带来性能上或正确性上的问题。   这两个问题最好都不要发生,不过若难以同时避免的话,那么一个略微慢一些但能保证正确的程序则要好过一个快速但不能正常工作的程序。

 

  C#中有两种类型的常量:编译期常量和运行时常量。二者有着截然不同的行为,使用不当将会带来性能上或正确性上的问题。

  这两个问题最好都不要发生,不过若难以同时避免的话,那么一个略微慢一些但能保证正确的程序则要好过一个快速但不能正常工作的程序。

 

  考虑到这些,你应该尽量使用运行时常量,而不是编译期常量。

  虽然编译期常量略微快一些,但是却没有运行时常量那么灵活。应紧紧在那些性能异常敏感,且常量的值在各个版本之间绝对不会变化时,再使用编译期常量。

 

  运行时常量使用readonly关键字声明,编译期常量则使用const关键字声明:

1 // Compile time constant
2 public const string Name = "张董";
3 
4 // Runtime constant
5 public static readonly int Age = 19;

  上述代码在类或struct的范围内演示了两种常量。编译期常量也可声明在方法中,而只读的运行时常量却不能声明在方法中。

 

  编译期常量与运行时常量行为的不同之处在于对他们的访问方式不同。编译期常量的值是在目标代码中进行替换的。

  下列代码:

1 if (user.Age == Age)

  将与如下代码编译成同样的IL(中间语言):

1 if (user.Age == 19)

 

  运行时常量将在运行时求职。引用运行时常量生成的IL将引用到readonly的变量,而不是变量的值。

 

  这个差别会带来几个限制,会影响到选用哪种类型的常量。

  编译期常量仅能用于基本类型(内建的整数和浮点类型)、枚举或字符串。只有这些类型才允许我们在初始化器中指定有意义的常量值。

  在代码编译后得到的IL代码中,只有这些常量可直接被替换为它们的字面值。

 

  例如,下面的代码就不会通过编译。

  即使要初始化的常量类型属于值类型,也无法在C#中使用new操作符来初始化编译期常量:

// Does not compile, use readonly instead
private const DateTime classCreation = new DateTime(1996, 6, 25, 10, 45, 00);

 

  编译期常量const仅能用于数字和字符串。运行时常量readonly也是一种常量,因为在构造函数执行后未能被再次修改。

  二者的区别在于,只读的值将在运行时给出,这自然会带来更好的灵活性。

  例如,运行时常量可以为任意类型。运行时常量必须在构造函数或初始化器中初始化。

  你可以让某个readonly值为一个DateTime结构,而不是指定某个const为DateTime。

 

  你可以用readonly值保存实例常量,为类的每个实例存放不同的值。而编译期常量就是静态的常量。

 

  二者最重要的区别在于,readonly值是在运行时解析的引用一个readonly常量时声称的IL引用的是readonly变量,而不是其值

  这一点就会对日后的维护产生深远的影响。

  编译期常量将生成同样的IL,就像直接在代码中给出数字一样,即使是跨程序集也是如此,即使另一个程序集中引用了某个程序集中的某个常量,相应常量也会被直接替换成这个值。

 

  编译期常量与运行时常量的求值方式将会影响运行的兼容性。

  假设我们在一个名为Infrastructure的程序集中分别定义了一个const字段和一个readonly字段:

1 public class UsefulValues
2 {
3     public static readonly int StartValue = 5;
4     public const int EndValue = 10;
5 }

  在另一个程序集中,我们使用了这两个值:

1 for(int i=UsefulValues.StartValue; i<UsefulValues.EndValue; i++)
2 {
3     Console.WriteLine("value is {0}", i);
4 }

 

  执行该程序,输出结果如下:

value is 5
value is 6
...
value is 9

 

  过了一段时间,我们更新了Infrastructure程序集,做出了如下修改:

1 public class UsefulValues
2 {
3     public static readonly int StartValue = 105;
4     public const int EndValue = 120;  
5 }

  随后,在分发Infrastructure程序集时,并没有重新编译整个应用程序的所有程序集,我们期待的是如下的输出:

value is 105
value is 106
...
value is 119

 

  但实际上,你却看不到任何输出。因为现在那个循环语句将使用105作为他的起始值,使用10作为他的结束条件

  C#编译器在第一次编译Application程序集时,将其中的EndValue替换成了它对应的常量值10.

  而对于StartValue来说,因为它被声明为readonly,所以对其求值会发生在运行时。

  因此,Application程序集在没有被重新编译的情况下,只要简单地更新一个Infrastructure程序集就够了。

 

  相反,更改一个工友的编译期常量的值应该被看做是对类型接口的修改,你必须重新编译所有引用该常量的代码。

  而更改只读常量的值缺紧紧算作是对类型实现的修改,它与其客户代码在二进制层次上是兼容的。

 

  不过,有时你确实需要让某个值在编译时确定。

  例如,考虑用一系列常量来标出对象的不同版本的序列化形式,其中这一组常量用来区分不同版本的对象。

  这时,用来标记特定版本号的值就应该采用编译期常量,因为他们永远都不会改变。

  而标记版本号的值就应该采用运行时常量,因为它的值会随着每个不同版本的发布而变化。

1 private const int Version1_0 = 0x0100;
2 private const int Version1_1 = 0x0101;
3 private const int Version1_2 = 0x0102;
4 // major release
5 private const int Version2_0 = 0x0200;
6 
7 // check for the current version
8 private static readonly int CurrentVersion = Version2_0;

 

  使用运行时常量在每个存盘文件中保存当前的版本:

 1 // Read from persistent storage, check stored version against compile-time constant:
 2 protected MyType(Serialization info, StreamingContext cntxt)
 3 {
 4     int storedVersion = info.GetInt32("VERSION");
 5     switch(storedVersion)
 6     {
 7         case Version2_0:
 8             readVersion2(info, cntxt);
 9             break;
10         case Version1_1:
11             readVersion1Dot1(info, cntxt);
12             break;
13         // etc.
14     }
15 }
16 
17 // Write the current version
18 [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
19 void ISerializable.GetObjectData(SerializationInfo inf, StreamingContext cxt)
20 {
21     // use runtime constant for current version:
22     inf.AddValue("VERSION", CurrentVersion);
23     // write remaining elements...
24 }

 

  相比之下,const最终优于readonly的地方就是性能。使用已知常量值要比访问readonly值略微高效一些。

  不过这其中的效率提升可以说是微乎其微,而这样却能降低很多的灵活性。

  若是你最终还是计划要放弃灵活性,那么在决定之前一定要对两者的性能差别做一个实际的测试。

 

  在使用具名和可选参数时,你也会遇到同样的运行时/编译期常量的取舍。

  可选参数的默认值将被放置于调用端,就像编译期常量(用const声明)的默认值一样。

  正如选用readonly和const一样,在修改可选参数的值时也必须小心操作。

 

  在编译期必须得到确定数值时一定要使用const。

  例如特性(attribute)的参数和枚举的定义等,还有那些在各个版本发布之间不会变化的值。

  在除此之外的所有情况下,都应尽量选择更加灵活的readonly常量。

 

【来自:张董'Blogs:http://www.cnblogs.com/LonelyShadow,转载请注明出处。】

亲们。码字不容易,觉得不错的话记得点赞哦。。

目录
相关文章
|
28天前
|
缓存 C# Windows
C#程序如何编译成Native代码
【10月更文挑战第15天】在C#中,可以通过.NET Native和第三方工具(如Ngen.exe)将程序编译成Native代码,以提升性能和启动速度。.NET Native适用于UWP应用,而Ngen.exe则通过预编译托管程序集为本地机器代码来加速启动。不过,这些方法也可能增加编译时间和部署复杂度。
|
3月前
|
存储 开发框架 .NET
C#语言究竟隐藏了哪些秘密?一文带你揭开编程界的神秘面纱
【8月更文挑战第22天】C#是微软推出的面向对象编程语言,以其简洁的语法和强大的功能,在软件开发领域占据重要地位。作为一种强类型语言,C#确保了代码的可读性和可维护性。它支持多种数据类型,如整型、浮点型及复合类型如类和结构体。类是核心概念,用于定义对象的属性和行为。C#还包括方法、异常处理、集合类型如列表和字典,以及泛型和LINQ等高级特性,支持异步编程以提高应用响应性。.NET Core的推出进一步增强了C#的跨平台能力。
70 3
|
19天前
|
JSON C# 开发者
C#语言新特性深度剖析:提升你的.NET开发效率
【10月更文挑战第15天】C#语言凭借其强大的功能和易用性深受开发者喜爱。随着.NET平台的演进,C#不断引入新特性,如C# 7.0的模式匹配和C# 8.0的异步流,显著提升了开发效率和代码可维护性。本文将深入探讨这些新特性,助力开发者在.NET开发中更高效地利用它们。
28 1
|
1月前
|
存储 开发框架 .NET
C#语言如何搭建分布式文件存储系统
C#语言如何搭建分布式文件存储系统
63 2
|
2月前
|
前端开发 Java C#
C#语言的优缺点?
C#语言的优缺点?
82 3
|
2月前
|
安全 IDE Java
C#语言的
C#语言是一种面向对象的编程语言
29 1
|
2月前
|
IDE C# 开发工具
C# 语言的主要优势是什么?
C# 语言的主要优势是什么?
82 2
|
2月前
|
监控 安全 C#
C# 语言助力员工监控系统的完善
在数字化时代,企业日益重视员工管理的效率与精准度,员工监控系统因此成为提升管理水平的有效工具。C# 语言凭借其简洁、高效和安全的特点,在开发此类系统中扮演了重要角色,可实现实时监控员工电脑操作、网络行为及工作时间统计等功能,从而提高工作效率并保障企业利益。同时,企业在应用这些技术时也需关注员工隐私权的保护。
26 6
|
3月前
|
JSON C# 开发者
💡探索C#语言进化论:揭秘.NET开发效率飙升的秘密武器💼
【8月更文挑战第28天】C#语言凭借其强大的功能与易用性深受开发者喜爱。伴随.NET平台演进,C#持续引入新特性,如C# 7.0的模式匹配,让处理复杂数据结构更直观简洁;C# 8.0的异步流则使异步编程更灵活高效,无需一次性加载全部数据至内存。通过示例展示了模式匹配简化JSON解析及异步流实现文件逐行读取的应用。此外,C# 8.0还提供了默认接口成员和可空引用类型等特性,进一步提高.NET开发效率与代码可维护性。随着C#的发展,未来的.NET开发将更加高效便捷。
57 1
|
3月前
|
编译器 C# Windows
C#基础:手动编译一个.cs源代码文件并生成.exe可执行文件
通过上述步骤,应该能够高效准确地编译C#源代码并生成相应的可执行文件。此外,这一过程强调了对命令行编译器的理解,这在调试和自动化编译流程中是非常重要的。
227 2