记录实现的接口类型相等性问题及解决方案咨询
记录实现接口后的相等性问题解决方案
问题回顾
定义密封记录类型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
相关产品推荐
相关产品推荐

