第5章 属性与索引器
上一章介绍了面向对象编程的基本知识,着重讨论了类及其常量、字段、构造函数,以及方法等成员,其中常量和字段属于数据成员;构造函数和方法则属于函数成员。除了已经介绍过的这些成员外,类还有其他一些成员。本章将介绍类的另外两种函数成员,即属性和索引器。属性是字段的自然扩展,用于定义一些命名特性,以及与读取和写入这些特性相关的操作;索引器类似于属性,但使对象能够用与数组相同的方式索引。
5.1 属性
属性是类的一种函数成员,用于提供灵活的机制来读取、编写或计算私有字段的值。属性可以像使用公共数据成员一样来使用,但实际上属性是称为“访问器”(accessor)的特殊方法。通过属性不仅可以访问数据,并且还有助于提高方法的安全性和灵活性。
5.1.1 声明属性
属性是一种用于访问对象或类的特性的成员,字符串的长度、字体的大小、窗口的标题,以及客户的名称等都是属性的例子。它是字段的自然扩展,二者都是具有关联类型的命名成员,而且访问字段和属性的语法是相同的。与字段不同的是,属性不表示存储位置;相反,属性有访问器,这些访问器指定在其值被读取或写入时需执行的语句。因此属性提供了一种机制,它把读取和写入对象的某些特性与一些操作关联起来,甚至还可以对此类特性进行计算。
在类声明块中,属性的声明方式为指定字段的属性(可选)、访问级别、类型和名称,其后为声明get访问器和/或set访问器的代码块。属性声明的语法格式如下:
[attributes] [property-modifiers] type member-name { accessor-declarations }
其中attributes指定属性的特征,给出一些附加的声明信息;property-modifiers为属性修饰符,可以是new、public、protected、internal、private、static、virtual、sealed、override、abstract或extern。在有效的修饰符组合方面,属性声明与方法声明遵循相同的规则。
type指定该声明所引入的属性的类型,可以是任何一种数据类型。属性的type必须至少与属性本身具有同样的可访问性。
member-name指定该属性的名称,如果该属性不是显式接口成员实现,则member-name只是一个标识符;否则member-name为interface-type.identifier。其中interface-type为接口类型名称,identifier为标识符。
accessor-declarations为属性访问器声明,必须括在“{”和“}”标记中。用于声明属性的get访问器和set访问器,以指定与属性的读取和写入相关联的可执行语句。关于属性访问器声明的更多信息,请参阅下一节。
当属性声明包含extern修饰符时,该属性称为“外部属性”,因为外部属性声明不提供任何实际的实现,所以它的每个访问器声明都仅由一个分号组成。
虽然访问属性的语法与访问字段的语法相同,但是属性并不归类为变量。因此不能将属性作为ref或out实参传递,这也是属性与字段的不同之处。
类的属性把字段和方法的功能有机地结合起来,对于对象的用户来说,类的属性显示为字段,访问该属性需要相同的语法格式;对于类的实现者来说,类的属性则是一个或两个代码块,表示一个get访问器和/或一个set访问器。当读取属性时,执行get访问器的代码块;当为属性分配一个新值时,执行set访问器的代码块。
声明类的属性时,应当注意以下几点。
(1)可以将属性声明为public、private、protected、internal或protected internal,这些访问修饰符定义类的用户如何才能访问属性。实际上,同一属性的get和set访问器也可能具有不同的访问修饰符。例如,get访问器可能是public,以允许来自类型外的只读访问;而set访问器可能是private或protected。
(2)可以使用static关键字将属性声明为静态属性,使得调用方随时可以使用该属性,即使不存在类的实例。对static属性的访问器使用virtual、abstract或override修饰符是错误的。
(3)可以使用virtual关键字将属性声明为虚属性,这样派生类就可以通过使用override关键字来重写该属性。
(4)重写虚属性的属性还可以是sealed的,表示它对派生类不再是虚拟的。
(5)可以使用abstract关键字将属性声明为抽象属性,它的每个访问器都仅由一个分号组成。这意味着类中没有任何实现,派生类必须编写自己的实现。
类的属性成员有多种用法,可以在允许更改前验证数据,可以透明地公开某个类中的数据,该类的数据实际上是从其他源(例如数据库)检索到的。当数据被更改时,它们可以采取某种行为,如引发事件或更改其他字段的值。
5.1.2 属性访问器
属性访问器声明用于指定与读取和写入该属性相关联的可执行语句,它由一个get访问器声明或一个set访问器声明组成,或者由二者共同组成。get访问器方法用于检索该属性的值;set访问器方法用于为该属性赋值,其语法格式分别如下:
[attributes] [accessor-modifier] get accessor-body [attributes] [accessor-modifier] set accessor-body
其中attributes指定访问器的属性(Attribute),给出一些附加的声明信息。
accessor-modifier为修饰符,可以是protected、internal、private、protected internal或internal protected。使用accessor-modifiers的限制如下。
(1)accessor-modifier不可用在接口中或显式接口成员实现中。
(2)对于没有override修饰符的属性或索引器,仅当该属性或索引器同时带有get和set访问器时才允许使用accessor-modifier,并且只能用于其中的一个访问器。
(3)对于包含override修饰符的属性或索引器,访问器必须匹配被重写访问器的accessor-modifier(如果存在)。
(4)accessor-modifier声明的可访问性的限制性必须严格高于属性或索引器本身所声明的可访问性。准确地说,如果属性或索引器声明了public可访问性,则可使用任何accessor-modifier;如果声明了protected internal可访问性,则accessor-modifier可为internal、protected或private;如果声明了internal或protected可访问性,则accessor-modifier必须为private;如果声明了private可访问性,则任何accessor-modifier都不可使用。
accessor-body为一个语句块或一个分号,对于abstract和extern属性,每个指定访问器的accessor-body只是一个分号。非abstract及非extern属性可以是自动实现的属性,在这种情况下,必须给定get和set访问器,而且其访问器体均由一个分号组成。对于其他任何非abstract及非extern属性的访问器,accessor-body是一个语句块,用于指定调用相应访问器时需执行的语句。
根据get和set访问器是否存在,属性可分为以下3种情况。
(1)只具有ge访问器的属性称为“只读属性”,此get访问器用于返回私有变量的值。只读属性的值只能读取,而不能修改,将只读属性作为赋值目标会导致编译时错误。
(2)只具有set访问器的属性称为“只写属性”,此set访问器通过value参数来修改私有变量的值。只写属性的值只能设置,而不能读取。除了作为赋值的目标外,在表达式中引用只写属性是编译时错误。
(3)同时具有get和set访问器的属性称为“读写属性”,这两个访问器分别用于读取和设置属性的值。
1. get访问器
get访问器相当于一个具有属性类型返回值的无形参方法。除了作为赋值的目标之外,在表达式中引用属性时,将调用该属性的get访问器以计算该属性的值。该访问器必须以return语句或throw语句结束,并且控制权不能离开访问器体。此外所有return语句都必须指定一个可隐式转换为属性类型的表达式,用于返回属性成员的值,执行get访问器相当于读取字段的值。
下面的示例在Student类声明中包含一个私有字段name和一个属性Name,该属性的get访问器用于返回私有字段name的值:
class Student { private string name; // 私有字段name public string Name // 属性Name { get { return name; } } }
当引用属性时,除非该属性为赋值目标;否则将调用get访问器以读取该属性的值。例如:
Student st1 = new Student(); System.Console.Write(st1.Name); // 在此将执行get访问器
通过使用get访问器来更改对象的状态不是一种好的编程风格,如以下get访问器在每次访问number字段时都会产生更改对象状态的副作用:
private int number; public int Number { get { return number++; // 不要这样做! } }
get访问器可以用于返回字段值,也可以用于计算并返回字段值。例如,在下面的示例中,如果不为Name属性赋值,则将返回字符串“未知”:
class Student { private string name; public string Name { get { return name != null ? name : "未知"; } } }
2. set访问器
set访问器类似于返回类型为void的方法,它使用一个名为“value”的隐式参数,此参数的类型是属性类型。当一个属性作为赋值的目标,或者作为++或--运算符的操作数被引用时,就会调用set访问器,所传递的实参(其值为赋值右边的值或者++或--运算符的操作数)将提供新值。不允许set访问器体中的return语句指定表达式。由于set访问器隐式具有value形参,因此如果在该访问器中的局部变量或常量声明中出现该名称,则会导致编译时错误。
在下面的示例中为Name属性添加了set访问器:
class Student { private string name; // 私有字段name public string Name // 属性Name { get { return name; } set { name = value; } } }
当为属性赋值时,用提供新值的参数调用set访问器。例如:
Student st1 = new Student(); st1.Name = "李明"; // 在此将执行set访问器 System.Console.Write(st1.Name); // 在此将执行get访问器
【例5-1】创建一个C#控制台应用程序,说明如何通过声明get和set访问器为类定义读写属性,程序运行结果如图5-1所示。
图5-1 程序运行结果
【设计步骤】
(1)在Visual C#中创建一个控制台应用程序项目,项目名称为“ConsoleApplication5-01”。解决方案名称为“chapter05”,保存在“F:\Visual C sharp 2008\chapter05”文件夹中。
(2)在代码编辑器中打开源文件Program.cs,在命名空间“ConsoleApplication5_01”中声明一个名为“Student”的类并为该类定义两个属性,代码如下:
public class Student { private string studentID; // 私有字段studentID private string studentName; // 私有字段studentName public string StudentID // 读写属性StudentID { get { return studentID; } set { if (((string)value).Length == 6) // 若学号长度等于6 { studentID = value; // 则用value值设置私有字段studentID } else { // 若学号长度不等于6,则抛出一个异常 throw (new ArgumentOutOfRangeException("StudentID", value, "学号必须由6位数字组成。")); } } } public string StudentName // 读写属性StudentName { get { return studentName; } set { studentName = value; } } }
(3)在Program类的Main方法中添加以下代码:
Console.Title = "类的属性应用示例"; Student st1 = new Student(); Student st2 = new Student(); try { st1.StudentID = "080101"; st1.StudentName = "李明"; Console.WriteLine("学生1的信息:\n学号:{0};姓名:{1}\n", st1.StudentID, st1.StudentName); st2.StudentID = "0801068"; st2.StudentName = "吴刚"; Console.WriteLine("学生2的信息:\n学号:{0};姓名:{1}\n", st2.StudentID, st2.StudentName); } catch (Exception e) { Console.WriteLine("抛出异常:{0}", e.GetType().FullName); Console.WriteLine("异常信息:{0}\n", e.Message); }
(4)按Ctrl+F5组合健编译并运行程序。
5.1.3 静态属性与实例属性
当属性声明中包含static修饰符时,该属性称为“静态属性”。静态属性不与特定实例相关联,因此在静态属性的访问器内引用this会导致编译时错误。
当属性声明中不存在static修饰符时,该属性称为“实例属性”。实例属性与类的一个给定实例相关联,并且该实例可以在属性的访问器中作为this来访问。
在E.M形式的成员访问中引用属性时,如果M是静态属性,则E必须表示包含M的类型;如果M是实例属性,则E必须表示包含M的类型的一个实例。
【例5-2】创建一个C#控制台应用程序,说明如何为类定义实例属性和静态属性,程序运行结果如图5-2所示。
图5-2 程序运行结果
【设计步骤】
(1)在解决方案chapter05中添加一个控制台应用程序项目,项目名称为“ConsoleApplication5-02”。保存在F:\Visual C sharp 2008\chapter05文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication5_02中声明一个名为“Employee”的类,并为该类定义一个名为“Name”的可读写实例属性和一个名为“Counter”的只读静态属性,代码如下:
public class Employee { public static int NumberOfEmployees; // 公共静态字段,表示职工编号 private static int counter; // 私有静态字段,表示职工编号 private string name; // 私有实例字段,表示职工姓名 // 可读写实例属性Name,用于检索和设置私有实例字段name public string Name { get { return name; } set { name = value; } } // 只读静态属性Counter,只包含get访问器,用于检索私有静态字段counter public static int Counter { get { return counter; } } // 构造函数 public Employee() { counter = ++counter + NumberOfEmployees; // 计算职工编号 } }
(3)在Program类的Main方法中添加以下代码:
Employee.NumberOfEmployees = 1000; Employee e1 = new Employee(); e1.Name = "张昊"; // 为实例属性Name赋值 Console.WriteLine("职工编号:{0}", Employee.Counter); // 访问静态属性Employee.Counter Console.WriteLine("职工姓名:{0}", e1.Name);
(4)按Ctrl+F5组合健编译并运行程序。
5.1.4 属性与继承
通过继承机制可以在基类的基础上创建派生类,派生类将获得基类的所有非私有成员,包括字段、方法及属性等。除了在声明和调用语法中有所不同外,虚的、密封、重写和抽象访问器与虚的、密封、重写和抽象方法具有完全相同的行为。
在属性声明中使用修饰符时,应注意以下问题。
(1)如果在派生类中声明的属性隐藏了基类中的同名属性,则应当在派生类的属性声明中使用new修饰符。在这种情况下,若访问基类中的隐藏属性,可进行显式类型转换。
(2)如果在属性声明中使用virtual修饰符,则指定该属性的访问器是虚的,该属性称为“虚属性”。virtual修饰符适用于读写属性的两个访问器,读写属性的访问器不可能只有一个是虚的。
(3)如果在属性声明中使用abstract修饰符,则指定该属性的访问器是虚的,该属性称为“抽象属性”。但不能为抽象属性的访问器提供实际实现,其访问器体只由一个分号组成。另外,非抽象派生类还要求通过重写属性以提供自己的访问器实现。
(4)只能在抽象类中使用抽象属性声明。通过在属性声明中使用override修饰符,可以在派生类中来重写被继承的虚属性的访问器,称为“重写属性声明”。它并不声明新属性,而只是对现有虚属性的访问器的实现进行专用化。
(5)如果在属性声明中同时包含abstract和override修饰符,则表示该属性是抽象属性,并且重写了一个基属性。此类属性的访问器也是抽象的。
(6)重写属性声明必须指定与所继承的属性完全相同的可访问性修饰符、类型和名称。如果被继承的属性只有单个访问器,即该属性是只读或只写的,则重写属性必须只包含该访问器;如果被继承的属性包含两个访问器,即该属性是读写的,则重写属性既可以仅包含其中任一个访问器,也可包含两个访问器。
(7)如果在重写属性声明中包含sealed修饰符,则可以防止派生类进一步重写该属性。该属性称为“密封属性”,密封属性的访问器也是密封的。
【例5-3】创建一个C#控制台应用程序,说明如何通过类型转换来访问基类中由派生类中具有同一名称的属性所隐藏的属性,程序运行结果如图5-3所示。
图5-3 程序运行结果
【设计步骤】
(1)在解决方案chapter05中添加一个控制台应用程序项目,项目名称为“ConsoleApplication5-03”,保存在F:\Visual C sharp 2008\chapter05文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication5_03中声明一个名为“Employee”的类并以该类作为基类声明一个名为“Manager”的派生类,代码如下:
public class Employee { private string name; // 私有字段 public string Name // 实例属性 { get { return name; } set { name = value; } } } public class Manager : Employee { private string name; // 私有字段 public new string Name // 在派生类中使用new修饰符隐藏基类中的同名属性 { get { return name; } set { name = value + "经理"; } } }
(3)在Program类的Main方法中添加以下代码:
Manager m1 = new Manager(); m1.Name = "张华"; // 访问派生类属性 ((Employee)m1).Name = "李莉"; // 通过类型转换访问基类中的隐藏属性 Console.WriteLine("派生类中的Name属性:{0}\n", m1.Name); Console.WriteLine("基类中隐藏的Name属性:{0}\n", ((Employee)m1).Name);
(4)按Ctrl+F5组合健编译并运行程序。
【例5-4】创建一个C#控制台应用程序,通过Cube和Square两个类实现抽象类Shape,并重写其抽象Area属性。在程序运行时,从键盘输入边长并计算正方形和正方体的面积。并且接收输入的面积以计算正方形和正方体的相应边长,程序运行结果如图5-4所示。
图5-4 程序运行结果
【设计步骤】
(1)在解决方案chapter05中添加一个控制台应用程序项目,项目名称为“ConsoleApplication5-04”。保存在F:\Visual C sharp 2008\chapter05文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication5_04中声明一个名为“Shape”的类并以该类声明一个名为“Area”的抽象属性,代码如下:
abstract class Shape // 抽象类 { public abstract double Area // 抽象属性 { get; // 方法体只有一个分号 set; // 方法体只有一个分号 } }
(3)在命名空间ConsoleApplication5_04中声明非抽象类Square和Cube,用于实现抽象类Shape。分别重写抽象属性Area,代码如下:
class Square : Shape // 派生类Square,表示正方形 { public double side; // 公共字段,表示正方形边长 public Square(double s) // 构造函数 { side = s; } public override double Area // 重写抽象属性Area,为其提供实现 { get { return side * side; // 返回正方形面积 } set { side = Math.Sqrt(value); // 设置正方形边长 } } } class Cube : Shape // 派生类Square,表示正方体 { public double side; // 公共字段,表示正方体边长 public Cube(double s) // 构造函数 { side = s; } public override double Area // 重写抽象属性Area,为其提供实现 { get { return 6 * side * side; // 返回正方体表面积 } set { side = Math.Sqrt(value / 6); // 设置正方体边长 } } }
(4)在Program类的Main方法中添加以下代码:
Console.Title = "抽象属性应用示例"; // 输入边长 Console.Write("请输入边长:"); double side = double.Parse(Console.ReadLine()); // 计算面积 Square s = new Square(side); Cube c = new Cube(side); // 显示结果 Console.WriteLine("正方体面积 = {0:F2}", s.Area); Console.WriteLine("正方体面积 = {0:F2}", c.Area); Console.WriteLine(); // 输入面积 Console.Write("请输入面积:"); double area = double.Parse(Console.ReadLine()); // 计算边长 s.Area = area; c.Area = area; // 显示结果 Console.WriteLine("正方形边长 = {0:F2}", s.side); Console.WriteLine("正方体边长 = {0:F2}", c.side);
(5)按Ctrl+F5组合健编译并运行程序。
5.1.5 非对称访问器
如前所述,属性的get和set部分称为“访问器”。在默认情况下,这些访问器具有相同的可见性或访问级别,并与其所属属性的可见性或访问级别保持一致。不过,有时需要限制对其中某个访问器的访问。通常是在保持get访问器可公开访问的情况下,限制set访问器的可访问性。
在下面的示例中,为Name属性定义了一个get访问器和一个set访问器。其中get访问器接受该属性本身的可访问性级别(在此示例中为public);对于set访问器,则通过对该访问器本身应用protected访问修饰符来进行显式限制:
public string Name { get { return name; } protected set { name = value; } }
使用访问器的访问修饰符有以下限制。
(1)不能对接口或显式接口成员实现使用访问器修饰符。
(2)仅当属性同时具有set和get访问器时,才能使用访问器修饰符,并且只允许对其中一个访问器使用修饰符。
(3)如果属性具有override修饰符,则访问器修饰符必须与重写的访问器(如果有的话)匹配。
(4)访问器的可访问性级别必须比属性本身的可访问性级别具有更严格的限制。
在重写属性时,被重写的访问器对重写代码必须是可访问的。此外,属性和访问器的可访问性级别都必须与相应的被重写属性和访问器匹配。
在下面的示例中,类Parent有一个虚属性TestProperty。该属性的set访问器声明包含protected修饰符,get访问器则没有任何访问修饰符。在派生类Parent中对属性TestProperty进行了重写,该重写属性的set访问器也使用protected修饰符,对其get访问器不能使用任何访问修饰符:
public class Parent { public virtual int TestProperty { // 注意访问器的可访问性级别 protected set { } // 此处未使用访问修饰符 get { return 0; } } } public class Kid : Parent { public override int TestProperty { // 在重写的访问器中使用相同的可访问性级别 protected set { } // 此处不能使用访问修饰符 get { return 0; } } }
使用访问器实现接口时,访问器不能具有访问修饰符。如果使用一个访问器(如get)实现接口,则另一个访问器可以具有访问修饰符。
在下面的示例中,接口ISomeInterface有一个名为“TestProperty”的属性,该属性只有一个get访问器,不能对其使用访问修饰符。通过类TestClass实现接口ISomeInterface时,可以对TestProperty属性的set访问器使用访问修饰符(例中使用protected):
// 接口 public interface ISomeInterface { // 只使用get访问器的属性 int TestProperty { get; } } // 通过类TestClass实现接口ISomeInterface public class TestClass : ISomeInterface { public int TestProperty { // 由于这是一个接口实现,因此此处不能使用访问修饰符 get { return 10; } // 接口属性不具有set访问器,因此可以使用访问修饰符 protected set { } } }
如果对访问器使用访问某个修饰符,则访问器的可访问性域由该修饰符确定;否则访问器的可访问性域由属性的可访问性级别确定。
【例5-5】创建一个C#控制台应用程序,说明如何通过在派生类中对属性的某个访问器使用限制性访问修饰符来隐藏该属性,程序运行结果如图5-5所示。
图5-5 程序运行结果
【设计步骤】
(1)在解决方案chapter05中添加一个控制台应用程序项目,项目名称为“ConsoleApplication5-05”。保存在F:\Visual C sharp 2008\chapter05文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication5_05中声明一个名为“BaseClass”的类并为该类定义Name和Id两个属性,代码如下:
public class BaseClass { private string name = "Name-BaseClass"; // 私有字段 private string id = "ID-BaseClass"; // 私有字段 public string Name // 属性Name { get { return name; } set { // 无执行语句,当对Name属性赋值时将不会改变该属性的值 } } public string Id // 属性Id { get { return id; } set { // 无执行语句,当对Id属性赋值时将不会改变该属性的值 } } }
(3)在命名空间ConsoleApplication5_05中,以BaseClass作为基类声明一个名为“DerivedClass”的派生类,并为该类定义Name和Id两个属性。在属性声明中使用new修饰符,以隐藏基类中的同名属性,代码如下:
public class DerivedClass : BaseClass // 派生类 { private string name = "Name-DerivedClass"; // 私有字段 private string id = "ID-DerivedClass"; // 私有字段 new public string Name // 使用new修饰符隐藏基类中的同名属性 { get { return name; } // 若使用protected修饰符,将会使此set访问器不可访问 set { name = value; } } // 在Id属性中使用限制性修饰符private // 当为该属性赋值时将使用基类BaseClass中的Id new private string Id { get { return id; } set { id = value; } } }
(4)在Program类的Main方法中添加以下代码:
Console.Title = "非对称访问器应用示例"; BaseClass b1 = new BaseClass(); // 创建基类实例 DerivedClass d1 = new DerivedClass(); // 创建派生类实例 b1.Name = "Base"; // 不能改变属性值 b1.Id = "Base123"; // 不能改变属性值 d1.Name = "Derived"; d1.Id = "Derived123"; // 此处将调用BaseClass.Id属性 Console.WriteLine("基类属性:{0},{1}\n", b1.Name, b1.Id); Console.WriteLine("派生类属性:{0},{1}\n", d1.Name, d1.Id);
(5)按Ctrl+F5组合健编译并运行程序。
5.1.6 自动实现的属性
在属性声明中通常需要通过属性访问器声明来指定与读取和写入该属性相关联的可执行语句。当属性访问器中不需要其他逻辑时,可以使用自动实现的属性。此时访问器体仅由一个分号组成,从而使属性声明变得更加简洁。
将一个属性指定为自动实现的属性时,编译器将创建一个私有的匿名后备字段。该字段将自动可用于该属性,并实现访问器以执行对该后备字段的读写的操作。
在下面的示例中,为Point类定义了两个自动实现的属性X和Y,其get和set访问器仅由一个分号组成:
public class Point { public int X // 自动实现属性X { get; set; } public int Y // 自动实现属性Y { get; set; } }
上述类声明等效于下面的声明:
public class Point { private int x; // 私有后备字段 private int y; // 私有后备字段 public int X // 属性X { get // get访问器 { return x; // 返回后备字段x的值 } set // set访问器 { x = value; // 设置后备字段x的值 } } public int Y // 属性Y { get // get访问器 { return y; // 返回后备字段y的值 } set // set访问器 { y = value; // 设置后备字段y的值 } } }
注意
自动实现的属性(Property)不允许具有属性(Attribute),如果必须在属性(Property)的后备字段中使用属性(Attribute),则应该创建常规属性(Property)。
因为私有的后备字段是不可访问的,所以只能通过属性访问器对其执行读写操作,自动实现的属性必须同时声明get和set访问器。这就意味着自动实现的只读或只写属性没有意义,因而不允许使用,但是可以通过不同的方式为每个访问器设置访问级别。如果要创建只读的自动实现属性,则应当给予其private set访问器。
下面的示例说明如何实现私有后备字段的只读属性的效果:
public class ReadOnlyPoint { public int X // 只读的自动实现属性 { get; private set; // 对set访问器使用修饰符private } public int Y // 只读的自动实现属性 { get; private set; // 对set访问器使用修饰符private } public ReadOnlyPoint(int x, int y) // 构造函数 { X = x; // 设置属性X的值 Y = y; // 设置属性Y的值 } }
【例5-6】创建一个C#控制台应用程序,说明如何使用自动实现的属性实现轻量类,程序运行结果如图5-6所示。
图5-6 程序运行结果
【设计步骤】
(1)在解决方案chapter05中添加一个控制台应用程序项目,项目名称为“ConsoleApplication5-06”。保存在F:\Visual C sharp 2008\chapter05文件夹中,将该项目设置为启动项目。
(2)在命名空间ConsoleApplication5_06中声明一个名为“Contact”的类,并为该类声明4个自动实现的属性,代码如下:
public class Contact { public string Name // 实例属性 { get; set; } public string Address // 实例属性 { get; set; } public static int ContactNumber // 静态属性 { get; set; } public int ID // 只读属性 { get; private set; } public Contact() // 构造函数 { ID = ++ID + ContactNumber; } public void ShowInfo() // 公共方法 { Console.WriteLine("{0}\t{1}\t{2}", this.ID, this.Name, this.Address); } }
(3)在Program类的Main方法中添加以下代码:
Console.Title = "自动实现的属性应用示例"; Contact.ContactNumber = 100; Contact c1 = new Contact(); c1.Name = "张华"; c1.Address = "中国广州"; Contact.ContactNumber = c1.ID; Contact c2 = new Contact(); c2.Name = "郭强"; c2.Address = "中国上海"; Console.WriteLine(" ***联系人信息***"); Console.WriteLine("------------------------"); Console.WriteLine("编号\t姓名\t地址"); c1.ShowInfo(); c2.ShowInfo(); Console.WriteLine();
(4)按Ctrl+F5组合健编译并运行程序。
5.1.7 匿名类型
除了通过在类声明中使用属性访问器定义只读属性外,还可以通过使用匿名类型将一组只读属性直接封装到单个对象中,而不必首先显式定义一个类型。在这种情况下,类型名由编译器生成,并且不能在源代码级使用。这些匿名属性的类型由编译器推断。
匿名类型是由一个或多个公共只读属性组成的类类型,不允许包含其他类型的类成员。例如,方法或事件等。匿名类型使用new运算符和对象初始值设定项创建。如果要将匿名类型分配给变量,则必须使用var来构造初始化该变量,这是因为只有编译器能够访问匿名类型的基础名称。
在下面的示例中,声明了一个匿名类型。它使用两个分别名为“StudentID”和“StudentName”的属性来初始化,通过使用var关键字将此匿名类型分配给变量student:
var student = new{ StudentID = "080101", StudentName = "李明" };
其中var关键字用于在方法范围中声明隐式类型的局部变量,隐式类型的局部变量是强类型变量,如同已经声明该类型一样,但由编译器确定类型。
使用匿名类型时应注意以下几点。
(1)如果没有在匿名类型中指定成员名称,编译器会为匿名类型成员指定与用于初始化这些成员的属性相同的名称,必须为使用表达式初始化的属性提供名称。
(2)匿名类型是直接从对象派生的引用类型,尽管应用程序无法访问匿名类型,但编译器仍会为其提供一个名称。从公共语言运行库的角度来看,匿名类型与任何其他引用类型没有不同。
(3)如果两个或更多个匿名类型以相同的顺序具有相同数量和类型的属性,则编译器会将这些匿名类型视为相同的类型,并且它们共享编译器生成的相同类型信息。
(4)匿名类型具有方法范围,若要为方法边界外部传递一个匿名类型或一个包含匿名类型的集合,必须首先将匿名类型强制转换为对象,但是这会使匿名类型的强类型化无效。如果必须存储查询结果或者必须将查询结果传递到方法边界外部,可考虑使用普通的命名结构或类,而不是匿名类型。
(5)匿名类型不能像属性一样包含不安全类型。
(6)由于匿名类型中的Equals和GetHashCode方法是根据属性的Equals和GetHashcode定义的,因此仅当同一匿名类型的两个实例的所有属性都相等时,这两个实例才相等。
(7)在同一个应用程序中,指定一系列名称相同的属性并按同一顺序指定编译时类型的两个匿名对象初始值设定项将产生同一匿名类型的实例。
【例5-7】创建一个C#控制台应用程序,说明如何通过匿名类型把一些关联的数据封装到对象中并检查匿名类型的运行时类型,程序运行结果如图5-7所示。
图5-7 程序运行结果
【设计步骤】
(1)在解决方案chapter05中添加一个控制台应用程序项目,项目名称为“ConsoleApplication5-07”。保存在F:\Visual C sharp 2008\chapter05文件夹中,将该项目设置为启动项目。
(2)在Program类的Main方法中添加以下代码:
onsole.Title = "匿名类型应用示例"; var book1 = new { Title = "贯通Tomcat开发", Author = "钟经伟", ISBN = "978-7-121-06408-1" }; var book2 = new { Title = "贯通Hibernate开发", Author = "李刚", ISBN = "978-7-121-06871-3" }; Console.WriteLine(" ***图书信息***"); Console.WriteLine("--------------------------------------------------"); Console.WriteLine("书名\t\t\t作者\tISBN"); Console.WriteLine("{0}\t\t{1}\t{2}",book1.Title, book1.Author, book1.ISBN); Console.WriteLine("{0}\t{1}\t{2}\n", book2.Title, book2.Author, book2.ISBN); Console.WriteLine("***匿名类型名称***"); Console.WriteLine("{0}\n{1}\n", book1.GetType(), book2.GetType());
(3)按Ctrl+F5组合健编译并运行程序。
5.2 索引器
与属性一样,索引器也是类或结构的一种函数成员,它允许按与数组相同的方式来访问类或结构的实例。除了索引器访问器方法采用参数外,为属性访问器定义的所有规则同样适用于索引器访问器
5.2.1 声明索引器
索引器可以通过以下语法格式来声明:
[attributes] [indexer-modifiers] indexer-declarator { accessor-declarations }
其中attributes是可选的,用于指定索引器的属性。indexer-modifiers为索引器修饰符,可以是new、public、protected、internal、private、virtual、sealed、override、abstract和extern。
关于有效的修饰符组合,索引器声明与方法声明遵循相同的规则,唯一的例外是在索引器声明中不允许使用静态修饰符static。修饰符virtual、override和abstract相互排斥,但有一种情况除外,即abstract和override修饰符可以一起使用,以便使用抽象索引器来重写虚索引器。
indexer-declarator有以下两种格式:
type this[formal-parameter-list] type interface-type.this[formal-parameter-list]
其中type用于指定由该声明引入的索引器的元素类型,除非索引器是一个显式接口成员的实现;否则该type后要跟一个关键字this。而对于显式接口成员的实现,该type后要先跟一个interface-type、一个“.”,再跟一个关键字this。与其他类成员不同,索引器不具有用户定义的名称。
formal-parameter-list用于指定索引器的形参,索引器的形参表对应于方法的形参表。不同之处仅在于其中必须至少含有一个形参,并且不允许使用ref和out形参修饰符。
索引器的type和在formal-parameter-list中引用的每个类型都必须至少具有与索引器本身相同的可访问性,即索引器类型及其参数类型必须至少与索引器本身一样是可访问的。
accessor-declarations用于声明该索引器的get和set访问器,它们必须被括在“{”和“}”标记中,用来指定与读取和写入索引器元素相关联的可执行语句。
在下面的示例中,为IndexerClass类声明了索引器并通过其get访问器返回私有数组arr的元素值;通过其set访问设置该数组的元素值:
public class IndexerClass { public static int size=10; // 私有静态字段 private int[] arr = new int[size]; // 私有数组 public int this[int index] // 索引器,包含形参index { get // get访问器 { if(index >= 0 && index < size) return arr[index]; // 返回索引器元素的值 else throw new IndexOutOfRangeException(); } set // set访问器 { if(index >= 0 && index < size) arr[index]=value; // 设置索引器元素的值 else throw new IndexOutOfRangeException(); } } }
当索引器声明包含extern修饰符时,该索引器为称“外部索引器”。因为其声明不提供任何实际的实现,所以它的每个访问器声明都由一个分号组成。
虽然访问索引器元素的语法与访问数组元素的语法相同,但是索引器元素并不属于变量,因此不可能将索引器元素作为ref或out实参传递。
索引器的形参表定义索引器的签名,具体而言,索引器的签名由其形参的数量和类型组成,但索引器元素的类型和形参的名称都不是索引器签名的组成部分。如果在同一类中声明一个以上的索引器,则其必须具有不同的签名。
如果要为索引器提供一个其他语言可以使用的名称,可以在索引器声明中使用name属性。例如,在下面示例中声明的索引器将具有名称TheItem,不提供名称属性将生成默认名称Item:
[System.Runtime.CompilerServices.IndexerName("TheItem")] public int this [int index] // 索引器声明,索引器元素和形参的类型均为int { // 索引器的访问器声明 }
5.2.2 索引器与属性的比较
索引器和属性在概念上非常类似,区别如下。
(1)属性由其名称标识,而索引器由其签名标识。
(2)属性可以是static成员,而索引器始终是实例成员。
(3)属性的get访问器对应于不带形参的方法,而索引器的get访问器对应于与索引器具有相同的形参表的方法。
(4)属性的set访问器对应于具有名为“value”的单个形参的方法,而索引器的set访问器对应于与索引器具有相同的形参表加上一个名为“value”的附加形参的方法。
(5)如果在索引器访问器中使用与该索引器的形参相同的名称来声明局部变量,将会导致一个编译时错误。
(6)在重写属性声明中,被继承的属性是使用语法base.P来访问的,其中P为属性名称;在重写索引器声明中,被继承的索引器是使用语法base[E]来访问的,其中E是一个用逗号分隔的表达式列表。
(7)属性可以为静态成员或实例成员,索引器必须为实例成员。
(8)属性的get访问器没有参数。索引器的get访问器具有与索引器相同的形参表。
(9)属性的set访问器包含隐式value参数,索引器的set访问器除了值参数外,还具有与索引器相同的形参表。
(10)属性支持对自动实现的属性使用短语法,索引器不支持短语法。
(11)属性允许像调用公共数据成员一样调用方法,索引器允许对一个对象本身使用数组表示法来访问该对象内部集合中的元素。
5.2.3 使用索引器
为类或结构定义索引器并创建一个对象后,就可以使用数组表示法来访问该对象内部集合中的元素。即使用方括号表示法来读写该对象内部集合中元素的值,语法格式如下:
object-name[index]
其中object-name为从索引器所属的类或结构的对象的名称,方括号内的index表示传递给索引器形参的索引值。
【例5-8】创建一个C#控制台应用程序,说明如何通过索引器来实现对对象内部数组元素的访问,程序运行结果如图5-8所示。
图5-8 程序运行结果
【设计步骤】
(1)在解决方案chapter05中添加一个控制台应用程序项目,项目名称为“ConsoleApplication5-08”。保存在F:\Visual C sharp 2008\chapter05文件夹中,将该项目设置为启动项目。
(2)在代码编辑器打开源文件Program.cs,在命名空间ConsoleApplication5_08中声明一个名为“StringCollection”的类并为该类定义一个构造函数和一个索引器,代码如下:
public class StringCollection { private string[] arr; // 私有数组 public StringCollection(int size) // 构造函数 { arr = new string[size]; for (int i = 0; i < size; i++) // 遍历数组 { arr[i] = "Element #" + i; // 为数组元素赋值 } } public string this[int index] // 索引器,元素类型为string,形参类型为int { get // get访问器 { if (index >= 0 && index < arr.Length) // 检查形参的值 return arr[index]; // 返回数组元素的值 else throw new IndexOutOfRangeException(); // 引发异常,试图访问索引超出数组界限的数组元素 } set // set访问器 { if (index >= 0 && index < arr.Length) // 检查形参的值 arr[index] = value; // 设置数组元素的值 else throw new IndexOutOfRangeException(); // 引发异常 } } }
(3)在Program类的Main方法中添加以下代码:
Console.Title = "索引器应用示例"; int size = 6; StringCollection sc = new StringCollection(size); // 创建类的实例 // 设置索引器元素的值,此时将调用索引器的set访问器 sc[1] = "Hello World"; sc[3] = "这是一个索引器元素"; // 读取并显示索引器元素的值,此时将调用索引器的get访问器 for (int i = 0; i < size; i++) { Console.WriteLine(sc[i]); } Console.WriteLine(); // 设置和读取索引器元素的值,将调用set和get访问器。若形参值超出范围,则会引发异常 try { sc[10] = "New Element"; Console.WriteLine(sc[10]); } catch(Exception e) { Console.WriteLine("访问索引器元素时发生了异常:{0}\n", e.Message); }
(4)按Ctrl+F5组合健编译并运行程序。
5.2.4 索引器重载
与方法成员一样,声明类或结构时也可以重载索引器。即在同一个类型中声明一个以上的索引器,但要求它们必须具有不同的签名。
如前所述,索引器的签名由索引器的形参表来定义。具体来说,索引器的签名取决于形参的个数及其类型。需要注意的是,索引器元素的类型和形参的名称都不属于索引器签名的组成部分。
【例5-9】创建一个C#控制台应用程序,说明如何通过索引器来访问对象内部数组元素,程序运行结果如图5-9所示。
图5-9 程序运行结果
【设计步骤】
(1)在解决方案chapter05中添加一个控制台应用程序项目,项目名称为“ConsoleApplication5-09”。保存在F:\Visual C sharp 2008\chapter05文件夹中,将该项目设置为启动项目。
(2)在代码编辑器打开源文件Program.cs,在命名空间ConsoleApplication5_09中声明一个名为“StringCollection”的类并为该类定义两个索引器,代码如下:
public class StringCollection { private string[] arr; // 私有数组 public StringCollection(int size) // 对私有数组进行初始化 { arr = new string[size]; for (int i = 0; i < size; i++) { arr[i] = "第 " + i + " 个元素"; } } public string this[int index] // 第1个索引器,其元素类型为string,形参类型为int { get // get访问器用于返回索引器元素的值 { if (index >= 0 && index < arr.Length) // 检查形参值 return arr[index]; // 返回数组元素的值 else // 形参值(数组元素下标)越界 throw new IndexOutOfRangeException(); // 引发异常 } set // set访问器用于设置索引器元素的值 { if (index >= 0 && index < arr.Length) // 检查形参值 arr[index] = value; // 设置数组元素的值 else // 形参值(数组元素下标)越界 throw new IndexOutOfRangeException();// 引发异常 } } public string this[string key] // 第2个索引器,其元素类型和形参类型均为string { get // get访问器用于返回与指定字符串匹配的元素的索引 { for (int i = 0; i < arr.Length; i++) // 遍历数组 { if (arr[i] == key) return i.ToString(); // 若某元素的值等于形参key的值,则返回其索引值 } throw new Exception("未发现匹配的元素。"); // 引发异常 } set // set访问器用于设置与指定字符串匹配的元素的值 { for (int i = 0; i < arr.Length; i++) // 遍历数组 { if (arr[i] == key)arr[i] = value; // 若某元素的值等于形参key的值,则用隐含形参设置其值 } throw new Exception("未发现匹配的元素。"); // 引发异常 } } public void ShowElements() // 公共方法用于显示每个数组元素 { for (int i = 0; i < arr.Length; i++) { Console.WriteLine(arr[i]); } } }
(3)在Program类的Main方法中添加以下代码:
Console.Title = "索引器重载示例"; int size = 6; string str1 = "Hello, World!"; string str2 = "你好,世界!"; StringCollection sc=new StringCollection(size); // 创建类的新实例 try { // 设置索引器元素的值,这将调用第1个索引器的set访问器 sc[2] = str1; sc[3] = str2; sc.ShowElements(); // 显示每个索引器元素的值 // 以字符串作为索引读取索引器元素的值,这将第3个索引器的get访问器 Console.WriteLine("\n值为 \"{0}\" 的元素是数组中的第 {1} 个元素", str1, sc[str1]); Console.WriteLine("值为 \"{0}\" 的元素是数组中的第 {1} 个元素\n", str2, sc[str2]); // 以字符串作为索引来设置索引器元素的值,这将调用第3个索引器的set访问器,并可能导致异常 sc["Not Found"] = "这是一个新元素"; sc.ShowElements(); } catch (Exception e) { Console.WriteLine("访问索引器元素时发生异常:{0}\n",e.Message); }
(4)按Ctrl+F5组合健编译并运行程序。
5.2.5 多维索引器
索引器与属性很类似,它们都是类或结构的函数成员,而且都包含访问器方法。但是其访问器的形参不同,属性声明中不包含形参表;而索引器声明中包含形参表。该形参表至少包含一个形参,包含一个形参的索引器称为“一维索引器”,包含多个形参的索引器称为“多维索引器”。
具体到访问器层面,属性的get访问器对应于不带形参的方法,而索引器的get访问器对应于与索引器具有相同的形参表的方法;属性的set访问器对应于具有名为“value”的单个形参的方法,而索引器的set访问器对应于与索引器具有相同的形参表加上一个名为“value”的附加形参的方法。
【例5-10】创建一个C#控制台应用程序,演示一个具有带两个形参的索引器的10×26的网格类,第1个形参必须是A~Z范围内的大写或小写字母;第2个形参必须是0~9范围内的整数,程序运行结果如图5-10所示。
图5-10 程序运行结果
【设计步骤】
(1)在解决方案chapter05中添加一个控制台应用程序项目,项目名称为“ConsoleApplication5-09”。保存在F:\Visual C sharp 2008\chapter05文件夹中,将该项目设置为启动项目。
(2)在代码编辑器打开源文件Program.cs,在命名空间ConsoleApplication5_09中声明一个名为“Grid”的网格类并为该类定义一个二维索引器,代码如下:
public class Grid { // 声明两个常量 const int NumRows = 10; const int NumCols = 26; string [, ] cells = new string[NumRows, NumCols]; // 私有二维数组 public Grid() // 构造函数,用于对数组进行初始化 { for (int i = 0; i < NumRows; i++) { for (int j = 0; j < NumCols; j++) { cells[i, j] = "+"; // 每个数组元素都存储一个星号 } } } // 二维索引器,其元素类型为string,带有两个形参,形参类型分别为char和int public string this[char c, int col] { get // get访问器用于返回指定位置上的数组元素值 { c= Char.ToUpper(c); // 转换为大写 if (c < 'A' || c > 'Z') // 检查形参c的值 { throw new ArgumentException(); // 引发异常 } if (col < 0 || col >= NumCols) // 检查形参col的值 { throw new IndexOutOfRangeException(); // 引发异常 } return cells[c - 'A', col]; // 返回数组元素的值 } set // set访问器用于设置指定位置上的数组元素值 { c= Char.ToUpper(c); // 转换为大写 if (c < 'A' || c > 'Z') // 检查形参c的值 { throw new ArgumentException(); // 引发异常 } if (col < 0 || col >= NumCols) // 检索形参col的值 { throw new IndexOutOfRangeException(); // 引发异常 } cells[c - 'A', col] = value; // 设置数组元素的值 } } }
(3)在Program类的Main方法中添加以下代码:
Console.Title = "二维索引器应用示例"; Grid g = new Grid(); // 创建Grid类的新实例 try { // 设置索引器元素的值,这将调用索引器的set访问器 g['A', 2] = "A"; g['B', 3] = "B"; g['C', 4] = "C"; for (char i = 'A'; i < 'Z'; i++) { for (int j = 0; j < Grid.NumCols; j++) { Console.Write(g[i, j] + " "); // 读取索引器元素的值,这将调用索引器的get访问器 } Console.WriteLine(); } // 设置和读取索引器元素的值,这将分别调用索引器的set和get访问器,并且会导致异常 g['M', 33] = "Z"; Console.WriteLine(g['M', 33]); } catch (Exception e) { Console.WriteLine("\n访问索引器元素时发生异常:{0}\n", e.Message); }
(4)按Ctrl+F5组合健编译并运行程序。