Вызов Equals для анонимного типа зависит от того, в какой сборке был создан объект - PullRequest
1 голос
/ 17 марта 2020

Я обнаружил очень странное поведение в C# (. net core 3.1) для сравнения анонимных объектов, которое я не могу объяснить.

Насколько я понимаю, вызов Equals для анонимных объектов использует сравнение структурного равенства (отметьте, например, здесь ). Пример:

public static class Foo
{
    public static object GetEmptyObject() => new { };
}

static async Task Main(string[] args)
{   
    var emptyObject = new { };

    emptyObject.Equals(new { }); // True
    emptyObject.Equals(Foo.GetEmptyObject()); // True
}

Это выглядит правильно. Но ситуация становится совершенно другой, если я перемещаю 'Foo' в другую сборку!

emptyObject.Equals(Foo.GetEmptyObject()); // False

Точно такой же код возвращает другой результат, если анонимный объект из другой сборки.

Это ошибка в C#, подробности реализации или что-то, что я не совсем понимаю?

PS То же самое происходит, если я вычисляю выражение при быстром просмотре (true во время выполнения, false при быстром просмотре):

enter image description here

1 Ответ

4 голосов
/ 17 марта 2020

Это деталь реализации, которую вы не понимаете.

Если вы используете анонимный тип, компилятор должен сгенерировать новый тип (с неописуемым именем, таким как <>f__AnonymousType0<<A>j__TPar>), и он генерирует этот тип в сборке, которая его использует.

Он будет использовать этот же сгенерированный тип для всех случаев использования анонимных типов с одинаковой структурой в этой сборке. Тем не менее, каждая сборка будет иметь свои собственные определения анонимных типов: нет возможности поделиться ими между сборками. Конечно, как вы обнаружили, это можно обойти как object.

. Это ограничение является одной из основных причин, по которой нет способа разоблачить анонимные типы: вы не можете их вернуть. из методов, имейте их как поля et c. Это могло бы вызвать всевозможные проблемы, если бы вы могли передавать их между сборками.

Вы можете видеть это на работе в SharpLab , где:

var x = new { A = 1 };

вызывает это тип, который будет сгенерирован в той же сборке:

internal sealed class <>f__AnonymousType0<<A>j__TPar>
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly <A>j__TPar <A>i__Field;

    public <A>j__TPar A
    {
        get
        {
            return <A>i__Field;
        }
    }

    [DebuggerHidden]
    public <>f__AnonymousType0(<A>j__TPar A)
    {
        <A>i__Field = A;
    }

    [DebuggerHidden]
    public override bool Equals(object value)
    {
        global::<>f__AnonymousType0<<A>j__TPar> anon = value as global::<>f__AnonymousType0<<A>j__TPar>;
        if (anon != null)
        {
            return EqualityComparer<<A>j__TPar>.Default.Equals(<A>i__Field, anon.<A>i__Field);
        }
        return false;
    }

    [DebuggerHidden]
    public override int GetHashCode()
    {
        return -1711670909 * -1521134295 + EqualityComparer<<A>j__TPar>.Default.GetHashCode(<A>i__Field);
    }

    [DebuggerHidden]
    public override string ToString()
    {
        object[] obj = new object[1];
        <A>j__TPar val = <A>i__Field;
        obj[0] = ((val != null) ? val.ToString() : null);
        return string.Format(null, "{{ A = {0} }}", obj);
    }
}

ValueTuple имел те же проблемы, связанные с желанием определять типы анонимно, но все же передавать их между сборками, и решал его другим способом: определяя ValueTuple<..> в BCL и используя волшебный компилятор c, чтобы сделать вид, что их свойства имеют имена, отличные от Item1, Item2, et c.

...