Skip to content

Commit

Permalink
Optimization/grouped extensions (#3260)
Browse files Browse the repository at this point in the history
* Added ObservableGroup<TKey, TValue> debugger display

* Optimized the readonly ObservableGroupedCollection extensions

* Minor tweaks to RemoveGroup<TKey, TValue>

* Finished refactoring of observable collections extensions

* Minor style tweaks

Co-authored-by: Michael Hawker MSFT (XAML Llama) <michael.hawker@outlook.com>
  • Loading branch information
Sergio0694 and michael-hawker authored May 26, 2020
1 parent 6adc62f commit 3add501
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 22 deletions.
2 changes: 2 additions & 0 deletions Microsoft.Toolkit/Collections/ObservableGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;

namespace Microsoft.Toolkit.Collections
Expand All @@ -14,6 +15,7 @@ namespace Microsoft.Toolkit.Collections
/// </summary>
/// <typeparam name="TKey">The type of the group key.</typeparam>
/// <typeparam name="TValue">The type of the items in the collection.</typeparam>
[DebuggerDisplay("Key = {Key}, Count = {Count}")]
public sealed class ObservableGroup<TKey, TValue> : ObservableCollection<TValue>, IGrouping<TKey, TValue>, IReadOnlyObservableGroup
{
/// <summary>
Expand Down
16 changes: 16 additions & 0 deletions Microsoft.Toolkit/Collections/ObservableGroupedCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.CompilerServices;

#nullable enable

namespace Microsoft.Toolkit.Collections
{
Expand All @@ -30,5 +33,18 @@ public ObservableGroupedCollection(IEnumerable<IGrouping<TKey, TValue>> collecti
: base(collection.Select(c => new ObservableGroup<TKey, TValue>(c)))
{
}

/// <summary>
/// Tries to get the underlying <see cref="List{T}"/> instance, if present.
/// </summary>
/// <param name="list">The resulting <see cref="List{T}"/>, if one was in use.</param>
/// <returns>Whether or not a <see cref="List{T}"/> instance has been found.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool TryGetList(out List<ObservableGroup<TKey, TValue>>? list)
{
list = Items as List<ObservableGroup<TKey, TValue>>;

return !(list is null);
}
}
}
198 changes: 176 additions & 22 deletions Microsoft.Toolkit/Collections/ObservableGroupedCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Runtime.CompilerServices;

#nullable enable

namespace Microsoft.Toolkit.Collections
{
Expand All @@ -22,8 +26,18 @@ public static class ObservableGroupedCollectionExtensions
/// <param name="key">The key of the group to query.</param>
/// <returns>The first group matching <paramref name="key"/>.</returns>
/// <exception cref="InvalidOperationException">The target group does not exist.</exception>
[Pure]
public static ObservableGroup<TKey, TValue> First<TKey, TValue>(this ObservableGroupedCollection<TKey, TValue> source, TKey key)
=> source.First(group => GroupKeyPredicate(group, key));
{
ObservableGroup<TKey, TValue>? group = source.FirstOrDefault(key);

if (group is null)
{
ThrowArgumentExceptionForKeyNotFound();
}

return group!;
}

/// <summary>
/// Return the first group with <paramref name="key"/> key or null if not found.
Expand All @@ -33,8 +47,34 @@ public static ObservableGroup<TKey, TValue> First<TKey, TValue>(this ObservableG
/// <param name="source">The source <see cref="ObservableGroupedCollection{TKey, TValue}"/> instance.</param>
/// <param name="key">The key of the group to query.</param>
/// <returns>The first group matching <paramref name="key"/> or null.</returns>
public static ObservableGroup<TKey, TValue> FirstOrDefault<TKey, TValue>(this ObservableGroupedCollection<TKey, TValue> source, TKey key)
=> source.FirstOrDefault(group => GroupKeyPredicate(group, key));
[Pure]
public static ObservableGroup<TKey, TValue>? FirstOrDefault<TKey, TValue>(this ObservableGroupedCollection<TKey, TValue> source, TKey key)
{
if (source.TryGetList(out var list))
{
foreach (var group in list!)
{
if (EqualityComparer<TKey>.Default.Equals(group.Key, key))
{
return group;
}
}

return null;
}

return FirstOrDefaultWithLinq(source, key);
}

/// <summary>
/// Slow path for <see cref="First{TKey,TValue}"/>.
/// </summary>
[Pure]
[MethodImpl(MethodImplOptions.NoInlining)]
private static ObservableGroup<TKey, TValue>? FirstOrDefaultWithLinq<TKey, TValue>(
ObservableGroupedCollection<TKey, TValue> source,
TKey key)
=> source.FirstOrDefault(group => EqualityComparer<TKey>.Default.Equals(group.Key, key));

/// <summary>
/// Return the element at position <paramref name="index"/> from the first group with <paramref name="key"/> key.
Expand All @@ -47,6 +87,7 @@ public static ObservableGroup<TKey, TValue> FirstOrDefault<TKey, TValue>(this Ob
/// <returns>The element.</returns>
/// <exception cref="InvalidOperationException">The target group does not exist.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is less than zero or <paramref name="index"/> is greater than the group elements' count.</exception>
[Pure]
public static TValue ElementAt<TKey, TValue>(
this ObservableGroupedCollection<TKey, TValue> source,
TKey key,
Expand All @@ -62,18 +103,21 @@ public static TValue ElementAt<TKey, TValue>(
/// <param name="key">The key of the group to query.</param>
/// <param name="index">The index of the item from the targeted group.</param>
/// <returns>The element or default(TValue) if it does not exist.</returns>
[Pure]
public static TValue ElementAtOrDefault<TKey, TValue>(
this ObservableGroupedCollection<TKey, TValue> source,
TKey key,
int index)
{
var existingGroup = source.FirstOrDefault(key);
if (existingGroup is null)
var group = source.FirstOrDefault(key);

if (group is null ||
(uint)index >= (uint)group.Count)
{
return default;
return default!;
}

return existingGroup.ElementAtOrDefault(index);
return group[index];
}

/// <summary>
Expand All @@ -89,7 +133,7 @@ public static ObservableGroup<TKey, TValue> AddGroup<TKey, TValue>(
this ObservableGroupedCollection<TKey, TValue> source,
TKey key,
TValue value)
=> AddGroup(source, key, new[] { value });
=> AddGroup(source, key, new[] { value });

/// <summary>
/// Adds a key-collection <see cref="ObservableGroup{TKey, TValue}"/> item into a target <see cref="ObservableGroupedCollection{TKey, TValue}"/>.
Expand Down Expand Up @@ -141,15 +185,17 @@ public static ObservableGroup<TKey, TValue> AddItem<TKey, TValue>(
TKey key,
TValue item)
{
var existingGroup = source.FirstOrDefault(key);
if (existingGroup is null)
var group = source.FirstOrDefault(key);

if (group is null)
{
existingGroup = new ObservableGroup<TKey, TValue>(key);
source.Add(existingGroup);
group = new ObservableGroup<TKey, TValue>(key);
source.Add(group);
}

existingGroup.Add(item);
return existingGroup;
group.Add(item);

return group;
}

/// <summary>
Expand All @@ -172,6 +218,7 @@ public static ObservableGroup<TKey, TValue> InsertItem<TKey, TValue>(
{
var existingGroup = source.First(key);
existingGroup.Insert(index, item);

return existingGroup;
}

Expand All @@ -195,6 +242,7 @@ public static ObservableGroup<TKey, TValue> SetItem<TKey, TValue>(
{
var existingGroup = source.First(key);
existingGroup[index] = item;

return existingGroup;
}

Expand All @@ -209,11 +257,38 @@ public static ObservableGroup<TKey, TValue> SetItem<TKey, TValue>(
public static void RemoveGroup<TKey, TValue>(
this ObservableGroupedCollection<TKey, TValue> source,
TKey key)
{
if (source.TryGetList(out var list))
{
var index = 0;
foreach (var group in list!)
{
if (EqualityComparer<TKey>.Default.Equals(group.Key, key))
{
source.RemoveAt(index);

return;
}

index++;
}
}
else
{
RemoveGroupWithLinq(source, key);
}
}

/// <summary>
/// Slow path for <see cref="RemoveGroup{TKey,TValue}"/>.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void RemoveGroupWithLinq<TKey, TValue>(ObservableGroupedCollection<TKey, TValue> source, TKey key)
{
var index = 0;
foreach (var group in source)
{
if (GroupKeyPredicate(group, key))
if (EqualityComparer<TKey>.Default.Equals(group.Key, key))
{
source.RemoveAt(index);
return;
Expand All @@ -238,15 +313,51 @@ public static void RemoveItem<TKey, TValue>(
TKey key,
TValue item,
bool removeGroupIfEmpty = true)
{
if (source.TryGetList(out var list))
{
var index = 0;
foreach (var group in list!)
{
if (EqualityComparer<TKey>.Default.Equals(group.Key, key))
{
if (group.Remove(item) &&
removeGroupIfEmpty &&
group.Count == 0)
{
source.RemoveAt(index);
}

return;
}

index++;
}
}
else
{
RemoveItemWithLinq(source, key, item, removeGroupIfEmpty);
}
}

/// <summary>
/// Slow path for <see cref="RemoveItem{TKey,TValue}"/>.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void RemoveItemWithLinq<TKey, TValue>(
ObservableGroupedCollection<TKey, TValue> source,
TKey key,
TValue item,
bool removeGroupIfEmpty)
{
var index = 0;
foreach (var group in source)
{
if (GroupKeyPredicate(group, key))
if (EqualityComparer<TKey>.Default.Equals(group.Key, key))
{
group.Remove(item);

if (removeGroupIfEmpty && group.Count == 0)
if (group.Remove(item) &&
removeGroupIfEmpty &&
group.Count == 0)
{
source.RemoveAt(index);
}
Expand All @@ -268,16 +379,53 @@ public static void RemoveItem<TKey, TValue>(
/// <param name="key">The key of the group where the item at <paramref name="index"/> should be removed.</param>
/// <param name="index">The index of the item to remove in the group.</param>
/// <param name="removeGroupIfEmpty">If true (default value), the group will be removed once it becomes empty.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is less than zero or <paramref name="index"/> is greater than the group elements' count.</exception>
public static void RemoveItemAt<TKey, TValue>(
this ObservableGroupedCollection<TKey, TValue> source,
TKey key,
int index,
bool removeGroupIfEmpty = true)
{
if (source.TryGetList(out var list))
{
var groupIndex = 0;
foreach (var group in list!)
{
if (EqualityComparer<TKey>.Default.Equals(group.Key, key))
{
group.RemoveAt(index);

if (removeGroupIfEmpty && group.Count == 0)
{
source.RemoveAt(groupIndex);
}

return;
}

groupIndex++;
}
}
else
{
RemoveItemAtWithLinq(source, key, index, removeGroupIfEmpty);
}
}

/// <summary>
/// Slow path for <see cref="RemoveItemAt{TKey,TValue}"/>.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void RemoveItemAtWithLinq<TKey, TValue>(
ObservableGroupedCollection<TKey, TValue> source,
TKey key,
int index,
bool removeGroupIfEmpty)
{
var groupIndex = 0;
foreach (var group in source)
{
if (GroupKeyPredicate(group, key))
if (EqualityComparer<TKey>.Default.Equals(group.Key, key))
{
group.RemoveAt(index);

Expand All @@ -293,7 +441,13 @@ public static void RemoveItemAt<TKey, TValue>(
}
}

private static bool GroupKeyPredicate<TKey, TValue>(ObservableGroup<TKey, TValue> group, TKey expectedKey)
=> EqualityComparer<TKey>.Default.Equals(group.Key, expectedKey);
/// <summary>
/// Throws a new <see cref="InvalidOperationException"/> when a key is not found.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentExceptionForKeyNotFound()
{
throw new InvalidOperationException("The requested key was not present in the collection");
}
}
}

0 comments on commit 3add501

Please sign in to comment.