mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 16:52:07 +08:00
Make Variant equality reflexive for typed null arrays
A typed null array or matrix (e.g. an unset Int32[] or Variant[] structure field) equalled Variant.Null and hashed like it, but did not equal an identical typed null or even itself: the array accessors cannot read an absent payload. Two such variants of the same (or a convertible) type now compare equal, so equality agrees with GetHashCode and Structure.IsEqual holds for identical default structures (OPC 10000-6 5.1.11). Typed nulls of unrelated types stay unequal. The codec regression tests compare XmlParser and XmlDecoder fields with plain equality again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1146,7 +1146,7 @@ namespace Opc.Ua
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (!ValueIsValueType && ValueIsDefaultOrNull)
|
||||
if (IsAbsentPayload)
|
||||
{
|
||||
// A typed null payload (null array, matrix, byte string,
|
||||
// qualified name, ...) equals Variant.Null and must
|
||||
@@ -7906,7 +7906,7 @@ namespace Opc.Ua
|
||||
// letting it compare equal collapses both into one bucket of
|
||||
// every Dictionary and HashSet keyed on Variant.
|
||||
Variant typed = IsNull ? other : this;
|
||||
return !typed.ValueIsValueType && typed.ValueIsDefaultOrNull;
|
||||
return typed.IsAbsentPayload;
|
||||
}
|
||||
|
||||
TypeInfo ourTypeInfo = m_typeInfo;
|
||||
@@ -7918,6 +7918,15 @@ namespace Opc.Ua
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (IsAbsentPayload && other.IsAbsentPayload)
|
||||
{
|
||||
// Both hold the typed null of the same (or a convertible) type,
|
||||
// e.g. a null array field. Each already equals Variant.Null and
|
||||
// hashes like it, so they must equal each other as well; the
|
||||
// accessors below cannot read an absent payload and made such a
|
||||
// variant unequal even to itself.
|
||||
return true;
|
||||
}
|
||||
if (ourTypeInfo.ValueRank == 0 || otherTypeInfo.ValueRank == 0)
|
||||
{
|
||||
// A null MatrixOf carries no dimensions, so its type info
|
||||
@@ -8413,6 +8422,14 @@ namespace Opc.Ua
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True for a typed variant whose reference payload is absent (a null
|
||||
/// array, matrix, string, byte string, ...). Such a variant equals
|
||||
/// <see cref="Null"/> and hashes like it. A numeric, boolean or status
|
||||
/// code zero is a value, not an absent one.
|
||||
/// </summary>
|
||||
private bool IsAbsentPayload => !ValueIsValueType && ValueIsDefaultOrNull;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the variant holds an array or matrix without
|
||||
/// elements. A null array counts as empty because OPC 10000-6
|
||||
|
||||
@@ -629,6 +629,82 @@ namespace Opc.Ua.Types.Tests.BuiltIn
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VariantWithTypedNullArrayEqualsAnIdenticalTypedNull()
|
||||
{
|
||||
// A typed null array equalled Variant.Null but not itself: the
|
||||
// array accessors cannot read an absent payload. Equality must be
|
||||
// reflexive and agree with the hash code (OPC 10000-6 5.1.11).
|
||||
TypeInfo[] types =
|
||||
[
|
||||
TypeInfo.Arrays.Int32,
|
||||
TypeInfo.Arrays.String,
|
||||
TypeInfo.Arrays.Variant,
|
||||
TypeInfo.Arrays.ExtensionObject,
|
||||
TypeInfo.Create(BuiltInType.Double, ValueRanks.TwoDimensions)
|
||||
];
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (TypeInfo type in types)
|
||||
{
|
||||
Variant a = Variant.CreateDefault(type);
|
||||
Variant b = Variant.CreateDefault(type);
|
||||
Assert.That(IsEqual(a, a), Is.True, type.ToString());
|
||||
Assert.That(IsEqual(a, b), Is.True, type.ToString());
|
||||
Assert.That(a.GetHashCode(), Is.EqualTo(b.GetHashCode()), type.ToString());
|
||||
}
|
||||
|
||||
// the boxed null ArrayOf spelling equals the payload-less one
|
||||
Variant nullInt32s = Variant.CreateDefault(TypeInfo.Arrays.Int32);
|
||||
Assert.That(IsEqual(Variant.From(ArrayOf<int>.Null), nullInt32s), Is.True);
|
||||
|
||||
// a typed null still differs from a value and from another type
|
||||
Assert.That(IsEqual(nullInt32s, Variant.From(s_oneTwoThree.ToArrayOf())), Is.False);
|
||||
Variant nullBooleans = Variant.CreateDefault(TypeInfo.Arrays.Boolean);
|
||||
Variant nullStrings = Variant.CreateDefault(TypeInfo.Arrays.String);
|
||||
Assert.That(IsEqual(nullBooleans, nullStrings), Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StructureWithNullArrayFieldsEqualsAnIdenticalStructure()
|
||||
{
|
||||
var definition = new StructureDefinition
|
||||
{
|
||||
BaseDataType = DataTypeIds.Structure,
|
||||
StructureType = StructureType.Structure,
|
||||
Fields =
|
||||
[
|
||||
new StructureField
|
||||
{
|
||||
Name = "A",
|
||||
DataType = DataTypeIds.Int32,
|
||||
ValueRank = ValueRanks.OneDimension
|
||||
},
|
||||
new StructureField
|
||||
{
|
||||
Name = "V",
|
||||
DataType = DataTypeIds.BaseDataType,
|
||||
ValueRank = ValueRanks.OneDimension
|
||||
}
|
||||
]
|
||||
};
|
||||
var types = new Dictionary<string, BuiltInType>
|
||||
{
|
||||
["A"] = BuiltInType.Int32,
|
||||
["V"] = BuiltInType.Variant
|
||||
};
|
||||
var a = new global::Opc.Ua.Encoders.Structure(
|
||||
new System.Xml.XmlQualifiedName("S", Namespaces.OpcUaXsd),
|
||||
new ExpandedNodeId(1u), new ExpandedNodeId(2u), new ExpandedNodeId(3u),
|
||||
definition,
|
||||
types);
|
||||
var b = (global::Opc.Ua.Encoders.Structure)a.Clone();
|
||||
|
||||
Assert.That(a.IsEqual(b), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LocalizedTextWithEmptyLocaleAndNullTextHashesLikeNull()
|
||||
{
|
||||
|
||||
@@ -650,12 +650,7 @@ namespace Opc.Ua.Types.Tests.Encoders
|
||||
foreach (string field in new[] { "N", "I", "U", "M" })
|
||||
{
|
||||
Assert.That(parsed[field].TypeInfo, Is.EqualTo(output[field].TypeInfo), field);
|
||||
// Variant equality never matches a typed null array, so
|
||||
// null or empty arrays are compared by their emptiness.
|
||||
if (!output[field].IsEmptyArray || !parsed[field].IsEmptyArray)
|
||||
{
|
||||
Assert.That(parsed[field], Is.EqualTo(output[field]), field);
|
||||
}
|
||||
Assert.That(parsed[field], Is.EqualTo(output[field]), field);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user