#c# #.net
Вопрос:
Мы разрабатываем плагины в нашей компании. Чтобы лучше протестировать наш код, мы должны имитировать части основного приложения и использовать эту поддельную сборку в качестве зависимости в наших тестах. Поскольку исходная сборка имеет строгое имя, мы должны отключить проверку строгих имен через реестр Windows, иначе издевательская сборка не будет принята в качестве маскировки для исходной сборки.
В этой сборке есть тип Point
. Я буду ссылаться на исходное определение типа by Original.Point
и на издевательский тип by Mocked.Point
. Чтобы было понятно, Mocked.Point
заменяет Original.Point
. Оба типа имеют по 3 float
поля для X, Y и Z. Original.Point
Имеет несколько свойств и методов, которых Mocked.Point
нет, но они не должны иметь значения для равенства.
Вот источник Mocked.Point
:
public struct Point
{
public float X { get; }
public float Y { get; }
public float Z { get; }
public Point3f(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}
}
В наших тестах мы вызываем функцию Original.Point Host.GePoint()
. Host
относится к третьей сборке из SDK плагина. Над этим собранием не издеваются. Мы вызываем функцию следующим образом:
Mocked.Point a = Host.GetPoint();
Здесь не происходит никакого обращения. Mocked.Point
является нашей заменой Original.Point
. Что касается времени выполнения, то оба типа одинаковы.
Случай А
Однако, если мы проведем сравнение равенства между двумя примерами, скажем
Mocked.Point a = Host.GetPoint();
Mocked.Point b = new(1, 0, 0); // b is equivalent to a, no rounding errors or anything
a.Equals(b); // returns false
проверка на равенство не выполняется. Я это проверил a.GetType() == b.GetType()
.
Дело В
С другой стороны, если мы сделаем
Mocked.Point a = new(1, 0, 0);
Mocked.Point b = new(1, 0, 0);
a.Equals(b); // returns true
the equality check succeeds.
Я прошел через источник System.ValueType
во время отладки a.Equals(b)
:
[SecuritySafeCritical]
[__DynamicallyInvokable]
public override bool Equals(object obj)
{
if (obj == null)
return false;
RuntimeType type = (RuntimeType) this.GetType();
if ((RuntimeType) obj.GetType() != type)
return false;
object a = (object) this;
if (ValueType.CanCompareBits((object) this))
return ValueType.FastEqualsCheck(a, obj); // execution runs to here
FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
for (int index = 0; index < fields.Length; index)
{
object obj1 = ((RtFieldInfo) fields[index]).UnsafeGetValue(a);
object obj2 = ((RtFieldInfo) fields[index]).UnsafeGetValue(obj);
if (obj1 == null)
{
if (obj2 != null)
return false;
}
else if (!obj1.Equals(obj2))
return false;
}
return true;
}
и ValueType.FastEqualsCheck
в обоих случаях в конце концов вызывается путь выполнения, который является встроенным компилятором, поэтому я не могу видеть, что там происходит (если я не посмотрю на источник CLR). Этот вызов выполняет a memcmp
для обоих значений (источник ). Этот вызов возвращается false
в случае A и true
в случае B.
Вопрос
Почему происходит (или может произойти) сбой?
Я знаю, что это очень схематичный сценарий, но мы не можем обойти эту проблему каким-либо другим способом.
Комментарии:
1. «Что касается времени выполнения, то оба типа одинаковы». Вы подтвердили это, проверив
a.GetType() == b.GetType()
? Я подозреваю, что это вернет ложь в случае, когда равенство для вас не работает.2. Случайно: Отличается ли ОС/архитектура, на которой работает ваш «Хост», от машины, на которой вы выполняете проверку на равенство?
3. Если они являются структурой, преобразуйте их в байты и сравните
4. @Timo: Да, он проверяет это и вернет false, если они не совпадают — он возвращает false, поэтому я подумал, что, возможно, именно это и происходит. Было бы хорошо задать вопрос, который
a.GetType() == b.GetType()
возвращает значение true. (Теперь я понимаю, что вы имеете в виду, говоря о пути выполнения, но мне это было не совсем ясно, и я не удивлюсь, если отладка с помощью такого кода в любом случае может привести к ошибочным результатам, в то время как ведениеa.GetType() == b.GetType()
журнала не может вводить в заблуждение.)5. @Общая отличная идея, проверьте ответ.
Ответ №1:
Я нашел проблему; это хорошая проблема.
Я попытался посмотреть на сравниваемую память ValueType.FastEqualsCheck
, поэтому я упорядочил оба объекта
Mocked.Point a = Host.GetPoint();
Mocked.Point b = new(feetToMeter, 0, 0);
var size = Marshal.SizeOf<Point>();
var memA = new byte[size];
var memB = new byte[size];
unsafe
{
fixed (byte* pA = memA)
fixed (byte* pB = memB)
{
Marshal.StructureToPtr(a, (IntPtr) pA, false);
Marshal.StructureToPtr(b, (IntPtr) pB, false);
}
}
что дает мне
memA = 48 F9 51 40 00 00 00 80 00 00 00 00
memB = 48 F9 51 40 00 00 00 00 00 00 00 00
Обратите внимание, что 80
на 5-м последнем месте memA
? Это часть второго поплавка структуры ( Point.Y
). Я посмотрел битовое представление поплавков IEEE 754 и увидел, что это знаковый бит. Итак, здесь происходит то, что мы сравниваем -0f == 0f
, какая семантика с плавающей запятой верна, но поскольку эти два значения имеют два разных двоичных представления, побитовое сравнение не удается.
Не помогает то, что отладчик проглатывает знак при отображении, поэтому -0f
и 0f
то, и другое отображается как 0
. Кроме того, сравнение по элементам по-прежнему успешно, поскольку семантика с плавающей запятой считает их равными.
Это означает, что a.Equals(b)
это неверно, потому что среда выполнения использует быстрый путь через memcmp
. Если бы он воспользовался резервным вариантом (который представляет собой сравнение с точки зрения членов через отражение), a.Equals(b)
это было бы правдой.
Так что, в конце концов, это не имело никакого отношения к сборкам со строгими именами. Это всего лишь крайний случай в семантике равенства типов значений.
Комментарии:
1. У меня было предчувствие, что это закончится махинациями с поплавком, 1 за детективную работу