类型和接口可以包含以下任何成员:
-
方法
-
属性
-
构造函数
-
事件
-
字段
本节内容
-
成员重载
-
描述重载成员的准则。
成员的签名包含成员的名称和参数列表。每个成员签名在类型中必须是唯一的。只要成员的参数列表不同,成员的名称可以相同。如果类型的两个或多个成员是同类成员(方法、属性、构造函数等),它们具有相同的名称和不同的参数列表,则称该同类成员进行了重载。例如,Array 类包含两个 CopyTo 方法。第一个方法采用一个数组和一个 Int32 值,第二个方法采用一个数组和一个 Int64 值。
注意 如公共语言运行库规范所述,更改某个方法的返回类型并不能使该方法变得唯一。仅更改返回类型不能定义重载。
重载成员在同一功能上应有所不同。例如,某个类型具有两个 CopyTo 成员,其中第一个成员向数组复制数据,第二个成员向文件复制数据,这样是不正确的。对成员进行重载通常是为了提供带少量参数或不带参数且易于使用的重载。这些成员调用功能更强大、要求经验丰富才能正确使用的重载。易于使用的重载通过向复杂重载传递默认值,支持常见的方案。例如,File 类提供 Open方法的重载。简单重载 Open 采用文件路径和文件模式作为参数。它调用具有路径、文件模式、文件访问和文件共享参数的Open 重载,并为文件访问和文件共享参数提供常用的默认值。如果开发人员不需要复杂重载所具有的灵活性,则不必了解文件访问和共享模型就可以打开文件。
为了便于维护和版本控制,简单重载应使用复杂重载来执行操作;基础功能不应在多个位置实现。
下面的准则有助于确保正确设计重载成员。
尽量使用描述性参数名称指示简单重载所使用的默认值。
此准则尤其适用于 Boolean 参数。复杂重载的参数名称应通过描述相反的状态或操作来指示简单重载所提供的默认值。例如,String 类提供下面的重载:
[Visual Basic]
Overloads Public Shared Function Compare( _ ByVal strA As String, _ ByVal strB As String _ ) As Integer Overloads Public Shared Function Compare( _ ByVal strA As String, _ ByVal strB As String, _ ByVal ignoreCase As Boolean _ ) As Integer
[C#]
public static int Compare( string strA, string strB ); public static int Compare( string strA, string strB, bool ignoreCase );
第二个重载提供一个名为 ignoreCase 的 Boolean 参数。即简单重载区分大小写,仅当要忽略大小写时,才需要使用复杂重载。默认值通常应为 false。
避免随意更改重载中的参数名称。如果某个重载的一个参数与另一个重载的一个参数表示相同的输入,则这两个参数应具有同样的名称。
例如,不要执行下面的操作:
public void Write(string message, FileStream stream){} public void Write(string line, FileStream file, bool closeStream){}
这些重载的正确定义如下所示:
public void Write(string message, FileStream stream){} public void Write(string message, FileStream stream, bool closeStream){}
保持重载成员参数的顺序一致性。在所有重载中,同名参数的位置应该相同。
例如,不要执行下面的操作:
public void Write(string message, FileStream stream){} public void Write(FileStream stream, string message, bool closeStream){}
这些重载的正确定义如下所示:
public void Write(string message, FileStream stream){} public void Write(string message, FileStream stream, bool closeStream){}
此准则有两项约束:
-
如果重载采用变量参数列表,则该列表必须是最后一个参数。
-
如果重载采用 out 参数,按照约定,这类参数应作为最后的参数。
如果需要具有扩展性,将最长的重载作为虚(在 Visual Basic 中为 Overridable)重载。较短的重载只应逐步调用较长的重载。
下面的代码示例对此进行了演示。
public void Write(string message, FileStream stream) { this.Write(message, stream, false); } public virtual void Write(string message, FileStream stream, bool closeStream) { // Do work here. }
不要对重载成员使用 ref 或 out 修饰符。
例如,不要执行下面的操作。
public void Write(string message, int count) ...public void Write(string message, out int count)
通常,如果设计中出现这种情况,则很可能存在更深层的设计问题。考虑是否应该重命名某个成员,以便提供关于方法执行的确切操作的更多信息。
允许对可选参数传递 null(在 Visual Basic 中为 Nothing)。如果方法带有引用类型的可选参数,则允许传递 null 以指示应使用默认值。这样可不必在调用成员前检查 null。
例如,在下面的示例中,开发人员就不必检查 null。
public void CopyFile (FileInfo source, DirectoryInfo destination, string newName) { if (newName == null) { InternalCopyFile(source, destination); } else { InternalCopyFile(source, destination, newName); } }
使用成员重载而不要用默认参数定义成员。默认参数不符合 CLS,不能在某些语言中使用。
下面的代码示例演示的方法设计是不正确的。
Public Sub Rotate (data as Matrix, Optional degrees as Integer = 180) ' Do rotation here End Sub
此代码应重新设计为两个重载,由简单重载提供默认值。下面的代码示例演示的设计是正确的。
Overloads Public Sub Rotate (data as Matrix) Rotate(data, 180) End Sub Overloads Public Sub Rotate (data as Matrix, degrees as Integer) ' Do rotation here End Sub
-
-
显式实现接口成员
-
描述显式接口实现的准则。
接口是支持一些功能的协定。实现接口的类必须为接口中指定的成员提供实现细节。例如,IEnumerator 接口定义成员签名,必须实现成员签名才能支持对一组对象(如集合)进行枚举。若要实现 IEnumerator,类必须实现 Current、MoveNext 和 Reset 成员。
当接口成员由类显式实现时,只能通过使用对接口的引用来访问该成员。这将导致隐藏接口成员。显式实现接口成员的常见原因不仅是为了符合接口的协定,而且也是为了以某种方式改进它(例如,提供应用来代替接口的弱类型方法的强类型方法)。显式实现接口成员的另一个常见原因是存在不应由开发人员调用显式接口成员的时候。例如,GetObjectData 成员是最常显式实现的,因为它由序列化基础结构调用而不用于从代码调用。
下列设计准则有助于确保您的库设计仅在需要时使用显式接口实现。
如果没有充分理由,应避免显式实现接口成员。
要理解显式实现需要具备很高深的专业知识。例如,很多开发人员不知道显式实现的成员是可以公共调用的,即使其签名是私有的也一样。由于这个原因,显式实现的成员不显示在公共可见的成员列表中。显式实现成员还会导致对值类型的不必要装箱。
如果成员只应通过接口调用,则考虑显式实现接口成员。
这主要包括支持 .NET Framework 基础结构(如数据绑定或序列化)的成员。例如,IsReadOnly 属性只应由数据绑定基础结构通过使用对 ICollection接口的引用来访问。由于满足此准则,List 类显式实现该属性。
考虑显式实现接口成员以模拟变体(即,更改重写成员中的参数或返回类型)。
为了提供接口成员的强类型版本,通常会这么做。
考虑显式实现接口成员以隐藏一个成员并添加一个具有更好名称的等效成员。
不要将显式成员用作安全边界。
显式实现成员不提供任何安全性。通过使用对接口的引用,这些成员都是可以公共调用的。
如果显式实现的成员的功能意在由派生类特殊化,则一定要提供具有相同功能的受保护虚拟成员。
不能重写显式实现的成员。如果在派生类中重新定义成员,则派生类不能调用基类实现。应通过使用与显式接口成员相同的名称或将 Core 附加到接口成员名称来命名受保护成员。
-
在属性和方法之间选择
-
描述确定在哪些情况下将功能作为属性(与方法相对)实现的准则。
通常,方法代表操作而属性代表数据。属性应像字段一样使用,这意味着属性不应进行复杂的计算,也不应产生副作用。在不违反下列准则的情况下,应考虑使用属性而不是方法,因为属性对于经验较少的开发人员更易于使用。
如果成员表示类型的逻辑属性 (Attribute),请考虑使用属性 (Property)。
例如,BorderStyle 是一个属性 (Property),因为边框样式是 ListView 的属性 (Attribute)。
如果属性值存储在进程内存中并且该属性只是用于提供对值的访问,则要使用属性而不是方法。
下面的代码示例阐释了这一准则。EmployeeRecord 类定义对私有字段提供访问的两个属性。在本主题的末尾显示了完整示例。
public class EmployeeRecord { private int employeeId; private int department; public EmployeeRecord() { } public EmployeeRecord (int id, int departmentId) { EmployeeId = id; Department = departmentId; } public int Department { get {return department;} set {department = value;} } public int EmployeeId { get {return employeeId;} set {employeeId = value;} } public EmployeeRecord Clone() { return new EmployeeRecord(employeeId, department); } }
在下列情况下要使用方法而不是属性。
-
操作比字段集慢数个数量级。即使考虑提供异步版本的操作来避免阻止线程,该操作也很可能因开销太大而不能使用属性。特别是,访问网络或文件系统(一次性初始化除外)的操作最可能是方法,而不是属性。
-
操作是转换,如 Object.ToString method。
-
操作在每次调用时都返回不同的结果,即使参数不发生更改也是如此。例如,NewGuid 方法在每次调用时都返回不同的值。
-
操作具有很大的显而易见的副作用。注意,一般不将填充内部缓存视为是显而易见的副作用。
-
操作返回内部状态的副本(这不包括在堆栈上返回的值类型对象的副本)。
-
操作返回一个数组。
如果操作返回一个数组,应使用方法,原因是:要保留内部数组,必须返回数组的深层副本而不是对属性所使用的数组的引用。这一事实加之开发人员将属性视同字段一样使用的事实,可能会导致代码效率十分低下。在下面的代码示例中阐释了这一点,该示例使用属性返回一个数组。在本主题的末尾显示了完整示例。
public class EmployeeData { EmployeeRecord[] data; public EmployeeData(EmployeeRecord[] data) { this.data = data; } public EmployeeRecord[] Employees { get { EmployeeRecord[] newData = CopyEmployeeRecords(); return newData; } } EmployeeRecord[] CopyEmployeeRecords() { EmployeeRecord[] newData = new EmployeeRecord[data.Length]; for(int i = 0; i< data.Length; i++) { newData[i] = data[i].Clone(); } Console.WriteLine ("EmployeeData: cloned employee data."); return newData; } }
使用此类的开发人员假定属性不会比字段访问的开销大,因此根据这一假定编写了应用程序代码,如下面的代码示例所示。
public class RecordChecker { public static Collection<int> FindEmployees(EmployeeData dataSource, int department) { Collection<int> storage = new Collection<int>(); Console.WriteLine("Record checker: beginning search."); for (int i = 0; i < dataSource.Employees.Length; i++) { if (dataSource.Employees[i].Department == department) { Console.WriteLine("Record checker: found match at {0}.", i); storage.Add(dataSource.Employees[i].EmployeeId); Console.WriteLine("Record checker: stored match at {0}.", i); } else { Console.WriteLine("Record checker: no match at {0}.", i); } } return storage; } }
注意,在每次循环迭代中都会访问 Employees 属性,在部门匹配时也会访问该属性。每次访问该属性时,就会创建一个 employees 数组的副本,该副本在短暂的使用过后就需要进行垃圾回收。通过将 Employees 实现为一个方法,可以向开发人员表明:与访问字段相比,该操作在计算上的开销较大。开发人员更愿意调用一次方法,并将方法调用的结果进行缓存以便进行处理。
示例
下面的代码示例演示一个假定属性访问在计算上的开销不大的完整应用程序。EmployeeData 类不适当地定义了一个返回数组副本的属性。
using System; using System.Collections.ObjectModel; namespace Examples.DesignGuidelines.Properties { public class EmployeeRecord { private int employeeId; private int department; public EmployeeRecord() { } public EmployeeRecord (int id, int departmentId) { EmployeeId = id; Department = departmentId; } public int Department { get {return department;} set {department = value;} } public int EmployeeId { get {return employeeId;} set {employeeId = value;} } public EmployeeRecord Clone() { return new EmployeeRecord(employeeId, department); } } public class EmployeeData { EmployeeRecord[] data; public EmployeeData(EmployeeRecord[] data) { this.data = data; } public EmployeeRecord[] Employees { get { EmployeeRecord[] newData = CopyEmployeeRecords(); return newData; } } EmployeeRecord[] CopyEmployeeRecords() { EmployeeRecord[] newData = new EmployeeRecord[data.Length]; for(int i = 0; i< data.Length; i++) { newData[i] = data[i].Clone(); } Console.WriteLine ("EmployeeData: cloned employee data."); return newData; } } public class RecordChecker { public static Collection<int> FindEmployees(EmployeeData dataSource, int department) { Collection<int> storage = new Collection<int>(); Console.WriteLine("Record checker: beginning search."); for (int i = 0; i < dataSource.Employees.Length; i++) { if (dataSource.Employees[i].Department == department) { Console.WriteLine("Record checker: found match at {0}.", i); storage.Add(dataSource.Employees[i].EmployeeId); Console.WriteLine("Record checker: stored match at {0}.", i); } else { Console.WriteLine("Record checker: no match at {0}.", i); } } return storage; } } public class Tester { public static void Main() { EmployeeRecord[] records = new EmployeeRecord[3]; EmployeeRecord r0 = new EmployeeRecord(); r0.EmployeeId = 1; r0.Department = 100; records[0] = r0; EmployeeRecord r1 = new EmployeeRecord(); r1.EmployeeId = 2; r1.Department = 100; records[1] = r1; EmployeeRecord r2 = new EmployeeRecord(); r2.EmployeeId = 3; r2.Department = 101; records[2] = r2; EmployeeData empData = new EmployeeData(records); Collection<int> hits = RecordChecker.FindEmployees(empData, 100); foreach (int i in hits) { Console.WriteLine("found employee {0}", i); } } } }
-
-
属性设计
-
描述实现属性的准则。
通常,方法代表操作而属性代表数据。属性像字段一样使用,这意味着属性不应进行复杂的计算,也不应产生副作用。有关属性设计的更多信息,请参见索引属性设计和属性更改通知事件。
下列准则可帮助确保正确地设计属性。
如果调用方不应当更改属性值,则要创建只读属性。
注意,属性类型的可变性会影响最终用户可以更改的内容。例如,如果定义一个返回读/写集合的只读属性,则最终用户不能向该属性分配其他集合,但可以修改该集合中的元素。
不要提供仅支持 Set 操作的属性。
如果无法提供属性 getter,可以改用一个方法来实现该功能。方法名称应以 Set 开头,并按原样后跟属性名。例如,AppDomain使用一个名为 SetCachePath 的方法,而不是名为 CachePath 的仅支持 Set 操作的属性。
为所有属性提供适当的默认值,确保属性的默认值不会导致安全漏洞或设计效率非常低下。
允许按任意顺序设置属性,即便这样做会导致出现暂时无效的对象状态也如此。
如果属性 setter 引发异常,则保留以前的值。
避免从属性 getter 中引发异常。
属性 getter 应是没有任何前提条件的简单操作。如果 getter 可能会引发异常,请考虑将该属性重新设计为方法。此项建议不适用于索引器。索引可以因参数无效而引发异常。
在属性 setter 中引发异常是有效并可以接受的
-
构造函数设计
-
描述实现构造函数的准则。
构造函数是一类特殊的方法,用于初始化类型和创建类型的实例。类型构造函数用于初始化类型中的静态数据。类型构造函数由公共语言运行库 (CLR) 在创建类型的任何实例之前调用。类型构造函数是 static(在 Visual Basic 中为 Shared)方法,不能带任何参数。实例构造函数用于创建类型的实例。实例构造函数可以带参数,也可以不带参数。不带任何参数的实例构造函数称为默认构造函数。
下列准则描述了创建构造函数的最佳做法。
考虑提供简单的构造函数,最好是默认构造函数。简单构造函数的参数很少,并且所有参数都是基元类型或枚举。
如果所需操作的语义未直接映射到新实例的构造,或者按照构造函数设计准则是不合理的,则考虑使用静态工厂方法而不要使用构造函数。
将构造函数参数用作设置主要属性的快捷方式。
通过使用构造函数设置属性应与直接设置属性相同。下面的代码示例演示一个 EmployeeRecord 类,该类可以通过调用构造函数初始化,也可以通过直接设置属性初始化。EmployeeManagerConstructor 类演示如何使用构造函数初始化 EmployeeRecord 对象。EmployeeManagerProperties 类演示如何使用属性初始化 EmployeeRecord 对象。Tester 类演示了两种方式初始化的对象的状态是相同的。
using System; using System.Collections.ObjectModel; namespace Examples.DesignGuidelines.Constructors { // This class can get its data either by setting // properties or by passing the data to its constructor. public class EmployeeRecord { private int employeeId; private int department; public EmployeeRecord() { } public EmployeeRecord(int id, int department) { this.employeeId = id; this.department = department; } public int Department { get {return department;} set {department = value;} } public int EmployeeId { get {return employeeId;} set {employeeId = value;} } public void DisplayData() { Console.WriteLine("{0} {1}", EmployeeId, Department); } } // This class creates Employee records by passing // argumemnts to the constructor. public class EmployeeManagerConstructor { Collection<EmployeeRecord > employees = new Collection<EmployeeRecord>(); public void AddEmployee(int employeeId, int department) { EmployeeRecord record = new EmployeeRecord(employeeId, department); employees.Add(record); record.DisplayData(); } } // This class creates Employee records by setting properties. public class EmployeeManagerProperties { Collection<EmployeeRecord > employees = new Collection<EmployeeRecord>(); public void AddEmployee(int employeeId, int department) { EmployeeRecord record = new EmployeeRecord(); record.EmployeeId = employeeId; record.Department = department; employees.Add(record); record.DisplayData(); } } public class Tester { // The following method creates objects with the same state // using the two different approaches. public static void Main() { EmployeeManagerConstructor byConstructor = new EmployeeManagerConstructor(); byConstructor.AddEmployee(102, 102); EmployeeManagerProperties byProperties = new EmployeeManagerProperties(); byProperties.AddEmployee(102, 102); } } }
注意,在这些示例中,以及在设计良好的库中,这两种方式都可以创建具有相同状态的对象。开发人员首选哪种方式并不重要。
如果构造函数参数只用于设置一个属性,请务必为构造函数参数和该属性使用相同的名称。这类参数和属性之间的唯一差异应是大小写不同。
前面的示例已对此准则进行了演示。
尽量减少构造函数中的任务。除了获取构造函数参数之外,构造函数不应执行太多操作。任何其他处理都应延迟到必要时再进行。
根据需要,可在实例构造函数中引发异常。
构造函数与其他方法一样,应引发并处理异常。具体地说,构造函数不应捕捉和隐藏它无法处理的任何异常。有关异常的更多信息,请参见异常设计准则。
如果需要公共默认构造函数,请在类中进行显式声明。
如果类支持默认构造函数,则显式定义默认构造函数是最佳做法。尽管某些编译器会自动向类中添加默认构造函数,但显式添加默认构造函数会使代码更易于维护。即使由于您添加了带参数的构造函数,导致编译器停止发出默认构造函数,这样也可确保定义默认构造函数。
避免在结构中使用默认构造函数。
许多编译器(包括 C# 编译器)不支持在结构中使用无参数构造函数。
不要在对象的构造函数中调用对象的虚成员。
无论是否调用了定义派生程度最高的重写的类型的构造函数,调用虚成员都会导致调用派生程度最高的重写。下面的代码示例对此进行了演示。基类构造函数执行时,即使尚未调用派生类的构造函数,也会调用派生类的成员。此示例输出 BadBaseClass 以显示DerivedFromBad 构造函数尚未更新状态字段。
using System; namespace Examples.DesignGuidelines.MemberDesign { public class BadBaseClass { protected string state; public BadBaseClass() { state = "BadBaseClass"; SetState(); } public virtual void SetState() { } } public class DerivedFromBad : BadBaseClass { public DerivedFromBad() { state = "DerivedFromBad "; } public override void SetState() { Console.WriteLine(state); } } public class tester { public static void Main() { DerivedFromBad b = new DerivedFromBad(); } } }
-
事件设计
-
描述实现事件的准则。
事件是操作发生时允许执行特定于应用程序的代码的机制。事件要么在相关联的操作发生前发生(事前事件),要么在操作发生后发生(事后事件)。例如,当用户单击窗口中的按钮时,将引发一个事后事件,以允许执行特定于应用程序的方法。事件处理程序委托会绑定到系统引发事件时要执行的方法。事件处理程序会添加到事件中,以便当事件引发时,事件处理程序能够调用它的方法。事件可以具有特定于事件的数据,例如,按下鼠标事件可以包含有关屏幕光标位置的数据。
事件处理方法的签名与事件处理程序委托的签名是相同的。事件处理程序签名遵循下面的约定:
-
返回类型为 Void。
-
第一个参数命名为 sender,是 Object 类型。它是引发事件的对象。
-
第二个参数命名为 e,是 EventArgs 类型或 EventArgs 的派生类。它是特定于事件的数据。
-
该方法有且仅有两个参数。
有关事件的更多信息,请参见处理和引发事件。
对于事件,要使用术语“引发”,而不要使用“激发”或者“触发”。
使用 System.EventHandler<T>,而不要手动创建用作事件处理程序的新委托。
此准则主要适用于新的功能区域。如果您是在已经使用非泛型事件处理程序的区域中扩展功能,则可以继续使用非泛型事件处理程序,以保持设计一致。
如果您的库针对的是不支持泛型的 .NET Framework 版本,则无法遵循此准则。
考虑使用 System.EventArgs 的派生类作为事件参数,除非您完全确定事件决不会需要向事件处理方法传递任何数据(这种情况下可以直接使用 System.EventArgs 类型)。
如果您定义的事件采用 EventArgs 实例而不是您定义的派生类,则不能够在以后的版本中向该事件添加数据。出于上述原因,建议创建一个空的 EventArgs 派生类。这使您能够在以后的版本中在不引入重大更改的情况下向事件添加数据。
使用受保护的虚方法来引发每个事件。这只适用于未密封类的非静态事件,而不适用于结构、密封类或静态事件。
遵循此准则可使派生类能够通过重写受保护的方法来处理基类事件。受保护的 virtual(在 Visual Basic 中是 Overridable)方法的名称应该是为事件名加上 On 前缀而得到的名称。例如,名为“TimeChanged”的事件的受保护的虚方法被命名为“OnTimeChanged”。
要点 重写受保护的虚方法的派生类无需调用基类实现。即使没有调用基类的实现,基类也必须继续正常工作。
使用一个参数,该参数已类型化为引发事件的受保护方法的事件参数类。该参数应命名为 e。
FontDialog 类提供下面的方法,该方法引发 Apply 事件:
[Visual Basic]
Protected Overridable Sub OnApply( ByVal e As EventArgs )
[C#]
protected virtual void OnApply(EventArgs e);
当引发非静态事件时,不要将 null(在 Visual Basic 中为 Nothing)作为 sender 参数进行传递。
对于静态事件,sender 参数应该为 null(在 Visual Basic 中是 Nothing)。
当引发事件时,不要将 null(在 Visual Basic 中为 Nothing)作为事件数据参数进行传递。
如果没有事件数据,则传递 Empty,而不要传递 null。
事件处理方法中会发生任意代码执行,对此一定要做好准备。
考虑将引发事件的代码放置在 try-catch 块中,以防止由事件处理程序引发的未处理异常所导致的程序终止。
考虑引发最终用户可以取消的事件。这仅适用于事前事件。
如果您正在设计可取消的事件,请使用 CancelEventArgs(而非 EventArgs)作为事件数据对象 e 的基类。
-
-
字段设计
-
描述定义字段的准则。
字段保存对象的关联数据。在大多数情况下,库中的所有非静态字段对开发人员都应是不可见的。下面的准则有助于在库设计中正确使用字段。
不要提供公共的或受保护的实例字段。
公共字段和受保护字段未经版本控制,不受代码访问安全性要求的保护。使用私有字段,并通过属性公开这些私有字段,而不要使用公共可见字段。
对不会更改的常数使用常数字段。
对预定义对象实例使用公共静态只读字段。
不要将可变类型的实例指定给只读字段。
使用可变类型创建的对象可以在创建后进行修改。例如,数组和大多数集合是可变类型,而 Int32、Uri 和 String 是不可变类型。对于保存可变引用类型的字段,只读修饰符可防止字段值被改写,但不能防止可变类型被修改。
下面的代码示例演示使用只读字段会出现的问题。BadDesign 类创建一个只读字段,并使用只读属性公开该字段。这不能防止ShowBadDesign 类修改该只读字段的内容。
using System; namespace Examples.DesignGuidelines.Fields { public class BadDesign { public readonly int[] data = {1,2,3}; public int [] Data { get {return data;} } public void WriteData() { foreach (int i in data) { Console.Write ("{0} ", i); } Console.WriteLine(); } } public class ShowBadeDesign { public static void Main() { BadDesign bad = new BadDesign(); // The following line will write: 1 2 3 bad.WriteData(); int[] badData = bad.Data; for (int i = 0; i< badData.Length; i++) { badData[i] = 0; } // The following line will write: 0 0 0 // because bad's data has been modified. bad.WriteData(); } } }
-
运算符重载
-
描述重载运算符的准则。
运算符重载允许使用“+”、“-”、“=”和“!=”等运算符合并和比较类型。通过在类型中添加运算符重载,开发人员可以像使用内置基元类型一样使用该类型。只有在运算对类型具有很直观的意义(例如,支持表示数值的类型的两个实例相加)的情况下,才应进行运算符重载。不应使用运算符重载为非直观运算提供语法快捷方式。
下面的示例演示 DateTime 类的加法运算的签名。
[Visual Basic]
Public Shared Function op_Addition(ByVal d As DateTime, _ ByVal t As TimeSpan _ ) As DateTime
[C#]
public static DateTime op_Addition( DateTime d, TimeSpan t );
避免定义运算符重载,但在其用法应类似于基元(内置)类型的类型中除外。
考虑在其用法应类似于基元类型的类型中定义运算符重载。
例如,String 定义运算符 == 和 !=。
在表示数字的结构(如 System.Decimal)中定义运算符重载。
在定义运算符重载时,不要偏离直观意义。当重载运算符后运算结果非常直观的情况下才适于进行运算符重载。例如,用一个 System.DateTime 对象减去另一个 System.DateTime 对象得到一个 System.TimeSpan 对象这一操作有直观的意义。但是,使用逻辑 union 运算符联合两个数据库查询或使用 shift 运算符写入流则不合适。
除非至少有一个操作数属于定义重载的类型,否则不要提供运算符重载。
C# 编译器强制执行这一准则。
以对称方式重载运算符。
例如,如果重载相等运算符,也应重载不等运算符。同样,如果重载小于运算符,也应重载大于运算符。
考虑为每个重载运算符所对应的方法提供友好的名称。
必须遵守此项准则才能符合 CLS。下表列出了运算符符号、其相应的替换方法以及运算符名称。
C# 运算符符号 替换方法名称 运算符名称 未定义
ToXxx 或 FromXxx
op_Implicit
未定义
ToXxx 或 FromXxx
op_Explicit
+(二进制)
Add
op_Addition
-(二进制)
Subtract
op_Subtraction
*(二进制)
Multiply
op_Multiply
/
Divide
op_Division
%
Mod
op_Modulus
^
Xor
op_ExclusiveOr
&(二进制)
BitwiseAnd
op_BitwiseAnd
|
BitwiseOr
op_BitwiseOr
&&
And
op_LogicalAnd
||
Or
op_LogicalOr
=
Assign
op_Assign
<<
LeftShift
op_LeftShift
>>
RightShift
op_RightShift
未定义
LeftShift
op_SignedRightShift
未定义
RightShift
op_UnsignedRightShift
==
Equals
op_Equality
>
CompareTo
op_GreaterThan
<
CompareTo
op_LessThan
!=
Equals
op_Inequality
>=
CompareTo
op_GreaterThanOrEqual
<=
CompareTo
op_LessThanOrEqual
*=
Multiply
op_MultiplicationAssignment
-=
Subtract
op_SubtractionAssignment
^=
Xor
op_ExclusiveOrAssignment
<<=
LeftShift
op_LeftShiftAssignment
%=
Mod
op_ModulusAssignment
+=
Add
op_AdditionAssignment
&=
BitwiseAnd
op_BitwiseAndAssignment
|=
BitwiseOr
op_BitwiseOrAssignment
,
Comma
op_Comma
/=
Divide
op_DivisionAssignment
--
Decrement
op_Decrement
++
Increment
op_Increment
-(一元)
Negate
op_UnaryNegation
+(一元)
Plus
op_UnaryPlus
~
OnesComplement
op_OnesComplement
-
转换运算符
-
描述实现转换运算符的准则。
转换运算符可将对象从一种类型转换为另一种类型。转换运算符可以是隐式的也可以是显式的。隐式转换运算符不需要在源代码中指定类型转换即可执行转换。显式转换运算符则要求在源代码中指定类型转换才能执行转换。
下面的签名演示 Point 类的显式转换运算符,该转换运算符用于在 Point 和 Size 之间进行转换。
[Visual Basic]
Public Shared Function op_Explicit( _ ByVal p As Point _ ) As Size
[C#]
public static Size op_Explicit( Point p );
如果最终用户未明确要求此类转换,则不要提供相应的转换运算符。
理想情况下,应存在客户研究数据,以支持定义转换运算符。此外,如果存在一些示例,其中一个或多个类似类型需要此类转换,也可以支持定义转换运算符。
不要在类型域之外定义转换运算符。
例如,Int32、Double 和 Decimal 都是数字类型,而 DateTime 不是数字类型。将 Double 类型转换为 DateTime 类型不应以转换运算符的形式实现。如果要将一种类型转换为不同域中的另一种类型,请使用构造函数。
如果转换可能丢失信息,则不要提供隐式转换运算符。
不要在隐式强制转换中引发异常。
隐式强制转换是由系统调用的;用户可能不会觉察发生了转换,这会给调试代码带来困难。
如果对强制转换运算符的调用导致有损转换,而该运算符的协定不允许有损转换,则会引发 System.InvalidCastException。
-
参数设计
-
描述定义参数的准则。
本主题中的准则帮助您为成员参数选择正确的类型和名称。此外,下列主题还提供了参数设计准则。
使用派生程度最小的参数类型提供成员所需的功能。
下面的代码示例阐释了这一准则。BookInfo 类从 Publication 类继承。Manager 类实现两个方法:BadGetAuthorBiography 和 GoodGetAuthorBiography.。BadGetAuthorBiography 即使只使用 Publication 中声明的成员,也使用对 BookInfo 对象的引用。GoodGetAuthorBiography 方法演示了正确的设计。
// A class with some basic information. public class Publication { string author; DateTime publicationDate; public Publication(string author, DateTime publishDate) { this.author = author; this.publicationDate = publishDate; } public DateTime PublicationDate { get {return publicationDate;} } public string Author { get {return author;} } } // A class that derives from Publication public class BookInfo :Publication { string isbn; public BookInfo(string author, DateTime publishDate, string isbn) : base(author, publishDate) { this.isbn = isbn; } public string Isbn { get {return isbn;} } } public class Manager { // This method does not use the Isbn member // so it doesn't need a specialized reference to Books static string BadGetAuthorBiography(BookInfo book) { string biography = ""; string author = book.Author; // Do work here. return biography; } // This method shows the correct design. static string GoodGetAuthorBiography(Publication item) { string biography = ""; string author = item.Author; // Do work here. return biography; }
不要使用保留的参数。
库的未来版本可以添加采用其他参数的新重载。
下面的代码示例首先演示了违反此项准则的不正确方法,然后演示了采用正确设计的方法。
public void BadStoreTimeDifference (DateTime localDate, TimeZone toWhere, Object reserved) { // Do work here. } public void GoodCStoreTimeDifference (DateTime localDate, TimeZone toWhere) { // Do work here. } public void GoodCStoreTimeDifference (DateTime localDate, TimeZone toWhere, bool useDayLightSavingsTime) { // Do work here. }
不要使用公开显露的采用指针、指针数组或多维数组作为参数的方法。
使用大多数库时都无需了解这些高级功能。
将所有输出参数放在所有按值传递参数和引用传递参数(不包括参数数组)之后,即使这会导致参数在重载间排序不一致也要如此。
这种约定使得方法签名更易于理解。
在重写成员或实现接口成员时,要保持参数命名的一致性。
重写应使用相同的参数名。重载应使用与声明成员相同的参数名。接口实现应使用接口成员签名中定义的相同名称。