判断结构体是否为默认值,无需使用“Equals”;也就是针对结构体的ReferenceEquals。

8

我正在编写一些通用的辅助方法,希望在值为其类型的默认值时能够调用特殊处理。对于引用类型来说,这很容易 - 默认值是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在如何直接访问结构体的位上发布了一个答案;我添加了一个使用它来检测可能未初始化值的实现

2
请注意,有些微小的例外情况,例如双精度浮点数,其中-0.0 == 0.0,尽管位模式不同。 - Eamon Nerbonne
1
ReferenceEquals适用于结构体吗?那么一个只返回false的方法呢? - Ben Robinson
没错,确实没有“结构体的ReferenceEquals”这种说法。 - user541686
@Mehrdad,他的意思是直接比较两个结构体的值,而不考虑任何覆盖逻辑。类似于ReferenceEquals只比较值,而不比较值所指向的内容。 - Buh Buh
@Buh:在这种情况下,@Eamon 可能想要谷歌一下 FastEqualsCheckCanCompareBits - user541686
显示剩余4条评论
7个回答

4
如果你担心装箱的开销(并且你已经测量过这是一个瓶颈),你可以用不同的方法解决它:
创建两个结构体的临时装箱实例作为object,可以重复使用所有结构体。使用Reflection.Emit,创建一个使用Unbox操作码将结构体复制到装箱版本的方法。(这让你避免了分配。)对另一个装箱结构体执行相同的操作,然后在对象上调用Equals

注意:

我不知道委托调用的开销是否实际上更快,但您可以尝试并查看。如果发现不是这样,那么您始终可以一次进行多个比较--传递一个数组或类似的东西。这变得复杂,但如果您知道这是瓶颈,那么根据您的struct大小,可能值得一试。


曲线救国方案:

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

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
@DiVan:没错,这是另一个危险(除非你确定它们在堆栈上)。你可以使用GCHandle将相应的对象固定,或通过fixed固定对象的不同(已知)非托管成员。第一种方法可能会很慢,所以我不确定是否值得这样做。 - user541686
@DiVan:其实,如果你的代码是单线程的话,那就不是问题——这里没有发生任何分配,所以GC不应该启动。 - user541686
感谢您提供的出色解决方案!这确实可能有点棘手,也许需要采用另一种方法。 - Eamon Nerbonne
我已经将你的片段扩展为完整的解决方案,位于下面的答案中 - Eamon Nerbonne

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

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
}

使用 System.Runtime.CompilerServices.Unsafe 重新实现了这个答案。 - FunctorSalad

1
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

通用的结构比较需要使用 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循环遍历字段并单独处理结构体类型字段也可以实现同样的原理。


CLR的0行为已经明确定义,而且非常老旧和合理,我完全可以依赖它。 - Eamon Nerbonne

0

我能比较原始结构值吗? - 不行。CLR本身使用反射来逐个比较两个结构的字段。

Equals是你唯一的希望。值类型应该实现Equals,这与逐个字段反射比较没有区别。否则,该值类型就不是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));
}

你期望收到什么?我们在谈论结构体。

1
需要吗?...不知道。但它绝对需要。这就是为什么在结构上重写Equals非常好的原因。 - DiVan
检查;好的观点。Equals是Object.Equals,因此需要“反射”或类似的牛力。+1 - sehe
这肯定是“可能的”,但有点hacky。看看我的解决方案,了解如何做到这一点。(你只需要一个指向结构体的指针,这是可以获取的。) - user541686

0
如果您正在考虑的“值类型”都是“在您的控制范围内”,或者将被定制为与您的代码配合使用,那么您可以始终使它们实现一个readonly bool IsInitialized字段,并通过反射来检查它。
如果不是这样,我发现很难在不使用Equals的情况下实现您想要的功能。您可以理论上使用反射迭代字段,以检查是否所有字段都设置为它们的默认值。

我不这样做的原因是项目相当庞大,包含很多反映特定领域概念的类型。这意味着需要添加大量样板代码。拥有出色的默认设置非常有价值,因为这意味着少数类实际上根本不需要自定义任何内容。 - Eamon Nerbonne

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接