Skip to content

Commit

Permalink
Merge branch 'feature/KeyThrowingDictionary'
Browse files Browse the repository at this point in the history
* feature/KeyThrowingDictionary:
  Update readme
  New KeyThrowingDictionary
  Missing unit tests for Dictionary indexer setter
  Fixed - IDictionary contract is to throw ArgumentNullException on null key
  • Loading branch information
bcronce committed Jul 17, 2019
2 parents b857e4f + 8843f2a commit 111eb18
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 7 deletions.
4 changes: 2 additions & 2 deletions BoringHelpers/BoringHelpers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
<TargetFrameworks>netstandard1.3;net46</TargetFrameworks>
<RuntimeIdentifiers>win7-x86;win7-x64</RuntimeIdentifiers>
<Authors>Benjamin Cronce</Authors>
<Version>0.1.0.1</Version>
<Version>0.1.1.0</Version>
<Copyright>Copyright ©2019 Benjamin Cronce</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://github.com/bcronce/BoringHelpers</PackageProjectUrl>
<RepositoryUrl>https://github.com/bcronce/BoringHelpers</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<AssemblyVersion>0.1.0.0</AssemblyVersion>
<PackageTags>helper efficient netstandard collection</PackageTags>
<FileVersion>0.1.0.1</FileVersion>
<FileVersion>0.1.1.0</FileVersion>
<Description>General use library to help make your code cleaner and more efficient.</Description>
</PropertyGroup>

Expand Down
15 changes: 13 additions & 2 deletions BoringHelpers/Collections/Empty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ private EmptyCollection()
public ICollection<TValue> Values => Empty.Collection<TValue>();

public TKey this[int index] { get => throw new IndexOutOfRangeException(); set => throw new NotSupportedException(ReadOnlyErrorMessage); }
public TValue this[TKey key] { get => throw new KeyNotFoundException(); set => throw new KeyNotFoundException(); }
public TValue this[TKey key] { get
{
if (key == null) throw new ArgumentNullException("Key cannot be NULL");
throw new KeyNotFoundException();
}
set => throw new NotSupportedException(ReadOnlyErrorMessage);
}

public bool Add(TKey item) => throw new NotSupportedException(ReadOnlyErrorMessage);

Expand Down Expand Up @@ -82,10 +88,15 @@ public void CopyTo(TKey[] array, int arrayIndex) { } //no-op

public void Add(TKey key, TValue value) => throw new NotSupportedException(ReadOnlyErrorMessage);

public bool ContainsKey(TKey key) => false;
public bool ContainsKey(TKey key)
{
if (key == null) throw new ArgumentNullException("Key cannot be NULL");
return false;
}

public bool TryGetValue(TKey key, out TValue value)
{
if (key == null) throw new ArgumentNullException("Key cannot be NULL");
value = default;
return false;
}
Expand Down
10 changes: 7 additions & 3 deletions BoringHelpers/Collections/Individual.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,14 @@ public TValue this[TKey key]
set => throw new NotSupportedException(ReadOnlyErrorMessage);
}

public SingleDictionary(KeyValuePair<TKey, TValue> item) : base(item)
=> this.keyComparer = EqualityComparer<TKey>.Default;
public SingleDictionary(KeyValuePair<TKey, TValue> item) : this(item, EqualityComparer<TKey>.Default) { }

public SingleDictionary(KeyValuePair<TKey, TValue> item, IEqualityComparer<TKey> comparer)
: base(item) => this.keyComparer = comparer;
: base(item)
{
if (item.Key == null) throw new ArgumentNullException("Key cannot be NULL");
this.keyComparer = comparer;
}

public void Add(TKey key, TValue value) => throw new NotSupportedException(ReadOnlyErrorMessage);

Expand All @@ -256,6 +259,7 @@ public SingleDictionary(KeyValuePair<TKey, TValue> item, IEqualityComparer<TKey>

public bool TryGetValue(TKey key, out TValue value)
{
if (key == null) throw new ArgumentNullException("Key cannot be NULL");
if (this.keyComparer.Equals(key, this.item.Key))
{
value = this.item.Value;
Expand Down
46 changes: 46 additions & 0 deletions BoringHelpers/Collections/KeyThrowingDictionary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace BoringHelpers.Collections
{
public class KeyThrowingDictionary<TKey, TValue> : Dictionary<TKey, TValue>
{
public new TValue this[TKey key]
{
get
{
if (this.TryGetValue(key, out var result)) return result;
else
{
//key cannot be null, TryGetValue throws ArgumentNullException
throw new KeyNotFoundException($"Key `{key}` not found");
}
}
set
{
try
{
base[key] = value;
}
catch (KeyNotFoundException ex)
{
throw new KeyNotFoundException($"Key `{key}` not found", ex);
}
}
}

public new void Add(TKey key, TValue value)
{
try
{
base.Add(key, value);
}
catch (ArgumentException ex)
{
//key cannot be null, TryGetValue throws ArgumentNullException
throw new ArgumentException($"Key `{key}` already exists", ex);
}
}
}
}
62 changes: 62 additions & 0 deletions BoringHelpersTests/Collections/Dictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,40 @@ public void Individual_Index(int input)
Assert.True(dict[input]);
}

[Fact]
public void Individual_KeyNotNull_Instantiation()
{
Assert.Throws<ArgumentNullException>(() => Individual.Dictionary((object)null, true));
}

[Fact]
public void Individual_KeyNotNull_TryGet()
{
var dict = Individual.Dictionary(new object(), true);
Assert.Throws<ArgumentNullException>(() => dict.TryGetValue(null, out bool discard));
}

[Fact]
public void Individual_KeyNotNull_Contains()
{
var dict = Individual.Dictionary(new object(), true);
Assert.Throws<ArgumentNullException>(() => dict.ContainsKey(null));
}

[Fact]
public void Individual_KeyNotNull_Indexer()
{
var dict = Individual.Dictionary(new object(), true);
Assert.Throws<ArgumentNullException>(() => dict[null]);
}

[Fact]
public void Individual_SetIndexer()
{
var dict = Individual.Dictionary<int, bool>(default, default);
Assert.Throws<NotSupportedException>(() => dict[default] = default);
}

[Theory]
[InlineData(0)]
[InlineData(1)]
Expand Down Expand Up @@ -143,6 +177,34 @@ public void Individual_Values(int input)
Assert.Single(dict.Values, input);
}

[Fact]
public void Empty_KeyNotNull_TryGet()
{
var dict = Empty.Dictionary<object, bool>();
Assert.Throws<ArgumentNullException>(() => dict.TryGetValue(null, out bool discard));
}

[Fact]
public void Empty_KeyNotNull_Contains()
{
var dict = Empty.Dictionary<object, bool>();
Assert.Throws<ArgumentNullException>(() => dict.ContainsKey(null));
}

[Fact]
public void Empty_KeyNotNull_Indexer()
{
var dict = Empty.Dictionary<object, bool>();
Assert.Throws<ArgumentNullException>(() => dict[null]);
}

[Fact]
public void Empty_SetIndexer()
{
var dict = Empty.Dictionary<int, bool>();
Assert.Throws<NotSupportedException>(() => dict[default] = default);
}

[Theory]
[InlineData(0)]
[InlineData(1)]
Expand Down
173 changes: 173 additions & 0 deletions BoringHelpersTests/Collections/KeyThrowingDictionary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
using Xunit;
using System.Collections.Generic;
using BoringHelpers.Collections;
using System;


namespace BoringHelpersTests.Collections
{
public class KeyThrowingDictionary
{
[Theory]
[InlineData("{6F9E54B5-6E93-44B3-8C0A-C2D0B86268A0}")]
[InlineData("{4F7ECDAB-3BC9-49DF-8989-0B45C359455B}")]
[InlineData("{241517B8-57B7-4242-9770-CFDB197B147A}")]
public void Contains(string input)
{
KeyThrowingDictionary<string, bool> dict = new KeyThrowingDictionary<string, bool>();
dict.Add(input, true);
Assert.True(dict.ContainsKey(input));
}

[Theory]
[InlineData("{6F9E54B5-6E93-44B3-8C0A-C2D0B86268A0}")]
[InlineData("{4F7ECDAB-3BC9-49DF-8989-0B45C359455B}")]
[InlineData("{241517B8-57B7-4242-9770-CFDB197B147A}")]
public void Contains_Dictionary(string input)
{
Dictionary<string, bool> dict = new KeyThrowingDictionary<string, bool>();
dict.Add(input, true);
Assert.True(dict.ContainsKey(input));
}

[Theory]
[InlineData("{6F9E54B5-6E93-44B3-8C0A-C2D0B86268A0}")]
[InlineData("{4F7ECDAB-3BC9-49DF-8989-0B45C359455B}")]
[InlineData("{241517B8-57B7-4242-9770-CFDB197B147A}")]
public void Contains_IDictionary(string input)
{
IDictionary<string, bool> dict = new KeyThrowingDictionary<string, bool>();
dict.Add(input, true);
Assert.True(dict.ContainsKey(input));
}

[Theory]
[InlineData("{6F9E54B5-6E93-44B3-8C0A-C2D0B86268A0}")]
[InlineData("{4F7ECDAB-3BC9-49DF-8989-0B45C359455B}")]
[InlineData("{241517B8-57B7-4242-9770-CFDB197B147A}")]
public void DoubleAdd(string input)
{
KeyThrowingDictionary<string, bool> dict = new KeyThrowingDictionary<string, bool>();
dict.Add(input, true);
Assert.Throws<ArgumentException>(() => dict.Add(input, true));
try
{
dict.Add(input, true);
Assert.True(false);
}
catch(ArgumentException ex)
{
Assert.Contains(input.ToString(), ex.Message);
return;
}
Assert.True(false);
}

[Theory]
[InlineData("{6F9E54B5-6E93-44B3-8C0A-C2D0B86268A0}")]
[InlineData("{4F7ECDAB-3BC9-49DF-8989-0B45C359455B}")]
[InlineData("{241517B8-57B7-4242-9770-CFDB197B147A}")]
public void DoubleAdd_Dictionary(string input)
{
Dictionary<string, bool> dict = new KeyThrowingDictionary<string, bool>();
dict.Add(input, true);
Assert.Throws<ArgumentException>(() => dict.Add(input, true));
try
{
dict.Add(input, true);
Assert.True(false);
}
catch (ArgumentException ex)
{
Assert.Contains(input.ToString(), ex.Message);
return;
}
Assert.True(false);
}

[Theory]
[InlineData("{6F9E54B5-6E93-44B3-8C0A-C2D0B86268A0}")]
[InlineData("{4F7ECDAB-3BC9-49DF-8989-0B45C359455B}")]
[InlineData("{241517B8-57B7-4242-9770-CFDB197B147A}")]
public void DoubleAdd_IDictionary(string input)
{
IDictionary<string, bool> dict = new KeyThrowingDictionary<string, bool>();
dict.Add(input, true);
Assert.Throws<ArgumentException>(() => dict.Add(input, true));
try
{
dict.Add(input, true);
Assert.True(false);
}
catch (ArgumentException ex)
{
Assert.Contains(input.ToString(), ex.Message);
return;
}
Assert.True(false);
}

[Theory]
[InlineData("{6F9E54B5-6E93-44B3-8C0A-C2D0B86268A0}")]
[InlineData("{4F7ECDAB-3BC9-49DF-8989-0B45C359455B}")]
[InlineData("{241517B8-57B7-4242-9770-CFDB197B147A}")]
public void Missing(string input)
{
KeyThrowingDictionary<string, bool> dict = new KeyThrowingDictionary<string, bool>();
Assert.Throws<KeyNotFoundException>(() => dict[input]);
try
{
var discard = dict[input];
Assert.True(false);
}
catch (KeyNotFoundException ex)
{
Assert.Contains(input.ToString(), ex.Message);
return;
}
Assert.True(false);
}

[Theory]
[InlineData("{6F9E54B5-6E93-44B3-8C0A-C2D0B86268A0}")]
[InlineData("{4F7ECDAB-3BC9-49DF-8989-0B45C359455B}")]
[InlineData("{241517B8-57B7-4242-9770-CFDB197B147A}")]
public void Missing_Dictionary(string input)
{
Dictionary<string, bool> dict = new KeyThrowingDictionary<string, bool>();
Assert.Throws<KeyNotFoundException>(() => dict[input]);
try
{
var discard = dict[input];
Assert.True(false);
}
catch (KeyNotFoundException ex)
{
Assert.Contains(input.ToString(), ex.Message);
return;
}
Assert.True(false);
}

[Theory]
[InlineData("{6F9E54B5-6E93-44B3-8C0A-C2D0B86268A0}")]
[InlineData("{4F7ECDAB-3BC9-49DF-8989-0B45C359455B}")]
[InlineData("{241517B8-57B7-4242-9770-CFDB197B147A}")]
public void Missing_IDictionary(string input)
{
IDictionary<string, bool> dict = new KeyThrowingDictionary<string, bool>();
Assert.Throws<KeyNotFoundException>(() => dict[input]);
try
{
var discard = dict[input];
Assert.True(false);
}
catch (KeyNotFoundException ex)
{
Assert.Contains(input.ToString(), ex.Message);
return;
}
Assert.True(false);
}
}
}
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ In my expereience, it is very common to have a collection of a single element. W
I can't tell you how many times I've seen `new Dictionary {{ key, value }}`. Not only does the `Dictionary` collection need to make an `Array`, it also needs to hash the key. When working with a single collection, you can just hold the single `KeyValue` and skip hashing on both creation and lookups. Similar optimizations can be made to other collections when you only have a single element.

Also in my expereience, many of these single element collections tend to be allocated in hot paths. Sizable reductions in GC and CPU time can be had while making the code easier to read. Win-win-win.
## KeyThrowingDictionary
Admit it. You've had it where some quickly slapped together project throws a `KeyNotFound` exception and you get driven crazy that you have no idea what `key` caused the exception.

I was originally going to create a new class that wrapped a `Dictionary`, but that would cause more code paths to have to be tested and meant two objects would be created. I played around with just inheriting from `Dictionary` and it worked well. It allowed for a minimal class that only changed the few methods that I cared to change with the additional benefit that this can be downcasted into a `Dictionary` for those times someone didn't give an `IDictionary` signature.

0 comments on commit 111eb18

Please sign in to comment.