首页 > 其他分享 >第8章 使用准则

第8章 使用准则

时间:2025-01-13 23:23:08浏览次数:1  
标签:DO ... 准则 Equals 类型 使用 集合 public

第8章 使用准则

8.1 数组

  • DO​​​:公共 API 中 优先 使用集合, 避免 使用数组。

    public class Order {
        public Collection<OrderItem> Items { get { ... } }
        ...
    }
    

  • DON'T ​: 不要 使用 readonly 数组, 没有意义

    如果不想让用户修改数组的内容,可以使用只读集合或在返回数组前复制数组。

    // 只读集合
    public static ReadOnlyCollection<char> GetInvalidPathChars() {
        return Array.AsReadOnly(badChars);
    }
    // 复制数组
    public static char[] GetInvalidPathChars() {
        return (char[])badChars.Clone();
    }
    

  • AVOID:避免 使用数组类型的属性。

    鉴于数组的不一致性,建议使用基于集合的属性、ReadOnlyMemory<T>​ 或方法。

    基于数组的属性 get​ 方法可以通过 4 种基本方式实现:

    • 直接返回

      直接返回原数组。如 ArraySegment<T>.Array​​

    • 直接不可靠返回

      将数组加工后再返回,外部修改对内部不影响,但对外部的其他用户有影响。如 NameValueCollection.AllKeys​​​:

      public string[] AllKeys {
          get {
              if (_allKeys == null) {
                  _allKeys = new string[_enteries.Count];
                  for (int i = ; i < _enteries.Count; i++) {
                      _allKeys[i] = _enteries[i].Key;
                  }
              }
              return _allKeys;
          }
      }
      
    • 浅拷贝

      如果数组中保存的是值类型,修改元素对原数组无影响;如果是引用类型,修改元素内部数据则有影响。无论哪种类型,替换元素对原数组无影响。如 DataTable.PrimaryKey​​​。

    • 深拷贝

      数组、元素都进行了拷贝。这种方式 .NET 中很少见,少数执行了深拷贝的属性,本质上是在 get 中使用 String.Substring 带来的副作用。

      private List<Calendar> _calendars;
      public Calendar[] DeepCalendars {
          get {
              Calendar[] calendars = new Calendar[_calendars.Count];
              for (int i = ; i < _calendar.Count; i++) {
                  calendars[i] = (Calendar)_calendars[i].Clone();
              }
              return calendars;
          }
      }
      

    当数组元素的类型完全不可变时,浅拷贝可以和深拷贝一样有效地保护对象状态,且开销更低。NumberFormatInfo.NumberGroupSizes​ 使用了“值拷贝”这一深拷贝/浅拷贝混合技术:

    private int[] _numberGroupSizes;
    public int[] NumberGroupSizes {
        get => (int[])_numberGroupSizes.Clone();
    }
    

  • CONSIDER​:优先使用 交错 数组 (jagged array) ,而非 多维 数组。

    交错数组更节省空间,且 CLR 进行了优化,部分情况性能更好。

    // 不规则数组
    int[][] jaggedArray = {
        new int[] { 1, 2, 3, 4 },
        new int[] { 5, 6, 7 },
        new int[] { 8 },
        new int[] { 9 }
    }
    // 多维数组
    int [,] multiDimArray = {
        { 1, 2, 3, 4 },
        { 5, 6, 7, 0 },
        { 8, 0, 0, 0 },
        { 9, 0, 0, 0 }
    };
    

8.2 特性 Attribute

  • DO​​:自定义特性类命名后缀应为“ Attribute ”,且用 AttributeUsageAttribute ​​修饰。

    [AttributeUsage(...)]
    public class ObsoleteAttribute : Attribute { ... }
    

  • DO​​: 必选参数 的属性仅能通过构造函数赋值,且形参名与之对应,只读; 可选参数 的属性则为可读写。

    [AttributeUsage(...)]
    public class NameAttribute : Attribute {
        // 必选参数通过构造函数赋值,且属性设为只读。
        public NameAttribute(string userName) { ... }
        public string UserName { get { .. } }
        // 可选参数可访问性设为读写。
        public int Age { get { .. } set { .. } }
    }
    

  • AVOID​:构造函数不应该设置 可选参数 ,也不应该 重载

    构造函数的参数必须是必选参数,重载意味着某个参数既是必选,又是可选。

    可选参数仅能通过 setter 设置,必选参数仅能通过构造函数设置。

  • DO ​:自定义 Attribute ​类 应被 密封。

    这样的 Attribute​ ​查找更快。

    public sealed class NameAttribute : Attribute { ... }
    

8.3 集合

集合(collection),指实现了 IEnumerable ​ **==或 ** ​IEnumerable<T> ==的类型。

  • DON'T ​:公共 API 中 不要 使用非泛型集合。

    • // 坏设计
      public class ComponentDesigner {
          public IList Components { get { ... } }
          ...
      }
      // 好设计
      public class ComponentDesigner {
          public Collection<Component> Components { get { ... } }
          ...
      }
      

  • DON'T ​:公共 API 中 不要 使用 ArrayList​、List<T>​、Hashtable​、Dictionary<TKey, TValue>​。

    这些类型的设计目的是为了用于内部实现,而非公共 API。上述类型牺牲了 API 的清晰性和灵活性,且有过多的不需要的成员。用户还可以随意修改获取到的数据进而破坏数据的完整性。
    应使用 Collection<T> ​​、 IEnumerable<T> ​​、 IList<T> ​​、 IReadOnlyList<T> ​​、 IDictionary ​​、 IDictionary<TKey, TValue> ​​或实现了它们的自定义类型。

  • DON'T​:IEnumerator<T>​、IEnumerator​ 只能作为 GetEnumerator ​方法的返回类型。

  • DON'T ​:同一个类型 不应该 同时实现 IEnumerator​(IEnumerator<T>​)和 IEnumerable​(IEnumerable<T>​)。

    类型要么是集合器(IEnumerable​),要么是枚举器(IEnumerator​),不可能两者都是。

我们遵循的原则可以总结为一句话:形参类型越 越好,返回值类型越 越好。 当然,out 形参(越 越好)和接口、抽象类的返回值(越 越好)除外。

8.3.1 集合形参

  • DO​:用继承层次中最靠近 基类 的类型做形参类型。大多数以集合为参数的成员都使用 IEnumerable<T> ​ 接口。

  • AVOID​:不要仅仅为了访问 Count​ 属性,而使用 ICollectionICollection<T> 作为形参类型。

    ICollection​(ICollection<T>​)中还有其他方法,仅仅为了访问 Count​ 而使用大材小用了。更好的做法是使用 IEnumerator​(IEnumerator<T>​),并动态检查是否实现了 ICollection​(ICollection<T>​)

    public List<T>(IEnumerable<T> collection) {
        if (collection is ICollection<T> collection2){
            this.Capacity = collection2.Count;
        }
    
        foreach (T item in collection) {
            Add(item);
        }
    }
    

8.3.2 集合属性(get;set)与返回值

  • DO​:集合属性应该 只读

    防止用户修改该属性的引用。

    // 坏设计
    public class Order{
        public Collection<OrderItem> Items { get { ... } set { ... } }
        ...
    }
    // 好设计
    public class Order{
        public Collection<OrderItem> Items { get { ... } }
        ...
    }
    

  • DO​​:

    • 如果只需要向前迭代(foreach​​​),则应直接使用 IEnumerable<T> ​。

    • 如果属性或返回值表示的集合需要进行读写操作,应使用 Collection<T> ​ ​或其子类;

      如果 Collection<T>​ 不符合要求(如要求不能实现 IList​),可以通过实现 IEnumerable<T>​、ICollection<T>​ 或 IList<T>​ 来自定义。

    • 如果属性或返回值表示的集合只需读操作,应使用 ReadOnlyCollection<T> ​ ​或其子类;

      如果 ReadOnlyCollection<T>​ ​不符合要求(如要求不能实现 IList​),可以通过实现 IEnumerable<T>​、ICollection<T>​ ​或 IList<T>​ ​来自定义,且在实现 ICollection<T>.IsReadonly​ ​时让其返回 true​。

      public class SessionCollection : IList<Session> {
          bool ICollection<Session>.IsReadOnly { get { return true; } }
          ...
      }
      

  • CONSIDER​:属性和返回值优先使用 Collection<T> ​和 ReadOnlyCollection<T> ​的 派生类

    这样可以有更好的命名,还可以添加基类中没有的辅助成员、添加辅助方法,甚至后期变更集合的实现。

  • CONSIDER​:建议为非常常用的方法和属性返回 Collection<T>​ 或 ReadOnlyCollection<T>​ 的 子类

    方便未来添加新方法或改变集合的实现。

  • CONSIDER​:如果集合中存储的元素都有 独一无二的键值(ID 等) ,考虑使用有键集合(keyed collection)。

    有键集合通常派生自 KeyedCollection<TKey, TItem>​。

  • DON'T​:属性和返回值类型为集合的成员,当集合为空时,返回 空集合或空数组 ,而不是 null

    • 无论什么返回值,foreach 遍历时都要能正常运行:

      IEnumerable<string> list = GetList();
      foreach (string name in list) {
          ...
      }
      

快照集合(Snapshot Collection)与实况集合(Live Collection)

快照集合:某个时间点对当前集合进行拷贝或复制得到的集合,它不会随这数据的变化而更新。

实况集合:原集合,会实时变化的集合。

  • DON'T​:属性应返回 实况 集合,而非 快照 集合。

    • 属性的 getter 应该是非常轻量的操作,返回快照集需要为当前状态创造副本(以构成快照),该操作的时间复杂度为 O(n)。

  • DO​:不稳定的集合应使用实况的 IEnumerable<T> ​(或它的子类)或 快照 集合或来表示。

    • 对于实况集合,最好的方式是通过向前枚举器来实时反映集合状态。

8.3.3 数组与集合之间的选择

  • DO​:非底层 API 优先使用 集合 ;底层 API 优先使用 数组

    集合对内容有更好的控制,且能随时间演化,易于使用。此外,复制数组代价较高,不应该将数组用于只读。

    如果开发的是底层 API,为了追求性能和节省内存,则应使用数组。

  • DO​​​:无论底层、非底层,byte​​​都 应该 使用数组,而 集合。

    • // 不好的设计
      public Collection<byte> ReadBytes() { ... }
      // 好的设计
      public byte[] ReadBytes() { ... }
      

  • DON'T​​​:如果每次调用 属性的 getter 都必须返回一个新数组(如内部数组的副本),则该 属性 不应该为数组类型。

    每次都返回数组副本会造成如下的低效代码:

    // 不好的设计,此处会频繁访问customer的Orders属性,每次访问都会创建一个数组的副本,效率低下。
    for (int index = 0; index < customer.Orders.Length; index++) {
        Console.WriteLine(customer.Orders[i]);
    }
    

8.3.4 自定义集合的实现

  • CONSIDER​:建议在设计新的集合时,继承 Collection<T> ​、 ReadOnlyCollection<T> ​或 KeyedCollection<TKey, TValue> ​,而不要继承 非泛型集合的基类 (比如 CollectionBase )。

    public class OrderCollection : Collection<Order> {
        protected override void InsertItem(int index, Order item) { ... }
    }
    

  • DO​:自定义集合,如果不继承上述基类,则应实现 IEnumerable<T> ​。如果有需要,还可以考虑实现 ICollection<T> ​,甚至 IList<T> ​。

    虽然我们不继承上述基类,但实现尽量遵守 Collection<T> ​和 ReadOnlyCollection<T> ​建立好的契约。
    自定义集合用起来要像 Collection<T>​、ReadOnlyCollection<T> ​一样,显式的实现同样的成员、用相同的名字来命名参数,等等。

  • CONSIDER​:如果需要作为非泛型集合传递参数,自定义集合应该实现 非泛型 接口( IList ​ 和 ICollection ​)。

  • AVOID​:如果类不是作为集合使用的,不要实现 集合接口

    集合应该是一个很简单的类型,用于存储元素并对元素进行访问及操控。

自定义集合的命名

自定义集合主要目的有两个:

  1. 创建新型数据结构,用自定义方法处理数据。如 List<T>​、LinkedList<T>​、Stack<T> ​等;
  2. 创建专门的集合,用于存放特别的元素。如 StringCollection​。
  • DO​​: 新型数据结构 ,要使用合适的数据结构名进行命名。

    新型数据结构,目的已经不是收集数据,脱离了集合,因此不应该使用 Collection 后缀。

    public class LinkedList<T> : IEnumerable<T>, ... { ... }
    public class Stack<T> : ICollection<customer> { ... }
    

  • DO​:专门的集合,如果实现了 IDictionary​ 或 IDictionary<TKey, TValue>​ 接口,则命名要添加“ Dictionary ”后缀。

  • DO​:专门的集合,如果实现了 IEnumearble​ 接口(或它的子类),则命名要添加“ Collection ”后缀。

    public class OrderCollection : IEnumerable<Order> { ... }
    public class CustomerCollection : ICollection<Customer> { ... }
    public class AddressCollection : IList<Address> { ... }
    

  • AVOID​:集合的抽象类,命名时不应该添加 具体实现的 后缀。如 LinkedList ​ 后缀或 Hashtable ​ 后缀。

    添加此类后缀有如下缺点:

    1. 它会暴漏类型的 内部细节 ,而不是表达类型的功能和用途;

    2. 会限制类型的 扩展性灵活性 。如果后续要变更类型的实现方式,类型名也要修改,会有向后兼容性问题;

    3. 如果它的行为不符合后缀对应的 行为 ,会使调用者困惑和误解。

  • CONSIDER​:可以用元素的 类型名 作为集合名字的前缀。

    • 例如,存储 Address ​元素的集合(即实现了 IEnumerable<Address> ​接口的集合),应命名为 AddressCollection ​。
      如果元素是接口类型,则 I ​前缀可以省略。例如,实现了 IEnumerable<IDisposable> ​接口的集合可以命名为 DisposableCollection

  • CONSIDER​:自定义只读集合应添加 “ReadOnly” 前缀。

    如:ReadOnlyStringCollection​。

    避免未来可能添加相应的读写集合,或者当前框架已有该可读写集合。

8.4 DateTime 和 DateTimeOffset

DateTimeOffset ​是 DateTime ​的 UTC 版本,它以 UTC 时间为准,记录时间。

  • DO​:表达精确的时间点,应使用 DateTimeOffset ​。

    • 例如计算现在的时间、事务开始的时间、文件修改的时间、记录事件的时间等等。如果不知道时区,就用 UTC。

  • DON'T​:如果可以使用 DateTimeOffset ​,不要用 DateTime ​+ DateTimeKind ​ ​表示 UTC 时间。

    • DateTimeKind ​是 DateTime ​的枚举,用于标注当前时间是 UTC 时间、本地时间、无时区时间。

  • DO​:要在绝对时间点不适用时使用 DateTime ​。

    这类情况可能来自遗留数据源。

  • DO​:要使用时间段为 00:00:00 的 DateTime ​(而非 DateTimeOffset ​)表示完整日期。

    如出生日期。

  • DO​:用 TimeSpan ​ 表示没有日期的时间。

8.5 ICloneable

public interface ICloneable {
    object Clone();
}
  • DON'T:不要 实现 ICloneable​、 不要 在公开 API 中使用 ICloneable​。

    • 复制分为 深复制浅复制ICloneable​​的契约未指明采用哪种复制,因此不同的类型会有不同的实现,调用者也无从得知获得的对象是否为 深复制 。有鉴于此,我们建议 不要 实现 ICloneable​​。

  • CONSIDER​​​:如果类型需要克隆机制,则一定要在文档中说明该方法执行的是 深复制 还是 浅复制

8.6 IComparable<T>​ 与 IEquatable<T>

Info

除了这两个接口,还有 IEqualityComparer​,它用于自定义散列码(Hash Code)规则,见7.7.1 IEqualityComparer 和 EqualityComparer

  • IComparable<T>​ 比较的是 顺序(小于、等于、大于) ,用于 排序
  • IEquatable<T>​ 比较的是 相等性 ,用于 查找

  • DO​:值类型需要实现 IEquatable<T> ​。实现 IEquatable<T>.Equals() ​ 应遵循 Object.Equals​ 规范,且 语义Object.Equals​ 完全相同。

    值类型的 Object.Equals​ 方法会导致装箱,且默认使用反射,效率不高。自定义 IEquatable<T> ​ 可以提供更好的性能,且避免装箱。

    public struct PositiveInt32 : IEquatable<PositiveInt32> {
        public bool Equals(PositiveInt32 other) { ... }
        public override bool Equals(objcet object) {
            if(!obj is PositiveInt32) return false;
            return Equals((PositiveInt32)obj);
        }
    }
    

  • CONSIDER​:实现 IEquatable<T>​ 的同时重载 operator== ​ 和 operator!= ​。

  • DO​:如果实现了 IComparable<T>​,则 IEquatable<T> ​也要实现。

    注意:并非所有类型都支持排序,因此本条规范反过来并不成立。

  • CONSIDER​:在实现 IComparable<T>​ 的同时重载 比较 操作符( <、>、<=、> =)。

8.8 Nullable

Nullable<T> ​用于表示可空值类型:

Nullable<int> x = null;
Nullable<int> y = 5;
Console.WriteLine(x == null);    //打印true。
Console.WriteLine(y == null);    //打印false。

此外,C#对 Nullable<T> ​提供了特殊的语法支持:

int? x = null;    // Nullable<T>的别名。
long? d = x       // 调用了转换操作,将Int32转为Int64。
Console.WriteLine(d??10);  // 输出10。

其中,int?​​为提升运算符(lifted operator);??​​和 ??=​​为合并运算符(coalescing operator)。

  • CONSIDER​:用 Nullable<T>​ 来表示那些 可能不存在 的值,此外,非必要不要使用 Nullable<T>​。

    例如:从数据库中得到的一组数据中包含可空数据,此时应该使用 Nullable<T>​。

  • VOID​:不要滥用 Nullable<bool>​ 表示三种状态。如果想表达三种状态的值,应使用 枚举

  • VOID​:优先使用 Nullable<T> ​,而非 System.DBNull​。

    不过,System.DBNull​ ​支持一些额外功能,如 null 传播。如果你用得着这些额外功能,那还是继续用 System.DBNull​ ​吧。

8.9 Object

8.9.1 Object.Equals

  • DO​​:覆盖 Object.Equals​​方法时,要遵守它的契约:

    • 自反性

      x.Equals(x)​ 应该为 true ​。

    • 对称性

      x.Equals(y)​ 的返回值与 y.Equals(x)相同

    • 传递性

      x.Equals(y)​、y.Equals(z)​ 为 true​,则 x.Equals(z)​ 也应该为 true

    • 一致性

      只要 x 和 y 未被修改,x.Equals(y)​ 的返回值 都应该相同

    • 非空性

      x.Equals(null)​ 应返回 false ​。

  • DO​:覆盖 Equals​ 方法的同时要覆盖 GetHashCode ​ 方法。

  • CONSIDER​:覆盖 Object.Equals​ ​方法的同时实现 IEquatable<T> ​ ​接口。

    这么做有两个好处:

    • 性能:对于值类型,使用 IEquatable<T>​ ​可以避免装箱,有更好的性能。
    • 类型安全:Object.Equal​ 可以接受任何类型,而 IEquatable<T>​ 限制了类型,在编译阶段便能发现异常。

  • DON'T ​:Equals​ 方法 不应该 抛出异常。

1.值类型的 Equals

  • DO:必须 覆盖 Equals​ 方法。

    • Object.Equals​ 的默认实现使用了 反射 ,其性能 通常无法接受

  • DO​:通过实现 IEquatable<T> ​ ​来提供一个以该值类型本身作为参数的 Equals​ ​重载方法。

    避免装箱。

2.引用类型 Equals

  • CONSIDER​:如果引用类型表示的是一个值,考虑覆盖 Equals()​ 方法以提供值相等语义。

    例如:表示数值、数学实体的引用类型。

  • DON’T​:对于 可变引用 类型,不要实现值相等语义。

    • 对于 可变引用 类型,内部值发生变化(此时 HashCode 也会变化),引用却不会随之变化,HashCode 的变化会导致它在散列表中丢失

8.9.2 Object.GetHashCode

  • DO​:如果覆盖了 Object.Equals ​ ​方法,则必须覆盖 GetHashCode​ ​方法。如果 Object.Equals ​ ​返回 true ​,它们的 GetHashCode​ ​返回的 HashCode 也要相同。

    Hash 表使用哈希值作为索引存储元素,用 Contains​ ​查询是否包含该元素时,先用 HashCode 查找该对象,再用 Equals​ ​比较是否相等。只有两项都满足了,Contains​ ​才会返回 true​。
    不遵守该规范的类型,在 Hash 表中将无法得到正确结果。

GetHashCode 设计准则

  • DO​:要竭尽所能使 GetHashCode ​产生的 HashCode 随机分布

    这样可以避免散列表冲突,从而提高性能。我们可以使用内置的 HashCode 类创建哈希值(.NET Framework 不支持)

    public partical struct Size {
        public override readonly int GetHashCode() =>
          HashCode.Combine(Width, Height);
    }
    

  • DO​:确保 无论怎么更改 对象,GetHashCode​ 都会返回完全相同的值。

    HashCode 常用作哈希表的标识,如果发生变更,哈希表可能无法找到该对象。这样的缺陷很难发现。

    Suggest

    从这条准则出发,我们应该对值类型重载,因为值类型在插入哈希表时插入的是副本;对于可变引用类型(即非 readonly),应使用默认的 GetHashCode,即通过引用计算 HashCode,这样可以随意修改成员值而不改变 HashCode。

  • AVOID ​: 不要GetHashCode​ 中抛出异常。

8.9.3 Object.ToString

  • DO​​:覆盖 ToString​​方法,且返回内容尽可能简洁。

    ToString​ 的出发点是用来显示和调试,其默认实现返回了对象的类型名。重写方法的返回值应遵循如下原则:

    • 有用
    • 易于 阅读
    • 简短,方便 调试器 显示;
    • 不可返回 空字符串null
    • 不可 抛出异常;
    • 调用时 不会 产生副作用。
    • 如果获得许可,可以用于报告 安全性 相关信息。

  • DO​:与区域性(culture)相关的内容,应提供重载方法 ToString(string format) ​ ​或实现 IFormattable ​ ​接口,用于 ToString​ ​返回信息本地化、格式化。

    例如:DateTime​ ​既提供了重载方法,又实现了 IFormattable​ ​接口。

  • CONSIDER​​:建议使 ToString ​输出的内容可以作为该类型的 输入

    例如,DateTime.ToString​ ​的返回值可以被 DateTime.Parse​ ​解析。

8.10 序列化

以下是本书第二版关于序列化的描述,已过时

8.10 序列化(第二版)

以下是第三版书中内容:

  • AVOID ​: 避免 在通用库中的公开类型上使用序列化特性或接口。

    需要支持跨 AppDomain 的序列化类型除外,比如 Exception​ ​类型。

    Tips

    通用库中的通用类型尹工专注于编程环境中的功能和可用性,将使用何种序列化技术的决策权交给开发者。

    通过特性标注具体的序列化方式,对于通用的框架来说也会引入不必要的程序集,也限制了可使用的序列化方式。

    对于支持跨 AppDomain 的类型,应使用 [Serializable] ​ 特性进行标注。

  • DO​​​​:在创建和变更可序列化的类型时,考虑 前向兼容和后向 兼容。

  • DO​:在实现 ISerializable​ 时,要使 (SerializationInfo info, StreamingContext context)​ 序列化构造函数为 protected 。(对于密封类,则为 private

    [Serializable]
    public class Person : ISerializable {
        protected Person(SerializationInfo info, StreamingContext context) { ... }
    }
    

  • DO​:要 式实现 ISerializable​ 接口。

    [Serializable]
    public class Person : ISerializable {
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) {
            ...
        }
    }
    

  • DO​​​​​​:对 ISerializable.GetObjectData ​的实现,要应用 链接要求 ,确保只有完全受信任的程序集和运行时序列化器能够访问该成员。

    [Serializable]
    public class Person : ISerializable {
        [SecurityPermission(
        SecurityAction.LinkDemand, 
        Flags = SecurityPermissionFlag.SerializationFormatter)]
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) {
            ...
        }
    }
    

8.11 Uri

URI:统一的资源标识符。

  • DO​:使用 System.Uri​ 表示 URI​、URL​ 数据,并对常用的 System.Uri​ 成员提供 string​ 重载。

    该准则适用于参数、属性、返回值类型。

    另外,不要对所有成员进行重载,仅对常用成员重载即可。

    public class Navigator {
        public Navigator(Uri initialLocation);
        public Uri CurrentLocation { get; }
        public void NavigateTo(Uri location);
        // 常用成员,进行重载
        public void NavigateTo(string location) {
            NavigateTo(new Uri(location));
        }
        // 非常用成员,无需重载
        public void NavigateTo(Uri location, NavigationMode mode);
    }
    

8.11System.Uri 的实现规范

  • DO​​:如果既有 string​ 类型成员,又有 Uri​ 类型成员,优先使用 Uri ​。

  • DON'T ​: 不要 用字符串存储 URI​/URL​ 数据。

    如果数据以 string​ 形式输入,则应转化为 Uri​ 再存储。

    public class SomeResource {
        Uri location;
        public SomeResource(string location) {
            this.location = new Uri(location);
        }
        public SomeResource(Uri location) {
            this.location = location;
        }
    }
    

8.12 System.Xml 的使用

System.Xml 中有许多类型用于表示 XML 数据,本节讨论它们的使用规范。

  • DO​:尽量使用 IXPathNavigable​、XmlReader​、XmlWriter ​或 XNode ​的子类型表示 XML 数据,而非使用 XmlNode ​或 XmlDocument​。

    XmlNode​ 和 XmlDocument​ 是.NET Framework 早期的 XML 处理类,它们提供了一种在内存中表示 XML 文档的方式。由于这些类通常需要将整个 XML 文档加载到内存中,这可能会导致内存使用效率低下,并且可能会导致性能问题,因此它们并不适合用于公开的 API。

    相比之下,IXPathNavigable​、XmlReader​、XmlWriter ​和 XNode ​的子类型(如 XElement ​和 XDocument​)提供了一种更高效和灵活的方式来处理 XML 数据。例如,XmlReader ​和 XmlWriter ​类提供了一种基于流的方法来读取和写入 XML 数据,这意味着它们可以处理大型 XML 文档,而不需要将整个文档加载到内存中。而且,LINQ to XML(即 XNode ​的子类型)提供了一种强类型、易于使用的 API 来查询和操作 XML 数据。

    public class ServerConfiguration {
        // 坏设计
        public XmlDocument ConfigurationData { get { ... } }
        // 好设计
        public IXPathNavigable ConfigurationData { get { ... } }
    }
    

  • DO​:在接受 XML 或返回 XML 的成员中,以 XmlReader​、XmlWriter​ 或 XNode​ 的子类型作为输入或输出。

    这样不仅可以解除方法与 XML 具体实现的耦合,还可以让方法处理虚拟 XML 数据源(前提是数据源能提供 XNode​、XmlReader​ ​或 XPathNavigator​)。

    Info

    另见11.3.2 混合使用 XmlReader/XmlWriter 和 X-DOM

  • DON'T​​:除非为了表示 XML 视图,否则不要从 XmlDocument ​ 派生子类。

8.13 相等性操作符

operator== ​和 operator!=

  • DO​: 类型,如果相等性是有意义的,应重载该操作符。

    大多数编程语言没有为值类型的 operator==​ 提供默认实现。

  • AVOID​​: 引用 类型,应使用默认实现,不要重载。

    引用类型默认使用引用相等,如果把它改为值相等,会使调用者产生预期不相符的情况。

  • DO ​: 同时重载这两个操作符(operator==​ 和 operator!=​),且与 Object.Equals ​ 语义相同、性能相近, 且不要 抛出异常。

    也就是说,重载了这两个操作符,也要重载 Object.Equals ​ 方法。

  • DO​:对于定义了 operator==​ 的任何类型,要实现 IEquatable<T> ​ 接口,且行为一致。

  • DO​:operator==​ 的实现要遵循 自反 性、 传递 性。

    public partical struct SomeType {
        public static bool operator== (SomeType left, int right) { ... }
        public static bool operator== (int left, SomeType right) { ... }
        public static bool operator== (SomeType left, SomeType right) { ... }
    }
    

  • AVOID ​: 避免 使用不同类型的参数来定义 operator==​。

    比如,在比较 DateTime​ 和 DateTimeOffset​ 时,使用不同参数定义 operator==​ 需要实现 6 个方法,如果使用类型转换,3 个方法就够了(==、!=、Equals):

    public partial struct DateTimeOffset {
        public static bool operator== (DateTimeOffset left, 
            DateTime right) => left.Equals(right);
    
        public static bool operator== (DateTime left, 
            DateTimeOffset right) => right.Equals(left);
    
        public static bool operator!= (DateTimeOffset left, 
            DateTime right) => !left.Equals(right);
    
        public static bool operator!= (DateTime left, 
            DateTimeOffset right) => !right.Equals(left);
    
        public bool Equals(DateTime other) { ... }
    }
    
    // DateTime的扩展方法
    public static partial class DateTimeOffsetComparisons {
        public static bool Equals(this DateTime left, DateTimeOffset right) => right.Equals(left);
    }
    

    实现隐式转换:

    public partial struct DateTimeOffset {
        public static implicit operator DateTimeOffset(DateTime value) { ... }
    }
    

标签:DO,...,准则,Equals,类型,使用,集合,public
From: https://www.cnblogs.com/hihaojie/p/18669604/chapter-8-use-guidelines-z1wnwa5

相关文章

  • 使用GTD工作法提升效率
    前言近年来随着工作、副业的开展,每天要做的事情越来越多,而且还积攒了很多工作,每天大脑被各种事情充斥着,乱糟糟的,不仅效率很低,还很容易导致焦虑。为此我一直有在寻找合适的项目管理工具,也看了一些相关的书籍,不过很多方法都复杂且难以快速实践。直到我上网冲浪的时候看到「GTD工......
  • 使用Selenium进行网页自动化测试
    在使用Selenium进行网页自动化测试时,获取网络请求数据(即network数据)并不直接由Selenium库提供。Selenium主要用于与网页内容进行交互(如点击、输入文本、获取页面元素等),但它本身不拦截或记录网络请求。然而,你可以结合Selenium与其他工具(如BrowserMobProxy、Wireshark、mitm......
  • 使用OpenAI API进行文本生成的实践指南
    在AI技术日新月异的发展中,文本生成已经成为一项重要应用。通过使用OpenAI的API,开发者可以轻松地实现复杂的文本生成任务。在本文中,我们将深入探讨如何使用OpenAIAPI进行文本生成,从技术背景、核心原理到实际代码实现,并结合应用场景提供实践建议。技术背景介绍文本生成是自......
  • JAVA SE 方法的使用
    ......
  • 《Java核心技术II》网络使用telnet
    使用telnettelnet是一种用于网络编程的非常强大的调试工具,可以在命令shell中输入telnet来启动它。注释:在Windows中需要激活它,控制面板->程序->打开/关闭Windows特性->Telnet客户端。连接当日时间服务连接到当日时间服务,由美国国家标准与技术研究所运维,提......
  • 数据分析-使用Excel透视图/表分析禅道数据
    背景禅道,是目前国内用得比较多的研发项目管理系统,我们常常会用它进行需求管理,缺陷跟踪,甚至软件全流程的管理,如果能将平台上的数据结公司的实际情况进行合理的分析利用,相信会给我们的项目复盘总结带来比较高的价值。结果预览在写这篇文章时,突然想到可能会有人问:禅道已提......
  • 使用 Upstash 构建无服务器向量数据库与缓存系统的最佳实践
    技术背景介绍在构建现代智能应用时,开发者经常需要处理大量数据,包括向量嵌入、缓存管理和消息存储。然而传统数据库和内存存储的部署和运维往往带来复杂性。为了简化这些挑战,Upstash提供了基于HTTP的无服务器数据库和消息队列,其中包含两大关键产品:UpstashVector——......
  • 时间序列预测模型和 随机森林预测模型原理和使用
    让我们一起走向未来......
  • 使用嗅探大师(sniff master)进行手机端iOS抓包的配置步骤
    之前做网页端开发的时候找到一个抓包工具,嗅探大师,当时用来在Windows上面进行抓包,发现他在手机端iOS方面的抓包更为强大,而且还有HTTPS暴力抓包,无需设置代理,无需越狱,无需root,上手比市面上的一些抓包工具操作更简单。这篇文章先讲我使用普通的HTTPS代理抓包。如何使用嗅探大师(sniff......
  • 【2025最新】GPT镜像站盘点,最清晰的使用指南和对比体验,一个神奇的镜像站
    1.快速导航原生中转型镜像站点点此前往立即Chat——liji.chat2.两者对比官网立即Chat访问难度需要魔法直接访问支付手段国际支付国内支付封禁策略检测节点,随时封禁不会封禁价格每月140元订阅费用+每年70元虚拟卡一天仅需4.88,一月低至48.88随用性需要用时必须开月度订......