可変のLookup

一対多の Dictionary が欲しい - present

今だと、やっぱLookupがあるので、ILookupの実装という形で作った方が統一感取れていいのかな……? 通常のLookupはToLookupでしか作成できず不変なので、追加したり削除したり出来るILookupの実装を作りました。MultiDictionaryとかMultiMapとか言われているもの、ですが名前をimmutableの反対なのでMutableLookupとしてみました。かなり、微妙。名前ってむつかしい。んで、中身はDictionary<TKey, List<TValue>>のゆるふわラッピング。コンストラクタでIEqualityComparerを突っ込めるので、大文字小文字無視とかも出来ます。

// こんな風に使います
var mutableLookup = new MutableLookup<string, string>();
mutableLookup.Add("食べ物", "たこやき");
mutableLookup.AddRange("食べ物", new[] { "いかやき", "さかなやき" });
mutableLookup.Add("飲み物", "ぽかり");

IEnumerable<string> tabemono = mutableLookup["食べ物"]; // インデクサでアクセス
foreach (var item in mutableLookup) // 列挙するとIGroupingが出てくる
{
    var key = item.Key; // キー(食べ物,飲み物)
    var array = item.ToArray(); // 配列
}
public class MutableLookup<TKey, TValue> : ILookup<TKey, TValue>
{
    private readonly Dictionary<TKey, List<TValue>> dictionary;

    // Constructor

    public MutableLookup()
    {
        dictionary = new Dictionary<TKey, List<TValue>>();
    }

    public MutableLookup(IEqualityComparer<TKey> keyComparer)
    {
        dictionary = new Dictionary<TKey, List<TValue>>(keyComparer);
    }

    // Property

    public IEnumerable<TKey> Keys
    {
        get { return dictionary.Select(kvp => kvp.Key); }
    }

    public IEnumerable<TValue> Values
    {
        get { return dictionary.SelectMany(kvp => kvp.Value); }
    }

    // Methods

    public ILookup<TKey, TValue> AsReadOnly()
    {
        return dictionary.SelectMany(kvp => kvp.Value, (kvp, Value) => new { kvp.Key, Value })
            .ToLookup(t => t.Key, t => t.Value);
    }

    public void Add(TKey key, TValue value)
    {
        if (dictionary.ContainsKey(key)) dictionary[key].Add(value);
        else dictionary.Add(key, new List<TValue> { value });
    }

    public void AddRange(TKey key, IEnumerable<TValue> values)
    {
        if (dictionary.ContainsKey(key)) dictionary[key].AddRange(values);
        else dictionary.Add(key, new List<TValue>(values));
    }

    public void RemoveKey(TKey key)
    {
        dictionary.Remove(key);
    }

    public void RemoveValue(TKey key, TValue value)
    {
        if (!dictionary.ContainsKey(key)) return;

        var list = dictionary[key];
        list.Remove(value);
        if (!list.Any()) dictionary.Remove(key);

    }

    public void RemoveWhere(TKey key, Func<TValue, bool> predicate)
    {
        if (!dictionary.ContainsKey(key)) return;

        var list = dictionary[key];
        list.RemoveAll(new Predicate<TValue>(predicate));
        if (!list.Any()) dictionary.Remove(key);
    }

    public void Clear()
    {
        dictionary.Clear();
    }

    public bool Contains(TKey key, TValue value)
    {
        if (dictionary.ContainsKey(key))
        {
            return dictionary[key].Contains(value);
        }
        return false;
    }

    #region ILookup<TKey,TValue>

    public bool Contains(TKey key)
    {
        return dictionary.ContainsKey(key);
    }

    public int Count
    {
        get { return dictionary.Count; }
    }

    public IEnumerable<TValue> this[TKey key]
    {
        get
        {
            return (dictionary.ContainsKey(key))
                ? dictionary[key].AsEnumerable()
                : Enumerable.Empty<TValue>();
        }
    }

    #endregion

    #region IEnumerable<IGrouping<TKey,TValue>>

    public IEnumerator<IGrouping<TKey, TValue>> GetEnumerator()
    {
        return dictionary
            .Select(kvp => new Grouping(kvp.Key, kvp.Value))
            .Cast<IGrouping<TKey, TValue>>()
            .GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    #endregion

    // Nested Types

    private class Grouping : IGrouping<TKey, TValue>
    {
        private TKey key;
        private List<TValue> list;

        public Grouping(TKey key, List<TValue> list)
        {
            this.key = key;
            this.list = list;
        }

        #region IGrouping<TKey,TValue>

        public TKey Key
        {
            get { return key; }
        }

        #endregion

        #region IEnumerable<TValue>

        public IEnumerator<TValue> GetEnumerator()
        {
            return list.GetEnumerator();
        }

        #endregion

        #region IEnumerable

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        #endregion
    }
}

利用はご自由にどうぞ。パブリックドメイン、でいいのかな? あー、AsReadOnlyは新しいのに作り替えちゃってるのでAsじゃないですねえ。まあ、放置。あと、new Predicate()が果てしなくダサい。これ何とかならないのかな。delegateはFuncとAction以外は消滅しちゃえばいいのに。ジェネリクス以前のコレクションクラスと同じで、3.0以前の負の遺産だと何となく思ってる。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(.NET)
April 2011
|
July 2025

X:@neuecc GitHub:neuecc

Archive