目录
一、类成员
数据成员(保存数据) | 函数成员(执行代码) | |
---|---|---|
字段 | 方法 | 运算符 |
常量 | 属性 | 索引 |
构造函数 | 事件 | |
析构函数 |
二、成员修饰符的顺序
类成员声明语句由下列部分组成:核心声明、一组可选的修饰符和一组可选的特性。
用于描述这个结构的语法如下:
【特性】【修饰符】核心声明
注意:方括号表示方括号内的成分是可选的。
可选成分如下:
- 修饰符
- 如果有修饰符,必须放在核心声明之前;
- 如果有多个修饰符,可以任意顺序排列;
- 特性
- 如果有特性,必须放在修饰符和核心声明之前;
- 如果有多个特性,可以任意顺序排列;
例如,public和static都是修饰符,可以一起修饰某个声明,因为他们都是修饰符,所以可以任何顺序放置:
public static int MaxVal;
static public int MaxVal;
以上两行代码在语义上是等价的。
上图阐明了声明中各成分的顺序,到目前为止,它们可用于两种成员类型:字段和方法。
注意,字段的类型和方法的返回类型不是修饰符——它们是核心声明的一部分。
三、实例类成员
类成员可以关联到类的一个实例,也可以关联到整个类,即类的所有实例。默认情况下,成员被关联到一个实例。可以认为类的每个实例拥有自己的各个类成员的副本,这些成员称为实例成员。
改变一个实例字段的值不会影响任何其他实例中成员的值。迄今为止,你所看到的字段和方法都是实例字段和实例方法。
如下代码:
class D
{
public int Mem1;
}
class InstanceClassMember
{
static void Main()
{
D d1 = new D();
D d2 = new D();
d1.Mem1 = 10;
d2.Mem1 = 28;
Console.WriteLine($"d1 = {d1.Mem1}, d2 = {d2.Mem1}");
}
}
执行结果:
代码中声明了一个类D,它带有唯一整型字段Mem1,Main创建了该类的两个实例,每个实例都有自己的字段Mem1的副本,改变一个实例的字段副本的值不影响其他实例的副本的值,下图阐明了类D的两个实例:
四、静态字段
除了实例字段,类还有静态字段:
- 静态字段被类的所有实例共享,所有实例都访问同一内存位置。因此,如果该内存位置的值被一个实例改变了,这种改变对所有的实例都可见;
- 可以使用static修饰符将字段声明为静态;
class D
{
int Mem1;
static int Mem2;
}
class StaticField
{
static void Main()
{
D d1 = new D();
D d2 = new D();
}
}
代码中类D含有静态字段Mem2和实例字段Mem1。Main定义了类D的两个实例。上图表明静态成员Mem2是与所有实例分开保存的,从实例内部,访问或更新静态字段的语法和访问或更新其他成员字段一样。
- 因为Mem2是静态的,所以类D的两个实例共享一个Mem2字段,如果Mem2被改变了,这个改变在两个实例中都能看到;
- 成员Mem1没有声明为static,所以每个实例都有自己的副本;
五、从类的外部访问静态成员
与实例成员类似,静态成员也可以使用点运算符从类的外部访问。但因为没有实例,所以最常用的访问静态成员的方法使用类名,如下:
D.Mem2 = 5; //访问静态类成员
访问静态类成员的另一种方法根本不需要使用前缀,只需在该成员所属的类中包含一个using static声明,如下所示:
using static System.Console; //在其他成员中包含Writeline()
using static System.Math; //在其他成员中包含Sqrt()
WriteLine($"The square root of 16 is {Sqrt(16)}");
这等价于:
using System;
Console.WriteLine($"The square root of 16 is {Math.Sqrt(16)}");
说明:在这两种访问静态成员的方法中进行选择时,应该考虑哪种方法的代码对你和维护代码的人来说更加清晰易懂。
1、静态字段示例
class D
{
int Mem1;
static int Mem2;
public void SetVars(int v1,int v2)
{
Mem1 = v1;
Mem2 = v2; //像访问实例字段一样访问它
}
public void Display(string str)
{
Console.WriteLine($"{str}: Mem1 = {Mem1}, Mem2 = {Mem2}");
}
}
class StaticFieldExam
{
static void Main()
{
D d1 = new D(), d2 = new D(); //创建两个实例
d1.SetVars(2, 4); //设置d1的值
d1.Display("d1");
d2.SetVars(15, 17); //设置d2的值
d2.Display("d2");
d1.Display("d1"); //再次显示d1,注意静态成员的变化
}
}
执行结果:
注意,再次显示d1实例时,静态成员Mem2的值已随d2实例的赋值而改变。
2、静态成员的生存期
静态成员的生存期与实例成员的不同:
- 之前我们已经看到,只有在实例创建之后才产生实例成员,在实例销毁之后实例成员也就不存在了;
- 但即使类没有实例,也存在静态成员,并且可以访问;
class D
{
int Mem1;
static int Mem2;
}
static void Main()
{
D.Mem2 = 5;
Console.WriteLine("Mem2 = {0}", D.Mem2);
}
没有类实例的静态成员仍然可以被赋值并读取,因为字段与类有关,而与实例无关。
说明:即使不存在类实例,静态成员也存在。如果静态字段有初始化语句,那么会在使用该类的任何静态成员之前初始化该字段,但不一定在程序执行的开始就初始化。
六、静态函数成员
- 如同静态字段,静态函数成员独立于任何类实例。即使没有类的实例,仍然可以调用静态方法;
- 静态函数成员不能访问实例成员,但能访问其他静态成员;
如下代码:
class X
{
public static int A; //静态字段
public static void PrintValA() //静态方法
{
Console.WriteLine("Value of A: {0}", A);
}
}
class StaticFuncMember
{
static void Main()
{
X.A = 10; //使用点号语法
X.PrintValA(); //使用点号语法
}
}
执行结果:
注意,静态方法的方法体访问静态字段。
七、其他静态类成员类型
数据成员(存储数据) | 函数成员(执行代码) |
---|---|
√ 字段 | √ 方法 |
√ 类型 | √ 属性 |
常量 | √ 构造函数 |
√ 运算符 | |
索引器 | |
√ 事件 |
可以声明为static的类成员在表中做了勾选标记,其他成员类型不能声明为static。
八、成员常量
与局部常量类似,但是成员常量被声明在类声明中而不是方法内,如下:
class MyClass
{
const int IntVal = 100; //定义int类型常量
}
const double PI = 3.1415926; //错误:不能在类型声明之外声明
与局部常量类似,用于初始化成员常量的值在编译时必须是可计算的,而且通常是一个预定义简单类型或由它们组成的表达式。
class MyClass
{
const int IntVal1 = 100;
const int IntVal2 = IntVal1 * 2; //没问题,因为IntVal1的值前面已设置
}
与局部常量类似,不能在成员常量声明以后给它赋值。
class MyClass
{
const int IntVal; //错误:必须初始化
IntVal = 100; //错误:不允许赋值
}
说明:与C和C++不同,在C#中没有全局常量,每个常量都必须声明在类型内。
九、常量与静态量
与局部常量相比,成员常量表现得像静态值,它们对每个实例都是“可见的”,而且即使没有类的实例也可以使用。与真正的静态量不同,常量没有自己的存储位置,而是在编译时被编译器替换。这种方式类似于C和C++中的#define值。
class X
{
public const double PI = 3.1415926;
}
class ConstAndStatic
{
static void Main()
{
Console.WriteLine($"pi = {X.PI}");
}
}
执行结果:
虽然成员常量表现得像静态值,但不能将常量声明为static。
static const double PI = 3.1415926; //错误:不能将常量声明为static
十、属性
属性是代表类实例或类中的数据项的成员。使用属性就像写入或读取一个字段,语法相同。
MyClass mc = new MyClass();
mc.MyField = 5; //给字段赋值
mc.MyProperty = 10; //给属性赋值
Console.WriteLine($"{ mc.Field } { mc.MyProperty }"); //读取字段和属性
与字段类似,属性有如下特征:
- 它是命名的类成员;
- 它有类型;
- 它可以被赋值和读取;
然而和字段不同,属性是一个函数成员:
- 它不一定为数据存储分配内存;
- 它执行代码;
属性是一组(两个)匹配的、命名的、称为访问器的方法:
- set访问器为属性赋值;
- get访问器从属性获取值;
1、属性声明和访问器
set和get访问器有预定义的语法和语义,可以把set访问器想象成一个方法,带有单一的参数,它“设置”属性的值,get访问器没有参数并从属性返回一个值。
- set访问器总是:
- 拥有一个单独的、隐式的值参,名称为value,与属性的类型相同;
- 拥有一个返回类型void;
- get访问器总是:
- 没有参数;
- 拥有一个与属性类型相同的返回类型;
属性声明的语法和结构如图所示。
注意:
图中的访问器声明既没有显式的参数,也没有返回类型声明。不需要它们,因为它们已经隐含在属性的类型中了。
set访问器中的隐式参数value是一个普通的值参。和其他值参一样,可以用它发送数据到方法体或访问器块,在块的内部,可以像普通变量那样使用value,包括对它赋值。
访问器的其他要点:
- get访问器的所有执行路径必须包含一条return语句,它返回一个属性类型的值;
- 访问器set和get可以以任何顺序声明,并且,除了这两个访问器外,属性上不允许有其他方法;
2、属性示例
class C1
{
private int theRealValue;
public int MyValue
{
set { theRealValue = value; }
get { return theRealValue; }
}
如上代码,展示了一个名为C1的类的声明示例,它含有一个名为MyValue的属性。
- 请注意,属性本身没有任何存储。取而代之,访问器决定如何处理发送进来的数据,以及应将什么数据发送出去。在这种情况下,属性使用一个名为theRealValue的字段作为存储;
- set访问器接受它的输入参数value,并把它的值赋给字段theRealValue;
- get访问器只是返回字段theRealValue的值;
属性访问器常使用字段作为存储。
3、使用属性
写入和读取属性的方法与访问字段一样,访问器被隐式调用:
- 要写入一个属性,在赋值语句的左边使用属性的名称;
- 要读取一个属性,把属性的名称用在表达式中;
int MyValue //属性声明
{
set {……}
get {……}
}
MyValue = 5; //赋值:隐式调用set方法
z = MyValue; //表达式:隐式调用get方法
属性会根据是写入还是读取来隐式地调用适当的访问器,不能显式地调用访问器,因为这样做会产生编译错误。
y = MyValue.get(); //错误!不能显式调用get访问器
MyValue.set(); //错误!不能显式调用set访问器
4、属性和关联字段
属性和字段关联一种常见的方式是在类中将字段声明为private以封装该字段,并声明一个public属性来控制从类的外部对该字段的访问。和属性关联的字段常被称为后备字段或后备存储。
class C1
{
private int theRealValue = 10; //后备字段:分配内存
public int MyValue //属性:不分配内存
{
set { theRealValue = value; } //设置theRealValue字段的值
get { return theRealValue; } //获取字段的值
}
}
class PropAssociateField
{
static void Main()
{
C1 c = new C1();
Console.WriteLine("MyValue: {0}", c.MyValue);
c.MyValue = 20;
Console.WriteLine("MyValue: {0}", c.MyValue);
}
}
执行结果:
上述代码使用公有属性MyValue来控制对私有字段theRealValue的访问。
属性和它们的后备字段有两种命名约定:
- 约定两个名称使用相同的内容,但字段使用Camel大小写,属性使用Pasal大小写;
- 在Camel风格中,复合词标识符中每个单词的首字母大写——除了第一个单词,其余字母都是小写;
- 在Pasal大小写风格中,复合词中每个单词的首字母都是大写;
- 约定属性使用Pasal大小写,字段使用相同标识符的Camel大小写版本,并以下划线开始;
private int firstField; //Camel大小写
public int FirstField //Pascal大小写
{
get { return firstField; }
set { firstField = value; }
}
private int _secondField; //下划线及Camel大小写
public int SecondField
{
get { return _secondField; }
set { _secondField = value; }
}
5、执行其他计算
属性访问器并不局限于对关联的后备字段传进传出数据。访问器get和set能执行任何计算,也可以不执行任何计算。唯一必需的行为是get访问器要返回一个属性类型的值。
public int Useless
{
set { } //什么也不设置
get { return 5; } //只是返回5
}
上述代码展示了一个有效但没啥用的属性,它仅在get访问器被调用时返回5。当set访问器被调用时,它什么也不做。隐式参数value的值被忽略了。
int theRealValue = 10; //字段
int MyValue //属性
{
set { theRealValue = value > 100 ? 100 : value } //条件运算符
get { return theRealValue; }
}
上述代码展示了一个更现实和有用的属性,其中set访问器在设置关联字段之前实现过滤。set访问器把字段theRealValue的值设置成输入值,如果输入值大于100,就将theRealValue设置为100。
int MyValue
{
set => value > 100 ? 100 : value;
get => theRealValue;
}
C#7.0为属性的getter/setter引入了另一种语法,这种语法使用表达函数体(或者叫lambda表达式),如上代码。
6、只读和只写属性
要想不定义属性的某个访问器,可以忽略该访问器的声明:
- 只有get访问器的属性称为只读属性。只读属性能够安全地将一个数据项从类或类的实例中传出,而不必让调用者修改属性值;
- 只有set访问器的属性称为只写属性。只写属性很少见,因为几乎没有实际用途。如果想在赋值时触发一个副作用,应该使用方法而不是属性;
- 两个访问器中至少有一个必须定义,否则编译器会产生一条错误信息;
7、属性与公有字段
按照推荐的编码实践,属性比公有字段更好,理由如下:
- 属性是函数成员而不是数据成员,允许你处理输入和输出,而公有字段不行;
- 属性可以只读或只写,而字段不行;
- 编译后的变量和编译后的属性语义不同;
如果要发布一个由其他代码引用的程序集,那么第三点将会带来一些影响。例如,有时开发人员可能想用公有字段代替属性,因为如果以后需要为字段的数据增加处理逻辑的话,可以再把字段改为属性。这没错,但是如果那样修改的话,所有访问这个字段的其他程序集都需要重新编译,因为字段和属性在编译后的语义不一样。另外,如果实现的是属性,那么只需要修改属性的实现,而无需重新编译访问它的其他程序集。
8、计算只读属性示例
class RightTriangle
{
public double A = 3;
public double B = 4;
public double Hypotenuse
{
get { return Math.Sqrt((A * A) + (B * B)); }
}
}
class ReadonlyPropExamp
{
static void Main()
{
RightTriangle rt = new RightTriangle();
Console.WriteLine($"Hypotenuse: {rt.Hypotenuse}");
}
}
执行结果:
上述代码中,两个公有字段用于存储两条直角边的边长,第三条边由属性Hypotenuse表示,它是一个只读属性,其返回值基于另外两条边的长度,它没有存储在字段中,相反,它在需要时根据当前A和B的值计算正确的值。
9、自动实现属性
常简称为“自动属性”,允许只声明属性而不声明后备字段。编译器会为你创建隐藏的后备字段,并且自动挂接到get和set访问器上。
自动实现属性的要点如下:
- 不声明后备字段——编译器根据属性的类型分配存储;
- 不能提供访问器的方法体——它们必须被简单地声明为分号,get担当简单的内存读,set担当简单的写。但是,因为无法访问自动属性的方法体,所以在使用自动属性时调试代码通常会更加困难。
class C1
{
//没有声明后备字段
public int MyValue { get; set; } //分配内存,访问器的方法体被声明为分号
}
class AutoImplementedProp
{
static void Main()
{
C1 c = new C1();
Console.WriteLine($"MyValue: {0}", c.MyValue); //像使用规则属性那样使用自动属性
c.MyValue = 20;
Console.WriteLine($"MyValue: {0}", c.MyValue);
}
}
执行结果:
10、静态属性
属性也可以声明为static,静态属性的访问器和所有静态成员一样,具有以下特点:
- 不能访问类的实例成员,但能被实例成员访问;
- 不管类是否有实例,它们都是存在的;
- 在类的内部,可以仅使用名称来引用静态属性;
- 在类的外部,可以通过类名或者使用using static结构来引用静态属性;
using System;
using static Seven_Class.Trivial;
namespace Seven_Class
{
class Trivial
{
public static int MyValue { get; set; }
public void PrintValue()
{
Console.WriteLine($"Value from inside:{MyValue}"); //从类的内部访问静态属性
}
}
class StaticProp
{
static void Main()
{
Console.WriteLine($"Init Value: {0}", Trivial.MyValue); //从类的外部访问静态属性
Trivial.MyValue = 10;
Console.WriteLine($"New Value: {Trivial.MyValue}");
MyValue = 20; //从类的外部访问,但由于使用了using static,所以没有使用类名
Console.WriteLine($"New Value:{MyValue}");
Trivial tr = new Trivial();
tr.PrintValue();
}
}
}
执行结果:
十一、实例构造函数
实例构造函数是一个特殊的方法,它在创建类的每个新实例时执行:
- 构造函数用于初始化类的实例的状态;
- 如果希望能从类的外部创建类的实例,需要将构造函数声明为public;
与类声明中的其它方法不同:
- 构造函数的名称和类名相同;
- 构造函数不能有返回值;
class MyClass
{
DateTime TimmeOfInstantiation; //字段
public MyClass() //构造函数
{
TimeOfInstantiation = DateTime.Now; //初始化字段
}
}
上述代码中,定义了一个MyClass类,声明了一个名为TimeOfInstantiation的字段,在构造函数中被初始化为当前的日期和时间。
1、带参数的构造函数
构造函数在以下方面和其他方法相似:
- 构造函数可以带参数,参数的语法和其它方法完全相同;
- 构造函数可以被重载;
在使用创建对象表达式创建类的新实例时,要使用new运算符,后面跟着类的某个构造函数,new运算符使用该构造函数创建类的实例。
class Class1
{
int Id;
string Name;
public Class1() //构造函数0
{
Id = 28;
Name = "Nemo";
}
public Class1(int val) //构造函数1
{
Id = val;
Name = "Nemo";
}
public Class1(string name) //构造函数2
{
Name = name;
}
public void SoundOff()
{
Console.WriteLine($"Name = {Name}, Id = {Id}");
}
}
class ConstructorWithParam
{
static void Main()
{
Class1 a = new Class1(), //调用构造函数0
b = new Class1(7), //调用构造函数1
c = new Class1("Bill"); //调用构造函数2
a.SoundOff();
b.SoundOff();
c.SoundOff();
}
}
执行结果:
2、默认构造函数
如果在类的声明中没有显式地提供实例构造函数,那么编译器会提供一个隐式的默认构造函数,它有以下特征:
- 没有参数;
- 方法体为空;
如果你为类声明了任何构造函数,那么编译器将不会为该类定义默认构造函数。
class Class2
{
public Class2(int Value) { …… } //构造函数0
public Class2(string Value) { …… } //构造函数1
}
class Program
{
static void Main()
{
Class2 a = new Class2(); //错误!没有定义无参数的构造函数
}
}
上述代码中:
- 因为已经至少有一个显式定义的构造函数,所以编译器不会创建额外的构造函数;
- 在Main中,试图使用不带参数的构造函数创建新的实例,因为没有无参数的构造函数,所以编译器会产生一条错误信息;
十二、静态构造函数
构造函数也可以声明为static,实例构造函数初始化类的每个新实例,而static构造函数初始化类级别的项。通常,静态构造函数初始化类的静态字段。
- 初始化类级别的项
- 在引用任何静态成员之前;
- 在创建类的任何实例之前;
- 静态构造函数在以下方面与实例构造函数类似
- 静态构造函数的名称必须和类名相同;
- 构造函数不能返回值;
- 静态构造函数在以下方面和实例构造函数不同
- 静态构造函数声明中使用static关键字;
- 类只能有一个静态构造函数,而且不能带参数;
- 静态构造函数不能有访问修饰符;
class Class1
{
static Class1() //静态构造函数示例
{
…… //执行所有静态初始化
}
}
关于静态构造函数应该了解的其他重要内容如下:
- 类既可以有静态构造函数也可以有实例构造函数;
- 如同静态方法,静态构造函数不能访问所在类的实例成员,因此也不能使用this访问器;
- 不能从程序中显式调用静态构造函数,系统会自动调用它们:
- 在类的任何实例被创建之前;
- 在类的任何静态成员被引用之前;
静态构造函数实例
class RandomNumberClass
{
private static Random RandomKey; //私有静态字段
static RandomNumberClass() //静态构造函数
{
RandomKey = new Random(); //初始化RandomKey
}
public int GetRandomNumber()
{
return RandomKey.Next();
}
}
class StaticConstructor
{
static void Main()
{
RandomNumberClass a = new RandomNumberClass();
RandomNumberClass b = new RandomNumberClass();
Console.WriteLine($"Next Random #: {a.GetRandomNumber()}");
Console.WriteLine($"Next Random #: {b.GetRandomNumber()}");
}
}
执行结果:
十三、对象初始化语句
对象初始化语句扩展了创建语法,在式的尾部放置了一组成员初始化语句。利用对象初始化语句,可以在创建新的对象实例时,设置字段和属性的值。
该语法有两种形式,一种形式包括构造函数的参数列表,另一种不包括。注意,第一种形式甚至不使用括起参数列表的圆括号。
对于一个名为Point、有两个公有整型字段X和Y的类,可以使用下面的表达式创建一个新对象:
new Point { X = 5, Y = 6 }; //初始化X和Y
关于对象初始化语句要了解的重要内容如下:
- 创建对象的代码必须能够访问要初始化的字段和属性。如上述代码中X、Y必须是公有的;
- 初始化发生在构造方法执行之后,因此在构造方法中设置的值可能会在之后对象初始化中重置为相同或不同的值;
public class Point
{
public int X = 1;
public int Y = 2;
}
class ObjectInitialStatement
{
static void Main()
{
Point pt1 = new Point();
Point pt2 = new Point { X = 5, Y = 6 }; //对象初始化语句
Console.WriteLine($"pt1 : {pt1.X},{pt1.Y}");
Console.WriteLine($"pt1 : {pt2.X},{pt2.Y}");
}
}
执行结果:
十四、析构函数
析构函数执行在类的实例被销毁之前需要的清理或释放非托管资源的行为。非托管资源是指通过Win32 API获得的文件句柄,或非托管内存块。使用.Net资源是无法得到它们的,因此如果坚持使用.NET类,就不需要为类编写析构函数。
- 为.NET框架编写的代码称为托管代码,需要CLR;
- 不在CLR控制之下运行的代码,比如Win32 C/C++ DLL,称为非托管代码;
十五、readonly修饰符
字段可以用readonly修饰符声明。其作用类似于将字段声明为const,一旦值被设定就不能改变。
- const字段只能在字段的声明语句中初始化,而readonly字段可以在下列任意位置设置它的值
- 字段声明语句,类似于const;
- 类的任何构造函数,如果是static字段,初始化必须在静态构造函数中完成;
- const字段的值必须可在编译时决定,而readonly字段的值可以在运行时决定。这种自由性允许你在不同的环境或不同的构造函数中设置不同的值。
- const的行为总是静态的,而对于readonly字段以下两点是正确的
- 它可以是实例字段,也可以是静态字段;
- 它在内存中有存储位置;
class ReadOnlyModifier
{
readonly double PI = 3.1415926; //初始化
readonly int NumberOfSides; //未初始化
public ReadOnlyModifier(double side1,double side2)
{
NumberOfSides = 4; //在构造函数中设定
}
public ReadOnlyModifier(double side1,double side2,double side3)
{
NumberOfSides = 3; //在构造函数中设定
}
}
十六、this关键字
this关键字在类中使用,是对当前实例的引用。它只能被用在下列类成员的代码块中。
- 实例构造函数
- 实例方法
- 属性和索引器的实例访问器
很明显,因为静态成员不是实例的一部分,所以不能在任何静态函数成员的代码中使用this关键字。更适当地说,this用于下列目的:
- 用于区分类的成员和局部变量或参数
- 作为调用方法的实参
class ThisKeyWords
{
int Var1 = 10;
public int ReturnMaxSum(int Var1) //注意,形参与字段名相同
{
return Var1 > this.Var1 ? Var1 : this.Var1;
}
static void Main()
{
ThisKeyWords tkw = new ThisKeyWords();
Console.WriteLine($"Max: {tkw.ReturnMaxSum(30)}");
Console.WriteLine($"Max: {tkw.ReturnMaxSum(5)}");
}
}
执行结果:
虽然理解this关键字很重要,但它实际上很少在代码中使用。
十七、索引器
我们定义一个类Employee,带有三个string型字段,我们通过字段名访问它们,代码如下:
class Employee
{
public string LastName;
public string FirstName;
public string CityOfBirth;
}
class Indexer
{
static void Main()
{
Employee emp1 = new Employee();
emp1.LastName = "Doe";
emp1.FirstName = "Jane";
emp1.CityOfBirth = "Dallas";
}
}
如果能使用索引访问它们将会很方便:
static void Main()
{
Employee emp1 = new Employee();
emp1[0] = "Doe";
emp2[1] = "Jane";
emp3[2] = "Dallas";
}
请注意,没有使用点运算符,相反,索引器使用索引运算符,它由一对方括号和中间的索引组成。
1、什么是索引器
索引器是一组get和set访问器,与属性类似。
2、索引器和属性
索引器和属性在很多方面相似:
- 和属性一样,索引器不用分配内存来存储;
- 索引器和属性都主要被用来访问其他数据成员,它们与这些成员关联,并为它们提供获取和设置访问。
- 属性通常表示单个数据成员;
- 索引器通常表示多个数据成员;
说明:
可以认为索引器是为类的多个数据成员提供get和set访问的属性。通过提供索引器,可以在许多可能的数据成员中进行选择。索引本身可以是任何类型,而不仅仅是数值类型。
关于索引,还需注意:
- 和属性一样,索引器可以只有一个访问器,也可以两个都有;
- 索引器总是实例成员,因此不能被声明为static;
- 和属性一样,实现get和set访问器的代码不一定要关联到某个字段或属性。这段代码可以做任何事情也可以什么都不做,只要get访问器返回某个指定类型的值即可;
3、声明索引器
声明索引器语法如下:
- 索引器没有名称,在名称的位置是关键字this;
- 参数列表在方括号中间;
- 参数列表中必须至少声明一个参数;
ReturnType this [Type param1,……]
{
get
{
……
}
set
{
……
}
}
声明索引器类似于声明属性,二者的异同如下图:
4、索引器的set访问器
当索引器被用于赋值时,set访问器被调用,并接受两项数据:
- 一个名为value的隐式参数,其中持有要保存的数据;
- 一个或更多索引参数,表示数据该保存到哪里;
在set访问器中的代码必须检查索引参数,以确定数据应该存往何处,然后保存它。
上图阐明了set访问器的语法和语义:
- 它的返回值为void;
- 它使用的参数列表和索引器声明中的相同;
- 他有一个名为value的隐式参数,值参类型和索引器类型相同;
5、索引器的get访问器
get访问器方法体内的代码必须检查索引参数,确定它表示的是哪个字段,并返回该字段的值。
上图阐明了get访问器的语法和语义:
- 它的参数列表和索引器声明中的相同;
- 它返回与索引器类型相同的值;
6、关于索引器的更多内容
与属性一样,不能显式调用set和get访问器。取而代之,当索引器用在表达式中取值时,将自动调用get访问器。当使用赋值语句对索引器赋值时,将自动调用set访问器。
在“调用”索引器时,要在方括号中提供参数。
emp[0] = "Doe"; //调用set访问器
string NewName = emp[0]; //调用get访问器
7、声明索引器示例
class Employee
{
public string LastName; //调用字段0
public string FirstName; //调用字段1
public string CityOfBirth; //调用字段2
public string this[int index]
{
set
{
switch (index)
{
case 0:LastName = value;
break;
case 1:FirstName = value;
break;
case 2:CityOfBirth = value;
break;
default:
throw new ArgumentOutOfRangeException("index");
}
}
get
{
switch (index)
{
case 0:return LastName;
case 1:return FirstName;
case 2:return CityOfBirth;
default:
throw new ArgumentOutOfRangeException("index");
}
}
}
}
8、索引器示例
class Class1
{
int Temp0;
int Temp1;
public int this[int index]
{
get
{
return (index == 0) ? Temp0 : Temp1;
}
set
{
if(index == 0)
{
Temp0 = value;
}
else
{
Temp1 = value;
}
}
}
}
class IndexerExamTwo
{
static void Main()
{
Class1 c1 = new Class1();
Console.WriteLine($"Values -- T0 = {c1[0]}, T1 = {c1[1]}");
c1[0] = 15;
c1[1] = 20;
Console.WriteLine($"Values -- T0 = {c1[0]}, T1 = {c1[1]}");
}
}
执行结果:
9、索引器重载
只要索引器的参数列表不同,类就可以有任意多个索引器,索引器类型不同是不够的,这叫索引器重载,因为所有的索引器都有相同的“名称”:this访问引用。
class MyClass
{
public string this[int index]
{
get{……}
set{……}
}
public string this[int index1,int index2]
{
get{……}
set{……}
}
public int this[float index]
{
get{……}
set{……}
}
}
说明:
类中重载的索引器必须有不同的参数列表。
十八、访问器的访问修饰符
默认情况下,成员的两个访问器的访问级别和成员自身相同。
不过,可以为两个访问器分配不同的访问级别。
class Person
{
public string Name { get; private set; }
public Person(string Name)
{
this.Name = Name;
}
}
class AccessModifier
{
static void Main()
{
Person p = new Person("Capt. Ernest Evans");
Console.WriteLine($"Person's name is {p.Name}");
}
}
执行结果:
访问器的访问修饰符有以下几个限制:
- 仅当成员(属性或索引器)既有get访问器也有set访问器时,其访问器才能有访问修饰符;
- 虽然两个访问器都必须出现,但它们中只能有一个有访问修饰符;
- 访问器的访问修饰符的限制必须比成员的访问级别更严格;
上图阐明了访问级别的层次。
十九、分部类和分部类型
类的声明可以分割成几个分部类的声明:
- 每个分部类的声明都含有一些类成员的声明;
- 类的分部类声明可以在同一个文件中也可以在不同文件中;
每个分部类声明必须被标注partial class,而不是单独的关键字class。分部类声明看起来和普通类声明相同,只是增加了类型修饰符partial。
partial class MyPartClass //类名称与下面的相同
{
member1 declaration
member2 declaration
}
partial class MyPartClass //类名称与上面的相同
{
member3 declaration
member4 declaration
}
说明:
类型修饰符partial不是关键字,所以在其他上下文中,可以在程序中把它用作标识符。但直接用在关键字class、struct或者interface之前时,它表示分部类型。
组成类的所有分部类声明必须在一起编译,使用分部类声明的类必须有相同的含义,就好像所有类成员都声明在一个单独的类声明体内。
Visual Studio为标准Window程序模板使用了这个特性,如果你从标准模板创建ASP.NET项目、Windows Forms项目或Windows Presentation Foundation(WPF)项目,模板会为每一个Web页面、表单或WPF窗体创建两个类文件。对于ASP.NET或Windows Forms,有以下两点事实:
- 一个文件中的分部类包含由Visual Studio生成的代码,声明了页面上的组件,你不应该修改这个文件中的分部类,因为在修改页面组件时,Visual Studio会重新生成它;
- 另外一个文件包含的分部类可用于实现页面或表单组件的外观和行为;
除了分部类,还可以创建局部结构和局部接口两种分部类型。
二十、分部方法
分部方法是声明在分部类中不同部分的方法。分部方法的不同部分可以声明在分部类的不同部分中,也可以声明在同一个部分中。分部方法的两个部分如下:
- 定义分部方法声明
- 给出签名和返回类型;
- 声明的实现部分只是一个分号;
- 实现分部方法声明
- 给出签名和返回类型;
- 以普通的语句块形式实现;
关于分部方法需要了解的重要内容如下:
- 定义声明和实现声明的签名和返回类型必须匹配。签名和返回类型有如下特征:
- 返回类型必须是void;
- 签名不能包括访问修饰符,这使分部方法是隐式私有的;
- 参数列表不能包含out参数;
- 在定义声明和实现声明中都必须包含上下文关键字partial,并且直接放在关键字void之前;
- 可以有定义部分而没有实现部分,在这种情况下,编译器把方法的声明以及方法内部任何对方法的调用都移除。不能只有分部方法的实现部分而没有定义部分。
partial class MyClass
{
partial void PrintSum(int x, int y); //定义分部方法,返回值必须是void,只有声明部分,没有实现部分
public void Add(int x,int y)
{
PrintSum(x, y);
}
}
partial class MyClass
{
partial void PrintSum(int x, int y)
{
Console.WriteLine($"Sum is {x + y}"); //实现部分
}
}
class PartialMethod
{
static void Main()
{
var mc = new MyClass();
mc.Add(5, 6);
}
}
执行结果:
上述代码展示了一个名为PrintSum的分部方法的示例:
- PrintSum声明在分部类MyClass的不同部分:定义声明在第一个部分中,实现声明在第二个部分中,实现部分打印出两个整型参数的和;
- 因为分部方法是隐式私有的,所以PrintSum不能从类的外部调用,方法Add是调用PrintSum的公有方法;
- Main创建类MyClass的一个对象,并调用它的公有方法Add,Add方法调用方法PrintSum,PrintSum打印出输入参数的和;