确定结构是否具有默认值而没有“等于”;结构的ReferenceEquals

时间:2011-06-21 15:26:54

标签: c# reflection

对于我正在编写的一些常规辅助方法,我希望能够在该值是其类型的默认值时调用特殊处理。对于参考类型,这很容易 - 默认值为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在how to directly access the bits of structs上发布了答案;我添加了an implementation using that to detect possibly uninitialized values

7 个答案:

答案 0 :(得分:4)

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

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


注意:

我不知道委托调用的开销实际上是否更快,但你可以试试看看。如果您发现它不是,那么您可以一次进行多个比较 - 传入数组或其他内容。它变得复杂,但如果你知道这是瓶颈,那么它可能是值得的,这取决于你struct的大小。


Hackier解决方案:

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

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

答案 1 :(得分: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");
}

答案 2 :(得分:1)

原创海报:我已经决定...... ......使用下面的解决方案,从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
}

答案 3 :(得分:0)

通用结构比较需要使用像Reflection这样的东西来完成 - 基本上,你需要分别比较结构中的每个字段。例如,您可以使用不安全/非托管代码将结构复制到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循环并单独处理struct-type字段也可以按照相同的原则工作。

答案 4 :(得分:0)

如果您考虑的Value Types全部“在您的控制之下”,或者将根据您的代码量身定制,您可以随时让它们实现readonly bool IsInitialized字段并通过反思进行检查。< / p>

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

答案 5 :(得分:0)

我可以比较原始结构值吗? - 。 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));
}

您期望收到什么?我们在这里谈论结构。

答案 6 :(得分:0)

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