2021-09-20 18:20:01 +02:00

760 lines
23 KiB
C#

/*
Copyright (C) 2014-2019 de4dot@gmail.com
This file is part of dnSpy
dnSpy is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
dnSpy is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with dnSpy. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
namespace dnSpy.Debugger.DotNet.Metadata {
/// <summary>
/// Type and member comparer options
/// </summary>
[Flags]
public enum DmdSigComparerOptions {
/// <summary>
/// No bit is set
/// </summary>
None = 0,
/// <summary>
/// Don't compare type scope (assembly / module)
/// </summary>
DontCompareTypeScope = 1,
/// <summary>
/// Compare declaring type. It's ignored if it's a nested type and only used if it's a field, constructor, method, property, event, parameter
/// </summary>
CompareDeclaringType = 2,
/// <summary>
/// Don't compare return types
/// </summary>
DontCompareReturnType = 4,
/// <summary>
/// Case insensitive member names
/// </summary>
CaseInsensitiveMemberNames = 8,
/// <summary>
/// Project WinMD references
/// </summary>
ProjectWinMDReferences = 0x10,
/// <summary>
/// Check type equivalence
/// </summary>
CheckTypeEquivalence = 0x20,
/// <summary>
/// Compare optional and required C modifiers
/// </summary>
CompareCustomModifiers = 0x40,
/// <summary>
/// Compare generic type/method parameter's declaring member
/// </summary>
CompareGenericParameterDeclaringMember = 0x80,
/// <summary>
/// When comparing types, don't compare a multi-dimensional array's lower bounds and sizes
/// </summary>
IgnoreMultiDimensionalArrayLowerBoundsAndSizes = 0x100,
}
/// <summary>
/// Compares types and members
/// </summary>
public struct DmdSigComparer {
const int HASHCODE_MAGIC_TYPE = -674970533;
const int HASHCODE_MAGIC_NESTED_TYPE = -1049070942;
const int HASHCODE_MAGIC_ET_GENERICINST = -2050514639;
const int HASHCODE_MAGIC_ET_VAR = 1288450097;
const int HASHCODE_MAGIC_ET_MVAR = -990598495;
const int HASHCODE_MAGIC_ET_ARRAY = -96331531;
const int HASHCODE_MAGIC_ET_SZARRAY = 871833535;
const int HASHCODE_MAGIC_ET_BYREF = -634749586;
const int HASHCODE_MAGIC_ET_PTR = 1976400808;
const int HASHCODE_MAGIC_ET_FNPTR = 68439620;
DmdSigComparerOptions options;
const int MAX_RECURSION_COUNT = 100;
int recursionCounter;
bool DontCompareTypeScope => (options & DmdSigComparerOptions.DontCompareTypeScope) != 0;
bool CompareDeclaringType => (options & DmdSigComparerOptions.CompareDeclaringType) != 0;
bool DontCompareReturnType => (options & DmdSigComparerOptions.DontCompareReturnType) != 0;
bool CaseInsensitiveMemberNames => (options & DmdSigComparerOptions.CaseInsensitiveMemberNames) != 0;
//TODO: Use this option
bool ProjectWinMDReferences => (options & DmdSigComparerOptions.ProjectWinMDReferences) != 0;
bool CheckTypeEquivalence => (options & DmdSigComparerOptions.CheckTypeEquivalence) != 0;
bool CompareCustomModifiers => (options & DmdSigComparerOptions.CompareCustomModifiers) != 0;
bool CompareGenericParameterDeclaringMember => (options & DmdSigComparerOptions.CompareGenericParameterDeclaringMember) != 0;
bool IgnoreMultiDimensionalArrayLowerBoundsAndSizes => (options & DmdSigComparerOptions.IgnoreMultiDimensionalArrayLowerBoundsAndSizes) != 0;
/// <summary>
/// Constructor
/// </summary>
/// <param name="options">Options</param>
public DmdSigComparer(DmdSigComparerOptions options) {
this.options = options;
recursionCounter = 0;
}
bool IncrementRecursionCounter() {
if (recursionCounter >= MAX_RECURSION_COUNT)
return false;
recursionCounter++;
return true;
}
void DecrementRecursionCounter() => recursionCounter--;
bool MemberNameEquals(string? a, string? b) {
if (CaseInsensitiveMemberNames)
return StringComparer.OrdinalIgnoreCase.Equals(a, b);
return StringComparer.Ordinal.Equals(a, b);
}
int MemberNameGetHashCode(string? a) {
if (a is null)
return 0;
if (CaseInsensitiveMemberNames)
return StringComparer.OrdinalIgnoreCase.GetHashCode(a);
return StringComparer.Ordinal.GetHashCode(a);
}
/// <summary>
/// Compares two members
/// </summary>
/// <param name="a">First member</param>
/// <param name="b">Second member</param>
/// <returns></returns>
public bool Equals(DmdMemberInfo? a, DmdMemberInfo? b) {
if ((object?)a == b)
return true;
if (a is null || b is null)
return false;
switch (a.MemberType) {
case DmdMemberTypes.TypeInfo:
case DmdMemberTypes.NestedType:
return Equals((DmdType)a, b as DmdType);
case DmdMemberTypes.Field:
return Equals((DmdFieldInfo)a, b as DmdFieldInfo);
case DmdMemberTypes.Method:
case DmdMemberTypes.Constructor:
return Equals((DmdMethodBase)a, b as DmdMethodBase);
case DmdMemberTypes.Property:
return Equals((DmdPropertyInfo)a, b as DmdPropertyInfo);
case DmdMemberTypes.Event:
return Equals((DmdEventInfo)a, b as DmdEventInfo);
}
return false;
}
/// <summary>
/// Compares two types
/// </summary>
/// <param name="a">First type</param>
/// <param name="b">Second type</param>
/// <returns></returns>
public bool Equals(DmdType? a, DmdType? b) {
if ((object?)a == b)
return true;
if (a is null || b is null)
return false;
if (!IncrementRecursionCounter())
return false;
bool result;
var at = a.TypeSignatureKind;
if (at != b.TypeSignatureKind)
result = false;
else if (CompareCustomModifiers && !Equals(a.GetCustomModifiers(), b.GetCustomModifiers()))
result = false;
else {
switch (at) {
case DmdTypeSignatureKind.Type:
result = MemberNameEquals(a.MetadataName, b.MetadataName) &&
MemberNameEquals(a.MetadataNamespace, b.MetadataNamespace) &&
Equals(a.DeclaringType, b.DeclaringType);
// Type scope only needs to be checked if it's a non-nested type
if (result && !DontCompareTypeScope && a.DeclaringType is null) {
result = TypeScopeEquals(a, b);
if (!result) {
// One or both of the types could be exported types. We need to
// resolve them and then compare again.
var ra = a.ResolveNoThrow();
var rb = ra is null ? null : b.ResolveNoThrow();
result = ra is not null && rb is not null && TypeScopeEquals(ra, rb);
if (!result && CheckTypeEquivalence)
result = TIAHelper.Equivalent(ra, rb);
}
}
break;
case DmdTypeSignatureKind.Pointer:
case DmdTypeSignatureKind.ByRef:
case DmdTypeSignatureKind.SZArray:
result = Equals(a.GetElementType(), b.GetElementType());
break;
case DmdTypeSignatureKind.TypeGenericParameter:
result = a.GenericParameterPosition == b.GenericParameterPosition;
if (result && CompareGenericParameterDeclaringMember)
result = Equals(a.DeclaringType, b.DeclaringType);
break;
case DmdTypeSignatureKind.MethodGenericParameter:
result = a.GenericParameterPosition == b.GenericParameterPosition;
if (result && CompareGenericParameterDeclaringMember) {
options &= ~DmdSigComparerOptions.CompareGenericParameterDeclaringMember;
result = Equals(a.DeclaringMethod, b.DeclaringMethod);
options |= DmdSigComparerOptions.CompareGenericParameterDeclaringMember;
}
break;
case DmdTypeSignatureKind.MDArray:
result = a.GetArrayRank() == b.GetArrayRank() &&
(IgnoreMultiDimensionalArrayLowerBoundsAndSizes ||
(Equals(a.GetArraySizes(), b.GetArraySizes()) &&
Equals(a.GetArrayLowerBounds(), b.GetArrayLowerBounds()))) &&
Equals(a.GetElementType(), b.GetElementType());
break;
case DmdTypeSignatureKind.GenericInstance:
result = Equals(a.GetGenericTypeDefinition(), b.GetGenericTypeDefinition()) &&
Equals(a.GetGenericArguments(), b.GetGenericArguments());
break;
case DmdTypeSignatureKind.FunctionPointer:
result = Equals(a.GetFunctionPointerMethodSignature(), b.GetFunctionPointerMethodSignature());
break;
default: throw new InvalidOperationException();
}
}
DecrementRecursionCounter();
return result;
}
/// <summary>
/// Compares two fields
/// </summary>
/// <param name="a">First field</param>
/// <param name="b">Second field</param>
/// <returns></returns>
public bool Equals(DmdFieldInfo? a, DmdFieldInfo? b) {
if ((object?)a == b)
return true;
if (a is null || b is null)
return false;
return MemberNameEquals(a.Name, b.Name) &&
Equals(a.FieldType, b.FieldType) &&
(!CompareDeclaringType || Equals(a.DeclaringType, b.DeclaringType));
}
/// <summary>
/// Compares two methods or constructors
/// </summary>
/// <param name="a">First method or constructor</param>
/// <param name="b">Second method or constructor</param>
/// <returns></returns>
public bool Equals(DmdMethodBase? a, DmdMethodBase? b) {
if ((object?)a == b)
return true;
if (a is null || b is null)
return false;
return MemberNameEquals(a.Name, b.Name) &&
Equals(a.GetMethodSignature(), b.GetMethodSignature()) &&
(!CompareDeclaringType || Equals(a.DeclaringType, b.DeclaringType));
}
/// <summary>
/// Compares two properties
/// </summary>
/// <param name="a">First property</param>
/// <param name="b">Second property</param>
/// <returns></returns>
public bool Equals(DmdPropertyInfo? a, DmdPropertyInfo? b) {
if ((object?)a == b)
return true;
if (a is null || b is null)
return false;
return MemberNameEquals(a.Name, b.Name) &&
Equals(a.GetMethodSignature(), b.GetMethodSignature()) &&
(!CompareDeclaringType || Equals(a.DeclaringType, b.DeclaringType));
}
/// <summary>
/// Compares two events
/// </summary>
/// <param name="a">First event</param>
/// <param name="b">Second event</param>
/// <returns></returns>
public bool Equals(DmdEventInfo? a, DmdEventInfo? b) {
if ((object?)a == b)
return true;
if (a is null || b is null)
return false;
return MemberNameEquals(a.Name, b.Name) &&
Equals(a.EventHandlerType, b.EventHandlerType) &&
(!CompareDeclaringType || Equals(a.DeclaringType, b.DeclaringType));
}
/// <summary>
/// Compares two method parameters
/// </summary>
/// <param name="a">First method parameter</param>
/// <param name="b">Second method parameter</param>
/// <returns></returns>
public bool Equals(DmdParameterInfo? a, DmdParameterInfo? b) {
if ((object?)a == b)
return true;
if (a is null || b is null)
return false;
return a.Position == b.Position &&
Equals(a.ParameterType, b.ParameterType) &&
(!CompareDeclaringType || Equals(a.Member, b.Member));
}
/// <summary>
/// Compares two assembly names
/// </summary>
/// <param name="a">First assembly name</param>
/// <param name="b">Second assembly name</param>
/// <returns></returns>
public bool Equals(IDmdAssemblyName? a, IDmdAssemblyName? b) {
if ((object?)a == b)
return true;
if (a is null || b is null)
return false;
// We do not compare the version number. The runtime can redirect an assembly
// reference from a requested version to any other version.
// The public key token is also ignored. Only .NET Framwork checks it (.NET
// and Unity ignore it). We could add a new option to ignore the PKT but it would
// require too many changes to the code (they access singleton comparers) and isn't
// worth it. It's also being replaced by .NET. It's not common for two
// assemblies loaded in the same process to have the same assembly name but a
// different public key token.
const DmdAssemblyNameFlags flagsMask = DmdAssemblyNameFlags.ContentType_Mask;
return (a.RawFlags & flagsMask) == (b.RawFlags & flagsMask) &&
StringComparer.OrdinalIgnoreCase.Equals(a.Name, b.Name) &&
StringComparer.OrdinalIgnoreCase.Equals(a.CultureName ?? string.Empty, b.CultureName ?? string.Empty);
}
/// <summary>
/// Compares two method signatures
/// </summary>
/// <param name="a">First method signature</param>
/// <param name="b">Second method signature</param>
/// <returns></returns>
public bool Equals(DmdMethodSignature? a, DmdMethodSignature? b) {
if ((object?)a == b)
return true;
if (a is null || b is null)
return false;
return a.Flags == b.Flags &&
a.GenericParameterCount == b.GenericParameterCount &&
(DontCompareReturnType || Equals(a.ReturnType, b.ReturnType)) &&
Equals(a.GetParameterTypes(), b.GetParameterTypes()) &&
Equals(a.GetVarArgsParameterTypes(), b.GetVarArgsParameterTypes());
}
/// <summary>
/// Compares two custom modifiers
/// </summary>
/// <param name="a">First custom modifier</param>
/// <param name="b">Second custom modifier</param>
/// <returns></returns>
public bool Equals(DmdCustomModifier a, DmdCustomModifier b) => a.IsRequired == b.IsRequired && Equals(a.Type, b.Type);
bool Equals(ReadOnlyCollection<DmdCustomModifier>? a, ReadOnlyCollection<DmdCustomModifier>? b) {
if (a == b)
return true;
if (a is null || b is null)
return false;
if (a.Count != b.Count)
return false;
for (int i = 0; i < a.Count; i++) {
if (!Equals(a[i], b[i]))
return false;
}
return true;
}
bool Equals(ReadOnlyCollection<DmdType>? a, ReadOnlyCollection<DmdType>? b) {
if (a == b)
return true;
if (a is null || b is null)
return false;
if (a.Count != b.Count)
return false;
for (int i = 0; i < a.Count; i++) {
if (!Equals(a[i], b[i]))
return false;
}
return true;
}
bool Equals(ReadOnlyCollection<int>? a, ReadOnlyCollection<int>? b) {
if (a == b)
return true;
if (a is null || b is null)
return false;
if (a.Count != b.Count)
return false;
for (int i = 0; i < a.Count; i++) {
if (a[i] != b[i])
return false;
}
return true;
}
bool TypeScopeEquals(DmdType a, DmdType b) {
Debug2.Assert(a is not null && b is not null && !a.HasElementType && !b.HasElementType);
if (DontCompareTypeScope)
return true;
if ((object?)a == b)
return true;
var at = a.TypeScope;
var bt = b.TypeScope;
switch (at.Kind) {
case DmdTypeScopeKind.Invalid:
return false;
case DmdTypeScopeKind.Module:
switch (bt.Kind) {
case DmdTypeScopeKind.Invalid:
return false;
case DmdTypeScopeKind.Module:
return at.Data == bt.Data;
case DmdTypeScopeKind.ModuleRef:
return StringComparer.OrdinalIgnoreCase.Equals(((DmdModule)at.Data!).ScopeName, (string)bt.Data!) &&
Equals(((DmdModule)at.Data).Assembly.GetName(), (IDmdAssemblyName)bt.Data2!);
case DmdTypeScopeKind.AssemblyRef:
return Equals(((DmdModule)at.Data!).Assembly.GetName(), (IDmdAssemblyName)bt.Data!);
default:
throw new InvalidOperationException();
}
case DmdTypeScopeKind.ModuleRef:
switch (bt.Kind) {
case DmdTypeScopeKind.Invalid:
return false;
case DmdTypeScopeKind.Module:
return StringComparer.OrdinalIgnoreCase.Equals((string)at.Data!, ((DmdModule)bt.Data!).ScopeName) &&
Equals((IDmdAssemblyName)at.Data2!, ((DmdModule)bt.Data).Assembly.GetName());
case DmdTypeScopeKind.ModuleRef:
return StringComparer.OrdinalIgnoreCase.Equals((string)at.Data!, (string)bt.Data!) &&
Equals((IDmdAssemblyName)at.Data2!, (IDmdAssemblyName)bt.Data2!);
case DmdTypeScopeKind.AssemblyRef:
return Equals((IDmdAssemblyName)at.Data2!, (IDmdAssemblyName)bt.Data!);
default:
throw new InvalidOperationException();
}
case DmdTypeScopeKind.AssemblyRef:
switch (bt.Kind) {
case DmdTypeScopeKind.Invalid:
return false;
case DmdTypeScopeKind.Module:
return Equals((IDmdAssemblyName)at.Data!, ((DmdModule)bt.Data!).Assembly.GetName());
case DmdTypeScopeKind.ModuleRef:
return Equals((IDmdAssemblyName)at.Data!, (IDmdAssemblyName)bt.Data2!);
case DmdTypeScopeKind.AssemblyRef:
return Equals((IDmdAssemblyName)at.Data!, (IDmdAssemblyName)bt.Data!);
default:
throw new InvalidOperationException();
}
default:
throw new InvalidOperationException();
}
}
/// <summary>
/// Gets the hash code of a member
/// </summary>
/// <param name="a">Member</param>
/// <returns></returns>
public int GetHashCode(DmdMemberInfo? a) {
if (a is null)
return 0;
switch (a.MemberType) {
case DmdMemberTypes.TypeInfo:
case DmdMemberTypes.NestedType:
return GetHashCode((DmdType)a);
case DmdMemberTypes.Field:
return GetHashCode((DmdFieldInfo)a);
case DmdMemberTypes.Method:
case DmdMemberTypes.Constructor:
return GetHashCode((DmdMethodBase)a);
case DmdMemberTypes.Property:
return GetHashCode((DmdPropertyInfo)a);
case DmdMemberTypes.Event:
return GetHashCode((DmdEventInfo)a);
}
Debug.Fail($"Unknown type: {a.GetType()}");
return 0;
}
/// <summary>
/// Gets the hash code of a type
/// </summary>
/// <param name="a">Type</param>
/// <returns></returns>
public int GetHashCode(DmdType? a) {
if (a is null)
return 0;
if (!IncrementRecursionCounter())
return 0;
int hc = CompareCustomModifiers ? GetHashCode(a.GetCustomModifiers()) : 0;
switch (a.TypeSignatureKind) {
case DmdTypeSignatureKind.Type:
hc ^= a.DeclaringType is null ? HASHCODE_MAGIC_TYPE : HASHCODE_MAGIC_NESTED_TYPE;
hc ^= MemberNameGetHashCode(a.MetadataName);
hc ^= MemberNameGetHashCode(a.MetadataNamespace);
hc ^= GetHashCode(a.DeclaringType);
// Don't include the type scope in the hash since it can be one of Module, ModuleRef, AssemblyRef
// and we could only hash the common denominator which isn't much at all.
break;
case DmdTypeSignatureKind.Pointer:
hc ^= HASHCODE_MAGIC_ET_PTR ^ GetHashCode(a.GetElementType());
break;
case DmdTypeSignatureKind.ByRef:
hc ^= HASHCODE_MAGIC_ET_BYREF ^ GetHashCode(a.GetElementType());
break;
case DmdTypeSignatureKind.SZArray:
hc ^= HASHCODE_MAGIC_ET_SZARRAY ^ GetHashCode(a.GetElementType());
break;
case DmdTypeSignatureKind.TypeGenericParameter:
hc ^= HASHCODE_MAGIC_ET_VAR ^ a.GenericParameterPosition;
if (CompareGenericParameterDeclaringMember)
hc ^= GetHashCode(a.DeclaringType);
break;
case DmdTypeSignatureKind.MethodGenericParameter:
hc ^= HASHCODE_MAGIC_ET_MVAR ^ a.GenericParameterPosition;
if (CompareGenericParameterDeclaringMember) {
options &= ~DmdSigComparerOptions.CompareGenericParameterDeclaringMember;
hc ^= GetHashCode(a.DeclaringMethod);
options |= DmdSigComparerOptions.CompareGenericParameterDeclaringMember;
}
break;
case DmdTypeSignatureKind.MDArray:
hc ^= HASHCODE_MAGIC_ET_ARRAY;
hc ^= a.GetArrayRank();
if (!IgnoreMultiDimensionalArrayLowerBoundsAndSizes) {
hc ^= GetHashCode(a.GetArraySizes());
hc ^= GetHashCode(a.GetArrayLowerBounds());
}
hc ^= GetHashCode(a.GetElementType());
break;
case DmdTypeSignatureKind.GenericInstance:
hc ^= HASHCODE_MAGIC_ET_GENERICINST;
hc ^= GetHashCode(a.GetGenericTypeDefinition());
hc ^= GetHashCode(a.GetGenericArguments());
break;
case DmdTypeSignatureKind.FunctionPointer:
hc ^= HASHCODE_MAGIC_ET_FNPTR;
hc ^= GetHashCode(a.GetFunctionPointerMethodSignature());
break;
default: throw new InvalidOperationException();
}
DecrementRecursionCounter();
return hc;
}
/// <summary>
/// Gets the hash code of a field
/// </summary>
/// <param name="a">Field</param>
/// <returns></returns>
public int GetHashCode(DmdFieldInfo? a) {
if (a is null)
return 0;
int hc = MemberNameGetHashCode(a.Name);
hc ^= GetHashCode(a.FieldType);
if (CompareDeclaringType)
hc ^= GetHashCode(a.DeclaringType);
return hc;
}
/// <summary>
/// Gets the hash code of a method or constructor
/// </summary>
/// <param name="a">Method or constructor</param>
/// <returns></returns>
public int GetHashCode(DmdMethodBase? a) {
if (a is null)
return 0;
int hc = MemberNameGetHashCode(a.Name);
hc ^= GetHashCode(a.GetMethodSignature());
if (CompareDeclaringType)
hc ^= GetHashCode(a.DeclaringType);
return hc;
}
/// <summary>
/// Gets the hash code of a property
/// </summary>
/// <param name="a">Property</param>
/// <returns></returns>
public int GetHashCode(DmdPropertyInfo? a) {
if (a is null)
return 0;
int hc = MemberNameGetHashCode(a.Name);
hc ^= GetHashCode(a.GetMethodSignature());
if (CompareDeclaringType)
hc ^= GetHashCode(a.DeclaringType);
return hc;
}
/// <summary>
/// Gets the hash code of an event
/// </summary>
/// <param name="a">Event</param>
/// <returns></returns>
public int GetHashCode(DmdEventInfo? a) {
if (a is null)
return 0;
int hc = MemberNameGetHashCode(a.Name);
hc ^= GetHashCode(a.EventHandlerType);
if (CompareDeclaringType)
hc ^= GetHashCode(a.DeclaringType);
return hc;
}
/// <summary>
/// Gets the hash code of a method parameter
/// </summary>
/// <param name="a">Method parameter</param>
/// <returns></returns>
public int GetHashCode(DmdParameterInfo? a) {
if (a is null)
return 0;
int hc = a.Position;
hc ^= GetHashCode(a.ParameterType);
if (CompareDeclaringType)
hc ^= GetHashCode(a.Member);
return hc;
}
/// <summary>
/// Gets the hash code of an assembly name
/// </summary>
/// <param name="a">Assembly name</param>
/// <returns></returns>
public int GetHashCode(IDmdAssemblyName? a) {
if (a is null)
return 0;
return StringComparer.OrdinalIgnoreCase.GetHashCode(a.Name ?? string.Empty);
}
/// <summary>
/// Gets the hash code of a method signature
/// </summary>
/// <param name="a">Method signature</param>
/// <returns></returns>
public int GetHashCode(DmdMethodSignature? a) {
if (a is null)
return 0;
int hc = (int)a.Flags;
hc ^= a.GenericParameterCount;
if (!DontCompareReturnType)
hc ^= GetHashCode(a.ReturnType);
hc ^= GetHashCode(a.GetParameterTypes());
hc ^= GetHashCode(a.GetVarArgsParameterTypes());
return hc;
}
/// <summary>
/// Gets the hash code of a custom modifier
/// </summary>
/// <param name="a">Custom modifier</param>
/// <returns></returns>
public int GetHashCode(DmdCustomModifier a) => (a.IsRequired ? -1 : 0) ^ GetHashCode(a.Type);
int GetHashCode(ReadOnlyCollection<DmdCustomModifier>? a) {
if (a is null)
return 0;
int hc = a.Count;
for (int i = 0; i < a.Count; i++)
hc ^= GetHashCode(a[i]);
return hc;
}
int GetHashCode(ReadOnlyCollection<DmdType>? a) {
if (a is null)
return 0;
int hc = a.Count;
for (int i = 0; i < a.Count; i++)
hc ^= GetHashCode(a[i]);
return hc;
}
int GetHashCode(ReadOnlyCollection<int>? a) {
if (a is null)
return 0;
int hc = a.Count;
for (int i = 0; i < a.Count; i++)
hc ^= a[i];
return hc;
}
}
}