为什么 Object.Equals() 在从不同的程序集实例化相同的匿名类型时会返回 false?

Why does Object.Equals() return false for identical anonymous types when they're instantiated from different assemblies?

提问人:Dylan Beattie 提问时间:8/31/2016 更新时间:8/31/2016 访问量:1273

问:

我有一些代码将强类型业务对象映射到匿名类型,然后将其序列化为 JSON 并通过 API 公开。

在将我的解决方案重组为单独的项目后,我的一些测试开始失败。我做了一些挖掘,结果发现,在由不同程序集的代码返回的匿名类型上,行为不同 - 我不确定为什么,或者我可以做些什么来解决它。Object.Equals

https://github.com/dylanbeattie/AnonymousTypeEquality 处有完整的重现代码,但实际损坏的部分在下面。此代码位于 Tests 项目中:

[TestFixture]
public class Tests {
    [Test]
    public void BothInline() {
        var a = new { name = "test", value = 123 };
        var b = new { name = "test", value = 123 };
        Assert.That(Object.Equals(a,b)); // passes
    }

    [Test]
    public void FromLocalMethod() {
        var a = new { name = "test", value = 123 };
        var b = MakeObject("test", 123);
        Assert.That(Object.Equals(a, b)); // passes
    }

    [Test]
    public void FromOtherNamespace() {
        var a = new { name = "test", value = 123 };
        var b = OtherNamespaceClass.MakeObject("test", 123);
        Assert.That(Object.Equals(a, b)); // passes
    }


    [Test]
    public void FromOtherClass() {
        var a = new { name = "test", value = 123 };
        var b = OtherClass.MakeObject("test", 123);

        /* This is the test that fails, and I cannot work out why */
        Assert.That(Object.Equals(a, b));
    }

    private object MakeObject(string name, int value) {
        return new { name, value };
    }
}

然后,解决方案中有一个单独的类库,仅包含以下内容:

namespace OtherClasses {
  public static class OtherClass {
    public static object MakeObject(string name, int value) {
      return new { name, value };
    }
  }  
}

根据 MSDN 的说法,“只有当它们的所有属性都相等时,同一匿名类型的两个实例才相等。(我的强调) - 那么,出于比较目的,是什么控制两个实例是否属于相同的匿名类型?我的两个实例具有相等的哈希代码,并且似乎都是 - 但我猜匿名类型的相等必须考虑完全限定的类型名称,因此将代码移动到不同的程序集中可能会破坏事情。有人得到关于如何实现的确切来源/链接吗?<>f__AnonymousType0`2[System.String,System.Int32]

C# .NET 相等 匿名类型

评论


答:

9赞 Luaan 8/31/2016 #1

匿名类型本质上是有作用域的。您的示例打破了该范围,因此类型不同。在当前的 C# 编译器中,匿名类型不能超越程序集(或更准确地说是模块)。即使来自两个不同程序集的两个匿名类型具有相同的属性,它们也是两种不同的类型(它们是 ,因此请注意安全隐患)。当你将匿名类型降级为 时,你就知道你做错了。internalobject

TL;DR:你在滥用匿名类型。不要惊讶它会咬你。

评论

0赞 Dylan Beattie 8/31/2016
我偶尔被咬是没有问题的......确切地知道你做了什么来激怒咬人真是太好了。:)
1赞 Luaan 8/31/2016
@DylanBeattie 将匿名类型转换为 。永远不要那样做。要么保留泛型类型(就像 LINQ 一样),要么确保使用正确的类型将其保留在本地。其他一切都是禁止的。object
2赞 mcintyre321 8/31/2016 #2

匿名类型被编译为它们所在的程序集中的隐藏类型,如果定义匹配,则出于效率目的,将重用该类型。这意味着不同程序集中的类似 AT 将具有不同的类型,并且它们的 .Equals 将执行类型检查。

这是我最喜欢的与匿名类型有关的事情之一:

void Main()
{
    var json = "{ \"name\": \"Dylan\"}";
    var x = Deserialize(json, new { name = null as string});
    Console.WriteLine(x.name);
}

T Deserialize<T>(string json, T template)
{
    return (T) JsonConvert.DeserializeObject(json, typeof(T));
}

将 Deserialize 方法放在另一个程序集中会很有趣......

6赞 Michael Liu 8/31/2016 #3

如果使用 Reflector 等工具反汇编程序集,则会看到匿名类型由每个程序集中的一个类表示,如下所示(在解构编译器生成的标识符后):

internal sealed class AnonymousType<TName, TValue>
{
    private readonly TName _name;
    private readonly TValue _value;

    public TName name => this._name;
    public TValue value => this._value;

    public AnonymousType(TName name, TValue value)
    {
        this._name = name;
        this._value = value;
    }

    public override bool Equals(object value)
    {
        var that = value as AnonymousType<TName, TValue>;
        return that != null &&
            EqualityComparer<TName>.Default.Equals(this._name, that._name) &&
            EqualityComparer<TValue>.Default.Equals(this._value, that._value);
    }

    public override int GetHashCode()
    {
        // ...
    }
}

该方法的第一行检查是否是 的实例,专门引用当前程序集中定义的类。因此,来自不同程序集的匿名类型永远不会相等,即使它们具有相同的结构。EqualsvalueAnonymousType<TName, TValue>

您可能希望更改测试以比较对象的序列化 JSON,而不是对象本身。