.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是属性,因为colorbutton的一个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.Int32System.Int64System.StringSystem.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.Int32System.UriSystem.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){…}

}

û 如果没有明确的用户需求,不要提供类型转换操作符。

û 不要定义位于类型的领域之外的类型转换操作符。

例如,Int32Double都是数值类型,而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);

û 避免对指针参数进行更高开销的检查。

ü 要在设计用到指针的成员时遵循与指针相关的常用约定。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结