Comments 40
Сравнение двух структур разных типов по значению не имеет смысла. А структуры одного типа будут иметь одинаковые (и одинаково расположенные) backing fields, поэтому ваши вопросы не имеют смысла.
Вопросы имеют смысл, т.к. поведение структур с автосвойствами строго не описано (хотя и ясно, какое поведение можно ожидать).
В этом цикле я ставлю целью рассмотреть наиболее полно теоретические аспекты object equality, чтобы вывести законченные практические решения.
К вопросу об ожидаемом поведении: однажды у коллег в каких-то случаях были некие неясности при автоматической сериализации сущностей с автоствойствами, т.к. движок сериализации не знал, к каким полям обращаться, или что-то в таком духе.
Вопросы имеют смысл, т.к. поведение структур с автосвойствами строго не описано
Эмм. Автосвойства — это фича C#, в то время как поведение структур — фича BCL. С точки зрения CLR, структура с автосвойствами — это структура со странно именованными полями, вот и все. Поскольку поля создаются в типе — они, очевидно, имеют одинаковое наименование и расположение.
К вопросу об ожидаемом поведении: однажды у коллег в каких-то случаях были некие неясности при автоматической сериализации сущностей с автоствойствами, т.к. движок сериализации не знал, к каким полям обращаться, или что-то в таком духе.
Эта проблема возникает при попытке десериализации автосвойств от предыдущей версии сборки — известно, что именование backing fields не гарантировано стабильно. Но в вашем случае, поскольку вы имеете дело с типом в одной и той же сборке, вас это не касается.
Тогда не было бы мешанины явно объявленных backing field, к которым кто угодно может получить доступ вне геттера/сеттера и поменять их, и автосвойств с их отсутствием возможности получить явный доступ к полю и недетерминированым именем этого поля.
Возможно, с поддержкой этого даже не в языке, а в CLR.
А не выйдет. Если это поле будет доступно только в геттере/сеттере, у вас сломается рефлекшн, который работает на полях (например, сериализация и, как раз, value types), а если оно будет доступно через рефлекшн, то нет разницы с "обычными" backing fields.
Собственно, для задач, отличных от сериализации стандартным BinaryFormatter, я уже и не помню, зачем я использовал не-readonly backing fields.
Ну то есть да, фича милая, но я подозреваю, что она если и есть в списке команды .net, то о-о-очень далеко.
Ну и да, к сравнению структур она отношения не имеет.
Но тем не менее:
Сейчас свойства это сахар над полем, и методами — геттерам/сеттером.
При этом геттер и сеттер на уровне CLR имеют атрибуты, придающие им определенную семантику.
Получается, застряли где-то посередине.
Моя идея в том, чтобы свойство было своего рода объектом, инкапсулирующим в себе единственное значение.
Вручную можно так писать и сейчас — например, такой подход применен в MS-библиотеке работы с форматом ooxml.
Но хотелось бы видеть это именно в объектной модели/платформе.
Понятно, что в существующих платформах этого или не сделают, или когда-нибудь сделают, но криво, и это будет соседствовать со старыми подходами ради backward compatibility.
Моя идея в том, чтобы свойство было своего рода объектом, инкапсулирующим в себе единственное значение.
Я просто не очень понимаю, зачем это нужно.
(и иногда нужен доступ и через геттер, если в месте вызова лучше абстрагироваться от источника значение и/или выполнить проверку на инвариант объекта),
и т.д.?
Бывает всегда достаточно обращать изнутри всегда к полю, а все равно написана каша разнородных обращений.
А если авторы еще открыли internal-доступ к полю, то вообще тушите свет.
Так что эта идея ради лучшей инкапсуляции.
Так что эта идея ради лучшей инкапсуляции.
Когда вам нужна инкапсуляция внутри класса — что-то пошло не так (в моем понимании).
Ну то есть да, бывает, не спорю. Но обычно это признак того, что класс уже вышел из-под контроля, и с ним надо бороться всеми средствами.
@a-tk Свойство нужно еще и затем, что его можно описать в интерфейсе (ведь это просто пара методов), а вот потребовать в интерфейсе наличие поля нельзя.
На самом деле поля сами по себе это лишняя сущность, лучше бы изначально сделали только свойства, которые например являются врапперами других свойств. А на самом нижнем уровне автосвойства (то, что сейчас является полями). Но, тут уже наследие других языков и принятых архитектурных решений. Так и живем.
Однако поля не лишние, они нужны для того, чтобы хранить состояние. Свойство в общем случае может не быть частью состояния объекта. Иными словами, поле — это всегда данные объекта, свойство — это всего лишь пара специальных методов. Автореализуемые свойства опираются на backing-поля, реализуемые компилятором.
… и как это связано с тем, автосвойства в структуре, или нет?
Явно не объективными аргументами типа ildasm-а и прочих инструментов. Тем временем реализация типов System.Single aka float и System.Double aka double от Microsoft не указывают на сравнение с погрешностями. Если такие реализации есть, то хотелось бы увидеть пруф, чтобы принять позицию.
public override bool Equals(object obj)
{
if (obj == null)
return false;
RuntimeType runtimeType = (RuntimeType) this.GetType();
if ((RuntimeType) obj.GetType() != runtimeType)
return false;
object a = (object) this;
if (ValueType.CanCompareBits((object) this))
return ValueType.FastEqualsCheck(a, obj);
FieldInfo[] fields = runtimeType.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;
}
Первым делом проверяется идентичность типов структур, дальше рефлексией разбираются поля, потом для каждого поля с помощью метода экземпляра Equals(object) сравниваются значения двух сторон сравнения.
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;
Интересно. Т.е., при получении/сравнения значений полей используется упаковка/распаковка, несмотря на то, что скорее всего можно ожидать, что поля структуры — тоже структуры.
Для случая, когда все поля структуры — структуры, обычно срабатывает CanCompareBits
, который приводит к прямому сравнению памяти.
что-то недоработано со структурами в платформе
но даже в этом случае происходит упаковка самой структуры при передаче в CanCompareBits
А происходит ли? Я понимаю, что там сверху написано (object) this
, но нельзя однозначно сказать, это действительно боксинг, или просто "взятие адреса от" — потому что если бы туда был передан просто this
, было бы копирование. А CanCompareBits
— extern
и MethodImplOptions.InternalCall
, так что там может быть любая магия, на самом деле.
ldarg.0
call bool System.ValueType::CanCompareBits(object)
Упаковки нет.А то, что приведено в виде C# может быть ошибкой декомпилятора.
dotPeek не декомпилирует, а закачивает исходники с сайта MS — и для .NET 4.6.2 показывается код, отличающийся от приведенного в этой ветке, но очень похожий, те же вызовы, формально должные привести к упаковке.
Видимо, в .NET много магии в платформенных вызовах.
Вообще когда дело доходит до столь низкоуровневых вещей, начинаются прикольные вещи вроде такого (System.Double):
public static bool operator ==(double left, double right)
{
return left == right;
}
public static bool operator !=(double left, double right)
{
return left != right;
}
public static bool operator <(double left, double right)
{
return left < right;
}
// и много-много ещё
Если посмотреть IL через dotPeek, то там преобразований нет:
ldarg.0 // this
stloc.2 // thisObj
ldarg.0 // this
call bool System.ValueType::CanCompareBits(object)
brfalse.s IL_003a
ldloc.2 // thisObj
ldarg.1 // obj
call bool System.ValueType::FastEqualsCheck(object, object)
Для сравнения, боксинг выглядит вот так:
// int i = 12;
ldc.i4.s 12 // 0x0c
stloc.0 // i
// object obj = i;
ldloc.0 // i
box [mscorlib]System.Int32
stloc.1 // obj
@sand14 у меня к дотпику вообще много претензий. Например у нас был баг, что словарь не реализовывал IReadOnlyDictionary
или подобный интерфейс. Смотрим MSDN, да нет, должен реализовывать. Но в рантайме ошибка. Начали думать, что сбилдили не с той версией, смотрим, действительно, в 4.5.2 интерфейс такой появился. Декомпилируем сборку, чтобы понять, какой версии словарь там использовался — да нет, все нормально… Долго ломали голову, в итоге плюнули и поставили другой декомпилятор...
Так вот, дотпик каким-то образом кэширует сборки, и если у него есть сборка в кэше, он показывает данные из неё несмотря на то, что она может отличаться от того, что на диске… Очень неприятное поведение. Так что, ILSpy FTW.
Документация говорит, что если все поля значимого типа, то идет побитовое сравнение.
При этом поле тоже может быть структурой.
Насколько глубоко идет проверка возможности побитового сравнения?
Проблема в том, что в MSDN по многим «тонким» вопросам нет исчерпывающей документации.
И есть ли исчерпывающая спецификация на платформу?
Или только по фактическому поведению/исходниками смотреть?
P.S. То же самое со спецификацией на C# 6.0: много публикаций в технических блогах, включая блоги MSDN, то спецификацию в виде документа с сайта MS можно закачать только по версии 5.0.
А справочные разделы MSDN не дают всей точной картины.
struct SomeStruct {
public int SomeInt;
public double SomeDouble;
}
class Program {
static void Main(string[] args) {
int someInt = 42;
SomeStruct struct1 = new SomeStruct { SomeInt = someInt, SomeDouble = 2 };
SomeStruct struct2 = new SomeStruct { SomeInt = someInt, SomeDouble = 3 };
Console.WriteLine(struct1.GetHashCode());
Console.WriteLine(struct2.GetHashCode());
}
}
Выведет два одинаковых числа в консоль, но стоит поменять местами филды в структуре и хэши становятся разными. Проверено прямо сейчас в vs2015, .net 4.5.
Аналогично, если сделать первым филдом double и задать одинаковым его, то тоже хэши будут одинаковые.
К GetHashCode есть только два требования:
- Если хэш-коды двух объектов различны, то объекты различны.
- Хэш-код должен считаться очень быстро. Иначе не будет смысла сначала считать хэш-код, а потом при совпадении сравнивать по содержимому.
- Ещё желательно, чтобы он не менялся со временем.
Хэш-коду ничего не мешает быть одинаковым у разных объектов.
ValueType.GetHashCode remarks
Тут явно говорится, что для вычисления используется одно или несколько полей структуры.
А также, что реализация по умолчанию не очень подходит для хэш-таблиц.
О сравнении объектов по значению — 5: Structure Equality Problematic