.NET设计规范(四) 成员设计
第4章 成员设计4.1. 成员设计的一般规范4.1.1. 成员重载成员重载是指在同一个类型中创建两个或两个以上的成员,这些成员具有相同的名字,唯一不同的是参数的数量或参数的类型。因为只有方法、构造函数以及索引属性有参数,所以只有这些成员可以被重载。 ü 在一族对参数的数量进行重载的成员中,较长的重载应该用参数名来说明与之对应的较短的重载所使用的默认值。这最适用于布尔型参数。 例如: public class Type { public MethodInfo GetMethod(string name); //ignoreCase=false public MethodInfo GetMethod(string name,Boolean ignoreCase); //用ignoreCase而不用caseSensitive }
? 避免在重载中随意地给参数命名。如果两个重载中的某个参数表示相同的输入,那么该参数的名字应该相同。 例如: public class String { //correct public int IndexOf(string value){…} public int IndexOf(string value,int startIndex){…}
//incorrect public int IndexOf(string value){…} public int IndexOf(string str,int startIndex) }
? 避免使重载成员的参数顺序不一致。在所有的重载中,同名的参数应该出现在相同的位置。 例如: public class EventLog { public EventLog(); public EventLog(string logName); public EventLog(string logName,string machineName); public EventLog(string logName,string machineName,string source) } 只有在一些非常特殊的情况下才能打破这条非常严格的规则。 例如:params数组参数必须是参数列表中的最后一个参数。 参数列表中包含输出参数,这些参数一般出现在参数列表的最后。
ü 如果需要可扩展性,要把最长的重载做成虚函数,较短的重载应该仅仅是调用较长的重载。 public class String { public int IndexOf(string s) { return IndexOf(s,0); } public int IndexOf(string s,int startIndex) { retirm IndexOf(s,startIndex,s.Length) } public virtual int IndexOf(string s,int startIndex,int count) {…} }
? 不要在重载成员中使用 ref 或 out 修饰符。 例如: public class SomeType { public void SomeType(string name){…} public void SomeType(out string name){…} }
ü 如果方法有可选的引用类型参数,要允许它为null,以表示应该使用默认值。 if (geometry == null) DrawGeometry(brush,pen); else DrawGeometry(brush,pen,geometry);
ü 要优先使用成员重载,而不是定义有默认参数的成员,默认参数不符合CLS规范。 //4.0的新特性 public static void Show(string msg = "") { Console.WriteLine("Hello {0}",msg); } 4.1.2. 显式地实现接口成员如果显式实现接口成员,客户代码在调用这些接口成员时,必须把实例强制转换为接口类型。 例如: public struct Int32:IConvertible { int IConvertible.ToInt32(){…} }
客户代码: int i = 0; i.ToInt32(); //编译不通过 ((IConvertible)i).ToInt32(); //编译通过
? 尽量避免显式实现接口成员。
ü 如果希望接口成员只能通过该接口来调用,可考虑显式地实现接口成员。 例如:设计ICollection<T>.IsReadOnly 的主要目的是为了让数据绑定基础设施通过ICollection<T>接口来访问。在使用该接口类型时,几乎不会直接访问该方法。因此List<T>显示实现了该接口成员。
ü 当需要隐藏一个成员,并增加一个名字更合适的等价成员时,可考虑显式实现接口成员。 public class FileStream:IDisposable { IDisposable.Diopose(){Close();} public void Close(){…} }
ü 如果希望让派生类实现功能定制,要为显式实现的接口成员提供一个功能相同的受保护的虚方法。 [Serializable] public class List<T>:ISerializable { void ISerializable.GetObjectData(SerializationInfo info,StreamingContext context) { GetObjectData(info,context) } protected virtual void GetObjectData(SerializationInfo info,StreamingContext context) {…} }
4.1.3. 属性和方法之间的选择方法表示操作,属性表示数据,如果其他各方面都一样,那么应该使用属性而不是方法。
ü 如果成员表示类型的逻辑attribute,考虑使用属性。 例如:Button.Color是属性,因为color是button的一个attribute。
ü 如果属性的值储存在内存中,而提供属性的目的仅仅是访问该值,要使用属性不要使用方法。 public Customer { private string name; public Customer(string name) { this.name = name; } public string Name { get {return this.name;} } }
ü 要在下列情况下使用方法而不要使用属性: ? 操作开销较大。 ? 操作是一个转换操作,如Object.ToString方法。 ? 即使传入的参数不变,操作每次返回的结果都不同,如:Guid.NewGuid方法。 ? 操作返回一个数组。 4.2. 属性的设计ü 如果不应该让调用方改变属性的值,要创建只读属性。
? 不要提供只写属性,也不要让设置方法的存取范围比获取方法更广。 例如:不要把属性的设置方法设置为公有,而把获取方法设为受保护。
ü 要为所有属性提供合理的默认值。
ü 要允许用户以任何顺序来设属性的值。
ü 如果属性的设置方法抛出异常,要保留属性原来的值。
? 避免在属性的获取方法中抛出异常。 4.2.1. 索引属性的设计索引属性通常称为索引器,它的调用语法与数组索引相似。 public class String { public char this[int index] { get {…} } } … string city = “suzhou”; Console.WriteLine(city[0]); ü 考虑通过索引器的方式让用户访问存储在内部数组中的数据。
? 避免有一个以上参数的索引器。
? 避免用System.Int32、System.Int64、System.String、System.Object、枚举或泛型参数之外的类型来做索引器的参数。
? 不要同时提供语义上等价的索引器和方法。
4.2.2. 属性改变的通知事件有时候为用户提供通知事件来告诉他们属性值发生了改变是有用的。例如,System.Windows.Forms.Control在它的text属性值发生改变后会触发TextChange事件。 public class Control : Component { string text = String.Empty; public event EventHandler<EventArgs> TextChanged; public string Text { get { return text; } set { if (text != value) { text = value; OnTextChanged(); } } }
protected virtual void OnTextChanged() { EventHandler<EventArgs> handler = TextChanged; if (handler != null) { handler(this,EventArgs.Empty); } } }
ü 考虑在高层API(通常是设计器组件)的属性值被修改时触发属性改变的通知事件。
ü 考虑在属性值被外界修改时触发通知事件。 4.3. 构造函数的设计有两种类型的构造函数:类型构造函数和实例构造函数。 public class Customer { public Customer {…} //实例构造函数 static Customer {…} //类型构造函数 } 类型构造函数时静态的,CLR会在使用该类型之前运行它。实例构造函数在类型的实例创建时运行。类型构造函数不能带任何参数,实例构造函数则可以。
ü 考虑提供简单的构造函数,最好是默认构造函数。
ü 要把构造函数的参数用作设置主要属性的便捷方法。
ü 如果构造函数参数用于简单的设置属性,要使用相同的名字命名构造函数参数和属性。 public class EventLog { public EventLog(string logName) { this.logName = logName; } public string LogName() { get {…} set {…} } }
ü 要在构造函数中做最少的工作。
ü 要在类中显式地声明公用的默认构造函数。
? 避免在结构中显式地定义默认构造函数。
? 避免在对象的构造函数内部调用虚成员。 例如:它会在Derived的新实例创建时打印出“What’s wrong?”。 public abstract class Base { public Base() { Method(); } public abstract void Method(); } public class Derived : Base { private int value; public Derived() { value = 1; } public override void Method() { if (value == “1”) { Console.WriteLine(“All is good”); } else { Console.WriteLine(“What’s wrong?”); } } }
ü 要把静态构造函数声明为私有。如果静态函数不是私有,那么CLR之外的代码就可以调用它,这可能导致意料之外的行为。
? 不要再静态构造函数中抛出异常。如果抛出异常,就不能在当前应用程序域中使用该类型。
ü 考虑以内联的形式来初始化静态字段,而不要显式地定义构造函数。这是因为运行库能够对那些没有显式定义静态构造函数的类型进行性能优化。 //不性能优化 public class Foo { public static readonly int Value; static Foo() { value = 63; } } //性能优化 public class Foo { public static readonly int Value = 63; } 4.4. 事件的设计事件处理函数决定了事件处理方法的签名。根据约定,方法的返回类型为void,带两个参数。第一个参数表示触发事件的对象,第二个参数表示触发事件的对象希望传给事件处理方法的相关数据。数据通常称为事件参数(event argument)。
ü 要在事件中使用术语“raise”,而不要使用“fire”或“trigger”。
ü 要用System.EventHandler<T>来定义事件处理函数,不要手工创建新的委托。
ü 如果百分之百确信不需要给事件处理方法传递任何参数,这种情况下可直接使用EventArgs,其他情况,考虑用EventArgs的子类来做事件的参数。
ü 要用受保护的虚函数来触发事件。这只适用于非密封类中的非静态事件,不适用于结构、密封类以及静态事件。 public class AlarmClock { public event EventHandler<AlarmRaisedEventArgs> AlarmRaised;
protected virtual void OnAlarmRaised(AlarmRaisedEventArgs e) { EventHandler<AlarmRaisedEventArgs> handler = AlarmRaised; if (handler != null) //消除竞态条件 { handler(this,e); } } } 为每个事件提供一个对应的受保护的虚方法来触发该事件,其目的是为派生类提供一种方法,让他们能够通过覆盖来处理该事件。根据约定,方法的名字应该以“On”开头,随后是事件的名字。
ü 要让触发事件的受保护的方法带一个参数,该参数类型为事件参数类,该参数的名字应该为e。 protected virtual void OnAlarmRaised(AlarmRaisedEventArgs e){…}
? 不要在触发非静态事件时把null作为sender参数传入。
? 不要在触发事件时把null作为数据参数传入。
ü 考虑触发能够被最终用户取消的事件,这只适用于前置事件。 可以用System.ComponentModel.CancelEventArgs或它的子类作为参数,例: void ClosingHandler(object sender,CancelEventArgs e) { e.Cancel = true; }
自定义事件处理函数的设计: ü 要把事件处理函数的返回类型定义为void。
ü 要用object作为事件处理函数的第一个参数类型,并将其命名为sender。
ü 要用System.EventArgs 或其子类作为事件处理函数的第二个参数类型,并将其命名为e。
? 不要在事件处理函数中使用两个以上的参数。
4.5. 字段的设计? 不要提供公有的或受保护的实例字段。 public struct Point { private int x; private int y;
public Point(int x,int y) { this.x = x; this.y = y; }
public int X {return x;} public int Y{return y;} }
ü 要用常量字段来表示永远不会改变的常量。 public struct Int32 { public const int MaxValue = 0x7fffffff; }
ü 要用公有的静态字段来定义预定义的对象实例。 public struct Color { public static readonly Color Red = new Color(0x0000FF); }
? 不要把可变类型的实例赋值给只读字段。 可变类型是那些在实例化后仍能对其实例进行修改的类型。例如:数组、大多数集合以及stream都是可变类型。但system.Int32、System.Uri及System.String都是不可变类型。 public class SomeType { public static readonly int[] Numbers = new int[10]; }
SomeType.Numbers[5] = 10; //改变只读字段的值。 4.6. 操作符重载操作符重载允许框架中的类型看起来像是语言内部的基本类型。 ü 如果类型类似于基本类型,考虑定义操作符重载,否则避免定义操作符重载。
ü 操作符应该对定义它的类型进行操作。 public struct RangeInt32 { public static RangeInt32 operater-(RangeInt32 operand1,RangeInt32 operand2); public static RangeInt32 operater-(int operand1,RangeInt32 operand2); public static RangeInt32 operater-(RangeInt32 operand1,int operand2);
//无法编译 //public static RangeInt32 operater-(int operand1,int operand2); }
ü 要以对称的方式来重载操作符。 如果重载“operator ==” ,那么应该同时重载“operator != ”。
ü 考虑为每个重载过的操作符提供对应的方法,并用容易理解的名字来命名。 许多语言不支持操作符重载,所以建议为那些重载过操作符的类型提供功能上等价的方法。 public struct DateTime { public static TimeSpan operator- (DateTime t1,DateTime t2){…} public static TimeSpan Subtract (DateTime t1,DateTime t2){…} } 4.6.1. 重载见7.7.1节 Object.Equals。 4.6.2. 类型转换操作符类型转换操作符是可以把一种类型转换为另一种类型的一元操作符。类型转换操作符必须在操作数或返回值的类型中定义,必须为静态成员。有两种类型转换操作符:隐式的和显式的。 public struct RangeInt32 { public static implicit operator int(RangeInt32 operand){…} public static explicit operator RangeInt32(int operand){…} }
? 如果没有明确的用户需求,不要提供类型转换操作符。
? 不要定义位于类型的领域之外的类型转换操作符。 例如,Int32、Double都是数值类型,而DateTime不是。
? 如果类型转换可能丢失精度,不要提供隐式类型转换操作符。
? 不要在隐式类型转换中抛出异常。
ü 如果对显示类型操作符的调用会丢失精度,要抛出System.InvalidCastException。 4.7. 参数的设计? 不要使用保留参数。 如果将来成员需要更多的参数,可以增加一个重载成员。 //不好 public void Method(SomeOption option,object reserved);
//更好的做法是,给今后的版本增加参数 public void Method(SomeOption option); public void Method(SomeOption option,string path);
? 不要把指针、指针数组及多维数组作为共有方法的参数。
ü 即使导致重载成员之间参数顺序不一致,也要把所有的输出参数放在所有以值和引用方式传递的参数(不包括参数数组)后面。 public struct DataTime { bool TryParse(string s,out DateTime result); bool TryParse(string s,DateTimeStyles style,out DateTime result); }
ü 要在重载成员或者实现接口成员时保持参数命名的一致。 public interface IComparable<T> { int CompareTo(T other) }
public class Nullable<T> : IComparable<Nullable<T>> { //correct public int CompareTo(Nullable<T> other){…}
//incorrect public int CompareTo(Nullable<T> nullable){…} } 4.7.1. 枚举和布尔参数之间的选择ü 如果参数中有两个或两个以上的布尔类型,要用枚举。 比较下面的方法: FileStream f = File.Open(“foo.txt”,true,false); //不适宜 FileStream f = File.Open(“foo.txt”,CasingOptions.CaseSensitive,FileMode.Open); //适宜
ü 如果参数在下一个版本中,可能需要两个以上的值,不要使用布尔值。
ü 考虑在构造函数中,对确实只有两种状态值的参数以及用来初始化布尔属性的参数使用布尔类型。
4.7.2. 参数的验证ü 要验证传给公有的、受保护的或显式实现的参数。如果验证失败,那么应该抛出System.ArgumentException或其子类。
ü 如果传入的是null或该成员不支持null,要抛出ArgumentNullException。
ü 要验证枚举参数。 public void PickColor(Color color) { if(color > Color.Black || color < Color.White) { throw new ArgumentOutOfRangeException(…); } }
? 不要使用Enum.IsDefine来检验枚举的范围。
4.7.3. 参数的传递? 避免使用输出参数或引用参数。
? 不要以引用方式传递引用类型。 4.7.4. 参数数量可变的成员ü 如果预计用户会传入为数不多的数组元素,考虑给数组参数增加params关键字。
ü 如果调用方几乎总是有现成的数组作为输入,避免使用params关键字。 例如:Socket 传递消息用的字节数组。
? 如果数组会被以其为参数的的成员修改,不要使用params数组参数。
ü 考虑在简单的重载中使用params关键字。 public class Graphics { FillPolygon(Brush brush,params Point[] points){…} FillPolygon(Brush brush,PointF[] points,FillMode fillMode){…} }
ü 要对参数合理排序,以便使用params关键字。
ü 要注意传入的params数组参数可能为null。 4.7.5. 指针参数ü 要为任何以指针为参数的成员提供一个替补成员,这是因为指针不符合CLS规范。 [CLSCompliant(false)] public unsafe int GetBytes(char* chars,int charCount,byte* bytes,int byteCount);
public int GetBytes(char[] chars,int charIndex,byte[] bytes,int byteIndex,int byteCount);
? 避免对指针参数进行更高开销的检查。
ü 要在设计用到指针的成员时遵循与指针相关的常用约定。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |