首页 > 其他分享 >第5章 成员设计

第5章 成员设计

时间:2025-01-09 22:44:35浏览次数:1  
标签:... DO int 成员 参数 设计 public 构造函数

第5章 成员设计

1 成员设计的通用规范

1 成员重载

形参要求:

  • DO​:在较长的重载中,应使用 描述 性的参数名来说明较短的重载使用的默认值。

    以如下代码举例,ignoreCase 暗示“短重载大小写敏感”,如果参数名改为 caseSensitive,将不会隐含该含义。

    public class Type {
        public MethodInfo GetMethod(string name);    // ignoreCase = false
        public MethodInfo GetMethod(string name, Boolean ignoreCase);
    }
    

  • DO​​:重载成员的参数名要保持一致,且参数顺序一致。

    // 正确做法
    public int IndexOf(string value) { ... }
    public int IndexOf(string value, int startIndex) { ... }
    
    // 错误做法
    public int IndexOf(string value) { ... }
    public int IndexOf(string str, int startIndex) { ... }
    

    同名参数应该出现在相同的位置。极个别情况可以违反该规则,如 params 修饰的参数数组、out 参数

    public EventLog();
    public EventLog(string logName);
    public EventLog(string logName, string machineName);
    public EventLog(string logName, string machineName, string source);
    

实现要求:

  • DO​:如果需要可扩展,则要将最 的重载设置成虚成员。

    public class String {
        public int IndexOf(string s){
            return IndexOf(s, 0);
        }
        public int IndexOf(string s, int startIndex){
            return IndexOf(s, startIndex, s.Length);
        }
        public virtual int IndexOf(string s, int startIndex, int count){
            // 干活
        }
    }
    

  • DON'T:禁止 通过 ref、out 和 in 修饰符对成员进行重载。

    public class SomeType {
        public void SomeMethod(string name) { ... }
        // 编译不会报错
        public void SomeMethod(out string name) { ... }
    }
    

  • DON'T​​:同一位置的参数不应类型相似,语义不同。

    public void Print(long value, string terminator){ 
        Console.Write("{0}{1}", value, terminator);
    }
    public void Print(int repetitions, string str){
        for(int i = 0; i < repetitions; i++)
            Console.Write(str);
    }
    

    对于一些语言(特别是动态类型语言)无法解析这样的重载。

    不过,如果重载的方法做的事情完全一样,调用哪个都无关紧要,则可以重载:

    public static class Console {
        public void WriteLine(long value) { ... }
        public void WriteLine(int value) { ... }
    }
    

  • DO:允许 在传递参数时将可选参数设为 null。

    这样的目的是避免使用者进行如下的显式检查:

    if (geometry == null)
        DrawGeometry(brush, pen);
    else
        DrawGeometry(brush, pen, geometry);
    

  • DON'T ​:如果一个非泛型方法的重载使用了泛型参数,该方法的其他重载不要 使用特定的类型

    public class PrettyPrinter<T> {
        public void PrettyPrint(string format) { ... }
        public void PrettyPrint(T otherPrinter) { ... }
        public void PrettyPrint(T otherPrinter, string format) { ... }
    }
    
    // 此时,如果用T是string类型,泛型方法PrettyPrint(T)方法将无法被调用
    var printer = new PrettyPrinter<string>();
    ...
    // C#重载认为这是在调用PrettyPrint(string),而非PrettyPrinter<T>
    printer.PrettyPrint("hello world");
    

    此条规则不适用于泛型方法。

默认参数

  • CONSIDER​:建议在一个方法的最长重载中使用默认参数。如果该方法有两个及以上的默认参数,提供一个没有默认参数的简版重载。

    默认参数是较高级的特性,有些开发者不理解默认参数的意义。提供简版重载便于开发者调用。

  • DON'T ​:接口方法和类的虚方法 不要 提供任何默认参数, CancellationToken ​ 参数除外。

    如下代码将输出: 20105 ,符合预期吗?

    public interface IExample {
        void PrintValue(int value = 5);
    }
    
    public class Example : IExample {
        public virtual void PrintValue(int value = 10) {
            Console.WriteLine(value);
        }
    }
    
    public class DerivedExample : Example {
        public override void PrintValue(int value = 20){
            base.PrintValue(value);
        }
    }
    
    void Main() {
        var derived = new DerivedExample();
        Example e = derived;
        IExample ie = derived;
        derived.PrintValue();
        e.PrintValue();
        ie.PrintValue();
    }
    

  • DON'T ​:版本发布后,默认参数的默认值 不可 更改。

    除非发布时该默认值是一个非法的哨兵值,以表明应该使用运行时的默认值。

    // 此处provider是一个非法的哨兵值,后续版本可对它进行更改。
    public static BigInteger Parse(ReadOnlySpan<char> value,
        IFormatProvider provider = null) { ... }
    

  • AVOID​​:同一方法的两个重载,避免同时使用默认参数;

    否则会导致调用时出现争议。

  • DON'T​​:默认参数相同时,必选参数必须不同;

    如果必须参数相同,即方法签名相同,无法通过编译。

  • DO​​:相同方法的不同重载,默认参数的默认值必须相同。

    既然是不同的重载,方法签名必然不同,若使用同一默认参数,则默认值要相同。

  • DO​:在为现有方法添加新的可选参数时,要将所有默认参数都转移至整个新的、更长的重载中。

    同时将旧方法标记为 [EditorBrowsable(EditorBrowsableState.Never)]​。

    // 原方法的签名:
    // public static int TestMethod(string filename, bool ignoreCase = false)
    // 将原方法的ignoreCase的默认值去掉
    [EditorBrowsable(EditorBrowsableState.Never)]
    public static int TestMethod(string filename, bool ignoreCase) { ... }
    
    // 添加新方法,为ignoreCase添加默认值
    public static int TestMethod(string filename, bool ignoreCase = false, bool size = 512) { ... }
    

2 显式地实现接口成员

显式地实现接口方法如下:

public struct Int32 : IConvertible {
    // 显式实现
    int IConvertible.ToInt32() { ... }
    // 隐式实现
    public long ToInt64() { ... }
}

// 调用
int i = 0;
i.ToInt32();    // 无法调用
((IConvertible)i).ToInt32();    // 可以调用

  • AVOID​:避免显式实现接口成员。

    对调用者不友好,它不会出现在公有成员列表中,且在值类型上会导致不必要的 装箱

    一般来说,仅当 接口名和方法名出现了冲突 才会这么做:

    class Collection : IEnumerable {
        IEnumerator IEnumerable.GetEnumerator() { ... }
        public MyEnumerator GetEnumerator() { ... }
    }
    

  • CONSIDER​​​:如果希望接口成员只能通过 接口 调用,应显式实现接口成员。

    主要用于 .Design​​ ​中的类。例如:ICollection<T>.IsReadOnly​​ ​主要用于数据绑定基础类通过 ICollection<T>​​ ​接口访问,该接口的实现类几乎不会访问此方法。因此 List<T>​​ ​显式地实现了该接口成员。

  • CONSIDER​:通过显式地实现接口成员模拟 变体

    例如,IList​ 的实现通过显式实现隐藏了弱类型成员,并增加强类型公有成员,以改变参数和返回值类型:

    public class StringCollection : IList {
        public string this[int index] { ... }
        // 显式实现以隐藏该接口
        object IList.this[int index] { ... }
    }
    

  • CONSIDER​:如果想用名字更合适的成员隐藏接口成员,可以显式地实现接口成员。

    这相当于对成员进行重命名。如 System.IO.FileStream​ 显式地实现了 IDisposable.Dispose​,并将它重命名为 FileStream.Close​。

    // 以下并非 FileStream 的真正实现,仅用于演示
    public class FileStream : IDisposable {
        void IDisposable.Dispose() { Close(); }
        public void Close() { ... }
    }
    

    此方法尽量少用,相比名字不理想,重命名带来的混乱是更大的问题。

  • DON'T​​:不要把接口成员的显式实现当作安全壁垒。

    只要把实例强制转换为 接口 ,任何代码都可以调用该成员。

  • DO​:如果希望子类能覆写父类的显式实现的接口成员,要为显式实现的接口成员提供具有 相同功能的受保护的虚 成员。

    否则子类无法通过 base​ 的方式调用父类实现的接口成员

    public class List<T> : ISerializable {
        ...
        void Iserializable.GetObjectData (SerializationInfo info, StreamingContext context) {
            GetObjectData(info, context);
        }
        protected virtual void GetObjectData (SerializationInfo info, StreamingContext context) {
            ...
        }
    }
    

    接口重新实现的替代方法

    即使是显式实现的成员,接口重新实现还是容易出问题,这是因为:

    • 子类实例无法调用基类的方法

    当然,如下两个解决方案也无法解决此问题

    • 定义基类时不能预测方法是否会重新实现,或无法接受重新实现后的潜在问题

    让子类通过 new​ 关键字隐藏基类方法是最不理想的方式。基类有两种方法可以避免:

    • 当隐式实现成员时, 将其标记为 virtual

    • 当显式实现成员时,如果能够预测子类可能要重写某些逻辑,则使用下面的模式:

      public class TextBox : IUndoable {
      	void IUndoable.Undo()         => Undo();    // Calls method below
      	protected virtual void Undo() => throw new NotSupportedException();
      }
      
      public class RichTextBox : TextBox {
      	protected override void Undo() => Console.WriteLine ("RichTextBox.Undo");
      }
      

    如果你不希望添加任何的子类,则可以 把类标记为 sealed 以制止接口的重新实现。

3 属性和方法之间的选择

  • CONSIDER​​:如果成员表示类型的一种逻辑属性,则应定义为属性成员。

    例如 Button.Color​ 是属性,因为颜色是按钮的一种属性。

  • DO​​:如果属性值存储在进程内存中,且目的仅仅是为了访问该值,应使用属性而非方法。

  • DO​​​:以下情况应使用方法而非属性:

    以下可以分为几类: 耗时间耗内存有副作用是一个动作不可预测

    • 该操作比字段访问慢几个数量级。特别是那些访问网络或文件系统的操作。

    • 该操作是一个转换操作,比如 Object.ToString​ ​方法。

    • 该操作在每次调用时都返回不同的结果,即使传入参数不变。如 Gudi.NewGuid​ ​方法。.NET 中的 DateTime.Now​ ​方法也应该设计成方法,而非属性。

    • 该操作有严重的、显而易见的副作用。

      如 WinForm 控件的 Handle,调试器会为它创建句柄。

    • 该操作返回内部状态的一个副本(不包括返回的值类型副本)。

    • 该操作返回一个数组。

      通常我们会返回内部数组的副本,但这样会导致代码效率低下。如果开发者调用的是方法,他会清晰的意识到该方法会有副作用,反而会主动将方法的返回值保存起来,以便进行操作:

      // 坏设计
      Company data = GetCompanyData("MSFT");
      for (int i = 0; i < data.Employees.Length; i++) {
          if (data.Employees[i].Alias == "kcwalina") {
              ...
          }
      }
      
      // 好设计
      Company data = GetCompanyData("MSFT");
      Employees[] employees = data.GetEmployees();
      for (int i = 0; i < employees.Length; i++) {
          if (employees[i].Alias == "kcwalina") {
              ...
          }
      }
      

      这种情况还可以改用只读的 ReadOnlyCollection<T>​ 或读写受控的 Collection<T>​ 子类:

      public ReadOnlyCollection<Employee> Employees {
          get { return roEmployees;}
      }
      private Employee[] employees;
      private ReadOnlyCollection<Employee> roEmployees;
      

属性的使用有一条简单的原则:属性只用于简单的 计算 或对 数据 简单的访问。

2 属性的设计

  • DO​​:如果调用方不应该改变属性的值,它应该是 只读 属性。

    注意:如果属性的类型是可修改的引用类型,即使属性只读,调用方也能改变属性的值。

  • DON'T​:不要有 只写 属性,或 setter 的可访问性比 getter 更广

    如果不能为属性提供 getter,则该成员应设计为 方法 。如 AppDomain​ 有一个名为 SetCachePath​ 的方法,没有一个叫 CachePath​ 的只写属性。

  • DO​​:要为属性提供合理的默认值,并确保默认值不会导致安全漏洞或低效率代码。

  • DO​​:允许用户以任何顺序设计属性值,即使会导致对象在短时间内处于无效状态。

    属性相互关联很正常,有些属性值相互组合是无效的。此时应推迟抛出异常,在对象真正用到这些关联属性时才应抛出。

  • DO​​:若 setter 抛出异常,要保留 原值

  • AVOID ​: 避免 在 getter 中抛出异常。

    如果一个 getter 会抛出异常,它应该被设计为 方法

    注意:此条规则不适用于 索引器

1 索引属性的设计

索引属性是一种特殊的属性,它可以有参数,它的调用语法与数组索引相似。

public class String {
    public char this[int index] {
        get { ... }
    }
}

string city = "Seattle";
Console.WriteLine(city[0]);

注意:索引器可能会在循环中使用,所以要格外小心,一定要非常简单。

  • CONSIDER​​​:通过索引器的方式让用户访问 存储在内部数组中的 数据。
    string​​​。

  • CONSIDER​​:为代表元素集合的类型提供索引器。
    string​​。

  • AVOID​​:避免使用有 个以上参数的索引属性。

    如果索引器的参数超过 个,它应该被设计为方法。可以考虑以 Get 或 Set 作为方法名。

  • AVOID​​​:应使用如下类型作为索引器的参数:

    1. System.Int32
    2. System.Int64
    3. System.String
    4. System.Object
    5. System.Range
    6. System.Index
    7. 枚举

    如果考虑其他类型做索引器参数,它应该被设计为方法。可以考虑以 Get 或 Set 作为方法名。

  • DO​:要将 Item ​ 名称用于索引属性。

    除非有明显更好的名字(例如 System.String​ 的 Chars​ 属性)。

    在 C# 中,索引器默认的名字是 Item​。IndexerNameAttribute​ 可以用来对这个名字进行定制。

  • DON'T:不要 同时提供语义上等价的索引器和方法。

    以下例子应该把索引器改成方法:

    // 坏设计
    public class Type{
        [System.Runtime.CompilerServices.IndexerNameAttribute("Members")]
        public MemberInfo this[string memberName] { ... }
        public MemberInfo GetMember(string memberName, bool ignoreCase) { ... }
    }
    

  • DO​:要从接收 System.Range​ 的索引器中返回与声明类型 相同 的类型。

    // 正确,从基于范围的索引器中返回声明的类型。
    public partial class SomeCollection {
        public SomeCollection this[Range range] { get { ... } }
    }
    
    // 错误,从基于范围的索引器中返回错误的类型声明。
    public partial class SomeCollection {
        public SomeValue[] this[Range range] { get { ... } }
    }
    
    

2 当属性值发生改变时的通知事件

  • CONSIDER​​:在高层 API(通常是设计器组件)的属性值被修改时,触发通知事件。

    因通知事件有额外开销,且可能会频繁触发,低层 API 不值得触发此类通知事件。

  • CONSIDER​​​:在属性值 被外界修改 时触发通知事件。

    TextBox​ 控件的 Text​ 属性,当用户键入文本时,属性值会自动改变,同时触发事件。

3 构造函数的设计

构造函数有两种:类型构造函数和实例构造函数:

public class Customer {
    // 实例构造函数
    public Customer() { ... }
  
    // 类型构造函数
    static Customer() { ... }
}

  • CONSIDER​​:提供简单的构造函数,最好是默认构造函数。

    构造函数是创建类型实例的最自然的方式。大多数开发人员在开发时会优先尝试使用构造函数,其次才是工厂方法等方式。

  • CONSIDER​​:如果构造函数无法和新实例的语义相对应,或遵循构造函数的设计让人感觉不合理,考虑使用 静态工厂方法 来替代。

  • DO​:应把构造函数的参数列表作为设置主要属性的快捷方式。

    以下三种方法是等价的:

    // 1
    var applicationLog = new EventLog();
    applicationLog.MachineName = "BillingServer";
    applicationLog.Log = "Application";
    
    // 2
    var applicationLog = new EventLog("Application");
    applicationLog.MachineName = "BillingServer";
    
    // 3
    var applicationLog = new EventLog("Application", "BillingServer");
    
    

  • DO​:如果定义构造函数的目的就是为了设置相应的属性,则属性名和参数名应 一致

    名字应仅有 大小写的 区别:

    public class EventLog {
        public EventLog(string logName){
            this.LogName = logName;
        }
        public string LogName {
            get { ... }
            set { ... }
        }
    }
    

  • CONSIDER​:建议为构造函数中的每一个形参添加相应的 属性

    这样有助于调试时 记录日志 等操作。如果该参数是 引用 类型,则要斟酌是否要用属性暴漏。

  • DO​​:在构造函数中做最少的工作。

    构造函数不应该执行太多操作,开销大的操作应该 推迟到具体调用的方法 执行。

  • DO ​​:实例构造函数 可以 抛出异常。

    构造函数抛出异常时,虽然 new 操作不会返回对象的引用,但事实上对象已经创建了。即使构造函数只完成了一半工作,GC 仍会对其进行垃圾回收,调用 Finalize 方法。

  • DO​​:如果默认(无参)构造函数是必须的,应该 在类中显式地声明

    防止后期添加有参构造函数抑制编译器创建无参构造函数,最终导致已有代码无法运行。

  • AVOID ​​​:struct 中 不要 显式定义默认构造函数。

    struct 的有参构造函数不会抑制编译器自动创建无参构造函数。使用编译器生成的无参构造函数创建数组速度更快。

    其实这条原则和之前的 struct 属性设置为只读的相违背了。

  • AVOID​:避免在父类的构造函数中调用 内部虚 成员。

    以下例子便是子类尚未构造完成,父类(虚类)构造函数调用了内部虚成员:

    // 以下代码在创建Derived实例时会打印“未完成构造”。
    public abstract class Base {
        public Base() {
            Method();
        }
        public abstract void Method();
    }
    
    public class Dervied : Base {
        private int value;
        public Dervied() {
            value = 1;
        }
        public override void Method() {
            if(value == 1)
                Console.WriteLine("已完成构造");
            else
                Console.WriteLine("未完成构造");
        }
    }
    

类型构造函数的规范

  • DON'T ​​​: 禁止 在静态构造函数中抛出异常。

    抛出异常意味着禁止在当前的应用程序域中 使用该类型

  • CONSIDER​:以 内联 的方式初始化 static readonly​ ​字段,而不是 显式定义静态构造函数

    runtime 会对 内联 字段进行性能优化。

    注意:static 内联字段的初始化可能发生在 任何时候 (可能会提前,由 CLR 决定),而静态构造函数仅会在 第一个成员被访问 前运行,不会提前。

4 事件的设计

  • DO​​:在事件中使用术语“ raise ”来描述事件的触发,而非“ fire ”、“ trigger ”。

    raise ”这个名字更为中性。

  • DO​:使用 System.EventHandler<T> ​ 来定义事件委托,而非自定义委托。

    public class NotifyingContractCollection : Collection<Contact> {
        public event EventHandler<ContactAddedEventArgs> ContactAdded;
    }
    

  • CONSIDER​:优先用 EventArgs ​的 子类 ,而非 EventArgs​ 做事件参数。

    如果直接使用 EventArgs​ 做事件参数,后期想传递数据势必会破坏 兼容性 。如果使用 子类 ,即使一开始是空的,后续仍可以扩展。

  • DO​:用 受保护 方法触发事件(仅适用可派生类型),触发事件的方法应该带一个 EventArgs​ 类型的参数,参数名称为 e

    这样派生类可以通过覆盖虚方法来处理事件。根据约定,方法的名字应该以“ On ”开头,随后是事件的名字:

    public event EventHandler<AlarmRaisedEventArgs> AlarmRaised;
    protected virtual void OnAlarmRaised(AlarmRaisedEventArgs e) {
        var handler = AlarmRaised;
        if(handler != null) {
            handler(this, e);
        }
    }
    

    本条示例代码中,定义了一个名为 handle​ 的变量,目的是防止 AlarmRaised​ 在判断非空那一刻为 true,下一刻被其他线程设置为了 null

  • DON'T​:非静态事件禁止把 null 作为 sender 参数传入;静态事件必须把 null 作为 sender 参数传入。

    如果不想传递任何数据给事件处理程序,不要传入 null ,应传入 EventArgs.Empty ​。

    EventHandler<EventArgs> handler = ... ;
    if (handler != null) handler(null, ...);
    

  • CONSIDER​: 前置 事件应可以取消 后置 事件的触发。

    可以用 Systme.ComponentModel.CancelEventArgs ​ 或子类作为参数。例如:

    void ClosingHandler(object sender, CancelEventArgs e){
        e.Cancel = true;
    }
    

自定义事件处理程序的设计

  • DO​:事件处理程序的返回类型应为 void ​;用 object ​ 作为第一个参数的类型,并命名为 sender ​;用 System.EventArgs ​ 或其子类作为第二个参数的类型,并命名为 e ​。

    事件处理程序应有且仅有这两个参数。

5 字段的设计

  • DON'T​​​:实例字段不应该为 publicprotected

  • DO​​:永远不会改变的常量应该定义为 常量字段

    public struct Int32 {
        public const int MaxValue = 0x7fffffff;
        public const int MinValue = unchecked((int)0x80000000);
    }
    

  • DO​​:预定义的对象实例,应该定义为类型的 public ​​、 static ​​、 readonly ​​ 字段/属性。

    public struct Color {
        public static readonly Color Red = new Color(0x0000FF);
        public static readonly Color Green = new Color(0x00FF00);
        public static readonly Color Blue = new Color(0xFF0000);
    }
    

    预定义的静态只读字段的初始化会 一次性 完成,因此可能有 性能 问题;而静态只读属性,则可以 按需 构建,结合 JIT 内联调用,性能可以做到和字段相同。当预定义数据量较大,应考虑设置为只读 属性

  • DON'T​​:不要将“浅度不可变”实例当作“深度不可变”实例暴漏给外侧。

    public class SomeType {
        public static readonly int[] Numbers = new int[10]
    }
    ...
    // 虽然数组被声明为readonly,但仍可改变其中的值。
    SomeType.Numbers[5] = 10;
    

6 扩展方法

  • AVOID​​​:不要轻易定义扩展方法,尤其是为别人的类型定义扩展方法。

    大量使用扩展方法可能会 把类型的 API 搞乱

CONSIDER​​:以下场景建议使用扩展方法:

  • 为一个接口的所有实现提供相关的辅助功能,而且这些功能可以通过核心接口来表达。

  • 当实例方法会引入不必要的依赖时,建议使用扩展方法。

    例如 String ​类型不应该对 System.Uri ​有依赖性,因此 String ​中不应该实现 ToUri() ​方法。更好的设计是定义如下扩展方法:

    public static System.Uri Uri.ToUri(this string str)
    
  • 当泛型类型需要设计一个有部分类型限制的方法时,建议使用泛型扩展方法:

    // 该集合类型没有类型限制
    public class SomeCollection<T> { ... }
    
    // 非泛化的扩展类
    public static class SomeCollectionComparisons {
        public static T GetMinValue<T> (this SomeCollection<T> source) where T : IComparable<T> {
            ...
    	}
    }
    

    除了用 where ​进行约束,还可以限制特定泛型实例。如下代码限制仅有 ReadOnlySpan<char> ​可以调用:

    public static bool IsWhiteSpace(this ReadOnlySpan<char> span) { ... }
    

  • DO​​:当扩展方法中的 this​ ​参数为 null​ ​时,要抛出 ArgumentNullException ​ ​异常。

  • CONSIDER​:建议合理命名持有其功能的扩展方法的类型。

    例如,用 Routing ​ ​代替 [ExtendType]Extensions​。​

  • CONSIDER​:如果被扩展的是 接口 ,且该扩展方法常用,扩展方法可以放在同名 namespace 中。除此以外,它应该放在它的特性所表示的 namespace 中,而非同名 namespace 中。
    Uri.ToUri(this string str)​ 应该放在 System.Net ​中,而非 System​(String​ 所在 namespace)中。

  • AVOID​:扩展方法不应重名,即使在不同的 namespace 中。

    重名在调用时会存在二义性。当然,如果我们就是想 让开发者二选一 ,方法同名是可以的。

    如果出现同名,我们可以通过 完全限定的 方式消除二义性:

    // 定义
    namespace A {
        public static class AExtensions {
            public static void ExtensionMethod(this Foo foo) { ... }
        }
    }
    namespace B {
        public static class BExtensions {
            public static void ExtensionMethod(this Foo foo) { ... }
        }
    }
    
    // 调用
    using A;
    ...
    var someObj = ...
    // 调用AExtensions.ExtensionMethod
    someObj.ExtensionMethod();
    // 为消除二义性,通过完全限定的方式调用BExtensions.ExtensionMethod
    B.BExtensions.ExtensionsMethod(someObj);
    

  • DO​:扩展方法应该在与其特性相关的 namespace 中定义。

  • CONSIDER​:当类型是泛型,其扩展方法的泛型参数类型更 严格 时,扩展方法可以和该类型同 namespace。

    public partial struct ReadOnlyMemory<T> { ... }
    
    public static partial class MemoryExtensions {
        public static ReadOnlyMemory<char> Trim(this ReadOnlyMemory<char> memory) { ... }
    }
    

    该准则也适用于泛型限制的扩展方法

    public partial class CollectionDisposer {
        public static void DisposeAll<T>(this List<T> list) where T : IDisposable { ... }
    }
    

7 运算符重载

  • CONSIDER​​​:如果一个类型让人感觉是 基本 类型,可以重载操作符,否则不要重载运算符。

    System.String​​​定义了 operator==​​​和 operator!=​​​

  • DO ​​:表示数值的 struct 应该 重载运算符。

    比如 System.Decimal​​。

  • DON'T​:不要为了耍酷去定义运算符重载。

    不要设计类似于 C++ 的输入、输出流操作符“>>”。

  • DO​​:要对称地重载运算符。

    如果重载了 operator==​​,应该同时重载 operator!= ​​;重载了 operator<​​,也应该重载 operator> ​​。诸如此类。

  • DO​:为每个重载过的操作符提供对应的方法,并用易于理解的名字来命名。

    许多编程语言不支持重载操作符。

image

2 类型转换运算符

  • DON'T​​​​:除非 有明确的用户 需求,否则不要提供类型转换操作符。

  • DON'T​:不要在类型领域外定义转换运算符。

    例如 DateTime​ 中不应该包含 long​ 转换为 DateTime​ 的转换方式,这种情况最好是使用构造函数:

    public struct DateTime {
        public DateTime(long ticks) { ... }
    }
    

隐式转换:

  • DON'T ​​​:如果类型转换会出现精度丢失,则 不要 提供隐式转换。
  • DON'T ​​​:如果转换开销较大,则 不要 提供隐式转换运算符。
  • DON'T ​​​​:隐式转换 不应 抛出异常。

显式转换:

  • DO​​:如果一个强制转换承诺无损,而当前转换有损,要抛出 System.InvalidCastException ​ ​异常。

    public static explicit operator RangedInt32(long value){
        if(value < Int32.MinValue || value > Int32.MaxValue){
            throw new InvalidCastException();
        }
        return new RangedInt32((int)value, Int32.MinValue, Int32.MaxValue);
    }
    

3 比较运算符

  • DO​​​:仅为实现 IComparable<T> ​​​的类型实现比较运算符。

  • DO​​:要保证比较运算符的实现与 IComparable<T> ​​的实现一致。

  • DO​:要为实现 operator<​ 和 IEquatable<T>​ 的类型实现 operator<= ​。

  • DO​​:要为自定义的比较运算符返回 布尔值

  • DO​​:要确保比较运算符与其相应的 数学属性 保持一致。

    • 传递性: 如果 \(a<b\)\(b<c\),则 \(a<c\)
    • 逆转性: 如果 \(a<b\),则 \(b>a\)
    • 非自反性: 如果 \(a<a\) false,则 \(a>a\) 也是 false
    • 非对称性: 如果 \(a<b\) true,则 \(b<a\) false

  • AVOID​​:避免为不同的 类型 定义比较运算符。

    原因和该准则相同。

8 参数的设计

  • DO​:用类层次结构中最接近 类的类型作为参数类型。

    同时该类型应满足成员所需功能。

    例如,假设有一个方法对集合进行遍历并打印,最好选择 IEnumerable<T> ​ 作为参数,而非 List<T>​ 或 IList<T>​:

    public void WriteItemsToConsole(IEnumerable<object> items) {
        foreach (object item in items) {
            Console.WriteLine(item.ToString());
        }
    }
    

  • DON'T ​​​: 禁止 使用保留参数。

    如果将来需要更多参数,通过 重载 的方式实现,而非保留参数。

  • DON'T ​​: 不要 在公开暴漏的方法中使用指针、指针数组、多维数组。
    此类参数难以正确使用,大多情况下都可以重新设计 API 避免此类参数。

  • DO​​:out 类型参数应放在 最后 (不包括参数数组),即使它会造成参数顺序不一致。

    public struct DateTimeOffest {
        public static bool TryParse(string input, out DateTimeOffset result);
        public static bool TryParse(string input, IFormatProvider formatProvider, DateTimesStyles styles, out DateTimeOffset result);
    }
    

  • DO​​:覆写成员、实现接口成员时,参数名应 一致

    // 覆写成员
    public class Object {
        public virtual bool Equals(object obj) { ... }
    }
    public class String {
        // 正确
        public override bool Equals(object obj) { ... }
        // 错误
        public override bool Equals(object value) { ... }
    }
    
    // 实现接口成员
    public interface IComparable<T> {
        int CompareTo(T other);
    }
    
    public class Nullable<T> : IComparable<Nullable<T>> {
        // 正确做法,接口方法中参数名为other
        public int CompareTo(Nullable<T> other) { ... }
        // 错误做法,此处的参数名应该为other
        public int CompareTo(Nullable<T> nullable) { ... }
    }
    

1 枚举和布尔参数之间的选择

  • DO​:若有两个及以上的 bool​ 形参,应该使用 枚举

    以下方法用布尔值做形参就很不直观:

    // 有两个形参用布尔值,不直观
    Stream stream = File.Open("foo.txt", true, false);
    // 使用枚举,直观
    Stream stream = File.Open("foo.txt", CasingOptions.CaseSensitive, FileMode.Open);
    

  • DON'T​:除非 100% 确定 不会有两个以上的选项 ,不要使用 bool​ 形参。

    枚举 有拓展性,而 bool​ 值没有。

  • CONSIDER​:在构造函数中为成员赋值时,对确实只有两种状态的参数,且属性类型是 bool​,使用 bool ​ 形参。

    一个属性,如果常用构造函数来设置,那么构造函数用 枚举 较好;如果常用 setter​ 设置,则构造函数用 bool ​ 较好。

2 参数的验证

  • DO​:对 public​、protected ​或显式实现的成员,传入的参数应该进行验证。如果验证失败,应该抛出 System.ArgumentException ​或其子类。

    public class StringCollection : IList {
        int IList.Add(object item) {
            string str = item as string;
            if (str == null) throw new ArgumentException(...);
            return Add(str);
        }
    }
    

  • DO​:如果不支持 null​,应该抛出 ArgumentNullException ​异常。

  • DO​:验证枚举参数。但不要用 Enum.IsDefined ​ 来检查枚举范围。

    CLR 允许用户把任何整数强制转换为枚举值,即使该值在枚举中未定义,因此要进行检验。Enum.IsDefined​ 通过 反射 检查,开销非常高,不推荐使用。

    public void PickColor(Color color) {
        if (color > Color.Black || color < Color.White) {
            throw new ArgumentOutOfRangeException(...);
        }
    }
    

  • DO​:要意识到引用类型可能会在验证后发生 改变

    如果该成员涉及安全性,最好用它的副本进行验证和处理参数。

3 参数的传递(out 和 ref)

  • AVOID ​: 避免 使用 out 参数或 ref 参数。

    这两种参数涉及指针、值类型和引用类型的区别,有较高的使用难度。如果框架的目标用户很广泛,则不应该期望用户可以正确使用这两种参数。

  • DON'T ​: 不要 通过引用方式(ref)传递引用类型

    仅有几种情况例外,比如进行 交换引用

    public static class Reference {
        public void Swap<T>(ref T obj1, ref T obj2){
            T temp = obj1;
            obj1 = obj2;
            obj2 = temp;
        }
    }
    

4 参数数量可变的成员

  • CONSIDER​:作为参数的数组,若数组元素较 ,可以使用参数数组(params)。如果传入的数组元素 本来已经在一个数组中了 ,不要使用 params。

    如果传入的元素很多,用户一般不会用内联的方式传递这些元素,也就没有必要使用 params。

    比如 byte 数组,它的数据一般很多,且没人会对 byte[]挨个赋值,没有必要使用 params。

  • CONSIDER ​:简单的重载方法 可以 使用 params,即使复杂重载不能用 params。对参数进行合理排序,以便使用 params 关键字。

    以下重载方法,假设第一个很常用,加上 params 对于用户来说会方便很多。

    public class Graphics {
        FillPolygon(Brush brush, params Point[] points) { ... }
        FillPolygon(Brush brush, Point[] points, FillMode fillMode) { ... }
    }
    

  • DON'T​​:如果方法内部要对数组进行 修改 ,不要使用 params 参数数组。

    params 参数数组是 临时 数组,是一个 临时 对象,对于它的任何修改都会丢失。

  • CONSIDER​:如果 API 对性能要求非常高,应该添加 参数数量较少的 重载方法及相应实现。

    因 params 会创建 临时数组 导致性能损失,如果 API 对性能要求非常高,则要单独实现这些方法。实现不能是对 params 方法的简单调用。参数名也添加相应的数字后缀:

    void Format(string formatString, object arg1) { ... }
    void Format(string formatString, object arg1, arg2) { ... }
    void Format(string formatString, params object[] args) { ... }
    

  • DO​:要验证传入的 params 数组是否为 null

    static void Main(){
        Sum(1, 2, 3, 4, 5);
        Sum(null);
    }
    static int Sum(params int[] values){
        if (values == null) throw new ArgumentNullException(...);
        ...
    }
    

5 指针参数

  • DO ​: 为指针参数的成员提供替补成员。
    指针不符合 CLS 规范。

    [CLSCompliant(false)]
    public unsafe int GetBytes(char* chars, int charCount, byte* bytes, int byteCount);
    
    public int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex, int byteCount)
    

  • AVOID ​: 不要 对指针参数进行高开销的检查。

    如果 API 对性能的要求都到了必须使用指针的地步,检查参数造成的性能损失将不可接受。

  • DO​:指针成员的设计要遵循指针的常用约定。

    例如,在参数中传递起始索引是不必要的,我们可以通过简单的指针运算得到相同的结果:

    // 坏实现
    public unsafe int GetBytes(char* chars, int charIndex, int charCount, byte* bytes, int byteIndex, int byteCount);
    // 好实现
    public unsafe int GetBytes(char* chars, int charCount, byte* bytes, int byteCount);
    
    // 调用举例
    GetBytes(chars + charIndex, charCount, bytes + byteIndex, byteCount);
    

标签:...,DO,int,成员,参数,设计,public,构造函数
From: https://www.cnblogs.com/hihaojie/p/18663024/chapter-5-member-design-ntpe7

相关文章

  • 【开源】基于SpringBoot框架公司日常考勤系统(计算机毕业设计)+万字毕业论文 T134
    系统合集跳转源码获取链接一、系统环境运行环境:最好是javajdk1.8,我们在这个平台上运行的。其他版本理论上也可以。IDE环境:Eclipse,Myeclipse,IDEA或者SpringToolSuite都可以tomcat环境:Tomcat7.x,8.x,9.x版本均可操作系统环境:WindowsXP/7/8//8.1/10/11或者L......
  • JSP考试系统的设计与实现qih9c(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、项目背景与意义随着信息技术的飞速发展,教育领域正经历着深刻的变革。传统的考试方式已难以满足现代教育的需求,因此,设计并实现一套高效、智能的......
  • JSP考勤系统设计与实现4jhkn(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、项目背景与意义随着企业规模的扩大和管理的精细化,传统的考勤方式已难以满足现代企业的需求。因此,设计并实现一套高效、智能的考勤系统显得尤为......
  • 设计模式之简单工厂模式
    设计模式之简单工厂模式(SimpleFactoryPattern)模式定义简单工厂模式又叫静态工厂方法模式,属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。类图代码enumProd......
  • 设计模式之工厂方法模式
    设计模式之工厂方法模式(FactoryMethodPattern)模式定义工厂方法模式(FactoryMethodPattern)又称为工厂模式,也叫虚拟构造器(VirtualConstructor)模式或者多态工厂(PolymorphicFactory)模式,它属于类创建型模式。在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工......
  • 设计模式之抽象工厂模式
    设计模式之抽象工厂模式(AbstractFactoryPattern)模式定义抽象工厂模式(AbstractFactoryPattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象创建型模式。类图代码//抽象产品类AclassAbstractProductA{pub......
  • 设计模式之观察者模式
    设计模式之观察者模式(ObserverPattern)模式定义观察者模式(ObserverPattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Sourc......
  • flask框架硕士研究生院校报考推荐系统的设计与实现毕设源码+论文
    校园二手货物交易平台m1a2o本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、选题背景关于硕士研究生院校报考推荐系统的研究,现有研究主要集中在研究生教育的宏观层面,如招生政策、教育质量评估......
  • java超市管理系统的设计与实现论文+源码 2025毕设
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容一、研究背景随着社会经济的发展,超市行业规模不断扩大,传统的管理方式已难以满足日益增长的业务需求。在过去,超市管理多依赖人力,面临着诸多挑战。例如,数据管理......
  • 多智能体平台是一种专门设计用于开发、定制和部署AI Agents的平台或工具集
    多智能体平台是一种专门设计用于开发、定制和部署AIAgents的平台或工具集。以下是一些主流的多智能体平台,以及它们的优劣分析、生态情况、开源情况和是否支持离线部署与中文的相关信息:1.AutoGen介绍:由微软研发,专注于代码生成和软件开发,分为用户智能体和助手智能体,形成......