【C#】Zip圧縮できないファイルを作成するツール

タイトルの通りZip圧縮しても圧縮率0パーセントで圧縮前と後でサイズがほぼ変わらないファイルを作成するツールを作ってみました。

作成環境

  • .NET Frmamework 4.7.2(C#7.3)
  • VisualStudio 2019
  • Windows Form

成果物

コードとソリューション・バイナリは Github にあります。

github.com

ツールの見た目はこんな感じです。

f:id:Takachan:20210604220216p:plain

サイズを入力してCreateを押せば指定したフォルダにファイルが作成されます。

ツールの見た目

Windows Form でライブラリをラップしています。

  • FileCreateForm.cs、画面のコード
  • DummyFileUtil、ファイルを作るためのクラス

Zip圧縮しても圧縮されない性質は、ファイル内の全隣接バイトがバイト単位で異なっていれば圧縮されないので、乱数を使って以下のように記述して任意の文字数を書き込むと実現できます。

FileStream fs = new FileStream(filePath, FileMode.Create);

for(int i = 0; i < len; i++)
    fs.WriteByte((byte)r.Next(min, max)); // randはRandomクラスのインスタンス

肝心の処理は DummyFileUtil に書いてあります。

// 1つファイルを作成する
public static async Task CreateFile(string filePath, uint fileSize, 
    byte complexity = byte.MaxValue,  IProgress<double> progress = null,
        CancellationToken ct = default)

// 複数件ファイルを作成する
public static async Task CreateFiles(string dir, string fileName, uint fileCount, 
    uint fileSize, byte complexity = byte.MaxValue, 
        IProgress<double> progress = null, CancellationToken ct = default)

complexity がバイトごとに使用する文字数を表します。1で1文字しか使わない = ファイル内はすべて同じ文字なる = zip圧縮率が最大となります。大きくすると使用する文字が多くなって圧縮率が下がります。255を指定すればzipではほぼ圧縮できないファイルが作成できます。

async なので進捗通知は IProgress経由、キャンセルは CancellationToken の ThrowIfCancellationRequested で実装しています。

private static async Task createFileCore(string filePath, uint fileSize, 
    byte min, byte max, IProgress<double> progress, CancellationToken ct)
{
    await Task.Run(() =>
    {
        try
        {
            var r = new Random(); // 高速で並列実行起動を想定しない
            using (var fs = new FileStream(filePath, FileMode.Create))
            {
                for (long i = 0; i < fileSize; i++)
                {
                    ct.ThrowIfCancellationRequested();

                    if (i % 1024 == 0)
                    {
                        // 間違ってテキストで開くと大変なことになるので適当に改行
                        fs.WriteByte((byte)'\n');
                    }
                    else
                    {
                        fs.WriteByte((byte)r.Next(min, max));
                    }

                    if (progress != null && i % 1024 == 0)
                    {
                        double value = (i + 1) / (double)fileSize;
                        progress.Report(value);
                    }
                }
            }
        }
        catch (OperationCanceledException ex)
        {
            System.Diagnostics.Debug.WriteLine($"{ex}, path='{filePath}'");
            if (File.Exists(filePath)) File.Delete(filePath);
        }
    });
}

Github Releaseににバイナリをアップ時の注意

VisualStudio の Release ビルドでビルドしたものを配布する時は一緒に生成された PDB を削除するのではなく、プロパティ > 出力 > デバッグ情報 を「なし」にしてからビルドしないと exe の中に自分の PC のパスが入ってしまうので思わぬ身バレとかがあるかもしれないので設定を忘れないようにしましょう。当然 PDB は配布パッケージの中には含めないようにしましょう。

簡単ですが以上です。

【C#】ビットフィールドのenumから値をすべて取り出す

ビットフィールドして宣言された enum (=FlagsAttribute が付与されているenum型) に複数の値が指定されている場合に設定されてるすべての値を別々に取り出す実装例の紹介です。

変数内メンバーを全て列挙する

例えば以下のようにビットフィールドとして宣言された enum があります。

// 以下のようにenumが定義されている

[Flags]
public enum Sample
{
    Apple      = 0b0001,
    Orange     = 0b0010,
    Pineapple  = 0b0100,
    Grapefruit = 0b1000,
}

上記の enum はひとつの変数に複数の値を以下のように保持できます。

var s = Sample.Apple | Sample.Orange;

この時変数 s に設定されている Sample のメンバーを別々にすべて取り出す実装例は以下の通りです。

public static class EnumExtension
{
    public static IEnumerable<T> GetFlagMembers<T>(this T self) where T : struct, Enum, IConvertible
    {
        var att = typeof(T).GetCustomAttributes<FlagsAttribute>();
        if (att is null)
        {
            throw new NotSupportedException("This type is 'FlagsAttribute' not specified.");
        }

        ulong a = self.ToUInt64(System.Globalization.CultureInfo.InvariantCulture);

        foreach (T m in Enum.GetValues<T>())
        {
            ulong b = m.ToUInt64(System.Globalization.CultureInfo.InvariantCulture);
            if ((b & a) == b)
            {
                yield return m;
            }
        }
    }
}

パフォーマンスを追求するなら以下のように具体的に処理方がややに早いです。(.NET Core5で確認)

が、メンテを怠ると不具合が出る可能性があるのであまりおすすめはできません。

// こっちの方が15%弱くらい早い
private static readonly Sample[] items = new Sample[]
{
    Sample.Apple,
    Sample.Orange,
    Sample.Pineapple,
    Sample.Grapefruit,
};

public static IEnumerable<Sample> GetFlagMembers(Sample value)
{
    foreach (var s in items)
    {
        if ((value & s) == s)
        {
            yield return s;
        }
    }
}

これを以下のように使用するとメンバーが取り出せます。

var s = Sample.Apple | Sample.Orange;

// ビットを分解して個々のメンバーとして取得できる
foreach (var item in s.GetFlagMembers())
{
    Console.WriteLine($"{item}");
    // > Apple
    // > Orange
}

ビット演算の基本操作(判定・追加・削除)

以下、余談ですがビット演算は以下のように判定・追加・削除ができます。

enum 型以外も同じように操作できます。

var s = Sample.Apple | Sample.Orange;

// フラグの有無:単一の値
bool contains = Sample.HasFlag(Sample.Apple);
contains = (s & Sample.Apple) == Sample.Apple; // こっちの方が動作が軽い

// フラグの有無:AND判定
Sample s2 = Sample.Apple | Sample.Pineapple;
contains = (s & s2) == s2; // Apple と Pineapple の両方を持っているかどうかの判定

// フラグの有無:OR判定
contains = (s & s2) != 0; // Apple もしくは Pineapple を持っているかどうかの判定

// フラグの追加
s = s | Sample.Pineapple; // s に Pineapple を追加
s |= Sample.Pineapple;

// フラグの削除
s = s & ~Sample.Pineapple; // s から Pineapple を削除
s &= ~Sample.Pineapple;

ValueObjectでファイルパスとファイル名を区別する

string 変数が xxxFilePath と書いてあってファイル名しか入ってない、xxxFileName と書いてあったのに中身はファイルパスということが頻発したりこのstring型そもそも何が入ってるのかわからないなんて事が頻発したので対応策を考えました。プリミティブな変数の中身は自分で書いたコードならさておき他人が書いたコードだとすごい判別しづらいので ValueObject を使って型で判断したいと思います。

確認環境

  • VisualStudio2019 19.6
  • C# 8.0

実装コード

使い方

先に使い方です。こんな感じで使えます。readonlyな構造体です。

// string を代入できる
FileName fname = "sample.txt";
FilePath fpath = @"d:\sample.txt";

File.Exist(fname); // 既存の型と互換する

fname = fpath; // こういうことはできない

string value = fname; // stringに代入できる

折角型を使っても、宣言したときに入れ間違えたら終わりなので気を付けてください。どうしようもないです。

ファイル名

ファイル名を表す型です。

// FileName.cs

// ファイル名
public readonly struct FileName : IEquatable<FileName>
{
    readonly string value;

    public string Value => this.value;

    public FileName(string value) => this.value = value;

    public static implicit operator string(FileName value) => value.value;
    public static implicit operator FileName(string value) => new FileName(value);

    public bool Equals(FileName other) => this.value.Equals(other.value);

    public override bool Equals(object obj) => obj is FileName _obj && this.Equals(_obj);
    public override int GetHashCode() => this.value.GetHashCode();
    public override string ToString() => this.value;

    public static bool operator ==(in FileName x, in FileName y) => x.value.Equals(y.value);
    public static bool operator !=(in FileName x, in FileName y) => !x.value.Equals(y.value);
}

ファイルパス

ファイルパスを表す型です。

// FilePath.cs

// ファイルパス
public readonly struct FilePath : IEquatable<FilePath>
{
    readonly string value;

    public FilePath(string value) => this.value = value;

    public string Value => this.value;

    public static implicit operator string(FilePath value) => value.value;
    public static implicit operator FilePath(string value) => new FilePath(value);

    public bool Equals(FilePath other) => this.value.Equals(other.value);

    public override bool Equals(object obj) => obj is FilePath _obj && this.Equals(_obj);
    public override int GetHashCode() => this.value.GetHashCode();
    public override string ToString() => this.value;

    public static bool operator ==(in FilePath x, in FilePath y) => x.value.Equals(y.value);
    public static bool operator !=(in FilePath x, in FilePath y) => !x.value.Equals(y.value);
}

ディレクトリ名

ついでにディレクトリ名の型も作成しておきます。

// DirectoryName.cs

// ディレクトリ名
public readonly struct DirectoryName : IEquatable<DirectoryName>
{
    readonly string value;

    public string Value => this.value;

    public DirectoryName(string value) => this.value = value;

    public static implicit operator string(DirectoryName value) => value.value;
    public static implicit operator DirectoryName(string value) => new DirectoryName(value);

    public bool Equals(DirectoryName other) => this.value.Equals(other.value);

    public override bool Equals(object obj) => obj is DirectoryName _obj && this.Equals(_obj);
    public override int GetHashCode() => this.value.GetHashCode();
    public override string ToString() => this.value;

    public static bool operator ==(in DirectoryName x, in DirectoryName y) => x.value.Equals(y.value);
    public static bool operator !=(in DirectoryName x, in DirectoryName y) => !x.value.Equals(y.value);
}

ディレクトリパス

ディレクトリパス用の型です。

// DirectoryPath.cs

// ディレクトリパス
public readonly struct DirectoryPath : IEquatable<DirectoryPath>
{
    readonly string value;

    public string Value => this.value;

    public DirectoryPath(string value) => this.value = value;

    public static implicit operator string(DirectoryPath value) => value.value;
    public static implicit operator DirectoryPath(string value) => new DirectoryPath(value);

    public bool Equals(DirectoryPath other) => this.value.Equals(other.value);

    public override bool Equals(object obj) => obj is DirectoryPath _obj && this.Equals(_obj);
    public override int GetHashCode() => this.value.GetHashCode();
    public override string ToString() => this.value;

    public static bool operator ==(in DirectoryPath x, in DirectoryPath y) => x.value.Equals(y.value);
    public static bool operator !=(in DirectoryPath x, in DirectoryPath y) => !x.value.Equals(y.value);
}

最後に

ただ、ValueObject → string への変換コンストラクターを implicit で宣言していますが以下のように若干ガバいのでそれが気になる場合は、implicit を explicit に変更したりそもそも変換コンストラクタを削除することもできます。

// これは簡単にできたほうがいい
FileName fileName ="asdf";

// 基本型にも簡単に代入できてしまう
string str = fileName;

// explicit に変更するとキャストしないと代入できなくなる
string str2 = (string)fileName;

// 基本クラスへの変換コンストラクタを削除して .Value 経由じゃないと取り出せないようにする
string str3 = fileName.Value;

こうすることでより安全に使う事ができるようになりますが、File.Copy(fileName.Value, (string)fileName2) のように記述しないといけなくなるので大幅に利用性が低下するのでここらへんはお好みでカスタマイズするようにしてください。

以上です。

【C#】MainメソッドでIDE1006の警告が出る場合

標準テンプレートでは以下のような指摘事項が表示されます。割とうっとおしい。

// IDE1006 名前付けルール違反: 最初の単語 'Main' は、小文字で始まらなければなりません
static void Main(string[] args)

これは .NET の一般的な名前付けのガイドラインが private メソッドはキャメルケースとされているために発生しています。

今回はこの表示を抑制する方法です。

確認環境

確認環境は以下の通り。

  • VisualStudio2019(16.9)
  • Windows10

#pragmaで警告を抑制する

まずひとつ目。特定の警告を抑制するための「#pragma warning disable」を使用します。disable の後ろに警告のIDを指定して特定の警告を抑制することができます。

class Program
{
// IDE1006だけを抑制する
// ↓
#pragma warning disable IDE1006
    static void Main(string[] args)
#pragma warning restore
// ↑
// ここでIDE1006の抑制を解除する
    {

以下のようにIDを指定しないとすべての警告を表示しないようにできます。

他人のコードを編集するときに警告だらけだけど VisualStudio の設定は変えたくないときに指定したりします。

// 全部の警告、指摘、提案を無効化する
#pragma warning disable

publicにしてしまう

次に、Main メソッドを public にする方法です。

// internal句を付与してに制限する(ついでにstaticにしておく)
internal static Program
{
    // public を追加する
    public static void Main(string[] args)

この場合同じアセンブリ (exe, dll) 内からしか呼べなくなるのですが、同じアセンブリから Main を再度呼ぶ事は性善説的にしない思うのでこれでも一応解決します。

というか、Program クラスが外部に公開されていてもいい事がひとつもない(脆弱性のレベルなので)Program クラスは外部公開しないほうがいいですね(Main メソッドだけ public にして internal を付けない場合、アセンブリ外から Main が呼べる状態になるので注意してください)

メインクラスのベストプラクティス

で、警告の話とは別にメインクラスとメインメソッドのベストプラクティスの話です。

まず、これらに求められる要件は以下の通りです。

  • メインクラス
    • アセンブリ外部に公開しない
    • メンバー変数、メソッドなどをアセンブリ内にも公開しない
    • インスタンス化は禁止
  • メインメソッド
    • たとえアセンブリ内と言えども呼び出し禁止
    • 処理を色々記述せずにほかのインスタンスに制御をすぐに渡す

上記を踏まえて以下のように生成後に書き直しましょう。注意点はコード内のコメント参照してください。

// Program.cs

using System;

#pragmra warning disable IDE1006 // 警告が気になるようであれば最初に宣言しておく

// アセンブリ外にクラスを公開しない、インスタンス化も禁止
internal static class Program
{
    // public フィールド・プロパティ・メソッドをメインクラス内に置かない

    // Mainメソッドはprivateのままにして外部公開しない(重要)
    private static void Main(string[] args)
    {
        // ここに色々と処理を書かず他のクラスに即座に処理を移譲する
    }
}

DIとか単体テスト観点でも Main メソッドのテストはまぁしないと思うので大抵のケースで問題ないと思います。

以上です。

【C#】リフレクションでnullチェックを自動化する

リフレクションを使って null チェックを自動化する方法です。

C# で null チェックをする場合以下のようなコードを書くと思います。

// サンプル用のクラスと変数宣言
public class Sample
{
   public int A { get; set; }
   public int B { get; set; }
}
var s1 = new Sampme();

// 中身のフィールドをチェックする
if(s1.A is null)  Console.WriteLine("Aはnullです");
if(s1.B is null)  Console.WriteLine("Bはnullです");

もしくはオブジェクト自身にチェック機能を設けたり、「null許容参照型」を使用してnullに対処する事もできます(もっとも null 許容型参照を実装の途中から有効化した場合既存コードに対する影響が少々あるのでできればやりたくないですが…)今回は、そういった方方法ではなくリフレクションを使って null を許可していないプロパティやフィールドに null が設定されているかのチェックする方法の紹介をしたいと思います。

確認環境

確認環境は以下の通りです。

  • .NET Core3.1 + C# 8.0
  • VisuaStudio2019
  • Windows10

使用方法とサンプル

まずは成果物の実装の使い方と説明をしたいと思います。

使用するにあたって以下の条件があります。

  • チェック対象のオブジェクトは INullCheckable を継承していること
  • null チェックしたいフィールドもしくはプロパティに NotNullAttribute を付けること
    • あくまで自作の型を対象にしたチェックなので両方必要にしています
    • この制限はコードを変更すれば廃止できます

で、使い方は以下の通り。

// まず比較したいクラスを以下のように宣言する

public class Root : INullCheckable  // ★(1) null チェックできる型はインターフェースを継承する
{
    // ★(2) チェック較したいプロパティやフィールドには NotNull 属性を付ける
    [NotNull] public string Name1 { get; set; } = ""; 
    public string Name2 { get; set; }
    [NotNull] public int? Value1 { get; set; } = 0;
    public int? Value2 { get; set; }
    [NotNull] public Child1 Child1 { get; set; } = new Child1();
    [NotNull] public Child1 Child2 { get; set; }
    public Child1 Child3 { get; set; }
    public Child1 Child4 { get; set; } = new Child1();
}

public class Child1 : INullCheckable // 入れ子になるクラスも同様
{
    [NotNull] public string ChildName1 { get; set; }
    public string ChildName2 { get; set; }
}

チェックは以下のように行います。

public static void Main(params string[] args)
{
    Root obj = new Root();
    bool result = obj.ValidateNull(); // チェックできる型はメソッドが呼び出せるようになっている

    Console.WriteLine(result); // true : チェックOK / false : null を検出した
}

実装コード

実装コードは以下の通りです。

ちなみに、面倒から解放される代わりに注意点があって、リフレクションなので処理速度がすっごい遅いです。1秒間に1000回とか呼び出して使用するとパフォーマンスが残念な可能性があります。

INullCheckable

null チェック対象するクラスに付与するインターフェースの定義です。何もメソッドが無いマーク用のインターフェースです。

// INullCheckable.cs

/// <summary>
/// null チェックを行うオブジェクトを表すマーカーインターフェースです。
/// </summary>
public interface INullCheckable { }

NotNullAttribute

null チェック対象のプロパティ or フィールドに付与するための属性です。

// NotNullAttribute.cs

/// <summary>
/// null チェックするプロパティもしくはフィールドを指定します。
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class NotNullAttribute : Attribute { }

INullCheckableExtension

コード例の「bool result = obj.ValidateNull();」できるようにするための実装です。

プロパティとフィールドを全部列挙して比較する。INullCheckable があれば再帰してさらに中身を比較する処理になっています。

/// <summary>
/// <see cref="INullCheckable"/> に対する拡張メソッドを定義します。
/// </summary>
public static class INullCheckableExtension
{
    /// <summary>
    /// 指定したオブジェクトと内容を比較します。
    /// </summary>
    public static bool ValidateNull<T>(this T self) where T : INullCheckable
    {
        if (self == null)
        {
            throw new ArgumentNullException(); // ルート要素がnullは処理対象にしない
        }
        return check(self);
    }

    // 再帰的にオブジェクトをNullチェックする処理
    private static bool check<T>(T self) where T : INullCheckable
    {
        var type = self.GetType();

        // 対象プロパティの列挙
        foreach (PropertyInfo p in getProperties(type))
        {
            var value = p.GetValue(self);
            
            var att = p.GetCustomAttribute<NotNullAttribute>();
            if (att != null && value is null)
            {
                Console.WriteLine(p.Name);
                return false;
            }

            if (value is INullCheckable obj)
            {
                if (!check(obj))
                {
                    return false;
                }
            }
        }

        // 対象フィールドの列挙
        foreach (FieldInfo f in getFields(type))
        {
            var value = f.GetValue(self);

            var att = f.GetCustomAttribute<NotNullAttribute>();
            if (att != null && value is null)
            {
                return false;
            }

            if (value is INullCheckable obj)
            {
                if (!check(obj))
                {
                    return false;
                }
            }
        }
        
        return true;
    }

    // 対象プロパティを全部列挙
    public static IEnumerable<PropertyInfo> getProperties(Type type)
    {
        const BindingFlags attributes =
            BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
        return type.GetProperties(attributes);
    }

    // 対象フィールドを全部列挙
    public static IEnumerable<FieldInfo> getFields(Type type)
    {
        const BindingFlags attributes =
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
        return type.GetFields(attributes);
    }
}

以上です。

【C#】コンストラクタの挙動まとめ

C# のコンストラクターの宣言のされ方による呼び出しの基本的な動作のまとめです。暗黙のコンストラクターと継承したときの挙動を中心に確認しています。内容は自分用のメモです。久しぶりに気にすると動きを忘れていることがあったので改めて文字に起こしています。

デフォルトコンストラクターの暗黙的な追加

コンストラクターを宣言しないと引数のないデフォルトコンストラクターが暗黙的に追加されます。

public class Foo
{
    // コンストラクターを宣言しない
}

// 以下のように宣言が追加される
// ↓

public class Foo
{
    public Foo()
    {
        // これをコンパイラーが自動で追加してくれる
    }
}

また引数ありのコンストラクターを宣言すると自動で追加されません。

public class Foo
{
    // こうするとデフォルトコンストラクターが自動で追加されなくなる
    public Foo(int a) { }
}

フィールド初期化はコンストラクタが実行

C#はフィールド変数とプロパティを宣言したときに初期化できますが、これはコンパイルするとそれぞれのコンストラクター内に初期化が移動されます。所謂シンタックスシュガーですね。

public class Foo
{
    // フィールドをインラインで初期化
    private int a = 99; 
    private string b = "str";

    // プロパティをインラインで初期化
    public double V { get; private set; } = 8.88;

    // コンストラクタを2つ宣言する
    public Foo(int a) { }
    public Foo(string b) { }
}

// 以下のように宣言が移動される
// ↓

public class Foo
{
    private int a; 
    private string b;

    public double V { get; private set; };

    // こんな感じにそれぞれのコンストラクタに初期化が追加される
    public Foo(int a)
    {
        a = 99;
        b = "str";
        V = 8.88;
        
        // この後に自分で書いた処理が入る
    }
    public Foo(string b)
    {
        a = 99;
        b = "str";
        V = 8.88;
    }
}

ILで見ると以下のようにコンストラクター内に初期化が展開されていることが確認できます。

.method public hidebysig specialname rtspecialname 
    instance void .ctor (
        int32 a
    ) cil managed 
{
    // Method begins at RVA 0x2061
    // Code size 41 (0x29)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldc.i4.s 99
    IL_0003: stfld int32 C::a
    IL_0008: ldarg.0
    IL_0009: ldstr "str"
    IL_000e: stfld string C::b
    IL_0013: ldarg.0
    IL_0014: ldc.r8 8.88
    IL_001d: stfld float64 C::'<V>k__BackingField'
    IL_0022: ldarg.0
    IL_0023: call instance void [System.Private.CoreLib]System.Object::.ctor()
    IL_0028: ret
} // end of method C::.ctor

.method public hidebysig specialname rtspecialname 
    instance void .ctor (
        string b
    ) cil managed 
{
    // Method begins at RVA 0x2061
    // Code size 41 (0x29)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldc.i4.s 99
    IL_0003: stfld int32 C::a
    IL_0008: ldarg.0
    IL_0009: ldstr "str"
    IL_000e: stfld string C::b
    IL_0013: ldarg.0
    IL_0014: ldc.r8 8.88
    IL_001d: stfld float64 C::'<V>k__BackingField'
    IL_0022: ldarg.0
    IL_0023: call instance void [System.Private.CoreLib]System.Object::.ctor()
    IL_0028: ret
} // end of method C::.ctor

自クラス内の他のコンストラクタの呼び出し

コンストラクターから自分のクラス内の他のコンストラクター呼び出しを行うことができる。冗長なコードの重複をこれで避けることができる。

public class A
{
    public A() => Console.WriteLine("A");
    public A(string str) : this() => Console.WriteLine(str); // 自分のクラス内の他のコンストラクタ呼び出しを行う
}

var a = new A("str");
// > str
// > A

基底クラスのコンストラクタの暗黙的な呼び出し

特に指定しない場合、暗黙的に基底クラスのデフォルトコンストラクターの呼び出しが連鎖が発生します。

例えば、A → B → C のように継承関係のあるクラスで特に指定しない場合デフォルトコンストラクターが暗黙で A → B → C の順で親から順に呼び出されます。

public class A
{
    public A() => Console.WriteLine("A");
}

public class B : A
{
    public B() => Console.WriteLine("B");
}

public class C : B
{
    public C() => Console.WriteLine("C");
}

// こんな感じに呼び出すと出力が以下の通りになる
var c = new C();
// > A
// > B
// > C

自分で base(...) を指定すると任意の基底クラスのコンストラクターを指定できます。途中で指定しないとデフォルトコンストラクター呼び出しに切り替わります。

public class A
{
    public A() => Console.WriteLine("A");
}

public class B : A
{
    public B() => Console.WriteLine("B-1");
    public B(string b) => Console.WriteLine("B-2"); // 基底クラスのコンストラクタを指定しない
}

public class C : B
{
    public C() => Console.WriteLine("C-1");
    public C(string c) : base(c) => Console.WriteLine("C-2"); // 基底クラスのコンストラクタを指定
}

// この場合以下のような出力になる
C c = new C("str");
// > A
// > B-2 // ★ここからデフォルトコンストラクター呼び出しに変わる
// > C-2

コンストラクタ内での仮想メソッド呼び出し

C# はコンストラクタ内で仮想メソッドを呼び出しても正常に動作する(C++は違い保証があります)

仮想メソッドテーブル(vtable)は初期化が完了した状態でコンストラクターの処理に入ります。

public class A
{
    public virtual void Foo() => Console.WriteLine("A");
}

public class B : A
{
    public override void Foo() => Console.WriteLine("B");
}

public class C : B
{
    public override void Foo() => Console.WriteLine("C");
    public C() => this.Foo();
}

// コンストラクタ内で仮想メソッド呼び出ししても正常な動作が保証される
C c = new C();
// > C
A a = new C();
// > C

UnityのMonoBehaviorで自作コンストラクターの宣言

Unity で MonoBehavior を継承したクラスで自作コンストラクターを宣言してはいけません。というかデフォルトコンストラクターの無い自作のコンポーネントを作成してはいけません(Unity はユーザーが任意にに作成したコンストラクターの引数の事情が分からないのでデフォルトコンストラクターを固定で呼出そうとするためです)

従ってコンポーネントを Unity がインスタンス化する時には自動でデフォルトコンストラクターを呼び出そうとする → デフォルトコンストラクターを呼ぼうとする → 失敗 → 処理が失敗しているにも関わらずコンポーネントが生成された扱いになる → そのあと Awake, Update を呼び出そうとする → 初期化が済んでない不定な状態のオブジェクトの処理が開始さる → エラーが起きる、のような流れで動作がおかしくなります。

public class MyComponent : MonoBehavior
{
    // こうやって自作のコンストラクターを宣言すると
    // 暗黙のデフォルトコンストラクター生成がされずにおかしい動作になる
    public MyComponent(string a) => Debug.Log("★");
}

// 暗黙のコンストラクターで実行されるはずのフィールド初期化と
// Unity が自動で解決してる シリアライズされたフィールドへの値の設定が未完了のまま後の処理が開始される

また、Unity から呼び出されないので実際は無意味ですがいちおう以下のおとりデフォルトコンストラクターを宣言しておけばエラーになりません。

public class MyComponent : MonoBehavior
{
    // デフォルトコンストラクターを明示して宣言すればエラーは起きない
    public MyComponent() => Debug.Log("〇");
    public MyComponent(string a) => Debug.Log("★"); // ただしこっちは呼ばれないので無意味
}

なので、コンストラクター自体を一切宣言NGではなく、デフォルトコンストラクターを明示しその中であれば自由に処理を記述しても問題ありません(どうせコンパイラーがデフォルトコンストラクターを暗黙追加してメンバー初期化をコンストラクタ内で実行しているので明示して宣言しようがあまり大差ないです)

public class MyComponent : MonoBehavior
{
    public MyComponent()
    {
        // ★このコンストラクター内であれば自由に処理を記述してもいい
    }
}

但し、このコンストラクター内で例外が出るような処理を書いて、実際に例外が起きるとかなり変な事になるため初期値の設定などの基本的に失敗しない動作を書きましょう(とは言っても、シリアライズされるフィールドに値が設定されていなかったり、Editor 上で実行 → 終了する時に必ずコンストラクター(とファイナライザー)が呼び出されたりするのでそこに色々書くと考慮することが多くなって大変なことなりがちなのでやはりコンポーネントの初期化であれば Awake や Start, 自作の初期化メソッドに書くのが良いと思います。

以上です

【C#】ValueObjectの実装例

この記事は、int や string の代わりに使用する値はプリミティブ型だけど値が特定の意味を持つため型にして区別したい時に使用する immutable(不変性:一度作ったら以降に内容が変化しないよう) なオブジェクトを実現ための C# での実装例の紹介です。

この ValueObject の使い道ですが、例えば、主に単一フィールドを対象にして、中身は string なんだけど名前を表すから Name クラス、JSON 文字列だから string 型ではなく JSON クラス、double 型だが単位はミリだから Millimeter クラスのように型を作成して(昔あった、LPSTR型のようなエイリアスとして代用する)場合や、設計モデル上のドメイン内の(immutable な性質を持つ)データをクラスとして表現する際の実装に便利に使用できる実装の紹介になります。

ちなみにドメイン駆動設計における ValueObject の意義・意図および一般的な実装方法論は他の技術系サイトや書籍で繰り返し説明されているためこの記事中ではこれ以上詳しく説明しません。

実装環境

この記事は以下の環境で実装、動作確認しています。

  • .NET Core 3.1、C# 8.0
  • Visual Studio 2019

実装コード

通常のValueObjectの実装

単一型

単一の値を表す ValueObject の実装例は以下の通りです。readonly struct と immutable化 で値を表します。必要に応じて演算子の operator を足したりしますがそこにドメイン固有の処理が入らないように注意しましょう。

// SinleValueObjectSample.cs

// 単一型のValueObjectのテンプレ
public readonly struct SinleValueObjectSample : IEquatable<SinleValueObjectSample>
{
    readonly string value;

    public SinleValueObjectSample(string value) => this.value = value;

    public string Value => this.value;

    public static implicit operator string(SinleValueObjectSample value)
    {
        return value.value;
    }
    public static implicit operator SinleValueObjectSample(string value)
    {
        return new SinleValueObjectSample(value);
    }

    public bool Equals(SinleValueObjectSample other) => this.value.Equals(other.value);

    public override bool Equals(object obj)
    {
        return obj is SinleValueObjectSample _obj && this.Equals(_obj);
    }
    public override int GetHashCode() => this.value.GetHashCode();
    public override string ToString() => this.value;

    public static bool operator
        ==(in SinleValueObjectSample x, in SinleValueObjectSample y) => x.value.Equals(y.value);
    public static bool operator
        !=(in SinleValueObjectSample x, in SinleValueObjectSample y) => !x.value.Equals(y.value);
}
複合型

基本型 & immutable なオブジェクトを複数組み合わせる複合型の ValueObject の実装は以下の通りです。あまり意味を持たせすぎたり、ドメイン固有の処理を入れるとすぐ負債化するのでそういったものは極力方に入れないようにします。

// 複合型のValueObjectのテンプレ
public readonly struct MultiValueObject : IEquatable<MultiValueObject>
{
    // immutable
    public readonly int A;
    public readonly SinleValueObjectSample B;
    public readonly SinleValueObjectSample C;

    // immutable constructor
    public MultiValueObject(int a, SinleValueObjectSample b, SinleValueObjectSample c)
    {
        this.A = a;
        this.B = b;
        this.C = c;
    }

    // 演算子のオーバーライド
    public static bool operator ==(MultiValueObject a, MultiValueObject b) => Equals(a, b);
    public static bool operator !=(MultiValueObject a, MultiValueObject b) => !Equals(a, b);

    // 等値比較演算子の実装
    public override bool Equals(object obj) =>
        (obj is MultiValueObject _obj) && this.Equals(_obj);

    // IEquatable<T> の implement
    public bool Equals(MultiValueObject other)
    {
        if (other is null)
        {
            return false;
        }

        // 個別に記述する
        return ReferenceEquals(this, other) ||
               this.A == other.A &&
               this.B == other.B &&
               this.C == other.C;
    }

    public override int GetHashCode()
    {
        unchecked
        {
            var hashCode = this.A;
            hashCode = (hashCode * 397) ^ this.B.GetHashCode();
            hashCode = (hashCode * 397) ^ this.C.GetHashCode();
            return hashCode;
        }
    }
}

共通基底クラス版(非推奨)

以降かなりの分量を書いておいて申し訳ありませんが、以下内容はパフォーマンスが非常に悪いので全然オススメではありません。前述の内容で十分なので以下興味があればお読みください。

MSのサイトに値オブジェクトを実装するというページがあるのですがこのページに共通部分は基底クラスに切り分けるみたいな事が書いてあったので、その実装例も紹介したいと思います。MSサイト内のコードから少しブラッシュアップしつています。

先述の基本実装例だと実装量が多いので基底クラス側で「immutable に値を保持できること」、「内容で等値判定したい」、「保持する値はnullを許可しない」などの共通の性質を基底クラスに抽出し、ValueObject はこれを継承することで実現していきます。

単一型

まずは単一のフィールドを対象とした値オブジェクの実装です。

// ValueObject.cs

/// <summary>
/// 値オブジェクトを表します。
/// </summary>
public abstract class ValueObject<T> : IEquatable<ValueObject<T>>
{
    //
    // Fields
    // - - - - - - - - - - - - - - - - - - - -
    private readonly T Value; // immutable

    //
    // Operators
    // - - - - - - - - - - - - - - - - - - - -

    public static bool operator ==(ValueObject<T> a, ValueObject<T> b)
    {
        if (!(a is null)) return a.Equals(b);
        else if (!(b is null)) return b.Equals(a);
        return true;
    }
    public static bool operator !=(ValueObject<T> a, ValueObject<T> b) => !(a == b);

    //public static implicit operator T(ValueObject<T> v) => v.Value.Clone();

    //
    // Constructors
    // - - - - - - - - - - - - - - - - - - - -

    public ValueObject(T value) => this.Value = value ?? throw new ArgumentNullException();

    //
    // Public Methods
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// <see cref="object.Equals(object)"/> をオーバーライドします。
    /// </summary>
    public override bool Equals(object obj)
    {
        return obj is ValueObject<T> other && this.Value.Equals(other.Value);
    }

    /// <summary>
    /// 他の <see cref="ValueObject{T}"/> と比較します。
    /// </summary>
    public bool Equals(ValueObject<T> other)
    {
        if (other is null)
        {
            return false;
        }
        return EqualityComparer<T>.Default.Equals(this.Value, other.Value);
    }

    /// <summary>
    /// <see cref="object.GetHashCode"/> をオーバーライドします。
    /// </summary>
    public override int GetHashCode() => this.Value.GetHashCode();

    //
    // Non-Public Methods
    // - - - - - - - - - - - - - - - - - - - -

    protected virtual bool Validate(T v) => !(v is null);
}

使い方は以下の通りです。例えば string の値を何らかのキー要素として扱いた場合は以下のように実装します。

// Key.cs

// 実装例(1)
// T が string 型の ValueObject
public class Key : ValueObject<string>
{
    // 必要があれば定義
    public static readonly Key Key1 = new Key("type1");
    public static readonly Key Key2 = new Key("type2");

    // 完全コンストラクターパターンで最低限これだけ宣言すればよい
    public Key(string value) : base(value) { }
}

// 使用方法
string _a = "a";
Key a = new Key(_a);

string _b = "b";
Key b = new Key(_b);

// 中身の値ベースで等値判定
if (a == b) 
{
    Console.WriteLine("a == b");
}

比較演算子を実装しているので実際は中身の値で比較を行なっています。

また、自作のクラスを ValueObject 化することもできます。指定するクラス内でも immutable を保証しないといけないのでひと手間必要です。

// 実装例(2)

// 自作のクラス
public class Sample
{
    public string Name { get; set; }

    public object Clone() => new Sample() { Name = this.Name }; // DeepCopy

    public override bool Equals(object obj)
    {
        if (obj == null) return false;
        if (obj is Sample s) return s.Name == this.Name;
        return false;
    }

    public override int GetHashCode()
    {
        // ...
    }
}

// T が 自作型の値オブジェクト
public class SampleValue : ValueObject<Sample>
{
    public SampleValue(Sample value) : base(value) { }
}

// 宣言方法
var s1 = new Sample() { Name = "Tako"; }
a = new SampleValue(s1);

c = new SampeValue(null); //null はエラーになる

自作の型を ValueObject にする場合そのオブジェクトで Equals をオーバーライドして中身ベースの比較を実装する必要があります。

また以下のように、 既存APIと互換させる事もできます。

// 実装例(3)

// JSONを表すクラス
public class Json : ValueObject<string>
{
    public Json(string value) : base(value) { }
}

// こういった既存APIへの値の受け渡しが変換コンストラクタによってシームレスに操作できる
Json json = new Json("{ \"root:\" [\"ABC\", \"DEF\", \"GHI\"] }");
var s = JsonUtility.FromJson<Sample>(json);

この実装はUnity上で動作させた場合の GCAlloc は生成と比較時は基本の実装と同等、動作速度は1.3-1.5倍程度です(微妙…w

複合型

複合型の ValueObject の場合、複数のフィールドで1つの ValueObject を構成します。2つの string があり1つめが名前、2つ目が住所など複数の値のセットが一つのクラスを表します。MSのサイトに掲載されているのはこっちですね。ただ、パフォーマンスが向上するようにいくらか書き直しています。

/// <summary>
/// 複合型の値オブジェクトの基底クラスです。
/// </summary>
public abstract class CompositeValueObject<T> : IEquatable<CompositeValueObject<T>>
{
    //
    // Fields
    // - - - - - - - - - - - - - - - - - - - -

    private readonly object[] cache;

    //
    // Props
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// オブジェクトが持つフィールドの数を定義します。
    /// </summary>
    protected abstract int FieldCount { get; }

    //
    // Operators
    // - - - - - - - - - - - - - - - - - - - -

    // 同じ型どうし比較
    public static bool operator ==(CompositeValueObject<T> a, CompositeValueObject<T> b)
    {
        if (!(a is null)) return a.Equals(b);
        else if (!(b is null)) return b.Equals(a);
        return true;
    }
    public static bool operator !=(CompositeValueObject<T> a,
        CompositeValueObject<T> b) => !(a == b);

    //
    // Constructors
    // - - - - - - - - - - - - - - - - - - - -

    public CompositeValueObject() => this.cache = new object[this.FieldCount];

    //
    // Pulbic Methods
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// <see cref="object.Equals(object)"/> をオーバーライドし、等値判定を行います。
    /// </summary>
    public override bool Equals(object obj)
    {
        return (obj is CompositeValueObject<T> b) && this.Equals(b);
    }

    /// <summary>
    /// <see cref="object.GetHashCode"/> をオーバーライドし、オブジェクトのハッシュ計算を行います。
    /// </summary>
    public override int GetHashCode()
    {
        unchecked
        {
            int sum = 0;
            foreach (var item in this.__GetEqualityComponents())
            {
                if (item is null)
                {
                    sum ^= 0;
                }
                else
                {
                    sum ^= item.GetHashCode();
                }
            }
            return sum;
        }
    }

    /// <summary>
    /// <see cref="IEquatable.Equals(T)"/> を実装します。
    /// </summary>
    public bool Equals(CompositeValueObject<T> other)
    {
        if (other is null)
        {
            return false;
        }

        var self = this.__GetEqualityComponents();
        var bb = other.__GetEqualityComponents();
        if (self.Length != bb.Length)
        {
            return false;
        }

        for (int i = 0; i < self.Length; i++)
        {
            if (self[i] != bb[i])
            {
                return false;
            }
        }
        return true;
    }

    //
    // Non-Public Methods
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 派生クラスで定義される immutable なフィールドを配列に列挙します
    /// </summary>
    protected abstract void GetEqualityComponents(object[] array);
    bool isInit;

    // 指定した値の中に null が含まれているかを確認する
    // true : 含まれている / false : 含まれていない
    protected bool ContainsNull(params object[] args)
    {
        foreach (object o in args)
        {
            if (o is null) return true;
        }
        return false;
    }

    // 各フィールドを列挙
    private object[] __GetEqualityComponents()
    {
        if (!this.isInit)
        {
            this.GetEqualityComponents(this.cache);
            this.isInit = true;
        }
        return this.cache;
    }
}

基底クラスでは複数のフィールドを等値判定する機能と、派生クラスに対し比較対象にするメンバー変数群の登録の要求する機能を実装しています。

具体的な使い方は以下の通りです。

/// <summary>
/// 複合型の ValueObject
/// </summary>
public class Foo : CompositeValueObject
{
    // 複合型値オブジェクトで管理する値
    public string Name { get; private set; }
    public int ID { get; private set; }

    protected override int FieldCount => 2;

    public Foo(string name, int id)
    {
        // 自分のコンストラクタ内でチェックして代入する
        if (this.ContainsNull(name, id)) throw new ArgumentException();
        this.Name = name;
        this.ID = id;
    }

    // 比較するメンバーの設定
    protected override void GetEqualityComponents(object[] array)
    {
        array[0] = this.Name;
        array[1] = this.ID;
    }
}

ちょっと単一型と中身の変数値へのアクセス方法が異なるのが気になるところではありますが、もし本当に気になるようであれば各々で改良してもよいと思います。

派生クラス側はこれを継承すれば割とすっきり実装できると思います。MSDN のサイト内の実装例ではメンバーの列挙に毎回 Linq を用いるなどで比較やハッシュ計算時の動作速度がちょっと遅めだったので、この実装例ではメンバーの列挙結果はキャッシュし、IEnumerable からの Linq 操作は通常の構文に展開して速度を向上させています。

この実装、動作速度は通常版の1.2-1.4倍程度と単一フィールドのよりマシな速度で動作しますが、1フィールド辺り90BくらいGCAllocが増加するので同じ構造で3つのintで56B対384Bの割合で不要にメモリを使用します(intだとobject配列に代入されるときにBox化が発生 + キャッシュ領域のオブジェクト配列分でヒープを使用)なので、高頻度で生成と破棄を繰り返すと問題になる可能性があります。

C#9.0はrecodeキーワードが登場

まぁ通常版が一番優れているという他ないのですが、MSのサイトに掲載されているアイデアは一見よさそうでしたが、資源が限られている環境ですごく微妙なので(そのまま使うと動作速度が12-15倍、メモリ消費量に至っては7万倍(56B:390.6KB)でやばい)昨今サーバーサイドでもクラウドだと計算量が少ないに越したことはないのでスニペットやジェネレーターでコード生成したほうがよさそうです。

また、C# 9.0 からこういった実装は recode キーワードで非常に簡単に実装できるようになります。recode キーワードを付与したクラスは上記のような実装がコンパイル時に自動で付与されるとのことです。ただ C# 9.0 を使えるのは .NET 5 以降のため、普段の業務システムや Unity の標準環境ににもう少後になりそうです。

docs.microsoft.com

以上です。

【C#】リフレクションでオブジェクトの内容を比較する

リフレクションを使ってオブジェクトを中身で比較する方法です。

C#でオブジェクトを比較するときは以下のようにコードを書くと思います。

// サンプル用のクラスと変数宣言
public class Sample
{
   public int A { get; set; }
   public int B { get; set; }
}
var s1 = new Sampme();
var s2 = new Sampme();

// (1) 同じインスタンスかどうかを比較
if(s1 == s2) Console.WriteLine("インスタンスが同じ");

// (2) 同じ内容かを比較する
if(s1.A == s2.A && s1.B == s2.B) Console.WriteLine("内容が同じ");

ですが、(2) のように内容を比較するときは IEquatable を実装したり、== と != の演算子をオーバーロードしたりして中身どうしを比較する実装って考慮することが結構多いので割とめんどくさい(し、不注意から間違いやすい)コードを書く事が多いと思います。実装方法はだいたいこのページに書いてある通りです

なので面倒なのでリフレクションで、そういう実装をせずにオブジェクトの内容同士を比較しようと思います。

確認環境

確認環境は以下の通りです。

  • .NET Core3.1 + C# 8.0
  • VisuaStudio2019
  • Windows10

使用方法とサンプル

まずは成果物の実装の使い方と説明をしたいと思います。

使用するにあたって以下の条件があります。

  • 比較可能なオブジェクトは IReflectionEquatable を継承していること
  • 比較するフィールドもしくはプロパティに TargetAttribute を付けること
    • もしかすると IgnoreAttribute で基本全部比較するけど Ignore だけ対象にしないの方がいいかも?

で、使い方は以下の通り。

// まず比較したいクラスを以下のように宣言する

// 親のオブジェクト
public struct Value : IReflectionEquatable // ★(1) 比較できる型はインターフェースを継承する
{
    [Target] public SubValue PA { get; set; } // ★(2) 較したいプロパティに Target 属性を付ける
    [Target] public string PB { get; set; }
    [Target] private int pc { get; set; }

    [Target] public SubValue FA; // ★(3) 比較したいフィールドに Target 属性を付ける
    [Target] public string FB;
    [Target] private int fc;
    public void SetPC(int value) => this.pc = value;
    public void SetFC(int value) => this.fc = value;
}

// 子のオブジェクト
public struct SubValue : IReflectionEquatable
{
    [Target] public string PAS { get; set; }
    [Target] private int pbs { get; set; }
    [Target] public string FAS;
    [Target] private int fbs;
    public void SetPBS(int value) => this.pbs = value;
    public void SetFBS(int value) => this.fbs = value;
}

比較は以下のように行います。

public static void Main(string[] args)
{
    // テスト用データの取得
    (Value a, Value b) = createValue_1();
    
    // ★★★オブジェクトどうしの比較
    //   → Target属性がついているメンバーを全部比較してくれる
    bool ret = a.Equals<Value>(b);
    // ret = true
}

// テストデータ作成用のメソッド
private static (Value a, Value b) createValue_1()
{
    var sa = new SubValue() { PAS = "aaa", FAS = "bbb", };
    sa.SetPBS(10);
    sa.SetFBS(11);
    var fa = new SubValue() { PAS = "ccc", FAS = "ddd", };
    fa.SetPBS(12);
    fa.SetFBS(13);
    var a = new Value()
    {
        PA = sa,
        PB = "ccc",
        FA = fa,
        FB = "ddd",
    };
    a.SetPC(14);
    a.SetFC(15);

    var sb = new SubValue() { PAS = "aaa", FAS = "bbb", };
    sb.SetPBS(10);
    sb.SetFBS(11);
    var fb = new SubValue() { PAS = "ccc", FAS = "ddd", };
    fb.SetPBS(12);
    fb.SetFBS(13);
    var b = new Value()
    {
        PA = sb,
        PB = "ccc",
        FA = fb,
        FB = "ddd",
    };
    b.SetPC(14);
    b.SetFC(15);

    return (a, b);
}

実装コード

実装コードは以下の通りです。

リフレクションなので処理速度がすっごい遅いです。1秒間に1000回とか呼び出して使用するとパフォーマンスが残念な可能性があります

IReflectionEquatable

比較対象に付与するインターフェースの定義です。何もメソッドが無いマーク用のインターフェースです。

// IReflectionEquatable.cs

/// <summary>
/// リフレクションによって等値比較を行うことができることを表すマーカーインターフェース
/// </summary>
public interface IReflectionEquatable { }

TargetAttribute

比較したいプロパティ or フィールドに付与するための属性です。

// TargetAttribute.cs

/// <summary>
/// <see cref="IReflectionEquatable"/> を使用した比較対象であることを表します。
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class TargetAttribute : Attribute { }

IReflectionEquatableExtension

コード例の「bool ret = a.Equals(b);」できるようにするための実装です。

プロパティとフィールドを全部列挙して比較する。IReflectionEquatable があれば再帰してさらに中身を比較する処理になっています。

// IReflectionEquatableExtension.cs

/// <summary>
/// <see cref="IReflectionEquatable"/> に対する拡張メソッドを定義します。
/// </summary>
public static class IReflectionEquatableExtension
{
    /// <summary>
    /// 指定したオブジェクトと内容を比較します。
    /// </summary>
    public static bool Equals<T>(this T self, T target) where T : IReflectionEquatable
    {
        return equals(self, target);
    }

    // 再帰的にオブジェクトを比較する処理
    private static bool equals<T>(T a, T b) where T : IReflectionEquatable
    {
        if (a == null && b != null || a != null && b == null)
        {
            return false;
        }
        else if (a == null && b == null)
        {
            return true;
        }

        var type = a.GetType();

        // 対象プロパティの列挙
        foreach (PropertyInfo p in getProperties(type))
        {
            var pa = p.GetValue(a);
            var pb = p.GetValue(b);
            //Console.WriteLine($"[Property] {p.Name}: a={pa}, b={pb}");

            if (pa is IReflectionEquatable _pa)
            {
                bool ret = equals(_pa, pb as IReflectionEquatable);
                if (!ret)
                {
                    //Console.WriteLine($"false");
                    return false;
                }
            }

            if (pa?.Equals(pb) == false)
            {
                //Console.WriteLine($"false");
                return false;
            }
        }

        // 対象フィールドの列挙
        foreach (FieldInfo f in getFields(type))
        {
            var fa = f.GetValue(a);
            var fb = f.GetValue(b);
            //Console.WriteLine($"[Field] {f.Name}: a={fa}, b={fb}");

            if (fa is IReflectionEquatable _fa)
            {
                bool ret = equals(_fa, fb as IReflectionEquatable);
                if (!ret)
                {
                    //Console.WriteLine($"false");
                    return false;
                }
            }

            if (fa?.Equals(fb) == false)
            {
                //Console.WriteLine($"false");
                return false;
            }
        }

        return true;
    }

    // 対象プロパティを全部列挙
    public static IEnumerable<PropertyInfo> getProperties(Type type)
    {
        const BindingFlags attributes =
            BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
        return type.GetProperties(attributes).Where(p => p.GetCustomAttribute<TargetAttribute>() != null);
    }

    // 対象フィールドを全部列挙
    public static IEnumerable<FieldInfo> getFields(Type type)
    {
        const BindingFlags attributes =
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
        return type.GetFields(attributes).Where(p => p.GetCustomAttribute<TargetAttribute>() != null);
    }
}

以上です。

C#の文字列補完はToStringした方が高速になる理由を調べてみた

はじめに

C# の「文字列補完」機能という機能があります。ものすごい雑に言うと C# 6 以降で可能な文字列の先頭に「$」起動を付けることでフォーマット付き文字列の視認性を向上することができる機能です。例えば以下のような記述ができます。

// (1)
// 旧来の記述方法
int a = 10;
// + で連結する
string msg1 = "(" + a + ")";
// もしくは string.Format メソッドを使う
string msg2 = string.Format("({0})", a);
> (10)

// (2)
// 文字列補完を使った表現
string msg3 = $"({a})";
> (10)

string.Format をインラインで記述できるようになったイメージです。

確認環境

  • .NET Core3.1
  • VisualStudio2019
  • Windows10

{ } の中はToString した方が高速?

まずは確認ですが記述方法によって速度にが明確に差があります。

これがどういうことから調査したいと思います。以下のコードでは「case.3」が高速です。「全部 ToString した時だけ」動作が早いです。

int a = 10;
int b = 20;

// case.1
string msg1 = $"{a}, {b}";
// > 0.000258828msec

// case.2
string msg2 = $"{a}, {b.ToString()}";
// > 0.000250034msec

// case.3
string msg3 = $"{a.ToString()}, {b.ToString()}";
// > 0.000158491msec

約40%も速度が違うため実行資源が限られている環境(≒モバイル)では大半のケースで ToString するのがベストプラクティスだと思います。

どうして速度が違うのか?

では、どうしてこのような違いが出たのでしょうか?こういう時はSharpLabが便利です。

先述のコードをツールにかけると概ね以下のようになります。

// case.1
string msg1 = $"{a}, {b}";
↓
string msg1 = string.Format("{0}, {1}", a, b);

// case.2
string msg2 = $"{a}, {b.ToString()}";
↓
string msg2 = string.Format("{0}, {1}", a, b.ToString());

// case.3
string msg3 = $"{a.ToString()}, {b.ToString()}";
↓
string msg3 = string.Concat(a.ToString(), ", ", b.ToString());

速度の違いはコンパイラがコードをILにした時に違いがあるからのようです。展開のされ方が違います。

全部文字列だと判断された場合だけ「string.Format」ではなく「string.Concat」が使用されています。string.Format の方が実装が複雑なので処理時間が違うようです(この程度だとボックス化による処理速度の影響はほぼ無視できる程度のため純粋に使用したメソッドの性能によるところが大きいと思います)

つまり、全部文字列だったら早い ので、一部を ToString するだけでは効果がありません。

IDE0071 保管を簡略化することができますが煩わしい場合

VisualStudio2019 では文字列補完中に ToString を書くと「IDE0071 保管を簡略化することができます」と警告が表示されます。

ただ、上記の結果から明示して使用するケースがあるため煩わしい場合は、以下をコードの先頭に記述すればこの警告が抑制できます。

// コードの先頭 ~ using 後くらいの位置に以下を記述する
#pragma warning disable IDE0071

検証コード

この検証で使用したコードは以下の通りです。

using System;
using System.Diagnostics;

#pragma warning disable IDE0071, IDE0059

internal class AppMain
{
    public static void Main(string[] args)
    {
        int a = 10;
        int b = 20;

        var ast = new Stopwatch();
        var bst = new Stopwatch();
        var cst = new Stopwatch();

        string msg1 = "";
        string msg2 = "";
        string msg3 = "";

        int cnt = 100000;

        for (int i = 0; i < cnt; i++)
        {
            ast.Start();
            msg1 = $"{a}, {b}";
            ast.Stop();

            bst.Start();
            msg2 = $"{a}, {b.ToString()}";
            bst.Stop();

            cst.Start();
            msg3 = $"{a.ToString()}, {b.ToString()}";
            cst.Stop();
        }

        Console.WriteLine($"{ast.Elapsed.TotalMilliseconds / cnt}msec");
        Console.WriteLine($"{bst.Elapsed.TotalMilliseconds / cnt}msec");
        Console.WriteLine($"{cst.Elapsed.TotalMilliseconds / cnt}msec");
    }
}

【C#】Listと配列に対するforとforeachのアクセス速度の比較

先に結論ですが、全く同じ処理を for と foreach で行った場合、Listではfor、配列では foreach の方が早いです。

以下、検証内容です。

確認環境

  • .NET Core3.1
  • VisualStudio 2019
  • Windows10
  • Cire-i7 3770K + 16GB RAM

Relaseビルドしたバイナリをコンソールから実行して確認してます。

Listのアクセス速度

実装コード

確認コードは以下の通りです。

単純に10万件のリストを作成してそれに対して同一条件でアクセスしてリストの中身の合計数を求めています。

public static void Main(string[] args)
{
    // ループ回数=10万件
    int loopCnt = 100000;

    var list = createTestData(loopCnt);

    // キャッシュが効いて結果が不公平にならないように
    // 同じ処理を計測せず事前に1000回走らせておく
    for (int pp = 0; pp < 1000; pp++)
    {
        {
            var sw1 = Stopwatch.StartNew();

            int sum1 = 0;
            for (int i = 0; i < list.Count; i++)
            {
                sum1 += list[i];
            }
            sw1.Stop();

            var sw2 = Stopwatch.StartNew();
            int sum2 = 0;
            foreach (var p in list)
            {
                sum2 += p;
            }
            sw2.Stop();
        }
    }

    {
        List<double> a1 = new List<double>();
        List<double> a2 = new List<double>();
        for (int pp = 0; pp < 1000; pp++)
        {
            var sw1 = Stopwatch.StartNew();

            int sum1 = 0;
            for (int i = 0; i < list.Count; i++)
            {
                sum1 += list[i];
            }

            sw1.Stop();
            a1.Add(sw1.Elapsed.TotalMilliseconds);

            var sw2 = Stopwatch.StartNew();
            int sum2 = 0;
            foreach (var p in list)
            {
                sum2 += p;
            }
            sw2.Stop();
            a2.Add(sw2.Elapsed.TotalMilliseconds);
        }

        Console.WriteLine($"for, {a1.Average()}msec");
        Console.WriteLine($"foreach, {a2.Average()}msec");
        // > for, 0.16245250000000092msec
        // > foreach, 0.24426649999999864msec
    }
}

private static List<int> createTestData(int count)
{
    IEnumerable<int> f()
    {
        for (int i = 0; i < count; i++)
        {
            yield return i;
        }
    }
    return f().ToList();
}

計測結果

コードの通り1000回ほど計測した平均が以下の通りでした。

for foreach
0.1624525msec 0.2442665msec

for の方が早い。

配列のアクセス速度

確認コード

次は配列のアクセス速度の比較です。

先ほどのコードとほぼ変わりないですが計測コードと結果は以下の通りです。

public static void Main(string[] args)
{
    // ループ回数
    int loopCnt = 100000;

    var list = createTestData(loopCnt);

    // キャッシュが効いて結果が不公平にならないように
    // 同じ処理を計測せず事前に1000回走らせておく
    // for(... 省略

    {
        List<double> a1 = new List<double>();
        List<double> a2 = new List<double>();
        for (int pp = 0; pp < 1000; pp++)
        {
            var sw1 = Stopwatch.StartNew();

            int sum1 = 0;
            for (int i = 0; i < list.Length; i++)
            {
                sum1 += list[i];
            }

            sw1.Stop();
            a1.Add(sw1.Elapsed.TotalMilliseconds);

            var sw2 = Stopwatch.StartNew();
            int sum2 = 0;
            foreach (var p in list)
            {
                sum2 += p;
            }
            sw2.Stop();
            a2.Add(sw2.Elapsed.TotalMilliseconds);
        }

        Console.WriteLine($"for, {a1.Average()}msec");
        Console.WriteLine($"foreach, {a2.Average()}msec");
        // > for, 0.13522820000000016msec
        // > foreach, 0.053829400000000284msec
    }
}

private static int[] createTestData(int count) // ☆ここを配列にしている
{
    IEnumerable<int> f()
    {
        for (int i = 0; i < count; i++)
        {
            yield return i;
        }
    }
    return f().ToArray();
}

計測結果

1000回ほど計測した平均が以下の通りでした。

for foreach
0.1352282msec 0.0538294msec

配列の方がListよりだいぶ処理自体が高速ですね。

今度は foreach の方が倍以上早いです。

速度がListと配列で逆転する理由

どうして各々の foreach でこんなに速度差が出る(しかも逆転する)のか少し内容を見ていきたいと思います。

まず、以下コンパイルしたときに.NETのコンパイラーが追加したコードです。

List の foreach と配列の foreach だとコンパイラーが生成するコードが見かけ以上に全然違います。

// ★Listの場合、、、、

// var sw2 = Stopwatch.StartNew();
// int sum2 = 0;
// foreach (var p in list)
// {
//     sum2 += p;
// }
// sw2.Stop();

// ↓ ★List の場合 foreach は以下のように展開される

Stopwatch stopwatch2 = Stopwatch.StartNew();
int num2 = 0;
List<int>.Enumerator enumerator = list.GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
        int current = enumerator.Current;
        num2 += current;
    }
}
finally
{
    ((IDisposable)enumerator).Dispose();
}
stopwatch2.Stop();

// ↓ ★配列の場合 foreach は以下のように展開される

Stopwatch stopwatch2 = Stopwatch.StartNew();
int num2 = 0;
int[] array2 = array;
foreach (int num3 in array2)
{
    num2 += num3;
}
stopwatch2.Stop();
Console.WriteLine(string.Format("{0}msec, {1}", stopwatch2.Elapsed.TotalMilliseconds, num2));

イテレーターが MoveNext() するのは遅くて当然といった感じです。

では配列はどうかというと、更に配列に対して for するのと foreach は IL を見ると以下のようになります。

IL_0094: ldc.i4.0
IL_0095: stloc.s 14
IL_0097: ldc.i4.0
IL_0098: stloc.s 17
// sequence point: hidden
IL_009a: br.s IL_00ab
// loop start (head: IL_00ab)
    IL_009c: ldloc.s 14
    IL_009e: ldloc.0
    IL_009f: ldloc.s 17
    IL_00a1: ldelem.i4
    IL_00a2: add
    IL_00a3: stloc.s 14
    IL_00a5: ldloc.s 17
    IL_00a7: ldc.i4.1
    IL_00a8: add
    IL_00a9: stloc.s 17

    IL_00ab: ldloc.s 17
    IL_00ad: ldloc.0
    IL_00ae: ldlen
    IL_00af: conv.i4
    IL_00b0: blt.s IL_009c
// end loop


IL_00d7: ldc.i4.0
IL_00d8: stloc.s 16
IL_00da: ldloc.0
IL_00db: stloc.s 7
IL_00dd: ldc.i4.0
IL_00de: stloc.s 8
// sequence point: hidden
IL_00e0: br.s IL_00f6
// loop start (head: IL_00f6)
    IL_00e2: ldloc.s 7
    IL_00e4: ldloc.s 8
    IL_00e6: ldelem.i4
    IL_00e7: stloc.s 19
    IL_00e9: ldloc.s 16
    IL_00eb: ldloc.s 19
    IL_00ed: add
    IL_00ee: stloc.s 16
    // sequence point: hidden
    IL_00f0: ldloc.s 8
    IL_00f2: ldc.i4.1
    IL_00f3: add
    IL_00f4: stloc.s 8

    IL_00f6: ldloc.s 8
    IL_00f8: ldloc.s 7
    IL_00fa: ldlen
    IL_00fb: conv.i4
    IL_00fc: blt.s IL_00e2
// end loop

配列のforeachは i++ の足し算をしていない分早いようです(これは自分もだいぶ予想外でした…すごいですね

という事で配列にシーケンシャルアクセスする場合 foreach がいい感じという話になりました。

おまけ

悪い計測例

同一の考察を検索したときに参考にしてはいけない例を見たので一応注意しておこうと思います。以下ネットで見た悪い例です。

Google検索結果で"c# list foreach vs for performance"と検索して検索結果第2位のサイトです。

以下記事ではforeachの方が早いと結論していますが、記事中のコードではforeachの方がリストに対するアクセス回数が1000倍違うのにforeachの方が10倍早いと結論しています。

確かに「このコードでは」早いでしょうね。以下問題点をコメントで指摘しています。

// 悪い計測例
List<string> testlist = // 各要素に"a"が入っている100000件のリスト
string buf;
for (var i = 0; i < testlist.Count; i++)
{
    for (var cnt = 0; cnt < 1000; cnt++)
    {
        buf = testlist[i]; // 合計で1億回リストにインデックスアクセスしている
    }
}

foreach (var i in testlist)
{
    for (var cnt = 0; cnt < 1000; cnt++)
    {
        buf = i; // 合計で10万回リストにアクセスしている
    }
}

ちょっと何がしたいのか不明ですが各々のループの目的と条件が全く異なるので公平に計測できていません。

上記を公平に取り扱って計測するなら for のループは以下のように訂正したほうが良いでしょう。

// 悪い計測例
List<string> testlist = // 各要素に"a"が入っている100000件のリスト
string buf;

// ★(1)
// foreachの例とアクセス回数が揃うよう
// にいったんバッファに入れておくための一時変数
string _buf = "";
for (var i = 0; i < testlist.Count; i++)
{
    // ★(2)
    // ここでバッファーに値を入れておく
    _buf = testlist[i];
    for (var cnt = 0; cnt < 1000; cnt++)
    {
        // ★(3)
        // これで10万回にになるので同じ回数比較になる
        buf = _buf;
    }
}

foreach (var i in testlist)
{
    for (var cnt = 0; cnt < 1000; cnt++)
    {
        buf = i; // こっちは変更なし
    }
}

// > for Loop     : 144 ms ★foreachより早い
// > foreach Loop : 177 ms
// > ForEach Loop : 168 ms

こういったコードを公開するときはよく考えてからコード書いたほうがいいですね(自戒

C#で再帰を使わずにフォルダ内のファイルを列挙する

再帰処理を使わないでC#でフォルダ階層をたどってファイルをリストアップする方法の紹介です。

最後に記載がありますが実際はフォルダを巡回する必要ありません。C#はAPIをひとつつ呼ぶだけで実装できます。

再帰処理を使ってファイルを列挙する

まずは古典的な再帰処理を使ったファイルの探索方法です。

// Program.cs

public static void Main(string[] args)
{
  string root = @"c:\sample";
  Foo(root);
}

public static void Foo(string parent)
{
    foreach (string dir in Directory.GetDirectories(parent))
    {
        Console.WriteLine($"dir={dir}");
        foreach (string file in Directory.GetFiles(dir))
        {
            Console.WriteLine($"  file={Path.GetFileName(file)}");
        }
        Foo(dir);
    }
}

再帰処理を使わずにファイルを列挙する

次に再帰処理を使わないファイルの探索方法です。標準APIで実現できます。

すごく昔からあるAPIなので実はフォルダの巡回が必要な時に再帰処理が必要なケースは稀によくあるのですが通常使わないと思います。

// Program.cs

public static void Main(string[] args)
{
  string root = @"c:\sample";
  Foo(root);
}

foreach (var dir in Directory.GetDirectories(path, "*", SearchOption.AllDirectories))
{
    Console.WriteLine($"dir={dir}");
    foreach (string file in Directory.GetFiles(dir))
    {
        Console.WriteLine($"  file={Path.GetFileName(file)}");
    }
}

ちなみに再帰処理を使わない方がパフォーマンスが3倍くらい良いです。従って特殊な要件が無ければ再帰処理を使わない方がおすすめです。

そもそもAPIが用意されている

紹介しておいてあれですが、特定のファイルのリストアップがしたいだけの場合以下APIがあらかじめ用意されているので先述の巡回処理は必要ありません(しかもこのAPIはさらに高速に動作します)

// 特定のフォルダ以下の全部取得する
string[] fileList = Directory.GetFiles(path, "*.txt", SearchOption.AllDirectories);

// 逐次取得する
IEnumerable<string> fileList = Directory.EnumerateFiles(path, "*.txt", SearchOption.AllDirectories);

ファイル数が一万とか予想される場合は Enumerate の方を使った方がPCのリソースにやさしいです。

【C#】StartsWithを複数の文字列対応する

string クラスに StartsWith という特定の文字列で始まるかどうかをチェックできるメソッドがあります。

使い方はこんな感じです。

string str = "aaaabbbbcccc";
if(str.StartsWith("aaa"))  // str が "aaa" から始まるかどうかチェックする
{
    // 一致する
}
else
{
   // 一致しない
}

これによって指定した変数内の文字列が指定した文字列から始まっているかどうかを確認できますがこれを複数の文字列で確認できるように改善したいと思います。

string str = "aaabbbccc";
if(str.StartsWith("aaa", "bbb", "ccc"))
{
    // "aaa", "bbb", "ccc" のいずれかに一致する
}
else
{
    // 一致しない
}

確認環境

この記事の動作確認環境は以下の通りです。

  • C# 8.0
  • .NET Core 3.1
  • VisualStudio 2019
  • Windows10

実装コード

以下実装で StartsWith, EndsWith, Contains の3つのメソッドを複数対応します。

// StringExtension.cs

public static class StringExtension
{
    // 指定した複数の文字列のどれかに一致するかどうかを判定する
    // true : 存在する / false : 存在しない
    public static bool Contains(this string self, params string[] words) => Contains(self, false, params string[] words);
    public static bool Contains(this string self, bool ignoreCase, params string[] words)
    {
        for (int i = 0; i < words.Length; i++)
        {
            if (string.Compare(self, words[i], ignoreCase) == 0)
            {
                return true;
            }
        }
        return false;
    }

    // 指定した複数の文字列のいずれかから開始する文字列かどうかを判定します。
    // true : 存在する / false : 存在しない
    public static bool StartsWith(this string self, params string[] words) => StartsWith(self, false, params string[] words);
    public static bool StartsWith(this string self, bool ignoreCase, params string[] words)
    {
        for (int i = 0; i < words.Length; i++)
        {
            if (self.StartsWith(words[i], true, null))
            {
                return true;
            }
        }
        return false;
    }

    // 指定した複数の文字列のいずれかから開始する文字列かどうかを判定します。
    // true : 存在する / false : 存在しない
    public static bool EndsWith(this string self, params string[] words) => EndsWith(self, false, params string[] words);
    public static bool EndsWith(this string self, bool ignoreCase, params string[] words)
    {
        for (int i = 0; i < words.Length; i++)
        {
            if (self.EndsWith(words[i], true, null))
            {
                return true;
            }
        }
        return false;
    }
}