【问题标题】:Determining whether a struct is of default value without "Equals"; aka ReferenceEquals for structs确定结构是否具有不带“Equals”的默认值;又名结构的 ReferenceEquals
【发布时间】:2019-02-02 22:26:09
【问题描述】:

对于我正在编写的一些通用辅助方法,我希望能够在该值是其类型的默认值时调用特殊处理。对于引用类型,这很简单——默认值为null。我不能使用泛型类型参数,尽管我可以解决这个问题。

我可以这样做:

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

这就是我现在正在使用的,但这取决于Equals 的实现。这很好,但并不理想。特别是,某些实现可能会覆盖 Equals 以在正常情况下支持更多可用的语义。在这里将默认值视为特殊值实际上并不少见,因为在 .NET 中由于默认初始化,它是如此不可避免。

但是,在这种情况下,我只想知道对象是否已初始化,因此我不想要任何自定义相等或其他任何东西。基本上,我想知道 struct 占用的内存区域是否在初始化后填充为零作为 VM 保证,仅此而已。从某种意义上说,我正在为结构寻找类似于 ReferenceEquals 的东西:一种忽略底层对象自身实现的比较。

如何在不使用Equals 的情况下比较原始结构值?我可以比较原始结构值吗?

编辑:我正在使用它来连接代表特定领域概念的类+结构,这些概念由代表各种业务规则的基本上任意代码连接到 GUI。一些旧代码本质上处理可能嵌套的字符串字典到任意对象,因此需要一堆未经检查的强制转换或dynamic;创建这些很容易出错。因此,能够相对直接地使用类型化对象是件好事。另一方面,对 GUI 和包装代码以不同方式处理可能未初始化的值很有用;尽管可以采用逐个案例、逐个类型的解决方案,但这是很多代码;合理的默认设置很有用。我真正想要的是一种自动生成与另一个类型相同但所有属性/公共字段扩展为包含“未初始化”值的类型的方法,但这不是期望的现实特征 - 相比之下,在动态世界中,这将是微不足道的实现,虽然在其他地方没有类型安全......

答案: Mehrdad 在how to directly access the bits of structs 上发布了答案;我加了an implementation using that to detect possibly uninitialized values

【问题讨论】:

  • 可以使用反射吗?您可以比较结构中成员的值/引用。
  • 我绝对可以使用反射——但这只是延迟了问题。我该怎么办会员?递归......直到什么?
  • 请注意,有一些细微的例外,例如双精度数,其中 -0.0 == 0.0,即使位模式不同。
  • 结构的ReferenceEquals?只返回 false 的方法呢?
  • 没错——aka ReferenceEquals for structs 没有意义...

标签: c# reflection


【解决方案1】:

如果您担心拳击的开销(并且您已经测量这是一个瓶颈),您可以用不同的方式解决它:

将结构的两个临时装箱实例创建为object,可重复用于所有结构。使用Reflection.Emit,创建一个使用Unbox 操作码将结构复制到盒装版本的方法。 (这样可以避免分配。)对另一个盒装结构做同样的事情,然后在对象上调用Equals


注意:

我不知道委托调用的开销是否实际上更快,但您无论如何都可以尝试看看。如果你发现它不是,那么你总是可以一次做多个比较——传入一个数组或其他东西。它变得复杂,但如果您知道这是瓶颈,那么它可能是值得的,这取决于您的 structs 有多大。


黑客解决方案:

我不支持这个解决方案,只是暗示它存在。 如果你不知道这是做什么的,请不要使用它。

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 操作码),所以这不是问题。但是,这个例子可能会在其他平台上崩溃,所以要小心......

【讨论】:

  • 说到指针,在这种情况下你不需要固定它们。
  • @DiVan:是的,确实是另一个危险(除非你确定他们在堆栈上)。您可以使用GCHandle 固定相应的对象,或通过fixed 固定对象的不同(已知)非托管成员。第一个可能很慢,所以我不确定是否值得。
  • @DiVan:实际上,如果你的代码是单线程的,那不是问题——这里没有分配,所以 GC 不应该启动。
  • 感谢您的出色解决方案!这确实可能有点讨厌,可能需要另一种方法。
  • 我已经在this answer below 中将您的 sn-p 扩展为一个完整的解决方案。
【解决方案2】:

由于时间有限,我必须了解您的要求,我只是在这里抛出一些东西供您思考。虽然它确实涉及运算符重载(反过来,实现特定的):

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");
}

【讨论】:

    【解决方案3】:

    这里的原始海报:我已经确定......使用下面的解决方案,扩展自 Mehrdad 的笔记。它可以工作,但我认为在默认实现中捕获更多未初始化的值是不值得的。

    但如果其他人在这里关心的话,那就是:

    public static bool PossiblyUninitialized(object a) {
        if(a == null) return true;
        Type t = a.GetType();
        return t.IsValueType &&
            helpers.GetOrAdd(t, _=>{
                var method = typeof(StructHelpers<>).MakeGenericType(t)
                    .GetMethod("PossiblyUninitialized");
                var objParam = Expression.Parameter(typeof(object),"obj");
                return Expression.Lambda<Func<object,bool>>(
                        Expression.Call(method,Expression.Convert(objParam,t)),
                        objParam
                    ).Compile();
            })(a);
    }
    static ConcurrentDictionary<Type, Func<object,bool>> helpers = 
                        new ConcurrentDictionary<Type, Func<object,bool>>();
    
    unsafe static class StructHelpers<T> where T : struct { 
        public static readonly uint ByteCount = SizeOf();
    
        static uint SizeOf()
        {
            T[] arr = new T[2];
            var handle = GCHandle.Alloc(arr);
            TypedReference
                elem0 = __makeref(arr[0]),
                elem1 = __makeref(arr[1]);
            return (uint)((byte*)*(IntPtr*)(&elem1) - (byte*)*(IntPtr*)(&elem0)); 
            handle.Free();
        }
    
        public static bool PossiblyUninitialized(T a)
        {
            TypedReference pA = __makeref(a);
            var size = ByteCount;
            IntPtr* ppA = (IntPtr*)(&pA);
            int offset = 0;
            while(size - offset>=8) {
                if(*(long*)(*ppA+offset) != 0)
                    return false;
                offset+=8;
            }
            while(size - offset>0) {
                if(*(byte*)(*ppA+offset) != 0)
                    return false;
                offset++;
            }
            return true;
        }
    }
    
    void Main()//LINQpad
    {
        StructHelpers<decimal>.ByteCount.Dump();
        PossiblyUninitialized(0m).Dump();//true
        PossiblyUninitialized(0.0m).Dump();//false
        PossiblyUninitialized(0.0).Dump();//true
        PossiblyUninitialized(-0.0).Dump();//false
        PossiblyUninitialized("").Dump();//false
    }
    

    【讨论】:

    • 添加了一个使用 System.Runtime.CompilerServices.Unsafe 重新实现的答案。
    【解决方案4】:

    如果您正在考虑的 Value Types 都“在您的控制之下”或者将被定制以使用您的代码,您总是可以让它们实现一个 readonly bool IsInitialized 字段并通过反射检查它。

    如果没有,我发现不使用Equals 就很难做你想做的事。理论上,您可以使用反射遍历字段以检查是否所有字段都设置为默认值。

    【讨论】:

    • 我不这样做的原因是该项目相当大,其中 很多 类型反映了特定于域的概念。这意味着添加大量样板。拥有优秀的默认值是非常有价值的,因为这意味着更少的类实际上需要做任何自定义的事情。
    【解决方案5】:

    通用结构比较需要使用反射之类的东西来完成——基本上,您需要分别比较结构中的每个字段。例如,您可以使用不安全/非托管代码将结构复制到 byte[] 并扫描非零字节,但依赖像这样的底层 VM 保证可能不是一个好主意。 (C# 语言只保证每个字段都有其“默认”值——默认值为 0 的事实是 CLR 特定的细节,可能会发生变化。)

    Compare two structs' values in C# 的答案中,有几种比较结构的解决方案,包括一个相当紧凑的 LINQ 解决方案。

    您可以使用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();
    }
    

    编辑:

    如果您的结构反过来有自己的结构成员,那么不幸的是,这将依靠.Equals() 来比较它们。如果这是个问题,在字段上使用更冗长的 foreach 循环并分别处理结构类型字段也可以,原理相同。

    【讨论】:

    • CLR 的 0 行为已明确指定,而且如此古老且如此合理,我完全可以依赖它。
    【解决方案6】:

    我可以比较原始结构值吗? - 。 CLR 本身使用反射来逐字段比较两个结构。 平等是你唯一的希望。值类型应该实现 Equals,这与逐场反射比较没有区别。否则,Value Type 不是 ValueType。

    考虑以下

    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));
    }
    

    您希望收到什么?我们在这里讨论的是结构体。

    【讨论】:

    • 需要吗? ……不要。但它确实如此。这就是为什么在结构上覆盖 Equals 是一件好事。
    • 检查;好点子。 Equals 是 Object.Equals ,因此需要“反思”或类似的牛权。 +1
    • 这绝对是可能,但是很老套。请参阅我的解决方案以了解如何执行此操作。 (你只需要一个指向结构的指针,它是可获取的。)
    【解决方案7】:

    现在可以使用 System.Runtime.CompilerServices.Unsafe 来实现 Eamon Nerbonne 的答案,而无需使用未记录/不受支持的功能和原始指针:

    // 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;
      }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-05
      • 2013-02-24
      • 2010-10-19
      • 2014-01-17
      • 2016-09-19
      • 1970-01-01
      相关资源
      最近更新 更多