【C#】インターネット ショートカットを普通のショートカットに変換する

Windows 上にあるインターネットショートカットを普通のショートカットに変換するプログラムです。

既定のブラウザに関わらず指定したブラウザ(Chorome)で強制的に開くように変換します。

using IWshRuntimeLibrary;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    internal class Program
    {
        public static void Main(string[] args)
        {
            createShortcut(args[0], getUrl(args[0]));
        }

        /// <summary>
        /// 指定したインターネットショートカットからURLを取得します。
        /// </summary>
        private static string getUrl(string path)
        {
            string[] lines = System.IO.File.ReadAllLines(path);
            if (lines[0] != "[InternetShortcut]")
            {
                throw new NotSupportedException("1行目がショートカットではありません。");
            }

            for (int i = 1; i < lines.Length; i++)
            {
                if (lines[i].StartsWith("URL"))
                {
                    return lines[i].Split('=')[1];
                }
            }

            throw new InvalidDataException("URLタグが見つかりませんでした。");
        }

        private static void createShortcut(string path, string url)
        {
            // 起動するプログラム(=Chorome)
            string programPath = @"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe";

            // ショートカットの生成先
            string dir = Path.GetDirectoryName(path);
            string name = Path.GetFileNameWithoutExtension(path);
            string destPath = Path.Combine(dir, name + ".lnk");

            using (var gen = new ShortcutGenerator())
            {
                // (1) リンク先:起動するプログラムのパス
                IWshShortcut info = gen.GetInfo(destPath);
                info.TargetPath = programPath;
                // (2) 引数
                info.Arguments = url;
                // (3) 作業フォルダ
                info.WorkingDirectory = Path.GetDirectoryName(programPath);
                // (4) 実行時の大きさ 1が通常、3が最大化、7が最小化
                info.WindowStyle = 1;
                // (5)アイコンのパス 自分のEXEファイルのインデックス0のアイコン
                info.IconLocation = 
                    @"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" + ",0";
                gen.Save(info);
            }
        }
    }

    /// <summary>
    /// ショートカットを作成するためのクラス
    /// </summary>
    public class ShortcutGenerator : IDisposable
    {
        //
        // Fields
        // - - - - - - - - - - - - - - - - - - - -

        // ショートカット生成用のCOMオブジェクト
        private WshShell shell = new WshShell();
        // 生成したCOMコンテナの数
        private List<IWshShortcut> shortcutList = new List<IWshShortcut>();
        // Dispose したかどうかのフラグ
        // true : Dispose済み / false : まだ
        private bool isDisposed;

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

        /// <summary>
        /// オブジェクトを破棄します。
        /// </summary>
        ~ShortcutGenerator()
        {
            this.Dispose();
        }

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

        /// <summary>
        /// ショートカットへ与えるデータを格納するオブジェクトを生成します。
        /// </summary>
        public IWshShortcut GetInfo(string path)
        {
            var info = (IWshShortcut)shell.CreateShortcut(path);
            this.shortcutList.Add(info);
            return info;
        }

        /// <summary>
        /// 既存のショートカットをロードします。
        /// </summary>
        public IWshShortcut Load(string path)
        {
            var info = (IWshShortcut)shell.CreateShortcut(path);
            info.Load(path);
            this.shortcutList.Add(info);
            return info;
        }

        /// <summary>
        /// ショートカットを生成します。
        /// </summary>
        public void Save(IWshShortcut info) => info.Save();

        /// <summary>
        /// <see cref="IDisposable"/> の実装。
        /// </summary>
        public void Dispose()
        {
            if (isDisposed)
            {
                return;
            }
            isDisposed = true;

            for (int i = 0; i < this.shortcutList.Count; i++)
            {
                Marshal.FinalReleaseComObject(shortcutList[i]);
            }
            this.shortcutList.Clear();
            this.shortcutList = null;

            Marshal.FinalReleaseComObject(shell);
            this.shell = null;

            GC.SuppressFinalize(this);
        }
    }
}

【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#】ビットフィールドの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の実装は以下の通りです。(1) 等価演算子(==, !=)とEqualsをオーバーライドして (2) IEquatableをCRTP的に実装し (3) ハッシュ計算をオーバーライドして (4) 個々のフィールドで等値比較を行うような形になると思います。結構書かないといけません。

// ValueObjectSample.cs

// ValueObjectのテンプレ実装
public class ValueObjectSample : IEquatable<ValueObjectSample>
{
    // immutable
    public readonly int A;
    public readonly int B;
    public readonly int C;

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

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

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

    // IEquatable<T> の implement
    public bool Equals(ValueObjectSample 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;
            hashCode = (hashCode * 397) ^ this.C;
            return hashCode;
        }
    }
}

共通基底クラス版

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

先述の基本実装例だと実装量が多いので基底クラス側で「immutable に値を保持できること」、「内容で等値判定したい」、「保持する値はnullを許可しない」などの共通の性質を基底クラスに抽出し、ValueObject はこれを継承することで実現していきます(リスコフの置換原則に違反しているため賛否があると思いますが…)

単一フィールドの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

複合型の 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) => (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化が発生 + キャッシュ領域のオブジェクト配列分でヒープを使用)なので、高頻度で生成と破棄を繰り返すと問題になる可能性があります。

余談、まとめ

まぁ通常版が一番優れているという他ないのですが、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);
    }
}

以上です。