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

记录实现的接口类型相等性问题及解决方案咨询

记录实现接口后的相等性问题解决方案

问题回顾

定义密封记录类型Person:

public sealed record Person(int Id, string GivenName, string Surname)
{
    // 若干非平凡方法
}

为了测试依赖Person的代码,引入IEntity接口并让Person实现:

public interface IEntity
{
    int Id { get; }
    // 包含Person的所有非平凡方法
}

但在针对IEntity的单元测试中,相等性断言失败:

[Test]
public void Repro()
{
    // arrange
    IEntity lhs = new Person(1, "John", "Doe");
    IEntity rhs = new Person(1, "John", "Doe");
    // act
    var result = lhs == rhs;
    // assert
    result.Should().BeTrue(); // 断言失败
}

原因:Person作为记录会隐式生成基于值的operator==,但IEntity接口无法定义静态运算符。当变量编译时类型为IEntity时,==会默认调用object的引用相等逻辑,而非Person的结构相等逻辑。


可行解决方案

1. 让接口继承IEquatable<IEntity>,使用Equals替代==

修改IEntity接口继承IEquatable<IEntity>,并让Person显式实现该接口的相等逻辑:

public interface IEntity : IEquatable<IEntity>
{
    int Id { get; }
    // 其他方法
}

public sealed record Person(int Id, string GivenName, string Surname) : IEntity
{
    // 非平凡方法

    // 显式实现IEquatable<IEntity>,复用记录自带的相等逻辑
    bool IEquatable<IEntity>.Equals(IEntity? other)
    {
        return other is Person person && this.Equals(person);
    }
}

测试时改用Equals方法或Should库的Be方法(内部调用Equals):

[Test]
public void Repro()
{
    IEntity lhs = new Person(1, "John", "Doe");
    IEntity rhs = new Person(1, "John", "Doe");
    lhs.Should().Be(rhs); // 断言通过
}

2. 为IEntity编写相等性检查扩展方法

创建扩展方法封装相等逻辑,避免直接使用==:

public static class EntityEqualityExtensions
{
    public static bool IsEqualTo(this IEntity lhs, IEntity rhs)
    {
        if (ReferenceEquals(lhs, rhs)) return true;
        if (lhs is null || rhs is null) return false;
        return lhs.Equals(rhs);
    }
}

测试中调用扩展方法:

[Test]
public void Repro()
{
    IEntity lhs = new Person(1, "John", "Doe");
    IEntity rhs = new Person(1, "John", "Doe");
    lhs.IsEqualTo(rhs).Should().BeTrue(); // 断言通过
}

3. 用抽象类替代接口

抽象类可以定义静态运算符,统一相等性逻辑的入口:

public abstract class EntityBase : IEquatable<EntityBase>
{
    public abstract int Id { get; }

    public static bool operator ==(EntityBase? lhs, EntityBase? rhs)
    {
        if (ReferenceEquals(lhs, rhs)) return true;
        if (lhs is null || rhs is null) return false;
        return lhs.Equals(rhs);
    }

    public static bool operator !=(EntityBase? lhs, EntityBase? rhs)
    {
        return !(lhs == rhs);
    }

    public virtual bool Equals(EntityBase? other)
    {
        if (other is null) return false;
        // 基础相等逻辑,子类可重写
        return Id == other.Id;
    }

    public override bool Equals(object? obj)
    {
        return Equals(obj as EntityBase);
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Id);
    }
}

// Person继承抽象类,重写相等逻辑
public sealed record Person(int Id, string GivenName, string Surname) : EntityBase
{
    // 非平凡方法

    public override bool Equals(EntityBase? other)
    {
        return other is Person person && base.Equals(other) 
               && GivenName == person.GivenName 
               && Surname == person.Surname;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(base.GetHashCode(), GivenName, Surname);
    }
}

测试时使用抽象类类型变量,==会调用抽象类定义的运算符:

[Test]
public void Repro()
{
    EntityBase lhs = new Person(1, "John", "Doe");
    EntityBase rhs = new Person(1, "John", "Doe");
    (lhs == rhs).Should().BeTrue(); // 断言通过
}

4. 测试中显式转换为具体类型(不推荐)

如果仅针对特定测试场景,可将IEntity转换为Person后再比较,但会耦合测试与具体实现:

[Test]
public void Repro()
{
    IEntity lhs = new Person(1, "John", "Doe");
    IEntity rhs = new Person(1, "John", "Doe");
    ((Person)lhs == (Person)rhs).Should().BeTrue();
}

总结

这是C#静态运算符绑定机制导致的必然结果,你并未遗漏语言特性。推荐优先使用方案1或方案3,前者保持接口抽象的灵活性,后者通过抽象类统一相等性入口,都能兼顾测试便利性与业务逻辑正确性。

内容的提问来源于stack exchange,提问作者Modern Ronin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 11:05:18