diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs index c08031b58d63d9..df84f8b84bd29a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs @@ -22,8 +22,12 @@ public class Dictionary : IDictionary, IDictionary, private const string KeyValuePairsName = "KeyValuePairs"; // Do not rename (binary serialization) private const string ComparerName = "Comparer"; // Do not rename (binary serialization) - private int[]? _buckets; - private Entry[]? _entries; +#pragma warning disable CA1825 // avoid the extra generic instantiation for Array.Empty() + private static readonly Entry[] s_emptyArray = new Entry[0]; +#pragma warning restore CA1825 + + private int[] _buckets; + private Entry[] _entries; #if TARGET_64BIT private ulong _fastModMultiplier; #endif @@ -51,8 +55,14 @@ public Dictionary(int capacity, IEqualityComparer? comparer) if (capacity > 0) { - Initialize(capacity); + Initialize(HashHelpers.GetPrime(capacity)); } + else + { + InitializeEmpty(); + } + Debug.Assert(_buckets != null); + Debug.Assert(_entries != null); if (comparer is not null && comparer != EqualityComparer.Default) // first check for null to avoid forcing default comparer instantiation unnecessarily { @@ -116,8 +126,6 @@ private void AddRange(IEnumerable> collection) // This is not currently a true .AddRange as it needs to be an initalized dictionary // of the correct size, and also an empty dictionary with no current entities (and no argument checks). - Debug.Assert(source._entries is not null); - Debug.Assert(_entries is not null); Debug.Assert(_entries.Length >= source.Count); Debug.Assert(_count == 0); @@ -155,6 +163,9 @@ protected Dictionary(SerializationInfo info, StreamingContext context) // and we have a resonable estimate that GetHashCode is not going to fail. For the time being, // we'll just cache this. The graph is not valid until OnDeserialization has been called. HashHelpers.SerializationInfoTable.Add(this, info); + InitializeEmpty(); + Debug.Assert(_buckets != null); + Debug.Assert(_entries != null); } public IEqualityComparer Comparer @@ -243,9 +254,6 @@ public void Clear() int count = _count; if (count > 0) { - Debug.Assert(_buckets != null, "_buckets should be non-null"); - Debug.Assert(_entries != null, "_entries should be non-null"); - Array.Clear(_buckets); _count = 0; @@ -260,12 +268,12 @@ public bool ContainsKey(TKey key) => public bool ContainsValue(TValue value) { - Entry[]? entries = _entries; + Entry[] entries = _entries; if (value == null) { for (int i = 0; i < _count; i++) { - if (entries![i].next >= -1 && entries[i].value == null) + if (entries[i].next >= -1 && entries[i].value == null) { return true; } @@ -276,7 +284,7 @@ public bool ContainsValue(TValue value) // ValueType: Devirtualize with EqualityComparer.Default intrinsic for (int i = 0; i < _count; i++) { - if (entries![i].next >= -1 && EqualityComparer.Default.Equals(entries[i].value, value)) + if (entries[i].next >= -1 && EqualityComparer.Default.Equals(entries[i].value, value)) { return true; } @@ -290,7 +298,7 @@ public bool ContainsValue(TValue value) EqualityComparer defaultComparer = EqualityComparer.Default; for (int i = 0; i < _count; i++) { - if (entries![i].next >= -1 && defaultComparer.Equals(entries[i].value, value)) + if (entries[i].next >= -1 && defaultComparer.Equals(entries[i].value, value)) { return true; } @@ -318,10 +326,10 @@ private void CopyTo(KeyValuePair[] array, int index) } int count = _count; - Entry[]? entries = _entries; + Entry[] entries = _entries; for (int i = 0; i < count; i++) { - if (entries![i].next >= -1) + if (entries[i].next >= -1) { array[index++] = new KeyValuePair(entries[i].key, entries[i].value); } @@ -342,9 +350,9 @@ public virtual void GetObjectData(SerializationInfo info, StreamingContext conte info.AddValue(VersionName, _version); info.AddValue(ComparerName, Comparer, typeof(IEqualityComparer)); - info.AddValue(HashSizeName, _buckets == null ? 0 : _buckets.Length); // This is the length of the bucket array + info.AddValue(HashSizeName, _entries.Length); // This is the length of the entry array - if (_buckets != null) + if (_entries.Length > 0) { var array = new KeyValuePair[Count]; CopyTo(array, 0); @@ -360,84 +368,49 @@ internal ref TValue FindValue(TKey key) } ref Entry entry = ref Unsafe.NullRef(); - if (_buckets != null) + IEqualityComparer? comparer = _comparer; + if (comparer == null) { - Debug.Assert(_entries != null, "expected entries to be != null"); - IEqualityComparer? comparer = _comparer; - if (comparer == null) + uint hashCode = (uint)key.GetHashCode(); + int i = GetBucket(hashCode); + Entry[] entries = _entries; + uint collisionCount = 0; + if (typeof(TKey).IsValueType) { - uint hashCode = (uint)key.GetHashCode(); - int i = GetBucket(hashCode); - Entry[]? entries = _entries; - uint collisionCount = 0; - if (typeof(TKey).IsValueType) - { - // ValueType: Devirtualize with EqualityComparer.Default intrinsic - - i--; // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional. - do - { - // Should be a while loop https://github.com/dotnet/runtime/issues/9422 - // Test in if to drop range check for following array access - if ((uint)i >= (uint)entries.Length) - { - goto ReturnNotFound; - } - - entry = ref entries[i]; - if (entry.hashCode == hashCode && EqualityComparer.Default.Equals(entry.key, key)) - { - goto ReturnFound; - } - - i = entry.next; - - collisionCount++; - } while (collisionCount <= (uint)entries.Length); + // ValueType: Devirtualize with EqualityComparer.Default intrinsic - // The chain of entries forms a loop; which means a concurrent update has happened. - // Break out of the loop and throw, rather than looping forever. - goto ConcurrentOperation; - } - else + i--; // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional. + do { - // Object type: Shared Generic, EqualityComparer.Default won't devirtualize - // https://github.com/dotnet/runtime/issues/10050 - // So cache in a local rather than get EqualityComparer per loop iteration - EqualityComparer defaultComparer = EqualityComparer.Default; - - i--; // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional. - do + // Should be a while loop https://github.com/dotnet/runtime/issues/9422 + // Test in if to drop range check for following array access + if ((uint)i >= (uint)entries.Length) { - // Should be a while loop https://github.com/dotnet/runtime/issues/9422 - // Test in if to drop range check for following array access - if ((uint)i >= (uint)entries.Length) - { - goto ReturnNotFound; - } + goto ReturnNotFound; + } - entry = ref entries[i]; - if (entry.hashCode == hashCode && defaultComparer.Equals(entry.key, key)) - { - goto ReturnFound; - } + entry = ref entries[i]; + if (entry.hashCode == hashCode && EqualityComparer.Default.Equals(entry.key, key)) + { + goto ReturnFound; + } - i = entry.next; + i = entry.next; - collisionCount++; - } while (collisionCount <= (uint)entries.Length); + collisionCount++; + } while (collisionCount <= (uint)entries.Length); - // The chain of entries forms a loop; which means a concurrent update has happened. - // Break out of the loop and throw, rather than looping forever. - goto ConcurrentOperation; - } + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + goto ConcurrentOperation; } else { - uint hashCode = (uint)comparer.GetHashCode(key); - int i = GetBucket(hashCode); - Entry[]? entries = _entries; - uint collisionCount = 0; + // Object type: Shared Generic, EqualityComparer.Default won't devirtualize + // https://github.com/dotnet/runtime/issues/10050 + // So cache in a local rather than get EqualityComparer per loop iteration + EqualityComparer defaultComparer = EqualityComparer.Default; + i--; // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional. do { @@ -449,7 +422,7 @@ internal ref TValue FindValue(TKey key) } entry = ref entries[i]; - if (entry.hashCode == hashCode && comparer.Equals(entry.key, key)) + if (entry.hashCode == hashCode && defaultComparer.Equals(entry.key, key)) { goto ReturnFound; } @@ -464,8 +437,37 @@ internal ref TValue FindValue(TKey key) goto ConcurrentOperation; } } + else + { + uint hashCode = (uint)comparer.GetHashCode(key); + int i = GetBucket(hashCode); + Entry[] entries = _entries; + uint collisionCount = 0; + i--; // Value in _buckets is 1-based; subtract 1 from i. We do it here so it fuses with the following conditional. + do + { + // Should be a while loop https://github.com/dotnet/runtime/issues/9422 + // Test in if to drop range check for following array access + if ((uint)i >= (uint)entries.Length) + { + goto ReturnNotFound; + } - goto ReturnNotFound; + entry = ref entries[i]; + if (entry.hashCode == hashCode && comparer.Equals(entry.key, key)) + { + goto ReturnFound; + } + + i = entry.next; + + collisionCount++; + } while (collisionCount <= (uint)entries.Length); + + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + goto ConcurrentOperation; + } ConcurrentOperation: ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); @@ -478,9 +480,8 @@ internal ref TValue FindValue(TKey key) goto Return; } - private int Initialize(int capacity) + private void Initialize(int size) { - int size = HashHelpers.GetPrime(capacity); int[] buckets = new int[size]; Entry[] entries = new Entry[size]; @@ -491,8 +492,16 @@ private int Initialize(int capacity) #endif _buckets = buckets; _entries = entries; + } - return size; + private void InitializeEmpty() + { + _freeList = -1; +#if TARGET_64BIT + _fastModMultiplier = HashHelpers.FastModMultiplierDivisor1; +#endif + _buckets = HashHelpers.SizeOneIntArray; + _entries = s_emptyArray; } private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) @@ -502,14 +511,7 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } - if (_buckets == null) - { - Initialize(0); - } - Debug.Assert(_buckets != null); - - Entry[]? entries = _entries; - Debug.Assert(entries != null, "expected entries to be non-null"); + Entry[] entries = _entries; IEqualityComparer? comparer = _comparer; uint hashCode = (uint)((comparer == null) ? key.GetHashCode() : comparer.GetHashCode(key)); @@ -656,13 +658,13 @@ private bool TryInsert(TKey key, TValue value, InsertionBehavior behavior) { Resize(); bucket = ref GetBucket(hashCode); + entries = _entries; } index = count; _count = count + 1; - entries = _entries; } - ref Entry entry = ref entries![index]; + ref Entry entry = ref entries[index]; entry.hashCode = hashCode; entry.next = bucket - 1; // Value in _buckets is 1-based entry.key = key; @@ -698,7 +700,7 @@ public virtual void OnDeserialization(object? sender) if (hashsize != 0) { - Initialize(hashsize); + Initialize(HashHelpers.GetPrime(hashsize)); KeyValuePair[]? array = (KeyValuePair[]?) siInfo.GetValue(KeyValuePairsName, typeof(KeyValuePair[])); @@ -720,7 +722,7 @@ public virtual void OnDeserialization(object? sender) } else { - _buckets = null; + InitializeEmpty(); } _version = realVersion; @@ -733,7 +735,6 @@ private void Resize(int newSize, bool forceNewHashCodes) { // Value types never rehash Debug.Assert(!forceNewHashCodes || !typeof(TKey).IsValueType); - Debug.Assert(_entries != null, "_entries should be non-null"); Debug.Assert(newSize >= _entries.Length); Entry[] entries = new Entry[newSize]; @@ -789,58 +790,54 @@ public bool Remove(TKey key) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } - if (_buckets != null) + uint collisionCount = 0; + uint hashCode = (uint)(_comparer?.GetHashCode(key) ?? key.GetHashCode()); + ref int bucket = ref GetBucket(hashCode); + Entry[] entries = _entries; + int last = -1; + int i = bucket - 1; // Value in buckets is 1-based + while (i >= 0) { - Debug.Assert(_entries != null, "entries should be non-null"); - uint collisionCount = 0; - uint hashCode = (uint)(_comparer?.GetHashCode(key) ?? key.GetHashCode()); - ref int bucket = ref GetBucket(hashCode); - Entry[]? entries = _entries; - int last = -1; - int i = bucket - 1; // Value in buckets is 1-based - while (i >= 0) - { - ref Entry entry = ref entries[i]; + ref Entry entry = ref entries[i]; - if (entry.hashCode == hashCode && (_comparer?.Equals(entry.key, key) ?? EqualityComparer.Default.Equals(entry.key, key))) + if (entry.hashCode == hashCode && (_comparer?.Equals(entry.key, key) ?? EqualityComparer.Default.Equals(entry.key, key))) + { + if (last < 0) { - if (last < 0) - { - bucket = entry.next + 1; // Value in buckets is 1-based - } - else - { - entries[last].next = entry.next; - } - - Debug.Assert((StartOfFreeList - _freeList) < 0, "shouldn't underflow because max hashtable length is MaxPrimeArrayLength = 0x7FEFFFFD(2146435069) _freelist underflow threshold 2147483646"); - entry.next = StartOfFreeList - _freeList; - - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - entry.key = default!; - } + bucket = entry.next + 1; // Value in buckets is 1-based + } + else + { + entries[last].next = entry.next; + } - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - entry.value = default!; - } + Debug.Assert((StartOfFreeList - _freeList) < 0, "shouldn't underflow because max hashtable length is MaxPrimeArrayLength = 0x7FEFFFFD(2146435069) _freelist underflow threshold 2147483646"); + entry.next = StartOfFreeList - _freeList; - _freeList = i; - _freeCount++; - return true; + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + entry.key = default!; } - last = i; - i = entry.next; - - collisionCount++; - if (collisionCount > (uint)entries.Length) + if (RuntimeHelpers.IsReferenceOrContainsReferences()) { - // The chain of entries forms a loop; which means a concurrent update has happened. - // Break out of the loop and throw, rather than looping forever. - ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + entry.value = default!; } + + _freeList = i; + _freeCount++; + return true; + } + + last = i; + i = entry.next; + + collisionCount++; + if (collisionCount > (uint)entries.Length) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); } } return false; @@ -857,60 +854,56 @@ public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } - if (_buckets != null) + uint collisionCount = 0; + uint hashCode = (uint)(_comparer?.GetHashCode(key) ?? key.GetHashCode()); + ref int bucket = ref GetBucket(hashCode); + Entry[] entries = _entries; + int last = -1; + int i = bucket - 1; // Value in buckets is 1-based + while (i >= 0) { - Debug.Assert(_entries != null, "entries should be non-null"); - uint collisionCount = 0; - uint hashCode = (uint)(_comparer?.GetHashCode(key) ?? key.GetHashCode()); - ref int bucket = ref GetBucket(hashCode); - Entry[]? entries = _entries; - int last = -1; - int i = bucket - 1; // Value in buckets is 1-based - while (i >= 0) - { - ref Entry entry = ref entries[i]; + ref Entry entry = ref entries[i]; - if (entry.hashCode == hashCode && (_comparer?.Equals(entry.key, key) ?? EqualityComparer.Default.Equals(entry.key, key))) + if (entry.hashCode == hashCode && (_comparer?.Equals(entry.key, key) ?? EqualityComparer.Default.Equals(entry.key, key))) + { + if (last < 0) { - if (last < 0) - { - bucket = entry.next + 1; // Value in buckets is 1-based - } - else - { - entries[last].next = entry.next; - } - - value = entry.value; - - Debug.Assert((StartOfFreeList - _freeList) < 0, "shouldn't underflow because max hashtable length is MaxPrimeArrayLength = 0x7FEFFFFD(2146435069) _freelist underflow threshold 2147483646"); - entry.next = StartOfFreeList - _freeList; + bucket = entry.next + 1; // Value in buckets is 1-based + } + else + { + entries[last].next = entry.next; + } - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - entry.key = default!; - } + value = entry.value; - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - entry.value = default!; - } + Debug.Assert((StartOfFreeList - _freeList) < 0, "shouldn't underflow because max hashtable length is MaxPrimeArrayLength = 0x7FEFFFFD(2146435069) _freelist underflow threshold 2147483646"); + entry.next = StartOfFreeList - _freeList; - _freeList = i; - _freeCount++; - return true; + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + entry.key = default!; } - last = i; - i = entry.next; - - collisionCount++; - if (collisionCount > (uint)entries.Length) + if (RuntimeHelpers.IsReferenceOrContainsReferences()) { - // The chain of entries forms a loop; which means a concurrent update has happened. - // Break out of the loop and throw, rather than looping forever. - ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + entry.value = default!; } + + _freeList = i; + _freeCount++; + return true; + } + + last = i; + i = entry.next; + + collisionCount++; + if (collisionCount > (uint)entries.Length) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); } } @@ -972,10 +965,10 @@ void ICollection.CopyTo(Array array, int index) } else if (array is DictionaryEntry[] dictEntryArray) { - Entry[]? entries = _entries; + Entry[] entries = _entries; for (int i = 0; i < _count; i++) { - if (entries![i].next >= -1) + if (entries[i].next >= -1) { dictEntryArray[index++] = new DictionaryEntry(entries[i].key, entries[i].value); } @@ -992,10 +985,10 @@ void ICollection.CopyTo(Array array, int index) try { int count = _count; - Entry[]? entries = _entries; + Entry[] entries = _entries; for (int i = 0; i < count; i++) { - if (entries![i].next >= -1) + if (entries[i].next >= -1) { objects[index++] = new KeyValuePair(entries[i].key, entries[i].value); } @@ -1020,7 +1013,7 @@ public int EnsureCapacity(int capacity) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity); } - int currentCapacity = _entries == null ? 0 : _entries.Length; + int currentCapacity = _entries.Length; if (currentCapacity >= capacity) { return currentCapacity; @@ -1028,11 +1021,6 @@ public int EnsureCapacity(int capacity) _version++; - if (_buckets == null) - { - return Initialize(capacity); - } - int newSize = HashHelpers.GetPrime(capacity); Resize(newSize, forceNewHashCodes: false); return newSize; @@ -1067,9 +1055,8 @@ public void TrimExcess(int capacity) } int newSize = HashHelpers.GetPrime(capacity); - Entry[]? oldEntries = _entries; - int currentCapacity = oldEntries == null ? 0 : oldEntries.Length; - if (newSize >= currentCapacity) + Entry[] oldEntries = _entries; + if (newSize >= oldEntries.Length) { return; } @@ -1078,15 +1065,11 @@ public void TrimExcess(int capacity) _version++; Initialize(newSize); - Debug.Assert(oldEntries is not null); - CopyEntries(oldEntries, oldCount); } private void CopyEntries(Entry[] entries, int count) { - Debug.Assert(_entries is not null); - Entry[] newEntries = _entries; int newCount = 0; for (int i = 0; i < count; i++) @@ -1220,7 +1203,7 @@ void IDictionary.Remove(object key) [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref int GetBucket(uint hashCode) { - int[] buckets = _buckets!; + int[] buckets = _buckets; #if TARGET_64BIT return ref buckets[HashHelpers.FastMod(hashCode, (uint)buckets.Length, _fastModMultiplier)]; #else @@ -1272,7 +1255,7 @@ public bool MoveNext() // dictionary.count+1 could be negative if dictionary.count is int.MaxValue while ((uint)_index < (uint)_dictionary._count) { - ref Entry entry = ref _dictionary._entries![_index++]; + ref Entry entry = ref _dictionary._entries[_index++]; if (entry.next >= -1) { @@ -1395,10 +1378,10 @@ public void CopyTo(TKey[] array, int index) } int count = _dictionary._count; - Entry[]? entries = _dictionary._entries; + Entry[] entries = _dictionary._entries; for (int i = 0; i < count; i++) { - if (entries![i].next >= -1) array[index++] = entries[i].key; + if (entries[i].next >= -1) array[index++] = entries[i].key; } } @@ -1465,12 +1448,12 @@ void ICollection.CopyTo(Array array, int index) } int count = _dictionary._count; - Entry[]? entries = _dictionary._entries; + Entry[] entries = _dictionary._entries; try { for (int i = 0; i < count; i++) { - if (entries![i].next >= -1) objects[index++] = entries[i].key; + if (entries[i].next >= -1) objects[index++] = entries[i].key; } } catch (ArrayTypeMismatchException) @@ -1510,7 +1493,7 @@ public bool MoveNext() while ((uint)_index < (uint)_dictionary._count) { - ref Entry entry = ref _dictionary._entries![_index++]; + ref Entry entry = ref _dictionary._entries[_index++]; if (entry.next >= -1) { @@ -1588,10 +1571,10 @@ public void CopyTo(TValue[] array, int index) } int count = _dictionary._count; - Entry[]? entries = _dictionary._entries; + Entry[] entries = _dictionary._entries; for (int i = 0; i < count; i++) { - if (entries![i].next >= -1) array[index++] = entries[i].value; + if (entries[i].next >= -1) array[index++] = entries[i].value; } } @@ -1657,12 +1640,12 @@ void ICollection.CopyTo(Array array, int index) } int count = _dictionary._count; - Entry[]? entries = _dictionary._entries; + Entry[] entries = _dictionary._entries; try { for (int i = 0; i < count; i++) { - if (entries![i].next >= -1) objects[index++] = entries[i].value!; + if (entries[i].next >= -1) objects[index++] = entries[i].value!; } } catch (ArrayTypeMismatchException) @@ -1702,7 +1685,7 @@ public bool MoveNext() while ((uint)_index < (uint)_dictionary._count) { - ref Entry entry = ref _dictionary._entries![_index++]; + ref Entry entry = ref _dictionary._entries[_index++]; if (entry.next >= -1) { diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs index d06b27ea6f7d9b..80d6477f610e7c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/HashSet.cs @@ -27,6 +27,10 @@ public class HashSet : ICollection, ISet, IReadOnlyCollection, IRead /// Cutoff point for stackallocs. This corresponds to the number of ints. private const int StackAllocThreshold = 100; +#pragma warning disable CA1825 // avoid the extra generic instantiation for Array.Empty() + private static readonly Entry[] s_emptyArray = new Entry[0]; +#pragma warning restore CA1825 + /// /// When constructing a hashset from an existing collection, it may contain duplicates, /// so this is used as the max acceptable excess ratio of capacity to count. Note that @@ -37,8 +41,8 @@ public class HashSet : ICollection, ISet, IReadOnlyCollection, IRead private const int ShrinkThreshold = 3; private const int StartOfFreeList = -3; - private int[]? _buckets; - private Entry[]? _entries; + private int[] _buckets; + private Entry[] _entries; #if TARGET_64BIT private ulong _fastModMultiplier; #endif @@ -50,75 +54,93 @@ public class HashSet : ICollection, ISet, IReadOnlyCollection, IRead #region Constructors - public HashSet() : this((IEqualityComparer?)null) { } - - public HashSet(IEqualityComparer? comparer) - { - if (comparer is not null && comparer != EqualityComparer.Default) // first check for null to avoid forcing default comparer instantiation unnecessarily - { - _comparer = comparer; - } + public HashSet() : this(0, null) { } - // Special-case EqualityComparer.Default, StringComparer.Ordinal, and StringComparer.OrdinalIgnoreCase. - // We use a non-randomized comparer for improved perf, falling back to a randomized comparer if the - // hash buckets become unbalanced. - if (typeof(T) == typeof(string)) - { - IEqualityComparer? stringComparer = NonRandomizedStringEqualityComparer.GetStringComparer(_comparer); - if (stringComparer is not null) - { - _comparer = (IEqualityComparer?)stringComparer; - } - } - } + public HashSet(IEqualityComparer? comparer) : this(0, comparer) { } public HashSet(int capacity) : this(capacity, null) { } public HashSet(IEnumerable collection) : this(collection, null) { } - public HashSet(IEnumerable collection, IEqualityComparer? comparer) : this(comparer) + public HashSet(IEnumerable collection, IEqualityComparer? comparer) { if (collection == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection); } + InitializeComparer(comparer); + if (collection is HashSet otherAsHashSet && EqualityComparersAreEqual(this, otherAsHashSet)) { ConstructFrom(otherAsHashSet); + Debug.Assert(_buckets != null); + Debug.Assert(_entries != null); } else { // To avoid excess resizes, first set size based on collection's count. The collection may // contain duplicates, so call TrimExcess if resulting HashSet is larger than the threshold. - if (collection is ICollection coll) + int count = (collection as ICollection)?.Count ?? 0; + if (count > 0) { - int count = coll.Count; - if (count > 0) - { - Initialize(count); - } + Initialize(HashHelpers.GetPrime(count)); } + else + { + InitializeEmpty(); + } + Debug.Assert(_buckets != null); + Debug.Assert(_entries != null); UnionWith(collection); - if (_count > 0 && _entries!.Length / _count > ShrinkThreshold) + if (_count > 0 && _entries.Length / _count > ShrinkThreshold) { TrimExcess(); } } } - public HashSet(int capacity, IEqualityComparer? comparer) : this(comparer) + public HashSet(int capacity, IEqualityComparer? comparer) { if (capacity < 0) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity); } + InitializeComparer(comparer); + if (capacity > 0) { - Initialize(capacity); + Initialize(HashHelpers.GetPrime(capacity)); + } + else + { + InitializeEmpty(); + } + Debug.Assert(_buckets != null); + Debug.Assert(_entries != null); + } + + // Only to be called from constructor + private void InitializeComparer(IEqualityComparer? comparer) + { + if (comparer is not null && comparer != EqualityComparer.Default) // first check for null to avoid forcing default comparer instantiation unnecessarily + { + _comparer = comparer; + } + + // Special-case EqualityComparer.Default, StringComparer.Ordinal, and StringComparer.OrdinalIgnoreCase. + // We use a non-randomized comparer for improved perf, falling back to a randomized comparer if the + // hash buckets become unbalanced. + if (typeof(T) == typeof(string)) + { + IEqualityComparer? stringComparer = NonRandomizedStringEqualityComparer.GetStringComparer(_comparer); + if (stringComparer is not null) + { + _comparer = (IEqualityComparer?)stringComparer; + } } } @@ -129,6 +151,9 @@ protected HashSet(SerializationInfo info, StreamingContext context) // fail. For the time being, we'll just cache this. The graph is not valid until // OnDeserialization has been called. HashHelpers.SerializationInfoTable.Add(this, info); + InitializeEmpty(); + Debug.Assert(_buckets != null); + Debug.Assert(_entries != null); } /// Initializes the HashSet from another HashSet with the same element type and equality comparer. @@ -136,19 +161,17 @@ private void ConstructFrom(HashSet source) { if (source.Count == 0) { - // As well as short-circuiting on the rest of the work done, - // this avoids errors from trying to access source._buckets - // or source._entries when they aren't initialized. + InitializeEmpty(); return; } - int capacity = source._buckets!.Length; + int capacity = source._entries.Length; int threshold = HashHelpers.ExpandPrime(source.Count + 1); if (threshold >= capacity) { _buckets = (int[])source._buckets.Clone(); - _entries = (Entry[])source._entries!.Clone(); + _entries = (Entry[])source._entries.Clone(); _freeList = source._freeList; _freeCount = source._freeCount; _count = source._count; @@ -158,12 +181,12 @@ private void ConstructFrom(HashSet source) } else { - Initialize(source.Count); + Initialize(HashHelpers.GetPrime(source.Count)); - Entry[]? entries = source._entries; + Entry[] entries = source._entries; for (int i = 0; i < source._count; i++) { - ref Entry entry = ref entries![i]; + ref Entry entry = ref entries[i]; if (entry.Next >= -1) { AddIfNotPresent(entry.Value, out _); @@ -186,9 +209,6 @@ public void Clear() int count = _count; if (count > 0) { - Debug.Assert(_buckets != null, "_buckets should be non-null"); - Debug.Assert(_entries != null, "_entries should be non-null"); - Array.Clear(_buckets); _count = 0; _freeList = -1; @@ -205,71 +225,45 @@ public void Clear() /// Gets the index of the item in , or -1 if it's not in the set. private int FindItemIndex(T item) { - int[]? buckets = _buckets; - if (buckets != null) - { - Entry[]? entries = _entries; - Debug.Assert(entries != null, "Expected _entries to be initialized"); + Entry[] entries = _entries; - uint collisionCount = 0; - IEqualityComparer? comparer = _comparer; + uint collisionCount = 0; + IEqualityComparer? comparer = _comparer; - if (comparer == null) + if (comparer == null) + { + int hashCode = item != null ? item.GetHashCode() : 0; + if (typeof(T).IsValueType) { - int hashCode = item != null ? item.GetHashCode() : 0; - if (typeof(T).IsValueType) + // ValueType: Devirtualize with EqualityComparer.Default intrinsic + int i = GetBucketRef(hashCode) - 1; // Value in _buckets is 1-based + while (i >= 0) { - // ValueType: Devirtualize with EqualityComparer.Default intrinsic - int i = GetBucketRef(hashCode) - 1; // Value in _buckets is 1-based - while (i >= 0) + ref Entry entry = ref entries[i]; + if (entry.HashCode == hashCode && EqualityComparer.Default.Equals(entry.Value, item)) { - ref Entry entry = ref entries[i]; - if (entry.HashCode == hashCode && EqualityComparer.Default.Equals(entry.Value, item)) - { - return i; - } - i = entry.Next; - - collisionCount++; - if (collisionCount > (uint)entries.Length) - { - // The chain of entries forms a loop, which means a concurrent update has happened. - ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); - } + return i; } - } - else - { - // Object type: Shared Generic, EqualityComparer.Default won't devirtualize (https://github.com/dotnet/runtime/issues/10050), - // so cache in a local rather than get EqualityComparer per loop iteration. - EqualityComparer defaultComparer = EqualityComparer.Default; - int i = GetBucketRef(hashCode) - 1; // Value in _buckets is 1-based - while (i >= 0) + i = entry.Next; + + collisionCount++; + if (collisionCount > (uint)entries.Length) { - ref Entry entry = ref entries[i]; - if (entry.HashCode == hashCode && defaultComparer.Equals(entry.Value, item)) - { - return i; - } - i = entry.Next; - - collisionCount++; - if (collisionCount > (uint)entries.Length) - { - // The chain of entries forms a loop, which means a concurrent update has happened. - ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); - } + // The chain of entries forms a loop, which means a concurrent update has happened. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); } } } else { - int hashCode = item != null ? comparer.GetHashCode(item) : 0; + // Object type: Shared Generic, EqualityComparer.Default won't devirtualize (https://github.com/dotnet/runtime/issues/10050), + // so cache in a local rather than get EqualityComparer per loop iteration. + EqualityComparer defaultComparer = EqualityComparer.Default; int i = GetBucketRef(hashCode) - 1; // Value in _buckets is 1-based while (i >= 0) { ref Entry entry = ref entries[i]; - if (entry.HashCode == hashCode && comparer.Equals(entry.Value, item)) + if (entry.HashCode == hashCode && defaultComparer.Equals(entry.Value, item)) { return i; } @@ -284,6 +278,27 @@ private int FindItemIndex(T item) } } } + else + { + int hashCode = item != null ? comparer.GetHashCode(item) : 0; + int i = GetBucketRef(hashCode) - 1; // Value in _buckets is 1-based + while (i >= 0) + { + ref Entry entry = ref entries[i]; + if (entry.HashCode == hashCode && comparer.Equals(entry.Value, item)) + { + return i; + } + i = entry.Next; + + collisionCount++; + if (collisionCount > (uint)entries.Length) + { + // The chain of entries forms a loop, which means a concurrent update has happened. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + } + } + } return -1; } @@ -292,7 +307,7 @@ private int FindItemIndex(T item) [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref int GetBucketRef(int hashCode) { - int[] buckets = _buckets!; + int[] buckets = _buckets; #if TARGET_64BIT return ref buckets[HashHelpers.FastMod((uint)hashCode, (uint)buckets.Length, _fastModMultiplier)]; #else @@ -302,56 +317,52 @@ private ref int GetBucketRef(int hashCode) public bool Remove(T item) { - if (_buckets != null) - { - Entry[]? entries = _entries; - Debug.Assert(entries != null, "entries should be non-null"); + Entry[] entries = _entries; - uint collisionCount = 0; - int last = -1; - int hashCode = item != null ? (_comparer?.GetHashCode(item) ?? item.GetHashCode()) : 0; + uint collisionCount = 0; + int last = -1; + int hashCode = item != null ? (_comparer?.GetHashCode(item) ?? item.GetHashCode()) : 0; - ref int bucket = ref GetBucketRef(hashCode); - int i = bucket - 1; // Value in buckets is 1-based + ref int bucket = ref GetBucketRef(hashCode); + int i = bucket - 1; // Value in buckets is 1-based - while (i >= 0) - { - ref Entry entry = ref entries[i]; + while (i >= 0) + { + ref Entry entry = ref entries[i]; - if (entry.HashCode == hashCode && (_comparer?.Equals(entry.Value, item) ?? EqualityComparer.Default.Equals(entry.Value, item))) + if (entry.HashCode == hashCode && (_comparer?.Equals(entry.Value, item) ?? EqualityComparer.Default.Equals(entry.Value, item))) + { + if (last < 0) { - if (last < 0) - { - bucket = entry.Next + 1; // Value in buckets is 1-based - } - else - { - entries[last].Next = entry.Next; - } - - Debug.Assert((StartOfFreeList - _freeList) < 0, "shouldn't underflow because max hashtable length is MaxPrimeArrayLength = 0x7FEFFFFD(2146435069) _freelist underflow threshold 2147483646"); - entry.Next = StartOfFreeList - _freeList; - - if (RuntimeHelpers.IsReferenceOrContainsReferences()) - { - entry.Value = default!; - } - - _freeList = i; - _freeCount++; - return true; + bucket = entry.Next + 1; // Value in buckets is 1-based + } + else + { + entries[last].Next = entry.Next; } - last = i; - i = entry.Next; + Debug.Assert((StartOfFreeList - _freeList) < 0, "shouldn't underflow because max hashtable length is MaxPrimeArrayLength = 0x7FEFFFFD(2146435069) _freelist underflow threshold 2147483646"); + entry.Next = StartOfFreeList - _freeList; - collisionCount++; - if (collisionCount > (uint)entries.Length) + if (RuntimeHelpers.IsReferenceOrContainsReferences()) { - // The chain of entries forms a loop; which means a concurrent update has happened. - // Break out of the loop and throw, rather than looping forever. - ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); + entry.Value = default!; } + + _freeList = i; + _freeCount++; + return true; + } + + last = i; + i = entry.Next; + + collisionCount++; + if (collisionCount > (uint)entries.Length) + { + // The chain of entries forms a loop; which means a concurrent update has happened. + // Break out of the loop and throw, rather than looping forever. + ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported(); } } @@ -386,9 +397,9 @@ public virtual void GetObjectData(SerializationInfo info, StreamingContext conte info.AddValue(VersionName, _version); // need to serialize version to avoid problems with serializing while enumerating info.AddValue(ComparerName, Comparer, typeof(IEqualityComparer)); - info.AddValue(CapacityName, _buckets == null ? 0 : _buckets.Length); + info.AddValue(CapacityName, _entries.Length); - if (_buckets != null) + if (_entries.Length > 0) { var array = new T[Count]; CopyTo(array); @@ -438,7 +449,7 @@ public virtual void OnDeserialization(object? sender) } else { - _buckets = null; + InitializeEmpty(); } _version = siInfo.GetInt32(VersionName); @@ -466,14 +477,11 @@ public virtual void OnDeserialization(object? sender) /// public bool TryGetValue(T equalValue, [MaybeNullWhen(false)] out T actualValue) { - if (_buckets != null) + int index = FindItemIndex(equalValue); + if (index >= 0) { - int index = FindItemIndex(equalValue); - if (index >= 0) - { - actualValue = _entries![index].Value; - return true; - } + actualValue = _entries[index].Value; + return true; } actualValue = default; @@ -874,10 +882,10 @@ public void CopyTo(T[] array, int arrayIndex, int count) ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall); } - Entry[]? entries = _entries; + Entry[] entries = _entries; for (int i = 0; i < _count && count != 0; i++) { - ref Entry entry = ref entries![i]; + ref Entry entry = ref entries[i]; if (entry.Next >= -1) { array[arrayIndex++] = entry.Value; @@ -894,11 +902,11 @@ public int RemoveWhere(Predicate match) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match); } - Entry[]? entries = _entries; + Entry[] entries = _entries; int numRemoved = 0; for (int i = 0; i < _count; i++) { - ref Entry entry = ref entries![i]; + ref Entry entry = ref entries[i]; if (entry.Next >= -1) { // Cache value in case delegate removes it @@ -941,17 +949,12 @@ public int EnsureCapacity(int capacity) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity); } - int currentCapacity = _entries == null ? 0 : _entries.Length; + int currentCapacity = _entries.Length; if (currentCapacity >= capacity) { return currentCapacity; } - if (_buckets == null) - { - return Initialize(capacity); - } - int newSize = HashHelpers.GetPrime(capacity); Resize(newSize, forceNewHashCodes: false); return newSize; @@ -963,7 +966,6 @@ private void Resize(int newSize, bool forceNewHashCodes) { // Value types never rehash Debug.Assert(!forceNewHashCodes || !typeof(T).IsValueType); - Debug.Assert(_entries != null, "_entries should be non-null"); Debug.Assert(newSize >= _entries.Length); var entries = new Entry[newSize]; @@ -1019,9 +1021,8 @@ public void TrimExcess() int capacity = Count; int newSize = HashHelpers.GetPrime(capacity); - Entry[]? oldEntries = _entries; - int currentCapacity = oldEntries == null ? 0 : oldEntries.Length; - if (newSize >= currentCapacity) + Entry[] oldEntries = _entries; + if (newSize >= oldEntries.Length) { return; } @@ -1029,14 +1030,14 @@ public void TrimExcess() int oldCount = _count; _version++; Initialize(newSize); - Entry[]? entries = _entries; + Entry[] entries = _entries; int count = 0; for (int i = 0; i < oldCount; i++) { - int hashCode = oldEntries![i].HashCode; // At this point, we know we have entries. + int hashCode = oldEntries[i].HashCode; // At this point, we know we have entries. if (oldEntries[i].Next >= -1) { - ref Entry entry = ref entries![count]; + ref Entry entry = ref entries[count]; entry = oldEntries[i]; ref int bucket = ref GetBucketRef(hashCode); entry.Next = bucket - 1; // Value in _buckets is 1-based @@ -1060,9 +1061,8 @@ public void TrimExcess() /// Initializes buckets and slots arrays. Uses suggested capacity by finding next prime /// greater than or equal to capacity. /// - private int Initialize(int capacity) + private void Initialize(int size) { - int size = HashHelpers.GetPrime(capacity); var buckets = new int[size]; var entries = new Entry[size]; @@ -1073,8 +1073,16 @@ private int Initialize(int capacity) #if TARGET_64BIT _fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)size); #endif + } - return size; + private void InitializeEmpty() + { + _freeList = -1; +#if TARGET_64BIT + _fastModMultiplier = HashHelpers.FastModMultiplierDivisor1; +#endif + _buckets = HashHelpers.SizeOneIntArray; + _entries = s_emptyArray; } /// Adds the specified element to the set if it's not already contained. @@ -1083,14 +1091,7 @@ private int Initialize(int capacity) /// true if the element is added to the object; false if the element is already present. private bool AddIfNotPresent(T value, out int location) { - if (_buckets == null) - { - Initialize(0); - } - Debug.Assert(_buckets != null); - - Entry[]? entries = _entries; - Debug.Assert(entries != null, "expected entries to be non-null"); + Entry[] entries = _entries; IEqualityComparer? comparer = _comparer; int hashCode; @@ -1177,7 +1178,7 @@ private bool AddIfNotPresent(T value, out int location) { index = _freeList; _freeCount--; - Debug.Assert((StartOfFreeList - entries![_freeList].Next) >= -1, "shouldn't overflow because `next` cannot underflow"); + Debug.Assert((StartOfFreeList - entries[_freeList].Next) >= -1, "shouldn't overflow because `next` cannot underflow"); _freeList = StartOfFreeList - entries[_freeList].Next; } else @@ -1187,14 +1188,14 @@ private bool AddIfNotPresent(T value, out int location) { Resize(); bucket = ref GetBucketRef(hashCode); + entries = _entries; } index = count; _count = count + 1; - entries = _entries; } { - ref Entry entry = ref entries![index]; + ref Entry entry = ref entries[index]; entry.HashCode = hashCode; entry.Next = bucket - 1; // Value in _buckets is 1-based entry.Value = value; @@ -1263,10 +1264,10 @@ internal bool IsSubsetOfHashSetWithSameComparer(HashSet other) /// private void IntersectWithHashSetWithSameComparer(HashSet other) { - Entry[]? entries = _entries; + Entry[] entries = _entries; for (int i = 0; i < _count; i++) { - ref Entry entry = ref entries![i]; + ref Entry entry = ref entries[i]; if (entry.Next >= -1) { T item = entry.Value; @@ -1286,8 +1287,6 @@ private void IntersectWithHashSetWithSameComparer(HashSet other) /// private unsafe void IntersectWithEnumerable(IEnumerable other) { - Debug.Assert(_buckets != null, "_buckets shouldn't be null; callers should check first"); - // Keep track of current last index; don't want to move past the end of our bit array // (could happen if another thread is modifying the collection). int originalCount = _count; @@ -1312,7 +1311,7 @@ private unsafe void IntersectWithEnumerable(IEnumerable other) // FindFirstUnmarked method. for (int i = 0; i < originalCount; i++) { - ref Entry entry = ref _entries![i]; + ref Entry entry = ref _entries[i]; if (entry.Next >= -1 && !bitHelper.IsMarked(i)) { Remove(entry.Value); @@ -1401,7 +1400,7 @@ private unsafe void SymmetricExceptWithEnumerable(IEnumerable other) { if (itemsToRemove.IsMarked(i)) { - Remove(_entries![i].Value); + Remove(_entries[i].Value); } } } @@ -1444,8 +1443,6 @@ private unsafe (int UniqueCount, int UnfoundCount) CheckUniqueAndUnfoundElements return (UniqueCount: 0, UnfoundCount: numElementsInOther); } - Debug.Assert((_buckets != null) && (_count > 0), "_buckets was null but count greater than 0"); - int originalCount = _count; int intArrayLength = BitHelper.ToIntArrayLength(originalCount); @@ -1529,7 +1526,7 @@ public bool MoveNext() // dictionary.count+1 could be negative if dictionary.count is int.MaxValue while ((uint)_index < (uint)_hashSet._count) { - ref Entry entry = ref _hashSet._entries![_index++]; + ref Entry entry = ref _hashSet._entries[_index++]; if (entry.Next >= -1) { _current = entry.Value; diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs index a70b9bf5c0c897..20fae53bd17ece 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/HashHelpers.cs @@ -15,6 +15,11 @@ internal static partial class HashHelpers public const int HashPrime = 101; +#if SYSTEM_PRIVATE_CORELIB + // This field is shared between Dictionary and HashSet for initially empty collections and shouldn't be written to + public static readonly int[] SizeOneIntArray = new int[1]; +#endif + // Table of prime numbers to use as hash table sizes. // A typical resize algorithm would pick the smallest prime number in this array // that is larger than twice the previous capacity. @@ -88,6 +93,8 @@ public static int ExpandPrime(int oldSize) return GetPrime(newSize); } + public static readonly ulong FastModMultiplierDivisor1 = GetFastModMultiplier(1); + /// Returns approximate reciprocal of the divisor: ceil(2**64 / divisor). /// This should only be used on 64-bit. public static ulong GetFastModMultiplier(uint divisor) => diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlFormatGeneratorStatics.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlFormatGeneratorStatics.cs index dd69d7a67260d4..dbec97e973def4 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlFormatGeneratorStatics.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlFormatGeneratorStatics.cs @@ -212,7 +212,7 @@ internal static ConstructorInfo HashtableCtor { if (s_hashtableCtor == null) { - s_hashtableCtor = Globals.TypeOfHashtable.GetConstructor(Globals.ScanAllMembers, Type.EmptyTypes); + s_hashtableCtor = Globals.TypeOfHashtable.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, Type.EmptyTypes); Debug.Assert(s_hashtableCtor != null); } return s_hashtableCtor; diff --git a/src/mono/System.Private.CoreLib/src/System/Reflection/Emit/ModuleBuilder.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Reflection/Emit/ModuleBuilder.Mono.cs index bc0b59413b8128..8b3724d27650b9 100644 --- a/src/mono/System.Private.CoreLib/src/System/Reflection/Emit/ModuleBuilder.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Reflection/Emit/ModuleBuilder.Mono.cs @@ -343,10 +343,10 @@ public MethodInfo GetArrayMethod(Type arrayClass, string methodName, CallingConv public EnumBuilder DefineEnum(string name, TypeAttributes visibility, Type underlyingType) { ITypeIdentifier ident = TypeIdentifiers.FromInternal(name); - if (name_cache.ContainsKey(ident)) + if (name != null && name_cache.ContainsKey(ident)) throw new ArgumentException("Duplicate type name within an assembly."); - EnumBuilder eb = new EnumBuilder(this, name, visibility, underlyingType); + EnumBuilder eb = new EnumBuilder(this, name!, visibility, underlyingType); TypeBuilder res = eb.GetTypeBuilder(); AddType(res); name_cache.Add(ident, res);