确定一个结构是否具有默认值;等于“;;也就是结构的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发布了一个关于如何直接访问结构位的答案;我添加了一个使用它来检测可能未初始化的值的实现。
如果你担心拳击的开销(并且你已经测量过这是一个瓶颈),你可以用不同的方法来解决:
将结构的两个临时装箱实例创建为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));
}
你希望收到什么?我们在这里谈论结构。