同一インスタンスでない List を property でまとめて数える


概要

例えば、こういうスキルの class があったとして、

Skill.cs
public class Skill
{
    public int Code { get; private set; }
    public int Level { get; private set; }

    public Skill(int code, int level)
    {
        Code = code;
        Level = level;
    }
}

同じスキル・レベルでも別インスタンスである可能性のある List<Skill> から、同じスキル・レベルをまとめて数える IDictionary<Skill, int> SkillCountDictionary(List<Skill>) を実装します。

テスト

SkillTest.cs

public class SkillTest
{

    [Test]
    public void SkillCountDictionary()
    {
        CollectionAssert.AreEquivalent(
            Skill.SkillCountDictionary(Skills()),
            Expect()
        );
    }

    private List<Skill> Skills()
    {
        var skills = new List<Skill>();
        skills.Add(new Skill(1, 1));
        skills.Add(new Skill(1, 1));
        skills.Add(new Skill(2, 1));
        skills.Add(new Skill(2, 2));
        skills.Add(new Skill(2, 2));
        skills.Add(new Skill(2, 2));
        return skills;
    }

    private Dictionary<Skill, int> Expect()
    {
        return new Dictionary<Skill, int>
        {
            {new Skill(1, 1), 2},
            {new Skill(2, 1), 1},
            {new Skill(2, 2), 3}
        };
    }
}

foreach で Dictionary に入れていく実装

Skill.cs

    public static IDictionary<Skill, int> SkillCountDictionary(List<Skill> skills)
    {
        var testCountDictionary = new Dictionary<Skill, int>();
        foreach (var skill in skills)
        {
            // csharp> dict[0]++; => System.Collections.Generic.KeyNotFoundException
            if (!testCountDictionary.ContainsKey(skill))
            {

                testCountDictionary[skill] = 0;
            }
            testCountDictionary[skill]++;
        }
        return testCountDictionary;
    }

当然通りません。
インスタンスが違うので、それぞれ別の Key になってしまいます。

IEquatable を実装する

Skill.cs
public class Skill : IEquatable<Skill>
{

    public override int GetHashCode()
    {
        // Tuple の実装があればこっちの方がイケてる気がする
        // return Tuple.Create(Code, Level).GetHashCode();
        return Code.GetHashCode() ^ Level.GetHashCode();
    }

    bool IEquatable<Skill>.Equals(Skill other)
    {
        if (other == null)
        {
            return false;
        }

        return Code == other.Code && Level == other.Level;
    }

通りました。けど、長い。

LINQ GroupBy を使う実装

Skill.cs
    public static IDictionary<Skill, int> SkillCountDictinary(List<Skill> skills)
    {
        return skills.GroupBy(s => new { s.Code, s.Level })
            .ToDictionary(g => g.First(), g => g.Count());
    }

1行で書けました。

まとめ

  • GroupBy が便利でした。
  • IEquatable<T> は覚えておくといいかもしれない。
    • もうちょっと一般的な場面でも役に立ちそう。
    • Tuple が欲しくなる。