3. 字段和局部变量
为了区分作用域不同的对象或值类型的变量,C#对其进行了更细的划分:把类一级的对象或值类型的变量叫做字段;把在方法、事件以及构造函数内声明的变量叫做局部变量。声明变量的位置不同,其作用域也不同。
【例3-2】定义字段和局部变量。
using System;
using System.Collections.Generic;
using System.Text;
namespace DefinitionExample
{
public class Program
{
public static int j=20; //定义字段j
public static void Main()
{
int j=30; //定义局部变量j
Console.WriteLine(j); //输出结果:30
Console.WriteLine(Program.j); //输出结果:20
Console.ReadLine();
}
}
}
在这个例子中,定义了一个字段j,是类一级的变量;在Main方法中又定义了一个同名的局部变量j,此时,在Main方法中声明的新变量j隐藏了同名的类一级的变量,所以第一条Console语句输出的结果是30。
当字段和局部变量名相同时,如果要引用静态字段,可以使用下面的形式:
类名.字段名
如果是实例字段,则使用下面的形式:
this.字段名
这里的this指当前实例。
当然,如果没有出现字段和局部变量名重名的情况,引用字段的形式和引用局部变量的形式相同。
在类中定义的数据称为类的数据成员,数据成员包含字段、常量和事件等。而函数成员则提供操作类中数据的某些功能,函数成员包括方法、属性、构造函数等。对象中的数据成员和方法一般都是对象私有的,即只有对象本身才能够操作这些数据成员和调用这些方法,其他对象不能直接对其进行操作。可是有一些成员是所有对象共用的,如果把这样的成员保存在每一个对象上,当创建了一个类的多个对象时,在堆中就会出现很多相同的内容,显然浪费资源。此外,若要修改这些成员,也必须对每一个对象都进行修改,很明显这样做效率也比较低。
解决这个问题的办法是将成员定义为静态(static)的,当该类被装入内存时,系统就会在内存中专门开辟一部分区域保存这些静态成员,这样一来,其他类不必建立该类的实例就可以直接使用该类的静态成员。
把只有创建了类的实例才能够使用的成员称为实例成员。
需要注意的是,静态成员在内存中只有一份,不像实例成员可以有多个。而且静态成员要等到应用程序结束时才会消失,所以使用时要根据具体情况决定是否使用静态成员。
在C#中通过指定类名来调用静态成员,通过指定实例名来调用实例成员。
将数据和方法封装在类中是为了便于对数据和方法进行控制和修改,访问修饰符用于控制类中数据和方法的访问权限,C#中有以下几种成员访问修饰符。
1) public
指任何外部的类都可以不受限制的存取这个类的方法和数据成员。
2) private
指类中的所有方法和数据成员只能在此类中使用,外部无法存取。
3) protected
除了让本身的类可以使用之外,任何继承自此类的子类都可以存取。
4) internal
在当前项目中都可以存取。该访问权限一般用于基于组件的开发,因为它可以使组件以私有方式工作,而该项目外的其他代码无法访问。
5) protected internal
只限于当前项目,或者从该项目的类继承的类才可以存取。
6) partial
局部类型,类的定义和实现可以分布在多个文件中,但都要使用partial标注。注意,基类只需要声明一次,若多次声明则必须完全一致。
3.1.2 构造函数
构造函数是一个特殊的方法,用于在建立对象时进行初始化的动作,在C#中,每当创建一个对象时,都会先调用类中定义的构造函数。
使用构造函数的好处是它能够确保每一个对象在被使用之前都适当地进行了初始化的动作。
另外,构造函数还具有以下特点:
1) 每个类至少有一个构造函数。如果程序代码中没有构造函数,则系统会自动提供一个默认的构造函数。
2) 一个构造函数总是和它的类名相同。
3) 构造函数不包含任何返回值。
4) 构造函数总是public的。
一般在构造函数中作初始化工作,对于执行过程用时比较长的程序代码,最好不要放在构造函数中。
如果在类中不定义构造函数,系统会自动提供一个默认的构造函数,默认构造函数没有参数。提供默认构造函数的目的是为了保证能够在使用对象前先对未初始化的非静态类成员进行初始化工作,即将非静态成员初始化为下面的值:
1) 对数值型,比如int、double等,初始化为0。
2) 对bool类型,初始化为false。
3) 对引用类型,初始化为null。
如果在程序代码中定义了类的构造函数,则所有初始化工作由编程者自己完成。
有时候可能会遇到这样的情况:在一个类中的多个方法中都要用到某一个数据成员,而该成员值必须从其他类中传递过来。这时,无参数的构造函数就不能胜任了,解决这个问题最好的办法就是:重载(Overloading)构造函数。
【例3-3】重载构造函数。
using System;
using System.Collections.Generic;
using System.Text;
namespace OverloadingExample
{
class Program
{
public Program()
{
Console.WriteLine("null");
}
public Program(string str)
{
Console.WriteLine(str);
}
static void Main()
{
Program aa = new Program();
Program bb = new Program("How are you!");
Console.ReadLine();
}
}
}
输出结果:
null
How are you!3.1.3 方法
方法(Method)是一组程序代码的集合,每个方法都有一个方法名,便于识别和让其他方法调用。
C#程序中定义的方法都必须放在某个类中。定义方法的一般形式为:
访问修饰符 返回值类型 方法名称(参数序列)
{
语句序列
}
在定义方法时,需要注意以下几点:
1) 方法名称后面的小括号中可以有参数序列,也可以没有参数,但是不论是否有参数,小括号都是必需的。如果参数序列中的参数有多个,则以逗号分隔开。
2) 如果要结束某个方法的执行,可以使用return语句。程序遇到return语句后,会将执行流程交还给调用此方法的程序代码段。此外,还可以利用return语句返回一个值,注意,return语句只能返回一个值。
3) 如果声明一个void类型的方法,return语句可以省略不写;如果声明一个非void类型的方法,则方法中必须至少有一个return语句。
【例3-4】定义和调用方法。
using System;
using System.Collections.Generic;
using System.Text;
namespace MethodExample
{
class Program
{
public int MyMethod()
{
Console.WriteLine("this is MyMethod.");
int i = 10;
return i;
}
static void Main()
{
Program method = new Program();
int j = 5;
j = method.MyMethod();
Console.WriteLine("the value is {0}.", j);
Console.ReadLine();
}
}
}
输出结果:
this is MyMethod.
the value is 10.
定义方法时可以将参数传入方法中进行处理,也可以将方法中处理过的信息返回给调用者。传递变量参数到方法的方式有下面几种:
1) 传递值类型的参数
值类型参数的格式为:
参数类型 参数名
定义值类型参数的方式很简单,只要注明参数类型和参数名即可。当该方法被调用时,便会为每个值类型参数分配一个新的内存空间,然后将对应的表达式运算的值复制到该内存空间。在方法中更改参数的值不会影响到这个方法之外的变量。
【例3-5】方法中值类型参数的传递。
using System;
using System.Collections.Generic;
using System.Text;
namespace ValueTransferExample
{
class Program
{
public static void AddOne(int a)
{
a++;
}
static void Main()
{
int a = 3;
Console.WriteLine("调用AddOne之前,a={0}", a);
AddOne(a);
Console.WriteLine("调用AddOne之后,a={0}", a);
Console.ReadLine();
}
}
}
输出结果:
调用AddOne之前,a=3
调用AddOne之后,a=3
2) 传递引用类型的参数
引用类型参数的格式为:
ref 参数类型 参数名
与传递值类型参数不同,引用类型的参数并没有再分配内存空间,实际上传递的是指向原变量的指针,既引用参数和原变量保存的是同一个地址。为了和传递值类型参数区分,前面加上ref关键字(Reference),在方法中修改引用参数的值实际上也就是修改被引用的变量的值。
【例3-6】方法中引用类型参数的传递。
using System;
using System.Collections.Generic;
using System.Text;
namespace ReferenceTransferExample
{
class Program
{
public static void AddOne(ref int a)
{
a++;
}
static void Main()
{
int x = 3;
Console.WriteLine("调用AddOne之前,x={0}", x);
AddOne(ref x);
Console.WriteLine("调用AddOne之后,x={0}", x);
Console.ReadLine();
}
}
}
输出结果:
调用AddOne之前,x=3
调用AddOne之后,x=4
3) 输出多个引用类型的参数
有时候一个方法计算的结果有多个,而return语句一次只能返回一个结果,这时就用到了out关键字,使用out表明该引用参数是用于输出的,而且调用该参数时不需要对参数进行初始化。
输出引用类型参数的格式为:
out 参数类型 参数名
【例3-7】输出多个引用类型的参数。
using System;
using System.Collections.Generic;
using System.Text;
namespace ReferenceOutExample
{
class Program
{
public static void MyMethod(out int a, out int b)
{
a = 5;
b = 6;
}
static void Main()
{
int x, y;
MyMethod(out x, out y);
Console.WriteLine("调用MyMethod之后,x={0},y={1}", x, y);
Console.ReadLine();
}
}
}
输出结果:
调用MyMethod之后,x=5,y=6
4) 传递个数不确定的参数
当需要传递的参数个数不确定时,比如求几个数的平均值,由于没有规定是几个数,运行程序时,每次输入的值的个数就不一定一样。为了解决这个问题,C#采用params关键字声明参数的个数是不确定的。
【例3-8】传递个数不确定的参数。
using System;
using System.Collections.Generic;
using System.Text;
namespace UncertaintyTransferExample
{
class Program
{
public static float Average(params long[] v)
{
long total, i;
for (i = 0, total = 0; i < v.Length; ++i)
total += v[i];
return (float)total / v.Length;
}
static void Main()
{
float x = Average(1, 2, 3, 5);
Console.WriteLine("1、2、3、5的平均值为{0}", x);
x = Average(4, 5, 6, 7, 8);
Console.WriteLine("4、5、6、7、8的平均值为{0}", x);
Console.ReadLine();
}
}
}
输出结果:
1、2、3、5的平均值为2.75
4、5、6、7、8的平均值为6
方法重载是指具有相同的方法名,但参数类型或参数个数不完全相同的多个方法可以同时出现在一个类中。这种技术非常有用,在开发过程中,我们会发现C#中的很多方法均使用了重载技术。
【例3-9】方法重载。
using System;
using System.Collections.Generic;
using System.Text;
namespace MethodOverloadingExample
{
class Program
{
public static int Add(int i, int j)
{
return i + j;
}
public static string Add(string s1, string s2)
{
return s1 + s2;
}
public static long Add(long x)
{
return x + 5;
}
static void Main()
{
Console.WriteLine(Add(1, 2));
Console.WriteLine(Add("1", "2"));
Console.WriteLine(Add(10));
//按回车键结束
Console.ReadLine();
}
}
}
输出结果:
3
12
15
在这个例子中,虽然有多个Add方法,但由于方法中参数的个数和类型不完全相同,所以系统在调用时会自动找到最匹配的方法。