Skip to content

Commit e92b7d0

Browse files
authored
Update ICollection<T> usage to IReadOnlyCollection<T> where applicable (#101469)
Anywhere we're casting to `ICollection<T>` just to use its `Count`, we can instead now cast to `IReadOnlyCollection<T>`, as the former inherits the latter as of .NET 9. This expands the set of types that can light-up with the check; in a couple of places it also lets us remove what would then be a duplicative check. We can do the same for `IList<T>` and `IReadOnlyList<T>`. While dispatch via the DIM could result in an extra interface dispatch for types that weren't previously implementing the read-only interface, a) our collection types already implement both, and b) these cases all represent fast paths where the extra savings from the faster path should more than make up for additional call overheads. I audited for anywhere we were missing explicit implementations and added a few corner-cases in.
1 parent a8e74e3 commit e92b7d0

File tree

25 files changed

+121
-36
lines changed

25 files changed

+121
-36
lines changed

src/libraries/Common/src/System/Diagnostics/DiagnosticsHelper.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,22 @@ internal static bool CompareTags(List<KeyValuePair<string, object?>>? sortedTags
3535
int size = count / (sizeof(ulong) * 8) + 1;
3636
BitMapper bitMapper = new BitMapper(size <= 100 ? stackalloc ulong[size] : new ulong[size]);
3737

38+
#if NET9_0_OR_GREATER // ICollection<T> : IReadOnlyCollection<T> on .NET 9+
39+
if (tags2 is IReadOnlyCollection<KeyValuePair<string, object?>> tagsCol)
40+
#else
3841
if (tags2 is ICollection<KeyValuePair<string, object?>> tagsCol)
42+
#endif
3943
{
4044
if (tagsCol.Count != count)
4145
{
4246
return false;
4347
}
4448

49+
#if NET9_0_OR_GREATER // IList<T> : IReadOnlyList<T> on .NET 9+
50+
if (tagsCol is IReadOnlyList<KeyValuePair<string, object?>> secondList)
51+
#else
4552
if (tagsCol is IList<KeyValuePair<string, object?>> secondList)
53+
#endif
4654
{
4755
for (int i = 0; i < count; i++)
4856
{

src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSetInternalBase.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ private protected override bool IsProperSubsetOfCore(IEnumerable<T> other)
3232
{
3333
Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used.");
3434

35+
#if NET9_0_OR_GREATER // ICollection<T> : IReadOnlyCollection<T> on .NET 9+
36+
if (other is IReadOnlyCollection<T> otherAsCollection)
37+
#else
3538
if (other is ICollection<T> otherAsCollection)
39+
#endif
3640
{
3741
int otherCount = otherAsCollection.Count;
3842

@@ -59,7 +63,11 @@ private protected override bool IsProperSupersetOfCore(IEnumerable<T> other)
5963
{
6064
Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used.");
6165

66+
#if NET9_0_OR_GREATER // ICollection<T> : IReadOnlyCollection<T> on .NET 9+
67+
if (other is IReadOnlyCollection<T> otherAsCollection)
68+
#else
6269
if (other is ICollection<T> otherAsCollection)
70+
#endif
6371
{
6472
int otherCount = otherAsCollection.Count;
6573

@@ -103,7 +111,11 @@ private protected override bool IsSupersetOfCore(IEnumerable<T> other)
103111
Debug.Assert(_thisSet.Count != 0, "EmptyFrozenSet should have been used.");
104112

105113
// Try to compute the answer based purely on counts.
114+
#if NET9_0_OR_GREATER // ICollection<T> : IReadOnlyCollection<T> on .NET 9+
115+
if (other is IReadOnlyCollection<T> otherAsCollection)
116+
#else
106117
if (other is ICollection<T> otherAsCollection)
118+
#endif
107119
{
108120
int otherCount = otherAsCollection.Count;
109121

src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableExtensions.Minimal.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@ internal static bool TryGetCount<T>(this IEnumerable sequence, out int count)
3838
return true;
3939
}
4040

41+
#if !NET9_0_OR_GREATER // ICollection<T> : IReadOnlyCollection<T> on .NET 9+
4142
if (sequence is ICollection<T> collectionOfT)
4243
{
4344
count = collectionOfT.Count;
4445
return true;
4546
}
47+
#endif
4648

4749
if (sequence is IReadOnlyCollection<T> readOnlyCollection)
4850
{

src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ public void EnqueueRange(IEnumerable<TElement> elements, TPriority priority)
462462
ArgumentNullException.ThrowIfNull(elements);
463463

464464
int count;
465-
if (elements is ICollection<TElement> collection &&
465+
if (elements is IReadOnlyCollection<TElement> collection &&
466466
(count = collection.Count) > _nodes.Length - _size)
467467
{
468468
Grow(checked(_size + count));

src/libraries/System.Collections/src/System/Collections/Generic/SortedSet.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1323,7 +1323,7 @@ public bool Overlaps(IEnumerable<T> other)
13231323
if (Count == 0)
13241324
return false;
13251325

1326-
if (other is ICollection<T> c && c.Count == 0)
1326+
if (other is IReadOnlyCollection<T> c && c.Count == 0)
13271327
return false;
13281328

13291329
SortedSet<T>? asSorted = other as SortedSet<T>;

src/libraries/System.Linq.Expressions/src/System/Dynamic/ExpandoObject.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace System.Dynamic
1717
/// <summary>
1818
/// Represents an object with members that can be dynamically added and removed at runtime.
1919
/// </summary>
20-
public sealed class ExpandoObject : IDynamicMetaObjectProvider, IDictionary<string, object?>, INotifyPropertyChanged
20+
public sealed class ExpandoObject : IDynamicMetaObjectProvider, IDictionary<string, object?>, IReadOnlyDictionary<string, object?>, INotifyPropertyChanged
2121
{
2222
private static readonly MethodInfo s_expandoTryGetValue =
2323
typeof(RuntimeOps).GetMethod(nameof(RuntimeOps.ExpandoTryGetValue))!;
@@ -618,6 +618,10 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
618618

619619
ICollection<object?> IDictionary<string, object?>.Values => new ValueCollection(this);
620620

621+
IEnumerable<string> IReadOnlyDictionary<string, object?>.Keys => new KeyCollection(this);
622+
623+
IEnumerable<object?> IReadOnlyDictionary<string, object?>.Values => new ValueCollection(this);
624+
621625
object? IDictionary<string, object?>.this[string key]
622626
{
623627
get
@@ -636,6 +640,18 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
636640
}
637641
}
638642

643+
object? IReadOnlyDictionary<string, object?>.this[string key]
644+
{
645+
get
646+
{
647+
if (!TryGetValueForKey(key, out object? value))
648+
{
649+
throw System.Linq.Expressions.Error.KeyDoesNotExistInExpando(key);
650+
}
651+
return value;
652+
}
653+
}
654+
639655
void IDictionary<string, object?>.Add(string key, object? value)
640656
{
641657
this.TryAddMember(key, value);
@@ -650,6 +666,15 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
650666
return index >= 0 && data[index] != Uninitialized;
651667
}
652668

669+
bool IReadOnlyDictionary<string, object?>.ContainsKey(string key)
670+
{
671+
ArgumentNullException.ThrowIfNull(key);
672+
673+
ExpandoData data = _data;
674+
int index = data.Class.GetValueIndexCaseSensitive(key);
675+
return index >= 0 && data[index] != Uninitialized;
676+
}
677+
653678
bool IDictionary<string, object?>.Remove(string key)
654679
{
655680
ArgumentNullException.ThrowIfNull(key);
@@ -662,6 +687,11 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
662687
return TryGetValueForKey(key, out value);
663688
}
664689

690+
bool IReadOnlyDictionary<string, object?>.TryGetValue(string key, out object? value)
691+
{
692+
return TryGetValueForKey(key, out value);
693+
}
694+
665695
#endregion
666696

667697
#region ICollection<KeyValuePair<string, object>> Members

src/libraries/System.Linq.Parallel/src/System/Linq/ParallelEnumerable.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1852,7 +1852,7 @@ public static int Count<TSource>(this ParallelQuery<TSource> source)
18521852
// If the data source is a collection, we can just return the count right away.
18531853
if (source is ParallelEnumerableWrapper<TSource> sourceAsWrapper)
18541854
{
1855-
if (sourceAsWrapper.WrappedEnumerable is ICollection<TSource> sourceAsCollection)
1855+
if (sourceAsWrapper.WrappedEnumerable is IReadOnlyCollection<TSource> sourceAsCollection)
18561856
{
18571857
return sourceAsCollection.Count;
18581858
}
@@ -1923,7 +1923,7 @@ public static long LongCount<TSource>(this ParallelQuery<TSource> source)
19231923
// If the data source is a collection, we can just return the count right away.
19241924
if (source is ParallelEnumerableWrapper<TSource> sourceAsWrapper)
19251925
{
1926-
if (sourceAsWrapper.WrappedEnumerable is ICollection<TSource> sourceAsCollection)
1926+
if (sourceAsWrapper.WrappedEnumerable is IReadOnlyCollection<TSource> sourceAsCollection)
19271927
{
19281928
return sourceAsCollection.Count;
19291929
}

src/libraries/System.Linq/src/System/Linq/AnyAll.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public static bool Any<TSource>(this IEnumerable<TSource> source)
1515
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
1616
}
1717

18-
if (source is ICollection<TSource> gc)
18+
if (source is IReadOnlyCollection<TSource> gc)
1919
{
2020
return gc.Count != 0;
2121
}

src/libraries/System.Linq/src/System/Linq/AppendPrepend.SpeedOpt.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public override int GetCount(bool onlyIfCheap)
127127
return count == -1 ? -1 : count + 1;
128128
}
129129

130-
return !onlyIfCheap || _source is ICollection<TSource> ? _source.Count() + 1 : -1;
130+
return !onlyIfCheap || _source is IReadOnlyCollection<TSource> ? _source.Count() + 1 : -1;
131131
}
132132

133133
public override TSource? TryGetFirst(out bool found)
@@ -276,7 +276,7 @@ public override int GetCount(bool onlyIfCheap)
276276
return count == -1 ? -1 : count + _appendCount + _prependCount;
277277
}
278278

279-
return !onlyIfCheap || _source is ICollection<TSource> ? _source.Count() + _appendCount + _prependCount : -1;
279+
return !onlyIfCheap || _source is IReadOnlyCollection<TSource> ? _source.Count() + _appendCount + _prependCount : -1;
280280
}
281281
}
282282
}

src/libraries/System.Linq/src/System/Linq/Count.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public static int Count<TSource>(this IEnumerable<TSource> source)
1515
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
1616
}
1717

18-
if (source is ICollection<TSource> collectionoft)
18+
if (source is IReadOnlyCollection<TSource> collectionoft)
1919
{
2020
return collectionoft.Count;
2121
}
@@ -101,7 +101,7 @@ public static bool TryGetNonEnumeratedCount<TSource>(this IEnumerable<TSource> s
101101
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
102102
}
103103

104-
if (source is ICollection<TSource> collectionoft)
104+
if (source is IReadOnlyCollection<TSource> collectionoft)
105105
{
106106
count = collectionoft.Count;
107107
return true;

0 commit comments

Comments
 (0)