第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(){…}
}
ü 如果希望让派生类实现功能定制,要为显式实现的接口成员提供一个功能相同的受保护的虚方法。
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();