Description
Background and motivation
Various pieces of code today allocate new read-only collections (e.g. ReadOnlyCollection<T>
) around empty collections. Sometimes code implements its own singleton caching, e.g.
- https://github.com/dnSpy/dnSpy/blob/2b6dcfaf602fb8ca6462b8b6237fdfc0c74ad994/dnSpy/dnSpy/Documents/Tabs/DocViewer/DebugInfoDocumentViewerCustomDataProvider.cs#L33
- https://github.com/dnSpy/dnSpy/blob/2b6dcfaf602fb8ca6462b8b6237fdfc0c74ad994/Extensions/dnSpy.Debugger/dnSpy.Debugger.DotNet.Metadata/ReadOnlyCollectionHelpers.cs#L26
- https://github.com/dotnet/roslyn/blob/121d9b5cdba4c917c99fbcb6cfa85a5cf10e6e59/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/EvaluationContext.cs#L401-L402
- https://github.com/dotnet/maui/blob/dbe44e68802b7585918e4c001f75be9770dd063d/src/Controls/src/Core/Element.cs#L15
- https://github.com/PowerShell/PowerShell/blob/c978d467ab778fe544808227d915408eaba6b22c/src/System.Management.Automation/engine/Utils.cs#L1451-L1452
- https://github.com/dotnet/wpf/blob/a19b3d79d3dac18930f943e146cfce67a48bb0eb/src/Microsoft.DotNet.Wpf/src/System.Xaml/System/Xaml/XamlType.cs#L1823-L1824
- https://github.com/dotnet/SqlClient/blob/7d348eb0c985b34ce5b76a9330c57bc1015c93ff/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs#L75
- https://github.com/UiPath/CoreWF/blob/725ab544ec199e7d3013bab3740f77d47c10d892/src/UiPath.Workflow.Runtime/Runtime/DurableInstancing/InstanceOwnerQueryResult.cs#L12-L13
- https://github.com/elastic/elasticsearch-net/blob/fc5d848f3441279bc907ae2000a6ae1f9a2c606d/src/Elastic.Clients.Elasticsearch/Common/IsADictionaryBase.cs#L151
- https://github.com/Particular/NServiceBus/blob/56ca994085a487bbd9597ec1e37513cdf11f02a7/src/NServiceBus.Core/Transports/QueueAddress.cs#L12
- https://github.com/UiPath/CoreWF/blob/725ab544ec199e7d3013bab3740f77d47c10d892/src/UiPath.Workflow.Runtime/Runtime/DurableInstancing/InstanceKey.cs#L13
- https://github.com/datastax/csharp-driver/blob/27738b0ddd62c8e523fb2867c48d331257738a6c/src/Cassandra/Exceptions/ReadFailureException.cs#L29-L30
- https://github.com/googleapis/gax-dotnet/blob/01c7e7fd221649f98eb08586017f62d6ee2ffc22/Google.Api.Gax/EmptyDictionary.cs#L23-L24
and sometimes it doesn't, e.g.
- https://github.com/JosefPihrt/Roslynator/blob/8b27c6ac78c17f5abd8151d995bdb4f88f394b0e/src/Tools/Metadata/SourceFile.cs#L17
- https://github.com/dotnet-script/dotnet-script/blob/b5ff49b47134a2aa716d9dc9f08355d3e89d7638/src/Dotnet.Script.Core/ScriptContext.cs#L17
- https://github.com/UiPath/CoreWF/blob/725ab544ec199e7d3013bab3740f77d47c10d892/src/UiPath.Workflow.Runtime/HybridCollection.cs#L116
- https://github.com/dotnet/wcf/blob/41dfb82e834f7f798e091be235925d44469a9cac/src/dotnet-svcutil/lib/src/FrameworkFork/System.Runtime.Serialization/System/Runtime/Serialization/DataContractSerializer.cs#L156
- https://github.com/psibr/REstate/blob/0a3ae3b6d3c5dbf368385476ea4eec2aa5defbb2/src/REstate.Remote/GrpcStateMachine.cs#L66
In other cases, empty could be special-cased but isn't as doing so would have required someone to add their own cache for it to have meaningful benefits).
In any case, we can make this easier and more efficient by exposing our own singletons that we and others can use.
(Replaces #41147, which is limited to just ReadOnlyCollection.)
(Related to #38340, which asked for such Empty methods on mutable collections like List<T>
returning empty instances of those same collections. There we said we'd consider read-only variants with a proposal; this is that proposal.)
API Proposal
namespace System.Collections.Generic
{
public interface IReadOnlyCollection<out T>
{
+ public static IReadOnlyCollection<T> Empty { get; }
}
public interface IReadOnlyList<out T> : IReadOnlyCollection<T>
{
+ public static new IReadOnlyList<T> Empty { get; }
}
public interface IReadOnlyDictionary<TKey, TValue> : IReadOnlyCollection<KeyValuePair<TKey, TValue>>
{
+ public static new IReadOnlyDictionary<TKey, TValue> Empty { get; }
}
public interface IReadOnlySet<T> : IReadOnlyCollection<T>
{
+ public static new IReadOnlySet<T> Empty { get; }
}
}
namespace System.Collections.ObjectModel
{
public class ReadOnlyCollection<T>
{
+ public static ReadOnlyCollection<T> Empty { get; }
}
public class ReadOnlyDictionary<TKey, TValue>
{
+ public static ReadOnlyDictionary<TKey, TValue> Empty { get; }
}
public class ReadOnlyObservableCollection<T>
{
+ public static ReadOnlyObservableCollection<T> Empty { get; }
}
}
API Usage
public ReadOnlyCollection<T> GetValues() =>
_count != 0 ? new ReadOnlyCollection<T>(_values) : ReadOnlyCollection<T>.Empty;
Alternative Designs
No response
Risks
- These might be our first non-abstract/virtual static interface methods in the core libraries?