第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 序列化
以下是本书第二版关于序列化的描述,已过时
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