You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

C# 14中使用扩展运算符泛型实现值类型相等性的最佳实践问询

C# 14中使用扩展运算符泛型实现值类型相等性的最佳实践问询

最近我在折腾C# 14的新特性,想用扩展成员里的扩展运算符来统一实现值类型的相等性逻辑,省得每个实现IValue<TSelf>的结构体都要重复写==!=。具体写法大概是这样:

先定义一个带静态抽象Equals的接口:

public interface IValue<TSelf> where TSelf : struct, IValue<TSelf>
{
    static abstract bool Equals(TSelf left, TSelf right);
}

然后用扩展块把相等性运算符泛型实现了:

public static class ValueExtensions
{
    extension<T>(T) where T : struct, IValue<T>
    {
        public static bool operator ==(T left, T right) => T.Equals(left, right);
        public static bool operator !=(T left, T right) => !T.Equals(left, right);
    }
}

这样一来,像Distance这种结构体只要实现接口的静态Equals,就能自动用上==!=了:

public readonly struct Distance : IValue<Distance>
{
    private readonly double meters;

    public Distance(double meters) { this.meters = meters; }

    public static bool Equals(Distance left, Distance right) =>
        left.meters == right.meters;

    public override bool Equals(object obj) =>
        obj is Distance other && Equals(this, other);

    // 这里没写==和!=,靠扩展块提供
}

代码跑起来完全没问题,但架不住工具报警啊——Rider和部分Roslyn分析器都会弹CA2231警告,说我没实现相等性运算符,要让它们和Equals行为一致。显然工具还没跟上C#14的新特性,没识别到扩展块里的运算符。

现在我犯难了:

  1. 既然扩展运算符技术上是对的,那直接把这个警告压下去是不是推荐的做法?
  2. 还是说哪怕用了扩展,每个值类型还是得写一层薄的转发运算符,比如:
public static bool operator ==(Distance left, Distance right) => Equals(left, right);
public static bool operator !=(Distance left, Distance right) => !Equals(left, right);

想问问各位大佬:用C#14扩展块泛型实现值类型相等性,这算不算最佳实践?还是说值类型依然得自己显式声明运算符?


我的个人经验和看法

先给结论:两种方案都没问题,核心看你的项目环境和团队情况。

1. 泛型扩展+抑制警告:优雅但依赖工具适配

C#14引入扩展运算符就是为了解决这种“重复实现通用运算符”的问题,你的写法完全符合语言设计意图,运行时也没毛病。如果你的项目已经全量升级到C#14,团队成员都熟悉这个新特性,那统一抑制警告是非常优雅的选择——能省掉大量重复代码,完美符合DRY原则。

抑制警告的方式建议用项目级的全局抑制,比如在GlobalSuppressions.cs里加:

[assembly: SuppressMessage("Usage", "CA2231:Implement the equality operators and make their behavior identical to that of the Equals method", Justification = "Equality operators are provided via C# 14 extension members for IValue<T> types.", Scope = "type", Target = "YourNamespace.IValue`1")]

或者直接在项目的.editorconfig里针对实现IValue<TSelf>的类型禁用这条规则,比在每个结构体上单独加[SuppressMessage]要方便得多。

但缺点也很明显:目前工具链(Roslyn、Rider)还没完全适配C#14的扩展运算符,这个警告会一直存在,直到工具更新。如果团队里有不熟悉C#14的成员,可能会疑惑为什么要平白无故抑制警告,反而增加沟通成本。

2. 薄转发运算符:稳妥但有重复代码

如果你的项目还在过渡阶段,或者团队里有对C#14不熟悉的开发者,那写一层薄的转发运算符会更稳妥。这样工具不会报警告,每个类型的运算符实现也一目了然,新成员一看就懂“哦,这里是把运算符调用直接转成静态Equals了”。

虽然多了几行重复代码,但胜在直观,也不会有工具警告的困扰。而且这些转发代码非常简单,几乎不会引入bug——就是把运算符调用直接转成静态Equals调用,和扩展块里的逻辑完全一致。

3. 额外注意点:别漏了GetHashCode

哦对了,你上面的Distance例子里没实现GetHashCode,这个一定要补上!CA2231之外,还会有CA2234的警告(要求EqualsGetHashCode行为一致)。不管用哪种方案,GetHashCode都得自己实现,因为扩展运算符没法覆盖实例的GetHashCode方法:

public override int GetHashCode() => meters.GetHashCode();

长期和短期的权衡

  • 短期看:如果工具适配还没跟上,薄转发运算符更省心,不用跟警告较劲,团队沟通成本低。
  • 长期看:随着工具链更新(比如Roslyn 4.12+或者Rider后续版本),这些针对扩展运算符的误报肯定会被修复,到时候泛型扩展的方式会成为更优解。

最终建议

  • 若项目是C#14纯环境,团队熟悉新特性:选泛型扩展+全局抑制警告,最大化代码复用。
  • 若项目处于过渡阶段,或团队对C#14不熟悉:选薄转发运算符,避免困惑和警告。

另外,不管选哪种方案,一定要保证Equals方法、GetHashCode和相等性运算符的行为完全一致——你的扩展方案已经通过调用静态Equals保证了这一点,转发方案也一样,这是值类型相等性实现的核心原则。

火山引擎 最新活动