Равенство типов значений и сборки со строгими именами

#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 за детективную работу