Here is a real quick implementation of a class that does what you are thinking about. Feel free to add some extra features:
public class CountedCollection<T> : IEnumerable<KeyValuePair<T, int>>
{
private readonly Dictionary<T, int> _countDictionary;
public CountedCollection()
{
if (!typeof(IEquatable<T>).IsAssignableFrom(typeof(T)))
{
throw new ArgumentException($"The type {typeof(T).Name} must implement IEquatable of <{typeof(T).Name}> to be usable with CountedCollection");
}
_countDictionary = new Dictionary<T, int>();
}
public CountedCollection(IEqualityComparer<T> comparer)
{
_countDictionary = new Dictionary<T, int>(comparer);
}
public void Add(T item)
{
++TotalCount;
if (_countDictionary.TryGetValue(item, out var curCount))
{
_countDictionary[item] = ++curCount;
}
else
{
_countDictionary.Add(item, 1);
}
}
public void Add (IEnumerable<T> items)
{
foreach (var item in items)
{
Add(item);
}
}
public int TotalCount { get; private set; } = 0;
public int DistinctCount => _countDictionary.Count;
public IEnumerator<KeyValuePair<T, int>> GetEnumerator()
{
foreach (var item in _countDictionary)
{
yield return item;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
It's very lightly tested.
It implements the "Collection Initialization Pattern" and it can be initialized with an existing collection, so both of these work (the second one initializes it using an array of integers):
var dict1 = new CountedCollection<string> { "abc", "xyz", "abc", "123" };
var dict2 = new CountedCollection<int> { new[] { 34, 12, 65, 12, 101 } };
The class provides two constructors. If T doesn't implement IEquatable<T>, then you can provide your own implementation of an IEqualityComparer<T>. This also allows you to do case independent counting of instances. For example:
var dict3 = new CountedCollection<string>(StringComparer.OrdinalIgnoreCase)
{
"abc",
"xyz",
"ABC",
"123",
"Xyz"
};
That code will show two instances of "abc" and two instances of "xyz".
Because you can bring your own IEqualityComparer<T>, T is not constrained to implement IEquatable<T>, the first constructor will throw an ArgumentException if T fails to implement IEquatable<T> (things must be comparable for equality somehow).
For example, this will throw:
var dict4 = new CountedCollection<CountedCollection<string>>();
CountByoperator from the MoreLinq package.