Shuhari

C#编译器是如何判定某个变量没有使用过的?

这是我们某个组员在编程过程中提出的疑问。因为这个编译错误很容易避免,所以我一直也没有仔细想过这个问题,直到看过他的代码后才意识到,此问题并不是那么简单的。

先看看这段代码:

代码
class Program
{
    
static void Main(string[] args)
    {
        
byte[] buf = new byte[1024];
        T t 
= new T();
        
string str = "1234";
        
int n = 1234;
        
int? nn = 1234;
        DateTime dt 
= DateTime.Now;
        
object o = 1234;

        Console.WriteLine(
"finish");
    }
}

class T { }

 

你觉得这段代码里有几个变量没有使用过呢?

如果从程序员的角度来看,答案应该是所有变量都没有使用过。但编译器给出的结果却有点违反直觉:

变量“str”已赋值,但其值从未使用过
变量“n”已赋值,但其值从未使用过
变量“nn”已赋值,但其值从未使用过


奇怪的地方在于,虽然所有变量都是用同样的方式声明,但编译器却只认为其中一部分没有使用过。这是怎么回事呢?

我们一个一个来分析。首先看看数组,如果使用默认值的话,编译器给出的信息就不同了:

byte[] buf1 = null;     // 有警告
byte[] buf2 = new byte[1024]; // 没有警告



这个结果似乎表明,如果参数赋值为null,那么编译器并不会真的执行赋值,并且变量会当作没有使用过。用IL检查的结果也可以证明此说法:对第一行,编译器没有生成任何对应的语句;对第二条则使用了newattr指令来创建数组。


对于自定义的类:

T t1 = null;    // 有警告
T t2 = new T(); // 没有警告


这个结果应当是可以理解的(尽管可以理解,但我认为并不好,理由见后)。虽然我们并没有调用该类的任何方法,但是类的构造函数仍然可能执行某些操作,所以只要创建了一个类,编译器就会把它当作已经使用过的。

对于基本值类型,其表现和引用类型又有所不同,编译器并不把初始赋值当作对变量的使用:

int n1 = 0;  // 有警告
int n2 = 1234// 有警告
int? n3 = null// 有警告
int? n4 = 0// 有警告
int? n5 = 1234// 有警告


string从实现上来说应当算是引用类型,但表现上却更加类似于值类型,警告信息也和值类型相同。

对于稍微复杂一些的值类型,结果有点微妙:

DateTime dt1;   // 有警告
DateTime dt2 = new DateTime();  // 有警告
DateTime dt3 = new DateTime(2009,1,1);  // 没有警告
DateTime dt4 = DateTime.Now;  // 没有警告


这个结果有一点是需要注意的。尽管DateTime的默认构造函数和带参构造函数从用户角度看同样是构造函数,但在编译器的角度来看却是不一样的。用IL反编译也可以看出,如果调用默认构造函数的话,那么编译器调用的是initobj指令,而对带参构造函数调用的则是call ctor指令。此外,尽管从程序员的角度来看赋值代码的格式是完全相同的,但编译器却会根据所赋的值不同而采取不同的构造策略,这也是比较违反直觉的。

最后的结论比较遗憾,那就是C#的编译警告并不足以给予程序员足够的保护,特别是对于数组:
byte[] buf = new byte[1024];
如果仅构造这样一个数组而没有使用的话,那么编译器并不会给予程序员任何警告信息。

另外一个问题也是值得考虑的,声明一个类而不使用任何方法,比如仅仅
T t = new T()
这是合理的行为吗?编译器应该为此发出警告吗?
我个人的看法是,从使用的角度来说,这是不合理的,应当尽量避免,编译器发现此用法的话应该提出警告。如果确实有需要的话,可以通过编译指令或Attribute的方法来特别声明来避免警告信息。然而C#编译器的行为却是不发出警告,这一点我是不认同的。当然,我也希望大家提出自己的想法。

posted on 2009-12-12 12:21  Shuhari  阅读(2668)  评论(9编辑  收藏  举报

导航