diff --git a/src/Opc.Ua.Types/BuiltIn/Variant.cs b/src/Opc.Ua.Types/BuiltIn/Variant.cs
index 16c75c0045..67579df72e 100644
--- a/src/Opc.Ua.Types/BuiltIn/Variant.cs
+++ b/src/Opc.Ua.Types/BuiltIn/Variant.cs
@@ -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
}
}
+ ///
+ /// True for a typed variant whose reference payload is absent (a null
+ /// array, matrix, string, byte string, ...). Such a variant equals
+ /// and hashes like it. A numeric, boolean or status
+ /// code zero is a value, not an absent one.
+ ///
+ private bool IsAbsentPayload => !ValueIsValueType && ValueIsDefaultOrNull;
+
///
/// Returns true if the variant holds an array or matrix without
/// elements. A null array counts as empty because OPC 10000-6
diff --git a/tests/Opc.Ua.Types.Tests/BuiltIn/VariantAuditRegressionTests.cs b/tests/Opc.Ua.Types.Tests/BuiltIn/VariantAuditRegressionTests.cs
index c4fa9201ec..3cc734c673 100644
--- a/tests/Opc.Ua.Types.Tests/BuiltIn/VariantAuditRegressionTests.cs
+++ b/tests/Opc.Ua.Types.Tests/BuiltIn/VariantAuditRegressionTests.cs
@@ -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.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
+ {
+ ["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()
{
diff --git a/tests/Opc.Ua.Types.Tests/Encoders/CodecReviewRegressionTests.cs b/tests/Opc.Ua.Types.Tests/Encoders/CodecReviewRegressionTests.cs
index 50be6b73e3..00d14e0e36 100644
--- a/tests/Opc.Ua.Types.Tests/Encoders/CodecReviewRegressionTests.cs
+++ b/tests/Opc.Ua.Types.Tests/Encoders/CodecReviewRegressionTests.cs
@@ -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: