【C#】リストのジェネリックを親クラスに変換する

List<T> の T を親クラスやインターフェースに変換したいこと無いですか?継承関係があって安全に変換できるならジェネリックの型は親クラスに互換してても良さそうですが List の T では認められていません。この操作はできないので代替案の話になります。

たとえば以下のような定義はだと型が違うエラーになります。

// <T> がこんな感じに宣言されている

// インターフェースの宣言
public interface ISample
{
    int A { get; set; }
}

// 実際の実装クラス
public class Sample : ISample
{
    public int A { get; set; }
    public int B { get; set; }
}

以下のように親クラスなジェネリックの引数には指定できません。

public static void Main(params string[] args)
{
    // 実装クラスでリストのジェネリックを宣言する
    var list = new List<Sample>();

    // CS1503 引数 1: は
    //  'System.Collections.Generic.List<ConsoleApp26.Derived>' から
    //  'System.Collections.Generic.List<ConsoleApp26.Base>' へ変換することはできません
    Foo(list);

    // これは呼び出せる
    Bar(list);
}

// エラー
public static void Foo(List<ISample> list)
{
    // any
}

// OK
public static void Bar(IEnumerable<ISample> list)
{
    // any
}

こういう時は、IEnumerable で渡すことができます。

また、どうしても渡したい場合以下のように Cast → ToList すれば渡せます、ただしこれ、新しく別のリストを作成し渡しているため、渡した先でリストにAdd/Removeしても呼び出し元のリストは変化しません(恐らく「そうじゃないんだよな」というケースが多いと思いますが…)

// 無理やり同じ型に変換する
Foo(list.Cast<ISample>().ToList());

なので、そういった用途が想定される場合、毎回中身をキャストする、もしくは、最初からインターフェースや親クラスでリストを宣言する、が最終的な答えかと思います。

// 左辺で受けるときにこうやってキャストできるためこれを利用する
foreach(ISample s in list)
{
   // ...

// 最初から T をインターフェースや親のクラスで宣言する
var list = new List<ISample>();

以上です。

【C++/CLI】std::functionにマネージドメソッドをバインドする

std::function にメソッドを関連付ける時は std::bind を使用しますが C++/CLI でマネージドメソッドを std::bind 渡したい場合の実装方法の紹介です。

C++11 以降で関数ポインタの代わりに std::function でコールバック呼び出しされるような局面でマネージクラスのメソッドを std::function に渡してネイティブ側から呼び出してもらいたいケースがあると思いますが std::bind にマネージドメソッドを指定すると以下のようにエラーが発生してしまいます。

// ネイティブ側の定義
class Native
{
public:

    void foo(std::function<void(int)> func) // コールバックがstd::functionなネイティブメソッド
    {
        func(999);
    };
}

// マネージド側の定義
public ref class Managed
{
public:

    Managed(Native* lib)
    {
        auto func_1 = std::bind(this->WhatBind, gcroot<Managed^>(this), std::placeholders::_1);
        // この式は指定不可能
        // E2071 pointer-to-member は マネージド クラスでは無効です

        auto func_2 = []()
        {
            this->Callback();
        };
        // マネージドなのでラムダも無理
        // E2093 マネージド クラスのメンバー関数ではローカルラムダは使用できません
        
        lib->foo(func_1) // ★★★渡せない
    }
    
    void WhatBind(int arg1) { /* ... */ }
}

言語仕様的に上無理なので マネージドメソッドは std::bind できません。そこで以下のようにフリー関数を1層経由させます。こうすることで制限を回避できます。

// ネイティブ側の定義
class Native
{
public:

    void foo(std::function func); // コールバックがstd::function
}

// マネージド側の定義
public ref class Managed
{
public:

    Managed(Native* lib);
    
    void WhatBind() { /* ... */ }
}

// 迂回用のネイティブ関数の定義
static void Proxy(Managed^ managed, int arg1)
{
    managed->WhatBind(arg1); // マネージドメソッドの呼び出し
}

Managed::Managed(Native* lib)
{
    // ★★★これならbindを作成できる
    auto func = std::bind(Proxy, gcroot<Managed^>(this), std::placeholders::_1);
    lib->foo(func);
}

以上です。

参考

How to use boost::bind in C++/CLI to bind a member of a managed class

https://stackoverflow.com/questions/163757/how-to-use-boostbind-in-c-cli-to-bind-a-member-of-a-managed-class

【C++/CLI】Action<T1, T2>, Func<..>がエラーになる

C++/CLI で Action は使用できるのに Action<T1, T2> 以降が「E2154 ジェネリック クラス "System::Action" の引数が多すぎます」でエラーになる場合の対処方法です。

ソリューションエクスプローラー > 該当のプロジェクト > 参照 > System.Core を追加

どうやら定義場所が違うみたいです。初期状態だと参照に入ってないためエラーになります。

// mscorlibで定義されている
Action
Action<T1>

// System.Coreで定義されている
Action<T1, T2...>
Func<...>

.NET 4.x 以降でこのエラーが出る場合構文ミスってないか確認します。

property Action<int, int> Func;
// ここでエラーが出るのは「^」の付け忘れ
// 正しくは
// property Action<int, int>^ Func;

メモ:

.NET 4.x 系でもビルドは通るんだけどインテリセンスでは赤い波線でエラー表示が出る場合は System.Core を追加してプロジェクトのコンテキストメニューから「ソリューションの再スキャン」すると治る一時的に収まる。

参照

https://stackoverflow.com/questions/2193808/c-cli-use-of-action-and-func-unsupported

【C#】少し変わった拡張メソッドを作成する

C#では既存のクラスにメソッドを追加できる「拡張メソッド」という機能があります。

今回はこの拡張メソッドの少々変わった使い方の紹介です。

確認環境

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

  • .NET Core5(C# 9.0)
  • VisualStudio 2019
  • Windows 10

拡張メソッドの基本

まずは基本的な書き方の説明です。

以下のように「this」をつけてメソッドを宣言します。

public static class IntExtension
{
    // int 型に PlusOne というメソッドを追加する
    // 拡張したい型を先頭に持ってきて this をつける
    public static int PlusOne(this int value) => value + 1; // 今の値に+1した値を返す
    
    // 1. 追加したい型を第一引数に指定する
    // 2. 引数の宣言の先頭に this を指定する
}

そうすると以下のように使用できるようになります。

public static void Foo()
{
    int value = 1;
    int value2 = value.PlusOne(); // 上記で追加したメソッドが使用できる
    // value2 = 2
}

少し変わった使い方

さて、タイトルの通り拡張メソッドの少し変わった使い方の紹介です。

Object に拡張メソッドを定義する

Object 型に拡張メソッドを定義するとすべての型で拡張メソッドが呼び出せるようになります。

以下のようにリフレクションで対象オブジェクトの private フィールド値を強制的に書き換える処理は汎用性があるかもしれません。インテリセンスに毎回出てくるようになるので少し邪魔かも?

public static class ObjectExtension
{
    // 指定したオブジェクトのprivateフィールドの値を強制定期に変更する
    public static void SetField(this object self, string name, object value)
    {
        self.GetType()
            .GetField(name, BindingFlags.InvokeMethod | 
                            BindingFlags.NonPublic | 
                            BindingFlags.Instance)
            .SetValue(self, value);
    }
}

// 他人が作ったクラスのprivateフィールドを強制的に書き換えられる
public static Foo()
{
    int i = 1;
    i.SetField("id", 10); // この例では無意味だけどこんな感じで使える

    double d = 2.0;
    d.SetField("v", 3.0);
}

ジェネリックやタプルと組み合わせる

通常ジェネリックの拡張メソッドはジェネリックで指定します。

// 通常ジェネリックの方を指定するときは拡張メソッドもジェネリックにする = 'T'
public static bool Foo<T>(this List<T> self, T value) { ... }

// 上記のように宣言するとどのListの型でも使用できるようになる
public static void Foo()
{
    List<int> intList = new();
    bool ret = intList.Foo(100);

    List<double> doubleList = new();
    ret = doubleList.Foo(100.0);
}

ただし特定の型を指定することもできます。

// List<int>の時にしか使用できない拡張メソッドの宣言
public static bool Foo(this List<int> self, int value) { ... }

// 今度はdouble型では使用できなくなる
public static void Foo()
{
    List<int> intList = new();
    bool ret = intList.Foo(100);

    List<double> doubleList = new();
    ret = doubleList.Foo(100.0); // エラーになる

    // エラー CS1929 'List<double>' に 'Foo' の定義が含まれておらず、最も適している
    // 拡張メソッド オーバーロード
    // 'SampleExtension.Foo(List<int>, int)' には 'List<int>' 型のレシーバーが必要です

}

で、このジェネリックはタプルが指定できるので以下のように特殊な値の時にしか使用できない拡張メソッドが定義できます。

以下例ではタプルでintが2つの組み合わせの時にしか使用できない拡張メソッドの定義です。

public static class SampleExtension
{
    // タプルでintが2つの組み合わせの時にしか使用できない拡張メソッド
    public static bool Foo(this List<(int, int)> self, int value)
    {
        foreach (var (a, b) in self)
        {
            if (value == a || value == b)
            {
                return true; // どちらか一方に一致すればtrue
            }
        }
        return false;
    }
}

public static void Foo()
{
    List<(int, int)> intList = new()
    {
        ( 0,  0),
        (10, 20),
        (30, 40),
    };
    bool ret = intList.Foo(100); // 個の組み合わせのタプルの時だけ使える

    List<(int, double)> doubleList = new()
    {
        (0, 1.1),
        (2, 3.3),
        (4, 5.5),
    };
    ret = doubleList.Foo(100.0); // こっちはエラーになる
    
    // エラー CS1929 'List<(int, double)>' に 'Foo' の定義が含まれておらず、
    // 最も適している拡張メソッド オーバーロード 
    // 'SampleExtension.Foo(List<(int, int)>, int)' には 'List<(int, int)>' 型のレシーバーが必要です
}

このジェネリックにタプルを使用する手法は局所的なデータの組み合わせにいちいちクラスを定義しなくても組み合わせを表現できるため実装テクニックとして覚えていても損はないと思います。

【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 6 + C#10.0
  • Visual Studio 2022

単一の値のValueObject

単一の値を表す ValueObject の実装例は以下の通りです。readonly struct(と readonly のフィールド)で immutable を表現しています。必要に応じて演算子やメソッドを追加しますが、あくまで値を表すだけなので、業務処理などは極力入らないように設計します。

// SinleValueObjectSample.cs

// 単一型のValueObjectのテンプレ
public readonly struct SingleValue : IEquatable<SingleValue>
{
    // 不変の値
    public readonly string Value;

    // immutable constructor
    public SingleValue(string value)
    {
        Value = value;
    }

    // 基本型と互換を取る(必要な時だけ実装)
    public static implicit operator string(SingleValue value) => value.Value;
    public static implicit operator SingleValue(string value) => new SingleValue(value);

    // 比較演算子
    public static bool operator ==(SingleValue left, SingleValue right) => !(left == right);
    public static bool operator !=(SingleValue left, SingleValue right) => left.Equals(right);

    // IEquatable<T>の実装
    public bool Equals(SingleValue other) 
        => EqualityComparer<string>.Default.Equals(Value, other.Value);

    public override bool Equals(object? obj)
    {
        return obj is SingleValue sample && Equals(sample);
    }
    public override int GetHashCode() => EqualityComparer<string>.Default.GetHashCode(Value);
    public override string ToString() => $"{{{nameof(Value)}={Value}}}";

    // Tupleの分解のサポート
    public void Deconstruct(out string value) => value = Value;
}

struct なので内容の値が同じであれば同じオブジェクトとなります。

class であれば基底クラスでジェネリックを使えば継承して実装を単純化できそうですが、struct で値型としてオブジェクトを宣言しているので単純化できないのでこれが最小の実装です。

複数の値のValueObject

複数の値を保持する ValueObject の実装例です。複数の値の組み合わせを immutable に保持することができます。複数の値の組み合わせ自体に既に意味があると思いますが、これも値を表すだけなので業務処理が入り込まないよう注意しましょう。

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

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

    // 演算子のオーバーライド
    public static bool operator ==(MultiValue left, MultiValue right) => Equals(left, right);
    public static bool operator !=(MultiValue left, MultiValue right) => !Equals(left, right);

    // IEquatable<T>の実装
    public bool Equals(MultiValue other)
    {
        return EqualityComparer<int>.Default.Equals(A, other.A) &&
               EqualityComparer<SingleValue>.Default.Equals(B, other.B) &&
               EqualityComparer<SingleValue>.Default.Equals(C, other.C);
    }

    public override bool Equals(object? obj) => obj is MultiValue && Equals((MultiValue)obj);
    public override int GetHashCode()
    {
        return EqualityComparer<int>.Default.GetHashCode(A) * -1521134295 +
               EqualityComparer<SingleValue>.Default.GetHashCode(B) * -1521134295 +
               EqualityComparer<SingleValue>.Default.GetHashCode(C);
    }
    public override string ToString() 
        => $"{{{nameof(A)}={A}, {nameof(B)}={B}, {nameof(C)}={C}}}";

    // Tupleの分解のサポート
    public void Deconstruct(out int a, out SingleValue b, out SingleValue c)
    {
        a = A;
        b = B;
        c = C;
    }
}

struct なので内容の値が同じであれば同じオブジェクトとなります。

recordキーワードでValueObject

record キーワードは C# 9.0 で追加された機能です。この機能を使うと簡単に ValueObject が表現できます。

上述の ValueObject は(struct のため継承できない制限もあり)型を作成するのが結構面倒で、実装量が多く、間違えが起きやすいデメリットがありましたが、record キーワードを使えば簡単に ValueObject が宣言できます。しかも値がひとつでも、複数個でも大差なく型を作成できます。

// record型の宣言例
//  → 書き換えできないstringのNameを持つクラスHogeが宣言できる
public record Hoge(string Name);

// 複数のimmutableなフィールドを持つ方も簡単に宣言できる
//   → 書き換えできない3つのフィールドをもつ Barクラスが宣言できる
public record Bar(string Name, int ID, int No);

上記コードはおおむね以下のように展開されます。

public class Hoge : IEquatable<Hoge>
{
    public int ID { get; init; }

    protected virtual Type EqualityContract => typeof(Hoge);

    public Hoge(int value)
    {
        ID = value;
    }
    protected Hoge(Hoge original)
    {
        ID = original.ID;
    }

    // 演算子のオーバーライド
    public static bool operator !=(Hoge left, Hoge right) 
        => !(left == right);
    public static bool operator ==(Hoge left, Hoge right) 
        => (object)left == right || ((object)left != null && left.Equals(right));

    public virtual Hoge Clone() => new Hoge(this);

    // IEquatable<T>の実装
    public virtual bool Equals(Hoge? other)
    {
        return (object)this == other ||
               other is not null &&
               EqualityContract == other.EqualityContract &&
               EqualityComparer<int>.Default.Equals(ID, other.ID);
    }

    public override bool Equals(object? obj) => Equals(obj as Hoge);
    public override int GetHashCode()
    {
        return EqualityComparer<Type>.Default.GetHashCode(EqualityContract) 
               * -1521134295 + EqualityComparer<int>.Default.GetHashCode(ID);
    }
    public override string ToString()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("Hoge");
        stringBuilder.Append(" { ");
        if (PrintMembers(stringBuilder))
        {
            stringBuilder.Append(" ");
        }
        stringBuilder.Append("}");
        return stringBuilder.ToString();
    }
    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append("value");
        builder.Append(" = ");
        builder.Append(ID);
        return true;
    }

    // Tupleの分解のサポート
    public void Deconstruct(out int value)
    {
        value = ID;
    }
}

触った感じですが

  • 展開されるコードは class
  • 実装がすごい楽
  • immutable なので ValueObject として使用可能
  • フィールドの名前をコンストラクタ風の引数名で指定する関係で、大文字になってるのがやや違和感あり
  • ToString メソッド内の StringBuilder の new はちょっと微妙な局面がありそう

class として展開されます。

record structキーワードでValueObject

C# 10.0から使用可能になった record struct 機能の紹介とValueObjectとして利用できる可能確認です。record キーワードと全く同じように使用できます。展開されるコードは struct になります。

試しに、複合型で生成されるコードを確認してみます。

// 2つのフィールドを持つFugaを生成する
public record  struct Fuga (string Name), int ID;

生成されるコードは以下の通りです。

public struct Fuga : IEquatable<Fuga>
{
    string _name;
    int _id;

    public string Name { get => _name; set => _name = value; }
    public int ID { get => _id; set => _id = value; }

    public Fuga(string name, int id)
    {
        _name = name;
        _id = id;
    }

    public override string ToString()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("Fuga");
        stringBuilder.Append(" { ");
        if (PrintMembers(stringBuilder))
        {
            stringBuilder.Append(' ');
        }
        stringBuilder.Append('}');
        return stringBuilder.ToString();
    }

    private bool PrintMembers(StringBuilder builder)
    {
        builder.Append("Name = ");
        builder.Append((object)Name);
        builder.Append(", ID = ");
        builder.Append(ID.ToString());
        return true;
    }

    public static bool operator !=(Fuga left, Fuga right) => !(left == right);
    public static bool operator ==(Fuga left, Fuga right) => left.Equals(right);

    public override int GetHashCode()
    {
        return EqualityComparer<string>.Default.GetHashCode(_name) * -1521134295
             + EqualityComparer<int>.Default.GetHashCode(_id);
    }

    public override bool Equals(object? obj) => obj is Fuga fuga && Equals(fuga);

    public bool Equals(Fuga other)
    {
        return EqualityComparer<string>.Default.Equals(_name, other._name) 
            && EqualityComparer<int>.Default.Equals(_id, other._id);
    }

    public void Deconstruct(out string Name, out int ID)
    {
        Name = this.Name;
        ID = this.ID;
    }
}

感想ですがrecord キーワードとほぼ同じです。

  • 展開されるコードは struct
    • readonly struct はサポートなし
  • 実装がすごい楽
  • setter があって書き換え可能
    • 書き換え可能のためstructとはいえ純粋なimmutableのValueObjectとして認識しづらい
  • フィールドの名前をコンストラクタ風の引数名で指定する関係で、大文字になってるのがやや違和感あり
  • ToString メソッド内の StringBuilder の new はちょっと微妙な局面がありそう
  • C#10.0 + VisualStudio 2022でしか使えない

readonly struct のサポートはありません。対応忘れ?また構文自体が C#10.0(≒ VS2022) でしか使えないのもマイナスかもしれません。当然 Unity では使用できません。Unity 的には readonly struct があったほうが嬉しかったかもしれません。

また setter があって書き換え可能ですが、struct なので性質は immutable だと思いますが、使用時に視覚的に値が変更できることは、ValueObject と想定していた場合、違和感を覚えるかもしれません。特に C# は利用時に class と struct の違いが見た目では区別できないのであれ?代入できる?となって宣言を確認すると struct でしたの流れは少し負担になるかもしれません。

最後に

完全に余談ですが、Unity2021.3で record が使える!ValueObject!と、一瞬盛り上がったような気がしますが、この record キーワード、生成される実装が(正しく ValueObject の形式ですが)class です。このため、new でオブジェクトを作成 → ヒープに生成 → パフォーマンス要求がシビアなゲームの実装ではやや使いづらい、の流れで Unity では意外に微妙でした。struct のほうが求められていたと思います(=大量の class を毎フレーム new したり破棄したりすると GC が原因でゲームがカクついたりしますす)

ですが、それ以外の用途では使用できると思います。サーバーサイドとか、GUIとか。

また、C# 10.0 からは record struct キーワードが追加され、先ほど紹介した struct の実装が簡単にできるようになりました。ただし、現状、Unity では使用できません(し、当面利用できないと思います)

2022年7月現在 C# 10.0 + Visual Studio 2022 の環境が挑戦的のため、現状この機能はほぼ誰も使えてないような気がします。いつものように時間が解決してくれると思いますが、いずれにしろ大多数の開発者が恩恵にあずかるのは当面先かなと思います。

record キーワードは Unity 視点では完全とは言えませんでしたが、このキーワードは十分強力で役立つ場面も沢山あると思うので積極的に使用していきたいですね。

【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);
    }
}

以上です。