Сравнение двух объектов с помощью сериализации с#
Почему не рекомендуется сравнивать два объекта путем их сериализации, а затем сравнивать строки, как в следующем примере?
public class Obj
{
public int Prop1 { get; set; }
public string Prop2 { get; set; }
}
public class Comparator<T> : IEqualityComparer<T>
{
public bool Equals(T x, T y)
{
return JsonConvert.SerializeObject(x) == JsonConvert.SerializeObject(y);
}
public int GetHashCode(T obj)
{
return JsonConvert.SerializeObject(obj).GetHashCode();
}
}
Obj o1 = new Obj { Prop1 = 1, Prop2 = "1" };
Obj o2 = new Obj { Prop1 = 1, Prop2 = "2" };
bool result = new Comparator<Obj>().Equals(o1, o2);
я протестировал его, и он работает, он является общим, поэтому он может стоять для большого разнообразия объектов, но я спрашиваю, какие недостатки этого подхода для сравнения объектов?
Я видел, что это было предложено в этот вопрос и он получил некоторые upvotes, но я не могу понять это почему это не считается лучшим способом, если кто-то хочет сравнить только значения свойств двух объектов?
EDIT: я строго говорю о сериализации Json, а не XML.
Я спрашиваю это потому, что я хочу создать простой и универсальный Comparator
для проекта модульного тестирования, поэтому производительность сравнения меня не беспокоит, так как я знаю, что это может быть одним из самых больших недостатков. Также проблему typeless можно отрегулировать используя в случае если из Newtonsoft.Json the TypeNameHandling
свойство имеет значение All
.
10 ответов
вы, вероятно, будете продолжать добавлять щедрость к вопросу, пока кто-то не скажет вам, что это просто нормально. Так что вы получили его, не стесняйтесь воспользоваться NewtonSoft.Библиотека Json для упрощения кода. Вам просто нужны хорошие аргументы для защиты вашего решения, если ваш код когда-либо пересматривался или если кто-то другой берет на себя обслуживание кода.
некоторые из возражений, которые они могут выдвинуть, и их контраргументы:
этот is очень неэффективный код!
Это, безусловно, так, особенно GetHashCode() может сделать ваш код жестоко медленным, если вы когда-либо использовали объект в словаре или HashSet.
лучший контраргумент-отметить, что эффективность мало беспокоит в модульном тесте. Наиболее типичный модульный тест занимает больше времени для начала, чем для фактического выполнения, и занимает ли он 1 миллисекунду или 1 секунду, не имеет значения. И проблема, которую вы, вероятно, обнаружите очень ранний.
вы единичное тестирование библиотеки, которую вы не писали!
Это, безусловно, действительная проблема, вы фактически тестируете NewtonSoft.Способность Json генерировать согласованное строковое представление объекта. Есть причина для беспокойства об этом, в частности, значения с плавающей запятой (float и double) никогда не являются проблемой. Существует также доказательства что автор библиотеки не уверен, как это сделать правильно.
лучший контраргумент заключается в том, что библиотека широкое используется и хорошо поддерживается, автор выпустил много обновлений на протяжении многих лет. Проблемы согласованности с плавающей запятой могут быть устранены, если вы убедитесь, что одна и та же программа с одной и той же средой выполнения генерирует обе строки (т. е. не сохраняет их), и убедитесь, что модульный тест построен с отключенной оптимизацией.
вы не проводите модульное тестирование кода это нужно проверить!
Да, вы бы написали этот код только в том случае, если сам класс не предоставляет способа сравнения объектов. Другими словами, сам не переопределяет Equals / GetHashCode и не предоставляет компаратор. Поэтому тестирование на равенство в модульном тесте выполняет функцию, которую тестируемый код фактически не поддерживает. То, что модульный тест никогда не должен делать, вы не можете написать отчет об ошибке при сбое теста.
встречный аргумент должен рассуждать что ты нужно для проверки равенства для проверки другой функции класса, такой как конструктор или задатчики свойств. Для этого достаточно простого комментария в коде.
основная проблема заключается в том, что он неэффективен
в качестве примера представьте, что это соответствует функции
public bool Equals(T x, T y)
{
return x.Prop1 == y.Prop1
&& x.Prop2 == y.Prop2
&& x.Prop3 == y.Prop3
&& x.Prop4 == y.Prop4
&& x.Prop5 == y.Prop5
&& x.Prop6 == y.Prop6;
}
если prop1 не то же самое, то другие 5 сравнений никогда не нужно проверять, если вы сделали это с JSON, вам придется преобразовать весь объект в строку JSON, а затем сравнить строку каждый раз, это поверх сериализации, которая является дорогостоящей задачей сама по себе.
тогда следующая проблема-сериализация предназначена для связи, например, из памяти в файл, по сети и т. д. Если вы использовали сериализацию для сравнения, вы можете ухудшить свою способность использовать ее для обычного использования, т. е. вы не можете игнорировать поля, не необходимые для передачи, потому что игнорирование их может сломать ваш компаратор.
следующий JSON в specific является Type-less, что означает, что значения, чем не в любом случае shape или form equal, могут быть ошибочно приняты за равные, а в flipside значения, которые равны, не могут сравниваться как равно из-за форматирования, если они сериализуются до того же значения, это снова небезопасно и неустойчиво
единственным плюсом этой техники является то, что программисту требуется мало усилий для реализации
Я хотел бы исправить GetHashCode
в начале.
public class Comparator<T> : IEqualityComparer<T>
{
public bool Equals(T x, T y)
{
return JsonConvert.SerializeObject(x) == JsonConvert.SerializeObject(y);
}
public int GetHashCode(T obj)
{
return JsonConvert.SerializeObject(obj).GetHashCode();
}
}
хорошо, Далее мы обсудим проблему этого метода.
во-первых, она просто не будет работать для типов с петлей связи.
Если у вас есть связь свойств, такая же простая, как A -> B -> A, она терпит неудачу.
к сожалению, это очень распространено в списках или карте, которые взаимосвязаны.
худшее, едва ли эффективное родовое обнаружение петли механизм.
во-вторых, сравнение с сериализацией просто неэффективно.
JSON нуждается в отражении и большом количестве типов, прежде чем успешно скомпилировать его результат.
таким образом, ваш компаратор станет серьезным узким местом в любом алгоритме.
обычно, даже если в тысячах случаев записей JSON считается достаточно медленным.
В-третьих, JSON должен пройти через каждое свойство.
это станет катастрофа, если ваш объект ссылается на какой-либо большой объект.
Что делать, если ваш объект ссылки на большой файл?
в результате C# просто оставляет реализацию пользователю.
перед созданием компаратора нужно хорошо знать свой класс.
сравнение требует хорошего обнаружения петли, раннего прекращения и рассмотрения эффективности.
универсального решения просто не существует.
Извините, я пока не могу писать комментарии, поэтому я напишу здесь.
сериализуя свои объекты в JSON, вы в основном меняете все свои объекты на другой тип данных, и поэтому все, что относится к вашей библиотеке JSON, будет влиять на ваши результаты.
поэтому, если в одном из объектов есть тег [ScriptIgnore], ваш код просто проигнорирует его, поскольку он был опущен из ваших данных.
кроме того, строковые результаты могут быть одинаковыми для объектов, которые не совпадают. как этот пример.
static void Main(string[] args)
{
Xb x1 = new X1()
{
y1 = 1,
y2 = 2
};
Xb x2 = new X2()
{
y1 = 1,
y2= 2
};
bool result = new Comparator<Xb>().Equals(x1, x2);
}
}
class Xb
{
public int y1 { get; set; }
}
class X1 : Xb
{
public short y2 { get; set; }
}
class X2 : Xb
{
public long y2 { get; set; }
}
Итак, как вы видите, x1 имеет другой тип от x2, и даже тип данных y2 отличается для этих двух, но результаты json будут одинаковыми.
кроме этого, так как x1 и x2 относятся к типу Xb, я мог бы вызвать ваш компаратор без каких-либо проблем.
вот некоторые из минусов:
A) производительность будет все более плохой, чем глубже ваше дерево объектов.
b)new Obj { Prop1 = 1 } Equals
new Obj { Prop1 = "1" } Equals
new Obj { Prop1 = 1.0 }
c)new Obj { Prop1 = 1.0, Prop2 = 2.0 } Not Equals
new Obj { Prop2 = 2.0, Prop1 = 1.0 }
во-первых, я заметил, что вы говорите "сериализовать их, а затем сравнить строки."В общем, обычное сравнение строк будет не работа для сравнения строк XML или JSON, вы должны быть немного более сложными, чем это. В качестве контрпримера для сравнения строк рассмотрим следующие строки XML:
<abc></abc>
<abc/>
Они явно не строка равна, но они определенно "означают" одно и то же. Хотя этот пример может показаться надуманным, получается что есть немало случаев, когда сравнение строк не работает. Например, пробелы и отступы значимы при сравнении строк, но не могут быть значимыми в XML.
ситуация не намного лучше для JSON. Вы можете сделать аналогичные контрпримеры для этого.
{ abc : "def" }
{
abc : "def"
}
опять же, ясно, что они означают одно и то же, но они не равны строке.
по существу, если вы делаете сравнение строк, Вы доверяете сериализатору всегда сериализуйте определенный объект точно так же (без каких-либо добавленных пробелов и т. д.), что в конечном итоге оказывается удивительно хрупким, особенно учитывая, что большинство библиотек, насколько мне известно, не предоставляют такой гарантии. Это особенно проблематично, если вы обновляете библиотеки сериализации в какой-то момент, и есть тонкая разница в том, как они делают сериализацию; в этом случае, если вы пытаетесь сравнить сохраненный объект, который был сериализован с предыдущей версией библиотеки с тем, который был сериализован с текущей версией, он не будет работать.
кроме того, так же, как краткое замечание о самом коде, оператор"==" -не правильный способ сравнения объектов. В общем, "==" тесты для ссылка равенства, не равенство объекта.
еще одно быстрое отступление от хэш-алгоритмов: насколько они надежны как средство тестирования равенства, зависит от того, насколько они устойчивы к столкновениям. Иначе говоря, учитывая два разных, неравных объекта, какова вероятность того, что они будут хэшироваться до одного и того же значения? И наоборот, если два объекта хэшируют одно и то же значение, каковы шансы, что они на самом деле равны? Многие люди считают само собой разумеющимся, что их хэш-алгоритмы на 100% устойчивы к столкновениям (т. е. два объекта будут хэшироваться с одинаковым значением, если и только если они равны), но это не обязательно верно. (Особенно известным примером этого является криптографическая хэш-функция MD5, чья относительно плохое сопротивление столкновению сделало его непригодным для дальнейшего использования). Для правильно реализованной хэш-функции в большинстве случаев вероятность того, что два объекта, хэширующие одно и то же значение, на самом деле равны, достаточно высока, чтобы быть подходящей в качестве средства тестирования равенства, но это не гарантируется.
сравнение объектов с помощью сериализации, а затем сравнение представлений строк не эффективно в следующих случаях:
когда свойство типа DateTime
существует в типах, которые необходимо сравнить
public class Obj
{
public DateTime Date { get; set; }
}
Obj o1 = new Obj { Date = DateTime.Now };
Obj o2 = new Obj { Date = DateTime.Now };
bool result = new Comparator<Obj>().Equals(o1, o2);
это приведет false
даже для очень близких объектов, созданных во времени, если они не имеют одного и того же свойства.
для объектов с двойными или десятичными значениями, которые необходимо сравнить с Epsilon для проверьте, если они в конечном итоге очень близко друг к другу
public class Obj
{
public double Double { get; set; }
}
Obj o1 = new Obj { Double = 22222222222222.22222222222 };
Obj o2 = new Obj { Double = 22222222222222.22222222221 };
bool result = new Comparator<Obj>().Equals(o1, o2);
Это также вернется false
даже двойные значения действительно близки друг к другу, и в программах, которые включают в себя расчет, это станет реальной проблемой из-за потери точности после нескольких операций деления и умножения, а сериализация не предлагает гибкости для обработки этих случаев.
также учитывая приведенные выше случаи, если вы не хотите сравнивать свойство, он столкнется с проблемой введения атрибута сериализации в фактический класс, даже если это не обязательно, и это приведет к загрязнению кода или проблемам, которые он должен будет фактически использовать сериализацию для этого типа.
Примечание: это некоторые из актуальных проблем этого подхода, но я с нетерпением жду, чтобы найти другие.
сериализация была сделана для хранения объекта или отправки его по каналу (сети), который находится вне текущего контекста выполнения. Не делать что-то внутри контекста выполнения.
некоторые сериализованные значения не могут считаться равными, что на самом деле они: десятичные "1.0" и целочисленные "1", например.
конечно, вы можете так же, как вы можете есть с лопатой, но вы этого не делаете, потому что вы можете сломать зуб!
можно использовать System.Reflections
пространства имен, чтобы получить все свойства экземпляра, как в ответ. С отражение вы можете сравнить не только public
свойства или поля (например, с помощью сериализации Json), но и некоторые private
, protected
, etc. для увеличения скорости расчета. И, конечно же, очевидно, что вам не нужно сравнивать все свойства или поля экземпляра, если два объекта отличаются (исключая пример, когда только последнее свойство или поле объекта отличается).
для модульных тестов вам не нужно писать свой компаратор. :)
просто используйте современные фреймворки. Например, попробовать библиотека FluentAssertions
o1.ShouldBeEquivalentTo(o2);