LINQ基礎 - Lookup

LINQ基礎 - Lookup

原始文章將記錄於此
https://github.com/s0920832252/LinQ-Note

Lookup

Lookup 是一個 key 可以對應到一個或是多個 value 的集合類型. Lookup 無法使用關鍵字 new 創造 , 只能透過 LinQ 的方法 ToLookup() 來產生. 其與 Dictionary 的差異如下圖.

圖示

Dictionary : 一個 key 只能對應一個 value
Key1Value1Key2Value2Key3Value3Key4Value4
Lookup : 一個 key 對應一個 group , 也就是可以對應至少一個 value
Group 3Group 1Group 2Value1Value2Value3Value4Value5Value6Key1Key2Key3

使用時機

通常是我們需要可以透過 key 去管理群體(複數)的時候使用. 因此在 LinQ 裡面 , Join 或是 GroupBy 皆會使用到 Lookup.

Lookup 的用法

static void Main(string[] args)
{
    List<(string groupName, string personName)> groups = new List<(string groupName, string personName)>
    {
        ("天空鳥隊","王大明"),
        ("小蠻牛隊","紐魔王"),
        ("臭雞蛋隊","樂高高"),
        ("臭雞蛋隊","蠟筆新"),
        ("大香蕉隊","小丸子"),
    };

    // 使用 name 作為群組的 key , 日後將透過這個 key 去調出群組
    ILookup<string, (string, string)> lookup = groups.ToLookup(student => student.groupName);
    // 叫出 臭雞蛋 群組的成員
    Console.WriteLine($"叫出 臭雞蛋隊的成員");
    foreach ((string name, string personName) in lookup["臭雞蛋隊"])
    {
        Console.WriteLine($"{name} {personName}");
    }

    Console.WriteLine($"\n走訪所有的 lookup 成員");
    foreach (IGrouping<string, (string, string)> group in lookup)
    {
        Console.WriteLine($"{group.Key} :");
        Console.WriteLine($"---- ");
        foreach ((string name, string personName) in group)
        {
            Console.WriteLine($"{name} {personName}");
        }
        Console.WriteLine($"---- ");
        Console.WriteLine($"");
    }

    Console.ReadKey();
}
輸出結果


Lookup 的 Code

public static ILookup<TKey, TSource> ToLookup<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer = null)
{
    if (source is null || keySelector is null)
    {
        throw new Exception("null");
    }
    return Lookup<TKey, TSource>.Create(source, keySelector, comparer);
}

public static ILookup<TKey, TElement> ToLookup<TSource, TKey, TElement>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer = null)
{
    if (source is null || keySelector is null || elementSelector is null)
    {
        throw new Exception("null");
    }
    return Lookup<TKey, TElement>.Create(source, keySelector, elementSelector, comparer);
}

public interface ILookup<TKey, TElement> : IEnumerable<IGrouping<TKey, TElement>>
{
    int Count { get; }
    IEnumerable<TElement> this[TKey key] { get; }
    bool Contains(TKey key);
}

public class Lookup<TKey, TElement> : IEnumerable<IGrouping<TKey, TElement>>, ILookup<TKey, TElement>
{
    readonly IEqualityComparer<TKey> comparer;
    Grouping[] groupings;
    Grouping lastGrouping;

    internal static Lookup<TKey, TElement> Create<TSource>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer)
    {
        Debug.Assert(source != null);
        Debug.Assert(keySelector != null);
        Debug.Assert(elementSelector != null);

        Lookup<TKey, TElement> lookup = new Lookup<TKey, TElement>(comparer);
        foreach (TSource item in source)
        {
            lookup.GetGrouping(keySelector(item), true).Add(elementSelector(item));
        }

        return lookup;
    }

    internal static Lookup<TKey, TElement> Create(IEnumerable<TElement> source, Func<TElement, TKey> keySelector, IEqualityComparer<TKey> comparer)
    {
        Debug.Assert(source != null);
        Debug.Assert(keySelector != null);

        Lookup<TKey, TElement> lookup = new Lookup<TKey, TElement>(comparer);
        foreach (TElement item in source)
        {
            lookup.GetGrouping(keySelector(item), true).Add(item);
        }

        return lookup;
    }

    internal static Lookup<TKey, TElement> CreateForJoin(IEnumerable<TElement> source, Func<TElement, TKey> keySelector, IEqualityComparer<TKey> comparer)
    {
        Lookup<TKey, TElement> lookup = new Lookup<TKey, TElement>(comparer);
        foreach (TElement item in source)
        {
            TKey key = keySelector(item);
            if (key != null) {
                lookup.GetGrouping(key, true).Add(item);
            }
        }
        return lookup;
    }

    Lookup(IEqualityComparer<TKey> comparer)
    {
        if (comparer == null) comparer = EqualityComparer<TKey>.Default;
        this.comparer = comparer;
        groupings = new Grouping[7];
    }

    public int Count { get; private set; }

    public IEnumerable<TElement> this[TKey key]
    {
        get
        {
            Grouping grouping = GetGrouping(key, false);
            return grouping ?? Array.Empty<TElement>() as IEnumerable<TElement>;
            // Array.Empty<TElement>() 是 Array 的靜態 field 
            // 其值為 new TElement[0] 
        }
    }

    public bool Contains(TKey key)
    {
        return GetGrouping(key, false) != null;
    }

    public IEnumerator<IGrouping<TKey, TElement>> GetEnumerator()
    {
        Grouping g = lastGrouping;
        if (g != null)
        {
            do
            {
                g = g.next;
                yield return g;

                Console.WriteLine("groupi喔  : " + g.Key);
            } while (g != lastGrouping);
        }
    }

    public IEnumerable<TResult> ApplyResultSelector<TResult>(Func<TKey, IEnumerable<TElement>, TResult> resultSelector)
    {
        Grouping g = lastGrouping;
        if (g != null)
        {
            do
            {
                g = g.next;
                if (g.count != g.elements.Length) 
                { 
                     Array.Resize<TElement>(ref g.elements, g.count); 
                }
                yield return resultSelector(g.key, g.elements);
            } while (g != lastGrouping);
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    
    internal int InternalGetHashCode(TKey key)
    {
        //Microsoft DevDivBugs 171937. work around comparer implementations that throw when passed null
        return (key == null) ? 0 : comparer.GetHashCode(key) & 0x7FFFFFFF;
    }

    internal Grouping GetGrouping(TKey key, bool create)
    {
        int hashCode = InternalGetHashCode(key);
        for (Grouping g = groupings[hashCode % groupings.Length]; g != null; g = g.hashNext)
            if (g.hashCode == hashCode && comparer.Equals(g.key, key)) return g;
        if (create)
        {
            if (Count == groupings.Length) Resize();
            int index = hashCode % groupings.Length;
            Grouping g = new Grouping
            {
                key = key,
                hashCode = hashCode,
                elements = new TElement[1],
                hashNext = groupings[index]
            };
            groupings[index] = g;
            if (lastGrouping == null)
            {
                g.next = g;
            }
            else
            {
                g.next = lastGrouping.next;
                lastGrouping.next = g;
            }
            lastGrouping = g;
            Count++;
            return g;
        }
        return null;
    }

    void Resize()
    {
        int newSize = checked(Count * 2 + 1);
        Grouping[] newGroupings = new Grouping[newSize];
        Grouping g = lastGrouping;
        do
        {
            g = g.next;
            int index = g.hashCode % newSize;
            g.hashNext = newGroupings[index];
            newGroupings[index] = g;
        } while (g != lastGrouping);
        groupings = newGroupings;
    }

    internal class Grouping : IGrouping<TKey, TElement>, IList<TElement>
    {
        internal TKey key;
        internal int hashCode;
        internal TElement[] elements;
        internal int count;
        internal Grouping hashNext;
        internal Grouping next;

        internal void Add(TElement element)
        {
            if (elements.Length == count) Array.Resize(ref elements, checked(count * 2));
            elements[count] = element;
            count++;
        }

        public IEnumerator<TElement> GetEnumerator()
        {
            for (int i = 0; i < count; i++)
            {
                yield return elements[i];
            }
        }

        public TKey Key => key;

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

        int ICollection<TElement>.Count => count;
        bool ICollection<TElement>.IsReadOnly => true;
        bool ICollection<TElement>.Contains(TElement item) => elements.Contains(item);
        void ICollection<TElement>.CopyTo(TElement[] array, int arrayIndex) => Array.Copy(elements, 0, array, arrayIndex, count);
        int IList<TElement>.IndexOf(TElement item) => Array.IndexOf(elements, item, 0, count);
        void IList<TElement>.Insert(int index, TElement item) => throw new NotImplementedException();
        void IList<TElement>.RemoveAt(int index) => throw new NotImplementedException();
        void ICollection<TElement>.Add(TElement item) => throw new NotImplementedException();
        void ICollection<TElement>.Clear() => throw new NotImplementedException();
        bool ICollection<TElement>.Remove(TElement item) => throw new NotImplementedException();

        TElement IList<TElement>.this[int index]
        {
            get
            {
                if (index < 0 || index >= count) throw new Exception("out of range");
                return elements[index];
            }
            set => throw new NotImplementedException();
        }
    }
}

Summary

  • Lookup 的建立流程

    • 使用 Create() 去建立 Lookup 物件 (建立分組資料集合)
    • 走訪資料集合並且使用 GetGrouping() 去得到 group
      • 若該資料的 key 與所有 group 的 key 都不同 , 則為其建立一個 group 回傳
      • 若該資料的 key 與某個 group 的 key 相同 , 則將該 group 回傳
    • 使用 Add() 去將元素放入上述使用 GetGrouping() 得到的 group 中
  • Lookup 的 grouping[] size 預設為 7 , 但在每次 GetGrouping() 時候 , 都會去檢查目前空間是否已經使用完畢. 若使用完畢則使用 Lookup 的 Resize() 這個方法去增加 grouping[] size — “目前的全部空間” * 2 +1

參考來源

Lookup.cs
Grouping.cs
Enumerable.cs
Lookup In LINQ
C#高级编程五十四天----Lookup类和有序字典

Thank you!

You can find me on

若有謬誤 , 煩請告知 , 新手發帖請多包涵

:100: :muscle: :tada: :sheep: