C#で自作クラス(カスタムクラス)をキーにする方法(Dictionary,HashSet)

ディクショナリの内部では、値(value)を特定するためのインデックスとしてハッシュコード(ハッシュ値)が使われています。

(1)まずGetHashCodeメソッドで、ハッシュ値が同じかを調べる
(2)ハッシュ値が同じときは、Equalsメソッドでオブジェクトの同値性を調べる
1と2が同じだったら、同じと判断する。

GetHashCodeメソッド、Equalsメソッドを自分でオーバーライドしないときは、Object型のEqualsメソッドが呼ばれます。
Object型のEqualsメソッドは参照アドレスの値を比較するみたいです。それぞれnewしたオブジェクト同士は、プロパティの値が同じでも参照アドレスが一致しないので、別物と判断されます。

◼︎ハッシュコード(ハッシュ値)
 →オブジェクトの値を元に何らかの計算を行なって求めたint型の値。

ディクショナリのキーに使うクラスの定義

例として、氏名と電話の2つをキーとして使い、2が同じ時に同じであると判断してほしいとします。

キーに使うクラスで
・GetHashCodeメソッドをオーバーライドする
・Equalsメソッドをオーバーライドする
をしておきます。

public class UserKey
{
    /// <summary>氏名</summary>
    public string Name { get; private set; }
    /// <summary>生年月日</summary>
    public string Birthday { get; private set; }
    /// <summary>電話</summary>
    public string Tel { get; private set; }

    public UserKey(string naem, string birthday, string tel)
    {
        this.Name = naem;
        this.Birthday = birthday;
        this.Tel = tel;
    }

    // 
    // overrideしておきます(´・ω・`)
    // 
    public override int GetHashCode()
    {
        // Javaでは31を乗算するのが一般的らしい
        //return Name.GetHashCode() * 31
        //        + Birthday.GetHashCode() * 31
        //        + Tel.GetHashCode() * 31;

        // C#では^演算子でXOR(排他的論理和)演算するのが普通
        return Name.GetHashCode() ^
                Birthday.GetHashCode() ^
                Tel.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        // asでは、objがnullでも例外は発生せずにnullが入ってくる
        var other = obj as UserKey;
        if (other == null) return false;

        // 何が同じときに、「同じ」と判断してほしいかを記述する
        return this.Name == other.Name &&
                this.Birthday == other.Birthday &&
                this.Tel == other.Tel;
    }
}

自作クラスをディクショナリのキーに使った例

上記のUserKeyクラスをディクショナリ型のキーに使ってみた例です。

var userKeyList = new List<UserKey>()
{
    new UserKey("名前1", "20180101", "00-0000"),// 1
    new UserKey("名前2", "20180101", "00-0000"),// 2
    new UserKey("名前1", "20180101", "00-0000"),// 3(1と同じキー)
    new UserKey("名前1", "20180101", "11-1111"),// 4
};

var userDic = new Dictionary<UserKey, List<string>>();
foreach (var userKey in userKeyList)
{
    if (userDic.ContainsKey(userKey))
    {
        // GetHashCodeメソッドで一致したとき
        // Equalsメソッドでも一致するかを確認する。
        // なので両方オーバーライドしないといけない。
        continue;
    }

    userDic.Add(userKey, new List<string>());
}

HashSetでの使用例

newしているので参照アドレスが違いますが、Addメソッドで同じと判断されてます。

var hashset = new HashSet<UserKey>();

var key1 = new UserKey("名前1", "20180101", "00-0000");// 1
var key2 = new UserKey("名前1", "20180101", "00-0000");// 2(1と同じキー)
var key3 = new UserKey("名前2", "20180101", "00-0000");// 3
                                
bool isAdded1 = hashset.Add(key1);// true
bool isAdded2 = hashset.Add(key2);// false
bool isAdded3 = hashset.Add(key3);// true

演算子 ==」の場合も同様

演算子 ==」は、既定では参照の等価(オブジェクトへの参照アドレスが同じかどうか)を調べます。それぞれnewしたオブジェクト同士だと、参照アドレスが違うため元々の動作ではfalseが返ります。

Equals() と演算子 == のオーバーロードに関するガイドライン (C# プログラミング ガイド)
https://msdn.microsoft.com/ja-jp/library/ms173147(v=vs.90).aspx

var uKey1 = new UserKey("名前1", "20180101", "00-0000");// 1
var uKey2 = new UserKey("名前1", "20180101", "00-0000");// 2(1と同じキー)

// 等しいというのを調べるのも、オーバーライドしたEqualsメソッドを使えばOK
bool isEqual1 = (uKey1 == uKey2);// false
bool isEqual2 = uKey1.Equals(uKey2);// true

C#のHashSetでDictionaryのKeyのみのリスト(コレクション)的な使い方ができるよ

HashSetコレクション型
https://msdn.microsoft.com/ja-jp/library/bb397727(v=vs.110).aspx

簡単に言うと、HashSetクラスは値のないDictionary<TKey, TValue>コレクションです。
つまりHashSetを使うと、DictionaryのKey部分のみのような使い方ができます。順序が不要な重複しない要素のリストで使います。

Containsメソッドで要素が存在するかをチェックすることもできますが、Addメソッドの戻り値を使って、要素が既に存在するかを知ることもできます。

HashSetのAddメソッドでは要素がすでに存在していても、Dictionaryのように例外は発生せずにfalseが返ってきます。

シンプルなサンプル

var codeList = new List<int>() 
{ 
    1, 2, 3, 1,
};

var codeKeys = new HashSet<int>();
foreach(var code in codeList)
{
    if (codeKeys.Add(code) == false) continue;

    // なんか処理
}

var cnt = codeKeys.Count;// 3

C# Formアプリケーション関連のイベント発生順

↓を見ればわかります…最初に知りたかったよ~(´・ω・`)

Windowsフォームのイベントの順序
https://msdn.microsoft.com/ja-jp/library/86faxx0d(v=vs.110).aspx
Windowsフォームにおけるマウスイベント
https://msdn.microsoft.com/ja-jp/library/ms171542(v=vs.110).aspx

 

Windowsフォームアプリケーションが起動したとき

メインフォームのスタートアップイベントが次の順序で発生します。

Control.HandleCreated
Control.BindingContextChanged
Form.Load
Control.VisibleChanged
Form.Activated
Form.Shown

 

Windowsフォームアプリケーションを閉じたとき

メインフォームのシャットダウンイベントが次の順序で発生します。

Form.Closing
Form.FormClosing
Form.Closed
Form.FormClosed
Form.Deactivate

 

フォーカスイベントと検証イベント

一般に、
フォーカスを得たとき → Enterイベント(GotFocusイベントは使わない)
フォーカスが外れたとき → Leaveイベント(LostFocusイベントは使わない)
昔はGotFocus・LostFocusを使っていたそうですが、今はEnter・Leaveを使います。

◼︎パターン1

以下の方法でフォーカスを変更したとき

・キーボード (Tab、Shift + Tab など) を使用する
・Select メソッドを呼び出す
・SelectNextControl メソッドを呼び出す
・ActiveControl プロパティを現在のフォームに設定する

次の順序で Control クラスのフォーカス イベントが発生します。

Enter
GotFocus
Leave
Validating
Validated
LostFocus


◼︎パターン2

以下の方法でフォーカスを変更したとき

・マウスの使用
・Focus メソッドの呼び出し

次の順序で Control クラスのフォーカス イベントが発生します。

Enter
GotFocus
LostFocus
Leave
Validating
Validated

 

マウスイベント

次の順序で発生します。

MouseEnter
MouseMove
MouseHover / MouseDown / MouseWheel
MouseUp
MouseLeave

 

マウスボタンのシングルクリックイベント

Windowsフォームのコントロールで、
Button、CheckBox、ComboBox、RadioButton以外は、
次の順序でクリックイベントが発生します。左右ボタンのどちらにも以下の順序で発生します。

MouseDown
Click
MouseClick
MouseUp

 

マウスボタンのダブルクリックイベント

Windowsフォームのコントロールでは、次の順序でイベントが発生します。
(Button、CheckBox、ComboBox、RadioButton以外)

MouseDown
Click
MouseClick
MouseUp
MouseDown
DoubleClick
MouseDoubleClick
MouseUp

 

DoubleClickイベントについて
コントロールのStandardDoubleClickスタイルビットがtrueに設定されているかどうかによって異なる場合があります。

 

グレープシティのイベント

FlexGrid for WinFormsにはBeforeMouseDownイベント(MouseDownイベントを処理する前に発生)などいろいろな便利イベントが準備されています。

 

C#でLINQを使ったToDictionaryの使い方(ListからDictionaryを作成、DictionaryからDictionaryを作成)

.NET Framework3.5(C#3.5)から使えます。
バージョンが低くて使えない時は、forやforeachで回すしかありません(´・ω・`)

構文

Enumerable.ToDictionary メソッド
https://msdn.microsoft.com/ja-jp/library/system.linq.enumerable.todictionary(v=vs.110).aspx

//        戻り値           メソッド名 
public static Dictionary<TKey, TSource> ToDictionary<TSource, TKey>(
    this IEnumerable<TSource> source,// 拡張メソッド(拡張する型を指定)
    Func<TSource, TKey> keySelector// System.Func<TSource, TKey>型
)

オーバーロードで4つのメソッドがありますがよく使うのは以下の2つだと思います。
・引数が1つ
・引数が2つ

ListからDictionaryを作成

ToDictionaryを使ってListからDictionaryを作ることが簡単にできます。

// 【1-1】引数が1つ
// Valueを指定しない場合
// IdをKey、UserオブジェクトをValueとしたディクショナリを作成
Dictionary<int, User> userDic1 = this.Users.ToDictionary(x => x.Id);

// 【1-2】引数が2つ
// IdをKeyに、NameをValueとしたディクショナリを作成
var userDic2 = this.Users.ToDictionary(x => x.Id, x => x.Name);

DictionaryからDictionaryを作成

ToDictionaryを使えばDictionaryから新しいDictionaryを作ることも簡単にできます。

// 【2-1】引数が1つ
// ValueはuserDic1の<Key, Value>がそのまま入ってくる
// 1-1と同じ使い方。
var userDic3 = userDic1.ToDictionary(x => x.Key);

// 【2-2】引数が2つ
// Keyを多少変更してディクショナリを作り直した例
// 1-2と同じ使い方。
var userDic4 = userDic1.ToDictionary(x => x.Key.ToString().PadLeft(3, '0'), x => x.Value);

サンプルコード全体

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ToDictionarySample
{
    public partial class Form1 : Form
    {
        // ユーザーリスト
        public List<User> Users { get; set; }

        // コンストラクタ
        public Form1()
        {
            InitializeComponent();

            this.Users = this.CreateUserList();

            // ■リストからディクショナリを作成

            // 【1-1】引数が1つ
            // Valueを指定しない場合
            // IdをKey、UserオブジェクトをValueとしたディクショナリを作成
            Dictionary<int, User> userDic1 = this.Users.ToDictionary(x => x.Id);

            // 【1-2】引数が2つ
            // IdをKeyに、NameをValueとしたディクショナリを作成
            var userDic2 = this.Users.ToDictionary(x => x.Id, x => x.Name);


            // ■ディクショナリからディクショナリを作成

            // 【2-1】引数が1つ
            // ValueはuserDic1の<Key, Value>がそのまま入ってくる
            // 1-1と同じ使い方。
            var userDic3 = userDic1.ToDictionary(x => x.Key);

            // 【2-2】引数が2つ
            // Keyを多少変更してディクショナリを作り直した例
            // 1-2と同じ使い方。
            var userDic4 = userDic1.ToDictionary(x => x.Key.ToString().PadLeft(3, '0'), x => x.Value);
        }

        #region CreateUserList:検証用のリストを作成
        private List<User> CreateUserList()
        {
            var list = new List<User>();

            for (int i = 1; i < 6; i++)
            {
                var user = new User()
                {
                    Id = i,
                    Name = "名前" + i.ToString(),
                    Birthday = "生年月日" + i.ToString(),
                    Email = "メール" + i.ToString(),
                };
                list.Add(user);
            }

            return list;
        }
        #endregion
    }
}

参考

ToDictionaryのソースコード(Reference Source)
File: System\Linq\Enumerable.cs
Project: ndp\fx\src\Core\System.Core.csproj (System.Core)
http://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,97b7f242af9248ab

C#のCompareメソッド、CompareToメソッドの戻り値の覚え方。すぐ忘れる人向け

覚え方

普通の算数式のようにイメージすると、合点がいく。
「左 - 右 = 結果」の「結果」が正か負かを調べると考えるとわかりやすい。

なるほど、これは覚えやすい(´・ω・`)!

string str01 = "01";
string str05 = "05";
string str10 = "10";

// 01 - 05 = -04(負の数のとき、負の-1が返る)
// 戻り値:-1 (>= 0)
var res1 = str01.CompareTo(str05);

// 10 - 05 = 05 (正の数のとき、正の1が返る)
// 戻り値:1 (<= 0)
var res2 = str10.CompareTo(str05);

// サンプル
if (str01.CompareTo(str05) < 0)
{
    // 05が大きいとき
}
if (str10.CompareTo(str05) > 0)
{
    // 05が小さいとき
}

参考

String.CompareTo メソッド
https://msdn.microsoft.com/ja-jp/library/35f0x18w(v=vs.110).aspx

Reference Source   http://referencesource.microsoft.com/#mscorlib/system/string.cs,141ddf57a009d676


CompareToメソッドの中では、CultureInfoのCompareInfoのCompareメソッドが呼ばれています。

String.Compare メソッド
https://msdn.microsoft.com/ja-jp/library/zkcaxw5y(v=vs.110).aspx