Определение того, имеет ли структура значение по умолчанию без «Равно»; aka ReferenceEquals для структур - PullRequest
6 голосов
/ 21 июня 2011

Для некоторых общих вспомогательных методов, которые я пишу, я хотел бы иметь возможность вызывать специальную обработку, когда это значение является значением по умолчанию для его типа.Для справочных типов это просто - значение по умолчанию null.Я не могу использовать параметр общего типа, хотя я мог бы обойти это.

Я могу сделать что-то вроде этого:

public bool DetectPossiblyUninitializedValue(object val) {
    return val== null ||
        val.GetType().IsValueType 
        && Equals(val, Activator.CreateInstance(val.GetType());
}

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

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

Как я могу сравнивать необработанные значения структуры без использования Equals?Могу ли я вообще сравнивать необработанные значения структуры?

Редактировать: Я использую это для соединения классов + структур, представляющих доменные понятия, связанные по существу произвольным кодом, представляющимразличные бизнес-правила для GUI.Некоторый старый код по существу имеет дело с возможно вложенными словарями строк в произвольные объекты, что, таким образом, требует кучу непроверенных приведений или dynamic;их создание подвержено ошибкам.Так что приятно иметь возможность работать с типизированными объектами относительно напрямую.С другой стороны, для GUI и кода переноса полезно по-разному обрабатывать неинициализированные значения;и хотя в каждом конкретном случае возможно решение типа за типом, это много кода;разумное значение по умолчанию полезно.На самом деле мне нужен метод автоматической генерации типа, идентичного другому, но со всеми свойствами / открытыми полями, расширенными для включения значения «неинициализированным», но это нереалистичная функция, чтобы ожидать - в отличие от динамического мира, это было бытривиально достижимо, но без защиты типов в других местах ...

Ответы: Mehrdad опубликовал ответ на , как получить прямой доступ к битам структур ;Я добавил реализацию, использующую это для обнаружения возможно неинициализированных значений .

Ответы [ 7 ]

4 голосов
/ 21 июня 2011

Если вас беспокоит накладные расходы на бокс ( и вы измерили , что это узкое место ), вы можете решить это по-другому:

Создайте два временных коробочных экземпляра вашей структуры как object, которые можно использовать повторно для всех структур. Используя Reflection.Emit, создайте метод, который использует код операции Unbox для копирования структуры в коробочную версию. (Это позволяет избежать выделения.) Сделайте то же самое с другой структурой в штучной упаковке, затем вызовите Equals для объектов.


Примечание:

Я не знаю, действительно ли накладные расходы на вызов делегата на самом деле быстрее, но вы все равно можете попробовать и посмотреть. Если вы обнаружите, что это не так, то вы всегда можете сделать более одного сравнения одновременно - передать массив или что-то еще. Это становится сложным, но если вы знаете , это узкое место, то это может стоить того, в зависимости от того, насколько велики ваши struct s.


Хакерское решение:

Я не поддерживаю это решение, просто предполагаю, что оно существует. Если вы не знаете, что это делает, не используйте его.

bool UnsafeHackyEquals<T>(ref T a, ref T b) where T : struct
{
    TypedReference pA = __makeref(a), pB = __makeref(b);
    var size = SizeOf<T>();
    IntPtr* ppA = (IntPtr*)&pA, ppB = (IntPtr*)&pB;
    //Now ppA[0] is a pointer to a, and ppB[0] is a pointer to b.
    //You have the size of both, so you can do a bitwise comparison.
}

Чтобы найти размер структуры:

static class ArrayOfTwoElements<T> { static readonly T[] Value = new T[2]; }

static uint SizeOf<T>()
{
    unsafe 
    {
        TypedReference
            elem1 = __makeref(ArrayOfTwoElements<T>.Value[0] ),
            elem2 = __makeref(ArrayOfTwoElements<T>.Value[1] );
        unsafe
        { return (uint)((byte*)*(IntPtr*)(&elem2) - (byte*)*(IntPtr*)(&elem1)); }
    }
}

Да, это было бы недокументировано. Но если вы беспокоитесь об этом, вы можете просто испустить этот метод вместо этого (потому что код операции MkRefAny действительно задокументирован), так что это не проблема. Однако этот пример может сломаться на других платформах, поэтому будьте осторожны ...

2 голосов
/ 21 июня 2011

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

public struct Foo
{
    public int Bar;

    public static bool operator ==(Foo a, Foo b)
    {
        return a.Bar == b.Bar;
    }
    public static bool operator !=(Foo a, Foo b)
    {
        return !(a.Bar == b.Bar);
    }
    public override bool Equals(object obj)
    {
        return base.Equals(obj);
    }
}

Тогда для сравнения:

Foo foo1 = new Foo();
Foo foo2 = new Foo { Bar = 1 };

if (foo1 == default(Foo))
{
    Console.WriteLine("foo1 is equal to default");
}

if (foo2 != default(Foo))
{
    Console.WriteLine("foo2 is not equal to default");
}
1 голос
/ 23 июня 2011

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

0 голосов
/ 02 февраля 2019

Ответ Eamon Nerbonne теперь может быть реализован с использованием System.Runtime.CompilerServices.Unsafe без использования недокументированных / неподдерживаемых функций и необработанных указателей:

// Essentially unchanged from Eamon Nerbonne's version
public static bool IsDefaultValue([CanBeNull] object a)
{
  if (a == null) return true;

  Type type = a.GetType();

  return type.IsValueType &&
         helpers.GetOrAdd(
           type,
           t =>
           {
             var method = typeof(StructHelpers<>).MakeGenericType(t)
              .GetMethod(nameof(StructHelpers<int>.IsDefaultValue));
             var objParam = Expression.Parameter(typeof(object), "obj");
             return Expression.Lambda<Func<object, bool>>(
                 Expression.Call(method, Expression.Convert(objParam, t)),
                 objParam)
              .Compile();
           })(a);
}

static readonly ConcurrentDictionary<Type, Func<object,bool>> helpers = 
  new ConcurrentDictionary<Type, Func<object,bool>>();

static class StructHelpers<T> where T : struct
{
  // ReSharper disable StaticMemberInGenericType
  static readonly int ByteCount = Unsafe.SizeOf<T>();
  static readonly int LongCount = ByteCount / 8;
  static readonly int ByteRemainder = ByteCount % 8;
  // ReSharper restore StaticMemberInGenericType

  public static bool IsDefaultValue(T a)
  { 
    if (LongCount > 0)
    {
      ref long p = ref Unsafe.As<T, long>(ref a);

      // Inclusive end - don't know if it would be safe to have a ref pointing
      // beyond the value as long as we don't read it
      ref long end = ref Unsafe.Add(ref p, LongCount - 1);

      do
      {
        if (p != 0) return false;
        p = ref Unsafe.Add(ref p, 1);
      } while (!Unsafe.IsAddressGreaterThan(ref p, ref end));
    }

    if (ByteRemainder > 0)
    {
      ref byte p = ref Unsafe.Add(
                     ref Unsafe.As<T, byte>(ref a),
                     ByteCount - ByteRemainder);

      ref byte end = ref Unsafe.Add(ref p, ByteRemainder - 1);

      do
      {
        if (p != 0) return false;
        p = ref Unsafe.Add(ref p, 1);
      } while (!Unsafe.IsAddressGreaterThan(ref p, ref end));
    }

    return true;
  }
}
0 голосов
/ 21 июня 2011

Могу ли я сравнить необработанные значения структуры вообще?- Нет .Сам CLR использует отражение для сравнения двух структур по полю.Равно ваша единственная надежда.Типы значений должны реализовывать Equals, что не отличается от полевого сравнения отражений.В противном случае тип значения не является типом значения.

Рассмотрим следующее

struct Bla
{
    int Data;
}
...
{
    Bla a = new Bla();
    Bla b = new Bla();
    a.Data = 10;
    a.Data = 0;

    Console.Writeline(IsDefault(a));
    Console.Writeline(IsDefault(b));
}

Что вы ожидаете получить?Мы говорим здесь о структурах.

0 голосов
/ 21 июня 2011

Если Value Types, который вы рассматриваете, все "под вашим контролем" или будет приспособлен для работы с вашим кодом, вы всегда можете заставить их реализовать поле readonly bool IsInitialized и проверить его с помощью отражения.

Если нет, мне трудно делать то, что вы хотите, без использования Equals. Вы можете теоретически перебирать поля, используя отражение, чтобы проверить, установлены ли для всех полей значения по умолчанию.

0 голосов
/ 21 июня 2011

Сравнение структур общего назначения должно выполняться с использованием чего-то вроде Reflection - в основном вам нужно сравнивать каждое поле в структурах отдельно. Вы можете использовать небезопасный / неуправляемый код, например, чтобы скопировать структуру в byte [] и сканировать ненулевые байты, но полагаться на такие гарантии VM, как это, может быть плохой идеей. (Язык C # гарантирует только то, что каждое поле имеет свое значение «по умолчанию» - тот факт, что значение по умолчанию равно 0, - это специфическая для CLR деталь, которая может измениться.)

Существует несколько решений для сравнения структур, в том числе довольно компактное решение LINQ, в ответах на Сравните значения двух структур в C # .

Вы можете использовать ключевое слово default, чтобы получить структуру по умолчанию для сравнения, например:

    var blank = default(type)

Основываясь на этом решении LINQ, это должно делать то, что вы хотите:

static bool IsDefault<T> ( T b ) where T : struct
{
    T a = default(T);
    var differences = from fielda in a.GetType().GetFields()
              join fieldb in b.GetType().GetFields() on fielda.Name equals fieldb.Name
              where !fielda.GetValue(a).Equals(fieldb.GetValue(b))
              select fielda.Name;
    return !differences.Any();
}

EDIT:

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...