You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何创建具有类似字符串相等语义的.NET不可变类型?

自定义驻留式不可变类型实现方案

完全可以实现这种类型,不需要依赖字符串的编译/运行时特殊机制,同时能通过合理设计避免过高内存占用。下面是具体的实现思路和代码示例:

核心设计原则

要满足需求,核心要抓两点:

  • 类型本身必须不可变:保证实例创建后字段无法修改,这样共享引用才不会出现数据不一致问题
  • 线程安全的实例缓存:用缓存存储已创建的唯一值实例,相同值复用引用;同时缓存的设计要兼顾内存效率

具体实现步骤

1. 定义不可变类型

先把类型的字段设为只读,构造器私有化,只通过静态工厂方法对外暴露创建入口,确保外部无法直接修改实例状态:

public sealed class ImmutableUser
{
    // 只读字段,确保不可变
    public int Id { get; }
    public string Name { get; }

    // 私有构造器,禁止外部直接实例化
    private ImmutableUser(int id, string name)
    {
        Id = id;
        Name = name ?? throw new ArgumentNullException(nameof(name));
    }
}

2. 实现驻留缓存逻辑

用ConcurrentDictionary作为缓存容器(天然线程安全,适合多线程场景),键用ValueTuple来存储字段组合——它默认实现了基于字段的哈希和相等判断,不用自己写额外的键类型,内存开销也小。

加上静态工厂方法Create,负责从缓存获取或创建实例:

// 静态缓存:键是字段组合,值是实例引用
private static readonly ConcurrentDictionary<(int Id, string Name), ImmutableUser> _instanceCache = new();

public static ImmutableUser Create(int id, string name)
{
    var valueKey = (id, name);
    // GetOrAdd原子操作:缓存中有则返回,没有则创建并加入缓存
    return _instanceCache.GetOrAdd(valueKey, key => new ImmutableUser(key.Id, key.Name));
}

这样,只要传入的Id和Name相同,就会返回同一个实例,实现了类似string.Intern的驻留效果。

3. 实现相等比较逻辑

按照要求,先判断引用相等,再回退到值语义。实现IEquatable<ImmutableUser>接口,重写Equals和GetHashCode,同时重载==和!=运算符,和string的相等逻辑对齐:

public bool Equals(ImmutableUser other)
{
    if (ReferenceEquals(null, other)) return false;
    // 先判断引用是否相同,相同直接返回true
    if (ReferenceEquals(this, other)) return true;
    // 引用不同时,再比较字段值
    return Id == other.Id && string.Equals(Name, other.Name);
}

public override bool Equals(object obj)
{
    // 先判断引用,再判断类型并调用强类型Equals
    return ReferenceEquals(this, obj) || (obj is ImmutableUser other && Equals(other));
}

public override int GetHashCode()
{
    // 和缓存键的哈希逻辑保持一致,避免哈希冲突
    return (Id, Name).GetHashCode();
}

// 重载运算符,符合开发者的使用习惯
public static bool operator ==(ImmutableUser left, ImmutableUser right)
{
    return Equals(left, right);
}

public static bool operator !=(ImmutableUser left, ImmutableUser right)
{
    return !Equals(left, right);
}

内存优化:避免缓存泄漏

如果你的场景中会创建大量实例,且很多实例后续会被废弃,直接用强引用缓存可能导致内存无法回收。这时可以把缓存值改成WeakReference<ImmutableUser>,让GC能回收不再被使用的实例:

private static readonly ConcurrentDictionary<(int Id, string Name), WeakReference<ImmutableUser>> _weakInstanceCache = new();

public static ImmutableUser Create(int id, string name)
{
    var valueKey = (id, name);
    // 先尝试从弱引用缓存中获取存活的实例
    if (_weakInstanceCache.TryGetValue(valueKey, out var weakRef) && weakRef.TryGetTarget(out var existingInstance))
    {
        return existingInstance;
    }

    // 缓存中没有存活实例,创建新实例并更新缓存
    var newInstance = new ImmutableUser(id, name);
    _weakInstanceCache.AddOrUpdate(valueKey, _ => new WeakReference<ImmutableUser>(newInstance), (_, _) => new WeakReference<ImmutableUser>(newInstance));
    return newInstance;
}

这种方式既保留了驻留的优势,又不会因为缓存占用过多内存,适合实例生命周期波动大的场景。

为什么不依赖字符串的特殊机制?

字符串的驻留是CLR层面的特殊处理(比如编译时常量自动驻留、string.Intern的全局池),但上面的方案完全用普通.NET类型和集合实现,所有逻辑都是可控的,没有依赖任何CLR对字符串的专属魔法。

内容的提问来源于stack exchange,提问作者Ivan Petrov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 21:35:01