Pull to refresh

Comments 40

Сравнение двух структур разных типов по значению не имеет смысла. А структуры одного типа будут иметь одинаковые (и одинаково расположенные) backing fields, поэтому ваши вопросы не имеют смысла.

Естественно, речь о сравнении структур одинакового типа.

Вопросы имеют смысл, т.к. поведение структур с автосвойствами строго не описано (хотя и ясно, какое поведение можно ожидать).
В этом цикле я ставлю целью рассмотреть наиболее полно теоретические аспекты object equality, чтобы вывести законченные практические решения.

К вопросу об ожидаемом поведении: однажды у коллег в каких-то случаях были некие неясности при автоматической сериализации сущностей с автоствойствами, т.к. движок сериализации не знал, к каким полям обращаться, или что-то в таком духе.
Вопросы имеют смысл, т.к. поведение структур с автосвойствами строго не описано

Эмм. Автосвойства — это фича C#, в то время как поведение структур — фича BCL. С точки зрения CLR, структура с автосвойствами — это структура со странно именованными полями, вот и все. Поскольку поля создаются в типе — они, очевидно, имеют одинаковое наименование и расположение.


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

Эта проблема возникает при попытке десериализации автосвойств от предыдущей версии сборки — известно, что именование backing fields не гарантировано стабильно. Но в вашем случае, поскольку вы имеете дело с типом в одной и той же сборке, вас это не касается.

Мне хотелось бы видеть в языке фичу, чтобы при объявлении свойства автоматом бы создавалось backing field (вида: свойство PropName, поле $PropName), и чтобы это поле было доступно только в геттере и сеттере.

Тогда не было бы мешанины явно объявленных backing field, к которым кто угодно может получить доступ вне геттера/сеттера и поменять их, и автосвойств с их отсутствием возможности получить явный доступ к полю и недетерминированым именем этого поля.

Возможно, с поддержкой этого даже не в языке, а в CLR.

А не выйдет. Если это поле будет доступно только в геттере/сеттере, у вас сломается рефлекшн, который работает на полях (например, сериализация и, как раз, value types), а если оно будет доступно через рефлекшн, то нет разницы с "обычными" backing fields.


Собственно, для задач, отличных от сериализации стандартным BinaryFormatter, я уже и не помню, зачем я использовал не-readonly backing fields.


Ну то есть да, фича милая, но я подозреваю, что она если и есть в списке команды .net, то о-о-очень далеко.


Ну и да, к сравнению структур она отношения не имеет.

Да, это уже не про тему структур.

Но тем не менее:
Сейчас свойства это сахар над полем, и методами — геттерам/сеттером.
При этом геттер и сеттер на уровне CLR имеют атрибуты, придающие им определенную семантику.
Получается, застряли где-то посередине.

Моя идея в том, чтобы свойство было своего рода объектом, инкапсулирующим в себе единственное значение.
Вручную можно так писать и сейчас — например, такой подход применен в MS-библиотеке работы с форматом ooxml.
Но хотелось бы видеть это именно в объектной модели/платформе.
Понятно, что в существующих платформах этого или не сделают, или когда-нибудь сделают, но криво, и это будет соседствовать со старыми подходами ради backward compatibility.
Моя идея в том, чтобы свойство было своего рода объектом, инкапсулирующим в себе единственное значение.

Я просто не очень понимаю, зачем это нужно.

Вам приходилось наблюдать в legacy-проекте разросшийся класс со множеством backing-полей и свойств, где внутри самого класса происходит бессистемное обращение то к полю, то свойству — и когда уже не восстановить логику, где точно нужен прямой доступ к полю, и где доступ нужен через сеттер с проверками, доп. действими,
(и иногда нужен доступ и через геттер, если в месте вызова лучше абстрагироваться от источника значение и/или выполнить проверку на инвариант объекта),
и т.д.?

Бывает всегда достаточно обращать изнутри всегда к полю, а все равно написана каша разнородных обращений.
А если авторы еще открыли internal-доступ к полю, то вообще тушите свет.

Так что эта идея ради лучшей инкапсуляции.
Так что эта идея ради лучшей инкапсуляции.

Когда вам нужна инкапсуляция внутри класса — что-то пошло не так (в моем понимании).


Ну то есть да, бывает, не спорю. Но обычно это признак того, что класс уже вышел из-под контроля, и с ним надо бороться всеми средствами.

Автореализуемые свойства — это тривиальные свойства, когда нет разницы, обращение идёт к полю или свойству. Рекомендуется использовать именно свойства вместо полей для того, чтобы всегда можно было изменить реализацию аксессоров без изменения контракта (интерфейса) класса. То есть через метаинформацию известно, что есть свойство с таким-то именем. А замена поля на свойство приводит к поломке зависимых сборок и требует их перекомпиляции для повторного разрешения ссылок.

@a-tk Свойство нужно еще и затем, что его можно описать в интерфейсе (ведь это просто пара методов), а вот потребовать в интерфейсе наличие поля нельзя.


На самом деле поля сами по себе это лишняя сущность, лучше бы изначально сделали только свойства, которые например являются врапперами других свойств. А на самом нижнем уровне автосвойства (то, что сейчас является полями). Но, тут уже наследие других языков и принятых архитектурных решений. Так и живем.

По поводу внесения в интерфейс — согласен.
Однако поля не лишние, они нужны для того, чтобы хранить состояние. Свойство в общем случае может не быть частью состояния объекта. Иными словами, поле — это всегда данные объекта, свойство — это всего лишь пара специальных методов. Автореализуемые свойства опираются на backing-поля, реализуемые компилятором.
Концепция полей не нужна на уровне разработчика, вот о чем речь. Автосвойства для него ничем не отличаются от полей, кроме иконки в IDE (ну и вышеупомянутой возможности описывать их в интерфейсах). Просто какая разница, написать приватное поле или приватное автосвойство? В обоих случаях гетеры и сетеры будут заинлайнены и будет прямой доступ к полю. Но теперь программисту нужно различать 2 разных вида полей (собственно поля и свойства), учитывать это в рефлексии (нельзя просто вызывать GetProperties(), потому что часть представления может быть в полях) и т.п…
А как насчёт компилятора? Рано или поздно надо опускаться до уровня данных.
На уровне компилятора есть много всего такого, чего нет на уровне языка (класс __Cannon, например). Так что на его уровне да, поля бы появлялись, но для разработчика была бы единая концепция.
у структуры может быть поле float или double, сравнение может учитывать погрешность вычислений, чтобы возврачащать true, несмотря на отличие, например, в 12-ом разряде

… и как это связано с тем, автосвойства в структуре, или нет?

никак, мне показалось, что ваш комментарий о бессмысленности кастомного сравнения структур

Нет, мой комментарий о том, что имплементация свойств (ручная или автоматическая) не влияет на встроенное поведение Equals и GetHashCode.

Нигде в недрах не найдено ничего, что указывало на какие-либо сравнения, отличные от побитовых для чисел (в смысле для не не-чисел)
Интересно, чем руководствовался человек, поставивший минус?
Явно не объективными аргументами типа ildasm-а и прочих инструментов. Тем временем реализация типов System.Single aka float и System.Double aka double от Microsoft не указывают на сравнение с погрешностями. Если такие реализации есть, то хотелось бы увидеть пруф, чтобы принять позицию.
Вот
реализация ValueType.Equals
    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
что-то недоработано со структурами в платформе
но даже в этом случае происходит упаковка самой структуры при передаче в CanCompareBits

А происходит ли? Я понимаю, что там сверху написано (object) this, но нельзя однозначно сказать, это действительно боксинг, или просто "взятие адреса от" — потому что если бы туда был передан просто this, было бы копирование. А CanCompareBitsextern и MethodImplOptions.InternalCall, так что там может быть любая магия, на самом деле.

Возможно там что-то действительно более хитрое прячется за внутренним вызовом, чем упаковка.
Посмотрел в IL. Там
ldarg.0
call bool System.ValueType::CanCompareBits(object)
Упаковки нет.
А то, что приведено в виде C# может быть ошибкой декомпилятора.
Странно:
dotPeek не декомпилирует, а закачивает исходники с сайта MS — и для .NET 4.6.2 показывается код, отличающийся от приведенного в этой ветке, но очень похожий, те же вызовы, формально должные привести к упаковке.
Видимо, в .NET много магии в платформенных вызовах.
Смотрел код через Ildasm.
Я использовал dotPeek с ValueType из сборки «mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089».
Вообще когда дело доходит до столь низкоуровневых вещей, начинаются прикольные вещи вроде такого (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 не дают всей точной картины.
У структур ещё GetHashCode написан так, что иногда он считает хэш только от первого филда. Например:
    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 есть только два требования:


  1. Если хэш-коды двух объектов различны, то объекты различны.
  2. Хэш-код должен считаться очень быстро. Иначе не будет смысла сначала считать хэш-код, а потом при совпадении сравнивать по содержимому.
  3. Ещё желательно, чтобы он не менялся со временем.

Хэш-коду ничего не мешает быть одинаковым у разных объектов.

я не спорю. Но плохой хэшкод будет вызывать большое количество сравнений в хэш-таблицах. И всё начинает тормозить у людей, которые не переопределяют Equals и GetHashCode у структур. Где-то читал что у структур GetHashCode работает в двух режимах: если есть ссылки на референс-типы в филдах структуры, или если поля в ней не выровнены, то считаем хэш от первого филда. В противном случае ксорим память структуры. Могу ошибаться.

ValueType.GetHashCode remarks
Тут явно говорится, что для вычисления используется одно или несколько полей структуры.
А также, что реализация по умолчанию не очень подходит для хэш-таблиц.

Sign up to leave a comment.

Articles