[C# Essentials] Discriminated Union

Tags

override members of ‘object’

ToString()
  • 设计规范
    • 更多用于面向开发者的诊断信息
    • 不用”” 代替 null
    • 尽可能返回可以标识对象的信息
    • 不要在ToString里造成副作用
 
int GetHashCode()
  • 重写Equals一定要重写GetHashCode()
  • 将类作为哈希表集合的key时也应重写
  • 重写原则
    • a.Equals(b) ⇒ a.GetHashCode() = b.GetHashCode()
    • 在对象的生存期内,其HashCode不应该改变。可以在某个地方缓存对象的HashCode
    • 该方法不应引发异常,必须成功返回一个值
    • 尽量唯一、在int范围内平均分布、开销小
  • HashCode.Combine()方法可用于结合几个特征字段来生成符合要求的哈希码
    • 2 ~ 8个参数
public static int Combine<T1, T2>(T1 value1, T2 value2); public static int Combine<T1, T2, T3>(T1 value1, T2 value2, T3 value3); public static int Combine<T1, T2, T3, T4>(T1 value1, T2 value2, T3 value3, T4 value4); // 其他重载...
 
Equals(object obj)
  • 要重写Equals也需要同时重写GetHashCode(), == 和 !=
  • 具体原则
    • 先判断null
    • 判断类型是否相同(GetType() == obj.GetType()
      • sealed类直接用is匹配就行了,因为不存在继承问题,没必要使用GetType()
    • 调用类型特定的Equals(DataType obj)方法
    • 判断引用相等 (ReferenceEqual())
    • 若HashCode的计算代价很小(例如cached)或不随属性改变,则比较HashCode
    • 若基类重写了Equals方法则调用
 
关于哈希表集合 ( Dictionary<TKey, TValue>, HashSet<T> ) 中对对象的寻址方法:
  1. 调用对象的GetHashCode获得HashCode
  1. 对HashCode & 0x7FFFFFFF(取绝对值)模 集合大小,得到桶的位置
  1. 处理哈希冲突,无论采用链地址法(.NET Framework早期)还是开放寻址法(.NET Core / .NET 5+的优化做法),「桶」都是可遍历的。按照这个遍历顺序遍历每个元素,调用对象的Equals方法进行「精准匹配」
 

operator override

public sealed class C public static bool operator == (C leftHandSide, C rightHandSide) { // ... // 不要在这里使用C类型的==,判断null可以用is } public static C operator + (C source, OtherType destination) { // ... // 实现二元操作符后,自动实现复合赋值操作符(如+=) } // 转换操作符 public static implicit operator double(C c) { // 向double类型的转换 // explicit则是显式转换 }
 

类型封装

对一个类型添加访问修饰符以控制其可访问范围
可用的是internal和public,默认为internal
💡
成员的可访问性无法大于所属类的
即internal class的public member无法被其他程序集访问
 
 

垃圾回收

.NET的垃圾回收
mark-and-compact算法:
  • 一次GC周期开始时,识别对象的所有根引用
    • 根引用:来自static变量、CPU寄存器、局部变量或参数实例、弱引用对象的引用
  • 基于根引用列表,遍历每个根引用所标识的树形结构,递归确定所有根引用指向的对象。识别出所有的可达对象 (mark)
 
GC时,用可达对象覆盖不可达对象所占用的内存(compact,如同磁盘碎片清理)
垃圾回收期运行期间,进程的所有托管线程会暂停,造成短暂卡顿(一般不明显)
可用System.GC.Collect()手动GC,在需要执行一些 不希望卡顿的 不太消耗内存的 方法前可以调用
越近产生的垃圾会被优先(更快的频率)回收
 

弱引用

System.WeakReference<T> 可以用于创建「不阻止GC对其进行回收的对象」
优点就是不会一直占用内存,在内存压力较大时可以先给它回收了;缺点是如果被回收了之后需要再次重建,存在一定的性能开销
 
public static class ByteArrayDataSource { static private byte[] LoadData() { byte[] data = new byte[1000]; // load it from somewhere return data; } static private WeakReference<Byte[]> Data { get; set; } static public byte[] GetData() { byte[]? target; if (Data is null) { target = LoadData(); // 注:一定别直接用方法返回值作为构造函数的参数, // 有可能还没赋给Data就被回收了= = Data = new WeakReference<byte[]>(target); return target; } } else if (Data.TryGetTarget(out target)) { return target; } else { // 弱引用被GC了,重新创建 target = LoadData(); Data.SetTarget(target); return target; } }
 
常用API
 
构造函数:WeakReference<T>(T target)
  • 作用:创建一个弱引用,指向指定的目标对象。
 
bool TryGetTarget(out T target)
  • 作用:尝试获取弱引用指向的目标对象。如果对象未被垃圾回收,则返回true并将对象赋值给target;否则返回false
 
void SetTarget(T target)
  • 作用:设置弱引用指向一个新的目标对象。
 
bool IsAlive
  • 作用:获取弱引用指向的对象是否仍然存活(未被垃圾回收)。
  • 注意:即使IsAlivetrue,在调用TryGetTarget时对象仍可能被回收(存在竞态条件)。
 

不确定性终结

~ClassName(){...}
由垃圾回收器在自动运行(不能显式调用)
正常来说在最后一次使用后,程序结束前调用一次,但也有可能一次都不调用
用于清理和内存无关的资源,如删除文件什么的
💡
由于Finalizer在自己的线程中执行,不确定性终结的未处理异常很难诊断,故一定要防止Finalizer中产生的异常逃逸
 

确定性终结

继承IDisposable 接口,重写Dispose() 方法,这样可以被显式调用
建议在末尾执行System.GC.suppressFinalize() ,从终结(f-reachable)队列中移除这个类
这个f-reachable队列的对象处于一个准备被终结并GC的状态,由专门的一个线程根据执行上下文,挑选合适实践进行处理
这会造成托管资源的GC时间推迟

using