Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ namespace System.Collections.Frozen
{
public static partial class FrozenDictionary
{
public static System.Collections.Frozen.FrozenDictionary<TKey, TValue> Create<TKey, TValue>(params System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<TKey, TValue>> source) where TKey : notnull { throw null; }
public static System.Collections.Frozen.FrozenDictionary<TKey, TValue> Create<TKey, TValue>(System.Collections.Generic.IEqualityComparer<TKey>? comparer, params System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<TKey, TValue>> source) where TKey : notnull { throw null; }
public static System.Collections.Frozen.FrozenDictionary<TKey, TValue> ToFrozenDictionary<TKey, TValue>(this System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<TKey, TValue>> source, System.Collections.Generic.IEqualityComparer<TKey>? comparer = null) where TKey : notnull { throw null; }
public static System.Collections.Frozen.FrozenDictionary<TKey, TSource> ToFrozenDictionary<TSource, TKey>(this System.Collections.Generic.IEnumerable<TSource> source, System.Func<TSource, TKey> keySelector, System.Collections.Generic.IEqualityComparer<TKey>? comparer = null) where TKey : notnull { throw null; }
public static System.Collections.Frozen.FrozenDictionary<TKey, TElement> ToFrozenDictionary<TSource, TKey, TElement>(this System.Collections.Generic.IEnumerable<TSource> source, System.Func<TSource, TKey> keySelector, System.Func<TSource, TElement> elementSelector, System.Collections.Generic.IEqualityComparer<TKey>? comparer = null) where TKey : notnull { throw null; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0016</DiagnosticId>
<Target>T:System.Collections.Frozen.FrozenDictionary`2:[T:System.Runtime.CompilerServices.CollectionBuilderAttribute]</Target>
<Left>ref/net10.0/System.Collections.Immutable.dll</Left>
<Right>lib/net10.0/System.Collections.Immutable.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0016</DiagnosticId>
<Target>T:System.Collections.Frozen.FrozenDictionary`2:[T:System.Runtime.CompilerServices.CollectionBuilderAttribute]</Target>
<Left>ref/net8.0/System.Collections.Immutable.dll</Left>
<Right>lib/net8.0/System.Collections.Immutable.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0016</DiagnosticId>
<Target>T:System.Collections.Frozen.FrozenDictionary`2:[T:System.Runtime.CompilerServices.CollectionBuilderAttribute]</Target>
<Left>ref/net9.0/System.Collections.Immutable.dll</Left>
<Right>lib/net9.0/System.Collections.Immutable.dll</Right>
</Suppression>
</Suppressions>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,50 @@ namespace System.Collections.Frozen
/// </summary>
public static class FrozenDictionary
{
/// <summary>Creates a <see cref="FrozenDictionary{TKey, TValue}"/> with the specified key/value pairs.</summary>
/// <param name="source">The key/value pairs to use to populate the dictionary.</param>
/// <typeparam name="TKey">The type of the keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of the values in the dictionary.</typeparam>
/// <remarks>
/// If the same key appears multiple times in the input, the latter one in the sequence takes precedence. This differs from
/// <see cref="M:System.Linq.Enumerable.ToDictionary"/>, with which multiple duplicate keys will result in an exception.
/// </remarks>
/// <returns>A <see cref="FrozenDictionary{TKey, TValue}"/> that contains the specified keys and values.</returns>
public static FrozenDictionary<TKey, TValue> Create<TKey, TValue>(params ReadOnlySpan<KeyValuePair<TKey, TValue>> source)
where TKey : notnull =>
Create(null, source);

/// <summary>Creates a <see cref="FrozenDictionary{TKey, TValue}"/> with the specified key/value pairs.</summary>
/// <param name="source">The key/value pairs to use to populate the dictionary.</param>
/// <param name="comparer">The comparer implementation to use to compare keys for equality. If <see langword="null"/>, <see cref="EqualityComparer{TKey}.Default"/> is used.</param>
/// <typeparam name="TKey">The type of the keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of the values in the dictionary.</typeparam>
/// <remarks>
/// If the same key appears multiple times in the input, the latter one in the sequence takes precedence. This differs from
/// <see cref="M:System.Linq.Enumerable.ToDictionary"/>, with which multiple duplicate keys will result in an exception.
/// </remarks>
/// <returns>A <see cref="FrozenDictionary{TKey, TValue}"/> that contains the specified keys and values.</returns>
public static FrozenDictionary<TKey, TValue> Create<TKey, TValue>(IEqualityComparer<TKey>? comparer, params ReadOnlySpan<KeyValuePair<TKey, TValue>> source)
where TKey : notnull
{
comparer ??= EqualityComparer<TKey>.Default;

if (source.IsEmpty)
{
return ReferenceEquals(comparer, FrozenDictionary<TKey, TValue>.Empty.Comparer) ?
FrozenDictionary<TKey, TValue>.Empty :
new EmptyFrozenDictionary<TKey, TValue>(comparer);
}

Dictionary<TKey, TValue> d = new(source.Length, comparer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity (out of scope for this PR), would it be possible to optimize the construction of frozen dictionaries from constant collections (as in, a collection expressions where all keys are compile time constants) so to avoid building the intermedia dictionary, and perhaps just enable some additional fast-path during construction? Would something like that be more like something that eg. ILC could do on NAOT, or something that Roslyn/.NET could improve in general?

I'm asking because we have a scenario in CsWinRT where we'd end up with a massive lookup table of string -> string that would have 700+ entries, and we're thinking about potential codegen strategies for it. An option would be a frozen dictionary, but paying for building all that at startup even though all values would just be constants feels fairly expensive 🤔

Copy link
Member

@am11 am11 Apr 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a massive lookup table of string -> string that would have 700+ entries

Internals of dotnet/runtime repo also come across similar requirements in areas like formats, globalization and security, we ended up source generating blittable RVA data fields with chain of switch-cases (and in some places without switch-cases using multiple ROS). An example of such generator which packs the lookup table in data section: sharplab (fill seedDataDictionary with your data). Would be nice to have something like this OOTB statically built data solely for lookup purposes (not necessarily via FrozenDictionary interface).

foreach (KeyValuePair<TKey, TValue> pair in source)
{
d[pair.Key] = pair.Value;
}

return CreateFromDictionary(d);
}

/// <summary>Creates a <see cref="FrozenDictionary{TKey, TValue}"/> with the specified key/value pairs.</summary>
/// <param name="source">The key/value pairs to use to populate the dictionary.</param>
/// <param name="comparer">The comparer implementation to use to compare keys for equality. If null, <see cref="EqualityComparer{TKey}.Default"/> is used.</param>
Expand Down Expand Up @@ -259,6 +303,7 @@ private static FrozenDictionary<TKey, TValue> CreateFromDictionary<TKey, TValue>
/// the remainder of the life of the application. <see cref="FrozenDictionary{TKey, TValue}"/> should only be
/// initialized with trusted keys, as the details of the keys impacts construction time.
/// </remarks>
[CollectionBuilder(typeof(FrozenDictionary), nameof(FrozenDictionary.Create))]
[DebuggerTypeProxy(typeof(ImmutableDictionaryDebuggerProxy<,>))]
[DebuggerDisplay("Count = {Count}")]
public abstract partial class FrozenDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IDictionary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public void NullSource_ThrowsException()
}

[Fact]
public void EmptySource_ProducedFrozenDictionaryEmpty()
public void EmptyEnumerableSource_ProducedFrozenDictionaryEmpty()
{
IEnumerable<KeyValuePair<TKey, TValue>>[] sources = new[]
{
Expand Down Expand Up @@ -118,6 +118,23 @@ public void EmptySource_ProducedFrozenDictionaryEmpty()
}
}

[Fact]
public void EmptySpanSource_ProducedFrozenDictionaryEmpty()
{
ReadOnlySpan<KeyValuePair<TKey, TValue>> source = default;

Assert.Same(FrozenDictionary<TKey, TValue>.Empty, FrozenDictionary.Create(source));

foreach (IEqualityComparer<TKey> comparer in new IEqualityComparer<TKey>[] { null, EqualityComparer<TKey>.Default })
{
Assert.Same(FrozenDictionary<TKey, TValue>.Empty, FrozenDictionary.Create(comparer, source));
}

Assert.NotSame(FrozenDictionary<TKey, TValue>.Empty, FrozenDictionary.Create(NonDefaultEqualityComparer<TKey>.Instance, source));

Assert.Equal(0, FrozenDictionary.Create(NonDefaultEqualityComparer<TKey>.Instance, source).Count);
}

[Fact]
public void EmptyFrozenDictionary_Idempotent()
{
Expand Down Expand Up @@ -202,23 +219,28 @@ public void ToFrozenDictionary_KeySelectorAndValueSelector_ResultsAreUsed()
}

public static IEnumerable<object[]> LookupItems_AllItemsFoundAsExpected_MemberData() =>
from useToFrozenDictionary in new[] { false, true }
from size in new[] { 0, 1, 2, 10, 99 }
from comparer in new IEqualityComparer<TKey>[] { null, EqualityComparer<TKey>.Default, NonDefaultEqualityComparer<TKey>.Instance }
from specifySameComparer in new[] { false, true }
select new object[] { size, comparer, specifySameComparer };
select new object[] { useToFrozenDictionary, size, comparer, specifySameComparer };

[Theory]
[MemberData(nameof(LookupItems_AllItemsFoundAsExpected_MemberData))]
public void LookupItems_AllItemsFoundAsExpected(int size, IEqualityComparer<TKey> comparer, bool specifySameComparer)
public void LookupItems_AllItemsFoundAsExpected(bool useToFrozenDictionary, int size, IEqualityComparer<TKey> comparer, bool specifySameComparer)
{
Dictionary<TKey, TValue> original =
GenerateUniqueKeyValuePairs(size)
.ToDictionary(p => p.Key, p => p.Value, comparer);
KeyValuePair<TKey, TValue>[] originalPairs = original.ToArray();

FrozenDictionary<TKey, TValue> frozen = specifySameComparer ?
original.ToFrozenDictionary(comparer) :
original.ToFrozenDictionary();
FrozenDictionary<TKey, TValue> frozen = (useToFrozenDictionary, specifySameComparer) switch
{
(true, true) => original.ToFrozenDictionary(comparer),
(true, false) => original.ToFrozenDictionary(),
(false, true) => FrozenDictionary.Create(comparer, originalPairs),
(false, false) => FrozenDictionary.Create(originalPairs),
};

// Make sure creating the frozen dictionary didn't alter the original
Assert.Equal(originalPairs.Length, original.Count);
Expand Down Expand Up @@ -283,8 +305,10 @@ public void EqualButPossiblyDifferentKeys_Found(bool fromDictionary)
}
}

[Fact]
public void MultipleValuesSameKey_LastInSourceWins()
[Theory]
[InlineData(false)]
[InlineData(true)]
public void MultipleValuesSameKey_LastInSourceWins(bool useToFrozenDictionary)
{
TKey[] keys = GenerateUniqueKeyValuePairs(2).Select(pair => pair.Key).ToArray();
TValue[] values = Enumerable.Range(0, 10).Select(CreateTValue).ToArray();
Expand All @@ -301,7 +325,9 @@ from value in values
source = source.Reverse();
}

FrozenDictionary<TKey, TValue> frozen = source.ToFrozenDictionary(GetKeyIEqualityComparer());
FrozenDictionary<TKey, TValue> frozen = useToFrozenDictionary ?
source.ToFrozenDictionary(GetKeyIEqualityComparer()) :
FrozenDictionary.Create(GetKeyIEqualityComparer(), source.ToArray());

Assert.Equal(values[reverse ? 0 : values.Length - 1], frozen[keys[0]]);
Assert.Equal(values[reverse ? 0 : values.Length - 1], frozen[keys[1]]);
Expand Down Expand Up @@ -387,7 +413,9 @@ public void ContainsKey_WithNonAscii(int percentageWithNonAscii)
expected.Add(value, value);
}

FrozenDictionary<string, string> actual = expected.ToFrozenDictionary(GetKeyIEqualityComparer());
FrozenDictionary<string, string> actual = percentageWithNonAscii % 2 == 0 ?
expected.ToFrozenDictionary(GetKeyIEqualityComparer()) :
FrozenDictionary.Create(GetKeyIEqualityComparer(), expected.ToArray());

Assert.All(expected, kvp => actual.ContainsKey(kvp.Key));
}
Expand Down
Loading