第8章 使用准则
第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 属性,而使用 ICollection ( ICollection<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:如果类不是作为集合使用的,不要实现 集合接口 。集合应该是一个很简单的类型,用于存储元素并对元素进行访问及操控。
自定义集合的命名
自定义集合主要目的有两个:
- 创建新型数据结构,用自定义方法处理数据。如
List<T>、LinkedList<T>、Stack<T>等; - 创建专门的集合,用于存放特别的元素。如
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 后缀。添加此类后缀有如下缺点:
-
它会暴漏类型的 内部细节 ,而不是表达类型的功能和用途;
-
会限制类型的 扩展性 和 灵活性 。如果后续要变更类型的实现方式,类型名也要修改,会有向后兼容性问题;
-
如果它的行为不符合后缀对应的 行为 ,会使调用者困惑和误解。
-
-
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 序列化
以下是本书第二版关于序列化的描述,已过时
以下是第三版书中内容:
-
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 提供了一种强类型、易于使用的 API 来查询和操作 XML 数据。public class ServerConfiguration { // 坏设计 public XmlDocument ConfigurationData { get { ... } } // 好设计 public IXPathNavigable ConfigurationData { get { ... } } }Info
更多内容见第10章 LINQ to XML
-
DO:在接受 XML 或返回 XML 的成员中,以XmlReader、XmlWriter 或XNode 的子类型作为输入或输出。这样不仅可以解除方法与 XML 具体实现的耦合,还可以让方法处理虚拟 XML 数据源(前提是数据源能提供
XNode、XmlReader 或XPathNavigator)。Info
-
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) { ... } }

浙公网安备 33010602011771号