确定一个结构是否具有默认值;等于“;;也就是结构的ReferenceEquals

本文关键字:结构 默认值 等于 ReferenceEquals 也就是 是否 一个 | 更新日期: 2023-09-27 18:00:09

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

确定一个结构是否具有默认值;等于“;;也就是结构的ReferenceEquals

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

将结构的两个临时装箱实例创建为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操作码确实有文档记录),所以这不是问题。然而,这个例子可能会在其他平台上崩溃,所以要小心。。。

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

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

这里的原始海报:我已经决定而不是。。。使用以下从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
}

Eamon Nerbonne的答案现在可以使用System.Runtime.CompilerServices来实现。不安全而不必使用未记录/不支持的功能和原始指针:

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

如果您正在考虑的Value Types都"在您的控制之下",或者将根据您的代码进行定制,您可以始终让它们实现readonly bool IsInitialized字段,并通过反射进行检查。

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

通用结构比较需要使用反射之类的东西来完成——基本上,您需要分别比较结构中的每个字段。例如,您可以使用不安全/非托管代码将结构复制到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();
}

编辑:

如果你的structs反过来又有自己的struct成员,那么不幸的是,这将取决于.Equals()来比较它们。如果这是一个问题,那么在字段上使用更冗长的foreach循环并单独处理结构类型字段也可以,原理相同。

我能比较原始结构值吗?-。CLR本身使用反射来逐字段比较两个结构。平等是你唯一的希望。值类型应该实现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));
}

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