数万個のファイルの重複チェックを行うプログラムをC#でささっと作る２～コメント欄で指導されて作り直す

# はじめに

先日，筆者は下記の記事を投稿したところ，コメント欄で非常に有益なご助言（ご指導）を頂いた。せっかくなのでプログラムを作り直したいと思う。

https://qiita.com/TETSURO1999/items/f3b4d881b14cd42259d8

# ご指摘一覧

まず @juner 様より頂いたコメント。

### No.1 ファイル一覧の取得には `IEnumerable<T>` 系の APIを使え
>配列にするよりも `IEnumerable<T>` 系の API により途中データを減らしてはどうだ感はある
>Directory.GetFiles → Directory.EnumerateFiles

ストレージが HDD の場合，数万個のファイル一覧を取得するだけで相当な時間がかかる。たしかにファイル一覧が全部揃うのを待たずに処理を開始できたほうが利用者もストレスが少ない。

### No.2 ファイル一覧のデータにはリストではなくハッシュを使え
>蓄積するのも `HashSet<T>` に ハッシュ値を存在しなければ入れるにすれば削除もそのままできるし、探索コストは リストから探索するよりも大分下がると思われる

ファイル一覧を生成してからまとめて重複要素を削除するよりも，ファイル一覧にファイルを追加する毎に重複チェックを行ったほうが高速であり，またデータ構造もリストよりハッシュのほうが効率的だろうという指摘。まったくその通りである。

### No.3 using エイリアスディレクティブを使え
>>System.IO と Microsoft.VisualBasic.FileIO で名前空間の一部衝突が起こってしまうので分離せざるを得なかった。
>
>そういう時の為の using エイリアスディレクティブ。下記の様にしてみてはいかがでしょうか？
>`using VisualBasicFileSystem = Microsoft.VisualBasic.FileIO.FileSystem;`

うん，これはすぐ使えるテクニックだ。有難い。

次に @albireo 様より頂いたコメント。

### No.4 ファイルを全部読み込まなくてもファイルの比較はできる
>この方法で大きなボトルネックになるのは「ハッシュ値を作るにはファイルの内容をすべて読み込む必要があるため、サイズの大きなファイルが多いと非常に時間がかかる」ことで、動画など数百MB単位のファイルが多いと「先頭から1バイトずつ比較」の方がずっと速いこともありえます。考えられる対策としては
>- ファイルサイズを比較
>- ファイルの先頭数KBだけでハッシュを作成

同じようなことは自分も着想していたのだが，シンプルな実装方法が思いつかなかった。しかし，今回のコメントを受けて良いアイディアが浮かんだので採用したいと思う。

### No.5 高速ハッシュアルゴリズム xxHash を使え
>MD5やSHAシリーズなどは「電子署名などのセキュリティ目的で使うことを想定したハッシュアルゴリズム」なので「データが同一か判断するためのハッシュアルゴリズム」より求められる条件が多く、その分複雑で時間のかかる処理になっています。「MD5ってセキュリティ目的だから無駄に重いのでは？」と調べたところ、後者の目的のハッシュとしては「xxHash」というものがあることを今知りました。現時点ではxxHash3が推奨らしい

これは全く知らなかったので非常に有難い。

<!--
#  `using` キーワードのおさらい

もともと C# において `using` キーワードにはいろいろな使い方があることは知っていたのだが，かなり不正確に認識していたようだ。

## その１）`using` ディレクティブ

C# では比較的冒頭に下記のように記述することが多い。C言語でも同じような場所で使う**おまじない** である `#include` のようなものと思っていたが，正しくは名前空間の省略だったのだ。後述するエイリアスディレクティブの特別な形と解釈したほうが良いかもしれない。

```c#:using ディレクティブの用例
using System;
using System.IO;
```

## その２）`using` エイリアスディレクティブ

これも存在自体は知っていたが，C言語で言う `typedef` のようなものと思っていて下記のように使っていたので，今回の用途にも適用できることが分からなかったのだ。これもまた正しくは名前空間のエイリアス（別名定義）だったのだ。

```c#:using エイリアスディレクティブの用例
using IntList = System.Collections.Generic.List<int>
using StrList = System.Collections.Generic.List<string>
```

## その３）`using` ステートメント

これは比較的正しく理解していたと思う。他人のコードを読むとよく使っているからだ。

```c#:using ステートメントの用例
using (FileStream fs = new FileStream(...)) {
    /* 中略 */
}
```

アンマネージドリソースを自動的に破棄してくれる便利な構文だが，下記のようにリソースが複数ある場合，ネストが深くなってしまうのが不格好だと思って採用を躊躇っていた。

```c#:using ステートメントの用例２
using (FileStream fsin = new FileStream(...)) {
    using (FileStream fsout = new FileStream(...)) {
        /* 中略 */
    }
}
```

C#8.0 からは下記のように書けるようになったらしい。コレが本命だと思う。

```c#:using ステートメントの用例３
{
    using var fsin  = new FileStream(...);
    using var fsout = new FileStream(...);
    /* 中略 */
}
```
-->

# `using` エイリアスディレクティブを使う (No.2)

ということで `using` エイリアスディレクティブを使ってプログラムを書き換えよう。

これまで `System.IO.SearchOption` と `Microsoft.VisualBasic.FileIO.SearchOption` が衝突してしまうので下記のクラスだけ別のファイルで宣言していた。

```c#:変更前
using Microsoft.VisualBasic.FileIO;
class VbLib {
	//--------------------------------------------------------------------------
	// ファイルをゴミ箱へ移動する
	//--------------------------------------------------------------------------
	public	static	void	RemoveFile(string path) {
		FileSystem.DeleteFile(path,
			UIOption.OnlyErrorDialogs,
			RecycleOption.SendToRecycleBin);
	}
}
```

ところが下記のように `using` エイリアスディレクティブを用いることで名前空間の衝突がなくなり，一つのファイルにまとめることができるようになった。

```c#:変更後
using VbFileIO = Microsoft.VisualBasic.FileIO;
class VbLib {
	//--------------------------------------------------------------------------
	// ファイルをゴミ箱へ移動する
	//--------------------------------------------------------------------------
	public	static	void	RemoveFile(string path) {
		VbFileIO.FileSystem.DeleteFile(path,
			VbFileIO.UIOption.OnlyErrorDialogs,
			VbFileIO.RecycleOption.SendToRecycleBin);
	}
}
```

こうして作り直したコードを以下に示す。

<details><summary><b>詳しくはコチラ</b></summary>

```c#:UNIQFILE.CS Rev.2
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Runtime.InteropServices;
using VbFileIO = Microsoft.VisualBasic.FileIO;
//------------------------------------------------------------------------------
// VisualBasic ライブラリクラス
//------------------------------------------------------------------------------
class VbLib {
	//--------------------------------------------------------------------------
	// ファイルをゴミ箱へ移動する
	//--------------------------------------------------------------------------
	public	static	void	RemoveFile(string path) {
		VbFileIO.FileSystem.DeleteFile(path,
			VbFileIO.UIOption.OnlyErrorDialogs,
			VbFileIO.RecycleOption.SendToRecycleBin);
	}
}
//------------------------------------------------------------------------------
// Explorer 互換ソートクラス
//------------------------------------------------------------------------------
class LogicalStringComparer {
	[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
	extern	static	int	StrCmpLogicalW(string s1, string s2);
	public	static	int	Compare(string s1, string s2) {
		return StrCmpLogicalW(s1, s2);
	}
}
//------------------------------------------------------------------------------
// 大文字・小文字を区別しない比較クラス
//------------------------------------------------------------------------------
class IgnoreCaseComparer : IEqualityComparer<string> {
	public	bool	Equals(string s1, string s2) {
		return string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase);
	}
	public	int	GetHashCode(string s) {
		return s.ToUpper().GetHashCode();
	}
}
//------------------------------------------------------------------------------
// エントリポイントを持つクラス
//------------------------------------------------------------------------------
class UNIQFILE {
	//--------------------------------------------------------------------------
	// ローカル関数用デリゲート
	//--------------------------------------------------------------------------
	delegate	int		CompareHash(int a, int b);	// ハッシュ値を比較する
	//--------------------------------------------------------------------------
	// メイン関数
	//--------------------------------------------------------------------------
	static	int	Main(string[] args) {
		//----------------------------------------------------------------------
		// ヘルプメッセージ
		//----------------------------------------------------------------------
		if(args.Length < 1) {
			Console.Error.WriteLine("重複するファイルを削除します。");
			Console.Error.WriteLine("");
			Console.Error.WriteLine("UNIQFILE(.EXE) (オプション) [ファイル名] ...");
			Console.Error.WriteLine("");
			Console.Error.WriteLine("＜オプション＞");
			Console.Error.WriteLine("/C　ファイル名のソートを従来方式（単純な文字列比較）で行います。");
			Console.Error.WriteLine("　　デフォルトは Explorer 互換アルゴリズムで比較します。");
			Console.Error.WriteLine("/S　ファイルを再帰的に検索します。");
			Console.Error.WriteLine("/T　ファイルをゴミ箱に移動します。");
			Console.Error.WriteLine("/V　ファイルを削除しません。");
			Console.Error.WriteLine("");
			Console.Error.WriteLine("＜注意事項＞");
			Console.Error.WriteLine("・ファイルの内容が重複する場合，ファイル名を昇順にソートして");
			Console.Error.WriteLine("　先頭のファイルのみ残し，残りのファイルは削除します。");
			Console.Error.WriteLine("・複数のファイル名を指定できます。");
			Console.Error.WriteLine("・ファイル名の指定にはワイルドカードを使用できます。");
			return -1;
		}
		//----------------------------------------------------------------------
		// ファイル一覧を作成する
		//----------------------------------------------------------------------
		bool	classicCompare  = false;	// ファイル名のソートを単純な文字列比較で行います。
		bool	recursiveSearch = false;	// ファイルを再帰的に検索します。
		bool	trashBoxMode    = false;	// ファイルをゴミ箱に移動します。
		bool	viewOnlyMode    = false;	// ファイルを削除しません。
		var		list = new List<string>();	// ファイル名（ワイルドカード可）
		for(int i = 0; i < args.Length; i++) {
			string	s = args[i];
			if(s[0] == '/' || s[0] == '-') {
				switch(s[1]) {
				  case 'C': case 'c':	classicCompare  = true;	break;
				  case 'S': case 's':	recursiveSearch = true;	break;
				  case 'T': case 't':	trashBoxMode    = true;	break;
				  case 'V': case 'v':	viewOnlyMode    = true;	break;
				  default:
					Console.Error.WriteLine("不正なオプション {0} を指定しました!!", s);
					return -1;
				}
			} else {
				list.Add(s);
			}
		}
		if(list.Count == 0) {
			Console.Error.WriteLine("ファイル名を指定して下さい!!");;
			return -1;
		}
		//----------------------------------------------------------------------
		// ファイル一覧の作成
		//----------------------------------------------------------------------
		var	filename = new List<string>();
		var	opt = recursiveSearch
				? SearchOption.AllDirectories
				: SearchOption.TopDirectoryOnly;
		for(int i = 0; i < list.Count; i++) {
			var	s = list[i];
			var	path = Path.GetDirectoryName(s);
			var	file = Path.GetFileName(s);
			if(path == "") {
				path = ".";
			} else if(!Directory.Exists(path)) {
				Console.Error.WriteLine("ディレクトリ {0} は存在しません!!", path);
				return -1;
			}
			path = Path.GetFullPath(path);
			filename.AddRange(Directory.GetFiles(path, file, opt));
		}
		filename = filename.Distinct(new IgnoreCaseComparer()).ToList();
		if(filename.Count == 0) {
			Console.Error.WriteLine("ファイルが見つかりません!!");
			return -1;
		} else if( filename.Count == 1) {
			Console.Error.WriteLine("ファイル数が不足しています!!");
			return -1;
		}
		//----------------------------------------------------------------------
		// MD5によるハッシュ値を生成する
		//----------------------------------------------------------------------
		var	md5 = MD5.Create();
		var	hash = new byte[filename.Count][];
		for(int i = 0; i < filename.Count; i++) {
			if(!File.Exists(filename[i])) {
				Console.Error.WriteLine("ファイル {0} が存在しません!!", filename[i]);
				return -1;
			}
			var	bytes = File.ReadAllBytes(filename[i]);
			hash[i] = md5.ComputeHash(bytes);
		}
		//----------------------------------------------------------------------
		// ハッシュ値の比較用ローカル関数
		//----------------------------------------------------------------------
		CompareHash	compareHash = delegate(int a, int b) {
			for(int i = 0; i < hash[a].Length; i++) {
				int	ret = hash[a][i] - hash[b][i];
				if(ret != 0) return ret;
			}
			return 0;
		};
		//----------------------------------------------------------------------
		// ハッシュ値に従ってインデクスをソートする
		//----------------------------------------------------------------------
		var	index = new int[filename.Count];
		for(int i = 0; i < index.Length; i++) index[i] = i;
		Comparison<int>	compareIndex = delegate(int a, int b) {
			int	ret = compareHash(a, b);
			if(ret != 0)
				return ret;
			else if(classicCompare)
				return string.Compare(filename[a], filename[b], StringComparison.OrdinalIgnoreCase);
			else
				return LogicalStringComparer.Compare(filename[a], filename[b]);
		};
		Array.Sort(index, compareIndex);
		//----------------------------------------------------------------------
		// ハッシュ値の重複するファイルを削除する
		//----------------------------------------------------------------------
		int	count = 0, prev = -1;
 		for(int i = 0; i < index.Length; i++) {
			int	n = index[i];
			Console.Out.Write("{0} {1}",
				BitConverter.ToString(hash[n]).Replace("-", ""),
				Path.GetFileName(filename[n]));
			string	s = "";
			if(prev >= 0 && compareHash(n, prev) == 0) {
				if(viewOnlyMode) {
					s = " ... (*)";		/* 何もしない */
				} else if(trashBoxMode) {
					s = " ... (TRASH)";	VbLib.RemoveFile(filename[n]);
				} else {
					s = " ... DELETED";	File.Delete(filename[n]);
				}
				count++;
			}
			Console.Out.WriteLine(s);
			prev = n;
		}
		string	format =   count == 0 ? "全ファイル {0} 個のうち，重複ファイルはありません。"
					   : viewOnlyMode ? "全ファイル {0} 個のうち，重複ファイル {1} 個を削除できます。"
					   : trashBoxMode ? "全ファイル {0} 個のうち，重複ファイル {1} 個をゴミ箱に送りました。"
					   :                "全ファイル {0} 個のうち，重複ファイル {1} 個を削除しました。";
		Console.Out.WriteLine(format, index.Length, count);
		return 0;
	}
}
```

</details>

# ファイルサイズで一次チェックを行う（No.4）

次にパフォーマンス向上に一番直結すると思われる No.4 の指摘に取り組みたい。

ファイル名一覧（`string` の配列）`filename[]` が与えられているものとして，ファイルサイズ一覧（`long` の配列）`filesize[]` を作成する。

```c#:ファイルサイズ一覧を作成する
var	filesize = new long[filename.Count];
for(int i = 0; i < filename.Count; i++) {
	if(!File.Exists(filename[i])) {
		Console.Error.WriteLine("ファイル {0} が存在しません!!", filename[i]);
		return -1;
	}
	var	info = new FileInfo(filename[i]);
	filesize[i] = info.Length;
}
```

次に **MD5** によるハッシュ値の配列 `hash[]` を作成するが，初期値 `null` とする。
 
```c#:
var	md5 = MD5.Create();
var	hash = new byte[filename.Count][];
for(int i = 0; i < filename.Count; i++)
	hash[i] = null;
```

次にハッシュ値をオンデマンドで作成するローカル関数だが，**Windows** 組み込みの C# コンパイラではローカル関数をサポートしていないので `delegate` で代用する。

```c#:ハッシュ値を計算するローカル関数
delegate	byte[]	ComputeHash(int n);

ComputeHash	computeHash = delegate(int n) {
	byte[]	bytes = File.ReadAllBytes(filename[n]);
	return md5.ComputeHash(bytes);
};
```

同様にハッシュ値を比較するローカル関数も `delegate` で代用する。ハッシュ値はデフォルトで `null` であり，ハッシュ値が必要な場合に限って計算するようにした。なお，ハッシュ値を文字列に変換して比較するのは無駄なのでバイナリ値のまま比較するようにした。※実は Rev.2 から変更済み

```c#:ハッシュ値を比較するローカル関数
delegate	int		CompareHash(int a, int b);

CompareHash	compareHash = delegate(int a, int b) {
	if(hash[a] == null) hash[a] = computeHash(a);
	if(hash[b] == null) hash[b] = computeHash(b);
	for(int i = 0; i < hash[a].Length; i++) {
		int	ret = hash[a][i] - hash[b][i];
		if(ret != 0) return ret;
	}
	return 0;
};
```

そしてファイルのインデクス配列 `index[]` をソートする関数を下記のように変更する。ファイルサイズが異なる場合は，まずファイルサイズの大小のみで比較を行い，ファイルサイズが一致する場合のみハッシュ値で比較するようにした。こうすればハッシュ値の計算を可能な限り減らせるはずだ。

```c#:ソート関数の定義
Comparison<int>	compareIndex = delegate(int a, int b) {
	var	lret = filesize[a] - filesize[b];
	/**/ if(lret > 0) return  1;
	else if(lret < 0) return -1;
	var	iret = compareHash(a, b);
	/**/ if(iret != 0)
		return iret;
	else if(classicCompare)
		return string.Compare(filename[a], filename[b], StringComparison.OrdinalIgnoreCase);
	else
		return LogicalStringComparer.Compare(filename[a], filename[b]);
};
```

こうして作り直したコードを以下に示す。

<details><summary><b>詳しくはコチラ</b></summary>

```c#:UNIQFILE.CS Rev.3
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Runtime.InteropServices;
using VbFileIO = Microsoft.VisualBasic.FileIO;
//------------------------------------------------------------------------------
// VisualBasic ライブラリクラス
//------------------------------------------------------------------------------
class VbLib {
	//--------------------------------------------------------------------------
	// ファイルをゴミ箱へ移動する
	//--------------------------------------------------------------------------
	public	static	void	RemoveFile(string path) {
		VbFileIO.FileSystem.DeleteFile(path,
			VbFileIO.UIOption.OnlyErrorDialogs,
			VbFileIO.RecycleOption.SendToRecycleBin);
	}
}
//------------------------------------------------------------------------------
// Explorer 互換ソートクラス
//------------------------------------------------------------------------------
class LogicalStringComparer {
	[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
	extern	static	int	StrCmpLogicalW(string s1, string s2);
	public	static	int	Compare(string s1, string s2) {
		return StrCmpLogicalW(s1, s2);
	}
}
//------------------------------------------------------------------------------
// 大文字・小文字を区別しない比較クラス
//------------------------------------------------------------------------------
class IgnoreCaseComparer : IEqualityComparer<string> {
	public	bool	Equals(string s1, string s2) {
		return string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase);
	}
	public	int	GetHashCode(string s) {
		return s.ToUpper().GetHashCode();
	}
}
//------------------------------------------------------------------------------
// エントリポイントを持つクラス
//------------------------------------------------------------------------------
class UNIQFILE {
	//--------------------------------------------------------------------------
	// 関数内関数を実現する delegate
	//--------------------------------------------------------------------------
	delegate	byte[]	ComputeHash(int n);			// ハッシュ値を計算する
	delegate	int		CompareHash(int a, int b);	// ハッシュ値を比較する
	//--------------------------------------------------------------------------
	// メイン関数
	//--------------------------------------------------------------------------
	static	int	Main(string[] args) {
		//----------------------------------------------------------------------
		// ヘルプメッセージ
		//----------------------------------------------------------------------
		if(args.Length < 1) {
			Console.Error.WriteLine("重複するファイルを削除します。");
			Console.Error.WriteLine("");
			Console.Error.WriteLine("UNIQFILE(.EXE) (オプション) [ファイル名] ...");
			Console.Error.WriteLine("");
			Console.Error.WriteLine("＜オプション＞");
			Console.Error.WriteLine("/C　ファイル名のソートを従来方式（単純な文字列比較）で行います。");
			Console.Error.WriteLine("　　デフォルトは Explorer 互換アルゴリズムで比較します。");
			Console.Error.WriteLine("/S　ファイルを再帰的に検索します。");
			Console.Error.WriteLine("/T　ファイルをゴミ箱に移動します。");
			Console.Error.WriteLine("/V　ファイルを削除しません。");
			Console.Error.WriteLine("");
			Console.Error.WriteLine("＜注意事項＞");
			Console.Error.WriteLine("・ファイルの内容が重複する場合，ファイル名を昇順にソートして");
			Console.Error.WriteLine("　先頭のファイルのみ残し，残りのファイルは削除します。");
			Console.Error.WriteLine("・複数のファイル名を指定できます。");
			Console.Error.WriteLine("・ファイル名の指定にはワイルドカードを使用できます。");
			return -1;
		}
		//----------------------------------------------------------------------
		// ファイル一覧を作成する
		//----------------------------------------------------------------------
		bool	classicCompare  = false;	// ファイル名のソートを単純な文字列比較で行います。
		bool	recursiveSearch = false;	// ファイルを再帰的に検索します。
		bool	trashBoxMode    = false;	// ファイルをゴミ箱に移動します。
		bool	viewOnlyMode    = false;	// ファイルを削除しません。
		var		list = new List<string>();	// ファイル名（ワイルドカード可）
		for(int i = 0; i < args.Length; i++) {
			string	s = args[i];
			if(s[0] == '/' || s[0] == '-') {
				switch(s[1]) {
				  case 'C': case 'c':	classicCompare  = true;	break;
				  case 'S': case 's':	recursiveSearch = true;	break;
				  case 'T': case 't':	trashBoxMode    = true;	break;
				  case 'V': case 'v':	viewOnlyMode    = true;	break;
				  default:
					Console.Error.WriteLine("不正なオプション {0} を指定しました!!", s);
					return -1;
				}
			} else {
				list.Add(s);
			}
		}
		if(list.Count == 0) {
			Console.Error.WriteLine("ファイル名を指定して下さい!!");;
			return -1;
		}
		//----------------------------------------------------------------------
		// ファイル一覧の作成
		//----------------------------------------------------------------------
		var	filename = new List<string>();
		var	opt = recursiveSearch
				? SearchOption.AllDirectories
				: SearchOption.TopDirectoryOnly;
		for(int i = 0; i < list.Count; i++) {
			var	s = list[i];
			var	path = Path.GetDirectoryName(s);
			var	file = Path.GetFileName(s);
			if(path == "") {
				path = ".";
			} else if(!Directory.Exists(path)) {
				Console.Error.WriteLine("ディレクトリ {0} は存在しません!!", path);
				return -1;
			}
			path = Path.GetFullPath(path);
			filename.AddRange(Directory.GetFiles(path, file, opt));
		}
		filename = filename.Distinct(new IgnoreCaseComparer()).ToList();
		if(filename.Count == 0) {
			Console.Error.WriteLine("ファイルが見つかりません!!");
			return -1;
		} else if( filename.Count == 1) {
			Console.Error.WriteLine("ファイル数が不足しています!!");
			return -1;
		}
		//----------------------------------------------------------------------
		// ファイルサイズを取得する
		//----------------------------------------------------------------------
		var	filesize = new long[filename.Count];
		for(int i = 0; i < filename.Count; i++) {
			if(!File.Exists(filename[i])) {
				Console.Error.WriteLine("ファイル {0} が存在しません!!", filename[i]);
				return -1;
			}
			var	info = new FileInfo(filename[i]);
			filesize[i] = info.Length;
		}
		//----------------------------------------------------------------------
		// MD5によるハッシュ値を生成する
		//----------------------------------------------------------------------
		var	md5 = MD5.Create();
		var	hash = new byte[filename.Count][];
		for(int i = 0; i < filename.Count; i++)
			hash[i] = null;
		//----------------------------------------------------------------------
		// ハッシュ値を計算するローカル関数
		//----------------------------------------------------------------------
		ComputeHash	computeHash = delegate(int n) {
			byte[]	bytes = File.ReadAllBytes(filename[n]);
			return md5.ComputeHash(bytes);
		};
		//----------------------------------------------------------------------
		// ハッシュ値を比較するローカル関数
		//----------------------------------------------------------------------
		CompareHash	compareHash = delegate(int a, int b) {
			if(hash[a] == null) hash[a] = computeHash(a);
			if(hash[b] == null) hash[b] = computeHash(b);
			for(int i = 0; i < hash[a].Length; i++) {
				int	ret = hash[a][i] - hash[b][i];
				if(ret != 0) return ret;
			}
			return 0;
		};
		//----------------------------------------------------------------------
		// ソート関数の定義
		//----------------------------------------------------------------------
		Comparison<int>	compareIndex = delegate(int a, int b) {
			var	lret = filesize[a] - filesize[b];
			/**/ if(lret > 0) return  1;
			else if(lret < 0) return -1;
			var	iret = compareHash(a, b);
			/**/ if(iret != 0)
				return iret;
			else if(classicCompare)
				return string.Compare(filename[a], filename[b], StringComparison.OrdinalIgnoreCase);
			else
				return LogicalStringComparer.Compare(filename[a], filename[b]);
		};
		//----------------------------------------------------------------------
		// インデクスをソートする
		//----------------------------------------------------------------------
		var	index = new int[filename.Count];
		for(int i = 0; i < index.Length; i++)
			index[i] = i;
		Array.Sort(index, compareIndex);
		//----------------------------------------------------------------------
		// ファイルサイズおよびハッシュ値の重複するファイルを削除する
		//----------------------------------------------------------------------
		int	count = 0, prev = -1;
 		for(int i = 0; i < index.Length; i++) {
			int	n = index[i];
			Console.Out.Write("{0, 12:#,##0} {1} {2}",
				filesize[n],
				hash[n] == null ? new string('-', 32)
								: BitConverter.ToString(hash[n]).Replace("-", ""),
				Path.GetFileName(filename[n]));
			string	s = "";
			if(prev >= 0 && filesize[n] == filesize[prev] && compareHash(n, prev) == 0) {
				/*--*/ if(viewOnlyMode) {
					s = " ... (*)";		/* 何もしない */
				} else if(trashBoxMode) {
					s = " ... (TRASH)";	VbLib.RemoveFile(filename[n]);
				} else {
					s = " ... DELETED";	File.Delete(filename[n]);
				}
				count++;
			}
			Console.Out.WriteLine(s);
			prev = n;
		}
		//----------------------------------------------------------------------
		// メッセージ出力
		//----------------------------------------------------------------------
		string	format = count == 0   ? "全ファイル {0} 個のうち，重複ファイルはありません。"
					   : viewOnlyMode ? "全ファイル {0} 個のうち，重複ファイル {1} 個を削除できます。"
					   : trashBoxMode ? "全ファイル {0} 個のうち，重複ファイル {1} 個をゴミ箱に送りました。"
					   :                "全ファイル {0} 個のうち，重複ファイル {1} 個を削除しました。";
		Console.Out.WriteLine(format, index.Length, count);
		return 0;
	}
}
```

</details>

# 実行サンプル

とある電子コミック全18巻，ファイル数 1845 個，合計 2,219,331,488 bytes を対象にする。ちなみに全 1845 個のファイル中，ファイルサイズが同一のものは一組しか存在しない。

# 実行時間計測

実行時間の計測には筆者の作成したバッチファイルを用いる。

https://qiita.com/TETSURO1999/items/d3a9e3380560962927be


# 実行結果

まず指摘 No.2 のみに対応した Rev.2 の結果を示す。データは全て SSD に入っているとはいえ，初回だけは時間を要するので，キャッシュに載った２回目の実行結果を以下に示す。

```:UNIQFILE Rev.2 の実行結果
～中略～
FEB67A1A2B4E9D4BE8E44D7425D79F3D 056-057.JPG
FEDB2E65CA2CCE99BB772D714723B4A4 192-193.JPG
FEE2292ED69C8F42CA21902CE072DE06 034-035.JPG
FF1499ECC88AF14D46BE5E3B687C2BDD 046-047.JPG
FF21E617F65A9B10593DDC2D07A4C929 152-153.JPG
FF51F8C35DD4B544C2C26DB65055E5E5 030-031.JPG
FF8AE969D0D53D59642EFC4CDB6EC4AD 210-211.JPG
FFA4E7A15A20DCBB1B2F8232B4FB7E6B 158-159.JPG
FFB64B74756DD6356506E86264BCE309 080-081.JPG
FFBE7F99BC78F64FCB769D8907E29D6A 032-033.JPG
全ファイル 1845 個のうち，重複ファイルはありません。
実行時間：4,990[ms]／00:00:04.990[s]
```

次に指摘 No.4 にも対応した Rev.3 の結果を示す。こちらはファイルサイズを昇順にソートする。ハッシュ値が初期値 `null` のままの場合はハイフン `-` で示すようにした。 

```:UNIQFILE Rev.3 の実行結果
～中略～
   2,352,127 -------------------------------- 072-073.JPG
   2,372,063 C51121E4801EF675876148DFD0A34120 088-089.JPG
   2,404,810 0E49AFCF454520D128AF9761B0BA6AFB 144-145.JPG
   2,431,600 -------------------------------- 088-089.JPG
   2,461,879 06084D846F204364841F68A1F4C701E7 146-147.JPG
   2,486,408 -------------------------------- 162-163.JPG
   2,557,145 588EEAA1246AEDFCD62ACCC9C245CBA5 080-081.JPG
   2,568,829 -------------------------------- 120-121.JPG
   2,599,109 41EB01B60EED8DE140270E7D757FBC7E 142-143.JPG
   2,645,270 -------------------------------- 058-059.JPG
全ファイル 1845 個のうち，重複ファイルはありません。
実行時間：3,320[ms]／00:00:03.320[s]
```

**あれれ？おかしいぞ？1.5倍ほどしか速くなっていない・・・**

そもそもほとんどの場合，ファイルサイズは重複しない（全 1845 個中 2 個のみ重複する）のでハッシュ値を計算する必要がないはずなのに，わざわざハッシュ値を計算しているのだ。何かおかしい・・・

どうやら見つけてはいけないものを見つけてしまったような気がする・・・

長くなったので次回に続く。

https://qiita.com/TETSURO1999/items/68422ff100dbecf2ff1a

# 参考文献

- [ファイルやディレクトリの一覧を効率的に取得するには？［4以降、C#、VB］- @ITmedia](https://atmarkit.itmedia.co.jp/fdotnet/dotnettips/1018enumfiles/enumfiles.html)

- [【C#】usingの色々な使い方 - Qiita](https://qiita.com/4_mio_11/items/145c658078a7fe5f36a7)