|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
//
// Lookup.cs
//
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
namespace System.Linq.Parallel
{
/// <summary>
/// Lookup class implements the ILookup interface. Lookup is very similar to a dictionary
/// except multiple values are allowed to map to the same key, and null keys are supported.
///
/// Support for null keys adds an issue because the Dictionary class Lookup uses for
/// storage does not support null keys. So, we need to treat null keys separately.
/// Unfortunately, since TKey may be a value type, we cannot test whether the key is null
/// using the user-specified equality comparer.
///
/// C# does allow us to compare the key against null using the == operator, but there is a
/// possibility that the user's equality comparer considers null to be equal to other values.
/// Now, MSDN documentation specifies that if IEqualityComparer.Equals(x,y) returns true, it
/// must be the case that x and y have the same hash code, and null has no hash code. Despite
/// that, we might as well support the use case, even if it is bad practice.
///
/// The solution the Lookup class uses is to treat the key default(TKey) as a special case,
/// and hold its associated grouping - if any - in a special field instead of inserting it
/// into a dictionary.
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TElement"></typeparam>
internal sealed class Lookup<TKey, TElement> : ILookup<TKey, TElement> where TKey : notnull
{
private readonly Dictionary<TKey, IGrouping<TKey, TElement>> _dict;
private readonly IEqualityComparer<TKey> _comparer;
private IGrouping<TKey, TElement>? _defaultKeyGrouping;
internal Lookup(IEqualityComparer<TKey> comparer)
{
_comparer = comparer;
_dict = new Dictionary<TKey, IGrouping<TKey, TElement>>(_comparer);
}
public int Count
{
get
{
int count = _dict.Count;
if (_defaultKeyGrouping != null)
{
count++;
}
return count;
}
}
// Returns an empty sequence if the key is not in the lookup.
public IEnumerable<TElement> this[TKey key]
{
get
{
if (_comparer.Equals(key, default))
{
if (_defaultKeyGrouping != null)
{
return _defaultKeyGrouping;
}
return Enumerable.Empty<TElement>();
}
else
{
IGrouping<TKey, TElement>? grouping;
if (_dict.TryGetValue(key, out grouping))
{
return grouping;
}
return Enumerable.Empty<TElement>();
}
}
}
public bool Contains(TKey key)
{
if (_comparer.Equals(key, default))
{
return _defaultKeyGrouping != null;
}
else
{
return _dict.ContainsKey(key);
}
}
//
// Adds a grouping to the lookup
//
// Note: The grouping should be cheap to enumerate (IGrouping extends IEnumerable), as
// it may be enumerated multiple times depending how the user manipulates the lookup.
// Our code must guarantee that we never attempt to insert two groupings with the same
// key into a lookup.
//
internal void Add(IGrouping<TKey, TElement> grouping)
{
if (_comparer.Equals(grouping.Key, default))
{
Debug.Assert(_defaultKeyGrouping == null, "Cannot insert two groupings with the default key into a lookup.");
_defaultKeyGrouping = grouping;
}
else
{
Debug.Assert(!_dict.ContainsKey(grouping.Key));
_dict.Add(grouping.Key, grouping);
}
}
public IEnumerator<IGrouping<TKey, TElement>> GetEnumerator()
{
// First iterate over the groupings in the dictionary, and then over the default-key
// grouping, if there is one.
foreach (IGrouping<TKey, TElement> grouping in _dict.Values)
{
yield return grouping;
}
if (_defaultKeyGrouping != null)
{
yield return _defaultKeyGrouping;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable<IGrouping<TKey, TElement>>)this).GetEnumerator();
}
}
}
|