C#高级编程(第10版) C# 6 & .NET Core 1.0 (.NET开发经典名著)
上QQ阅读APP看书,第一时间看更新

2.3 变量

在C#中声明变量使用下述语法:

        datatype identifier;

例如:

        int i;

该语句声明int变量i。实际上编译器不允许在表达式中使用这个变量,除非用一个值初始化了该变量。

声明i之后,就可以使用赋值运算符(=)给它赋值:

        i = 10;

还可以在一行代码中(同时)声明变量,并初始化它的值:

        int i = 10;

如果在一条语句中声明和初始化了多个变量,那么所有的变量都具有相同的数据类型:

        int x = 10, y =20; // x and y are both ints

要声明不同类型的变量,需要使用单独的语句。在一条多变量的声明中,不能指定不同的数据类型:

        int x = 10;
        bool y = true;              // Creates a variable that stores true or false
        int x = 10, bool y = true; // This won't compile!

注意上面例子中的“//”和其后的文本,它们是注释。“//”字符串告诉编译器,忽略该行后面的文本,这些文本仅为了让人更好地理解程序,它们并不是程序的一部分。本章后面会详细讨论代码中的注释。

2.3.1 初始化变量

变量的初始化是C#强调安全性的另一个例子。简单地说,C#编译器需要用某个初始值对变量进行初始化,之后才能在操作中引用该变量。大多数现代编译器把没有初始化标记为警告,但C#编译器把它当成错误来看待。这就可以防止我们无意中从其他程序遗留下来的内存中检索垃圾值。

C#有两个方法可确保变量在使用前进行了初始化:

● 变量是类或结构中的字段,如果没有显式初始化,则创建这些变量时,其默认值就是0(类和结构在后面讨论)。

● 方法的局部变量必须在代码中显式初始化,之后才能在语句中使用它们的值。此时,初始化不是在声明该变量时进行的,但编译器会通过方法检查所有可能的路径,如果检测到局部变量在初始化之前就使用了其值,就会标记为错误。

例如,在C#中不能使用下面的语句:

        static int Main()
        {
          int d;
          WriteLine(d); // Can't do this! Need to initialize d before use
          return 0;
        }

注意在这段代码中,演示了如何定义Main(),使之返回一个int类型的数据,而不是void类型。

在编译这些代码时,会得到下面的错误消息:

        Use of unassigned local variable 'd'

考虑下面的语句:

        Something objSomething;

在C#中,这行代码仅会为Something对象创建一个引用,但这个引用还没有指向任何对象。对该变量调用方法或属性会导致错误。

在C#中实例化一个引用对象,需要使用new关键字。如上所述,创建一个引用,使用new关键字把该引用指向存储在堆上的一个对象:

        objSomething = new Something(); // This creates a Something on the heap

2.3.2 类型推断

类型推断使用var关键字。声明变量的语法有些变化:使用var关键字替代实际的类型。编译器可以根据变量的初始化值“推断”变量的类型。例如:

        var someNumber = 0;

就变成:

        int someNumber = 0;

即使someNumber从来没有声明为int,编译器也可以确定,只要someNumber在其作用域内,就是int类型。编译后,上面两个语句是等价的。

下面是另一个小例子(代码文件VariablesSample/Program.cs ):

        using static System.Console;
        namespace Wrox
        {
          class Program
          {
            static void Main()
            {
              var name = "Bugs Bunny";
              var age = 25;
              var isRabbit = true;
              Type nameType = name.GetType();
              Type ageType = age.GetType();
              Type isRabbitType = isRabbit.GetType();
              WriteLine($"name is type {nameType}");
              WriteLine($"age is type {ageType}");
              WriteLine($"isRabbit is type {isRabbitType}");
            }
          }
        }

这个程序的输出如下:

        name is type System.String
        age is type System.Int32
        isRabbit is type System.Bool

需要遵循一些规则:

● 变量必须初始化。否则,编译器就没有推断变量类型的依据。

● 初始化器不能为空。

● 初始化器必须放在表达式中。

● 不能把初始化器设置为一个对象,除非在初始化器中创建了一个新对象。

第3章在讨论匿名类型时将详细探讨这些规则。

声明了变量,推断出了类型后,就不能改变变量的类型了。变量的类型确定后,就遵循其他变量类型遵循的强类型化规则。

2.3.3 变量的作用域

变量的作用域是可以访问该变量的代码区域。一般情况下,确定作用域遵循以下规则:

● 只要类在某个作用域内,其字段(也称为成员变量)也在该作用域内。

● 局部变量存在于表示声明该变量的块语句或方法结束的右花括号之前的作用域内。

● 在for、while或类似语句中声明的局部变量存在于该循环体内。

1.局部变量的作用域冲突

大型程序在不同部分为不同的变量使用相同的变量名很常见。只要变量的作用域是程序的不同部分,就不会有问题,也不会产生多义性。但要注意,同名的局部变量不能在同一作用域内声明两次。例如,不能使用下面的代码:

        int x = 20;
        // some more code
        int x = 30;

考虑下面的代码示例(代码文件VariableScopeSample/Program.cs ):

        using static System.Console;
        namespace VariableScopeSample
        {
          class Program
          {
            static int Main()
            {
              for (int i = 0; i < 10; i++)
              {
              WriteLine(i);
              }  // i goes out of scope here
              // We can declare a variable named i again, because
              // there's no other variable with that name in scope
              for (int i = 9; i >= 0; i -)
              {
              WriteLine(i);
              }  // i goes out of scope here.
              return 0;
            }
          }
        }

这段代码很简单,使用两个for循环打印0~9的数字,再逆序打印0~9的数字。重要的是在同一个方法中,代码中的变量i声明了两次。可以这么做的原因是i在两个相互独立的循环内部声明,所以每个变量i对于各自的循环来说是局部变量。

下面是另一个例子(代码文件VariableScopeSample2/Program.cs ):

        static int Main()
        {
          int j = 20;
          for (int i = 0; i < 10; i++)
          {
            int j = 30; // Can't do this - j is still in scope
            WriteLine(j + i);
          }
          return 0;
        }

如果试图编译它,就会产生如下错误:

        error CS0136: A local variable named 'j' cannot be declared in
        this scope because that name is used in an enclosing local scope
        to define a local or parameter

其原因是:变量j是在for循环开始之前定义的,在执行for循环时仍处于其作用域内,直到Main()方法结束执行后,变量j才超出作用域。第2个j (不合法)虽然在循环的作用域内,但作用域嵌套在Main()方法的作用域内。因为编译器无法区分这两个变量,所以不允许声明第二个变量。

2.字段和局部变量的作用域冲突

某些情况下,可以区分名称相同(尽管其完全限定名不同)、作用域相同的两个标识符。此时编译器允许声明第二个变量。原因是C#在变量之间有一个基本的区分,它把在类型级别声明的变量看成字段,而把在方法中声明的变量看成局部变量。

考虑下面的代码片段(代码文件VariableScopeSample3/Program.cs):

        using static System.Console;
        namespace Wrox
        {
          class Program
          {
            static int j = 20;
            static void Main()
            {
              int j = 30;
              WriteLine(j);
              return;
            }
          }
        }

虽然在Main()方法的作用域内声明了两个变量j,这段代码也会编译:一个是在类级别上定义的j,在类Program删除前(在本例中,是Main()方法终止,程序结束时)是不会超出作用域的;一个是在Main()中定义的j。这里,在Main()方法中声明的新变量j隐藏了同名的类级别变量,所以在运行这段代码时,会显示数字30。

但是,如果要引用类级别变量,该怎么办?可以使用语法object.fieldname,在对象的外部引用类或结构的字段。在上面的例子中,访问静态方法中的一个静态字段(静态字段详见2.3.4小节),所以不能使用类的实例,只能使用类本身的名称:

        // etc.
        static void Main()
        {
          int j = 30;
          WriteLine(j);
          WriteLine(Program.j);
        }
        // etc.

如果要访问实例字段(该字段属于类的一个特定实例),就需要使用this关键字。

2.3.4 常量

顾名思义,常量是其值在使用过程(生命周期)中不会发生变化的变量。在声明和初始化变量时,在变量的前面加上关键字const,就可以把该变量指定为一个常量:

        const int a = 100; // This value cannot be changed.

常量具有如下特点:

● 常量必须在声明时初始化。指定了其值后,就不能再改写了。

● 常量的值必须能在编译时用于计算。因此,不能用从变量中提取的值来初始化常量。如果需要这么做,应使用只读字段(详见第3章)。

● 常量总是隐式静态的。但注意,不必(实际上,是不允许)在常量声明中包含修饰符static。

在程序中使用常量至少有3个好处:

● 由于使用易于读取的名称(名称的值易于理解)替代了较难读取的数字和字符串,常量使程序变得更易于阅读。

● 常量使程序更易于修改。例如,在C#程序中有一个SalesTax常量,该常量的值为6%。如果以后销售税率发生变化,把新值赋给这个常量,就可以修改所有的税款计算结果,而不必查找整个程序去修改税率为0.06的每个项。

● 常量更容易避免程序出现错误。如果在声明常量的位置以外的某个地方将另一个值赋给常量,编译器就会标记错误。