C#でライフゲーム

今日はブログで書くネタがなかったので、C#でライフゲームを実装してみました。
コードは末尾に記載します。
※コンソールのカーソルを非表示にし忘れていました。コードでは修正済みです。

ライフゲームとは1970年に考案された
古典的かつシンプルな人工生命シミュレーションプログラムです。
ライフゲーム – Wikipedia
格子状に配置したセルに、生存と死亡の2つの状態を割り当て、各セルについて、
「現在のセルの状態及び周囲8セルの状態から次の状態を求めて、状態を更新する(※)」
を繰り返すことで、生命の生と死のようなサイクルをシミュレーションします。
※状態遷移は以下
・死亡かつ周囲生存3セル→生存(”誕生”)
・生存かつ周囲生存1セル以下→死亡(”過疎”)
・生存かつ周囲生存2セルor3セル→生存(”生存”)
・生存かつ周囲生存4セル以上→死亡(”過密”)

基本的にはどこかで変化が生じなくなりシミュレーションが止まるのですが、
中には変化をし続ける初期セルのパターンもあり、
現在でも愛好家により新しいパターンが発見されているようです。

近年の進化したAIに比べると随分とシンプルですが、単調過ぎず見ていて面白いですよね。
(若い頃に知って感動した故にそう感じるのかもしれませんが。。。)

10年ほど前、学部3年生の頃にゼミの課題で実装したことがありましたが、
(そのゼミは4年に上がる前に教授が他所に移ってしまったため消滅しました。。。)
当時はプログラミングを講義で習って1年ちょっとの段階だったのもあり、
動作はしたもののそれは酷いコードを書いていた覚えがあります。
(言語はほぼCの機能しか使用していない、なんちゃってC++で、描画はOpenGLで行っていました)

当時に比べると洗練されたプログラムを書けるようになったと思います。
継続は力なりということにして締めさせていただきます。

以上、老いを感じる、思い出語りを含んだ記事更新でした。

 // Program.cs
using LifeGame;

new Controller().Simulate(20, 0);
 // Lifegame.cs
using System.Diagnostics;
using System.Text;

namespace LifeGame;

/// <summary> モデルクラス </summary>
internal class Model {
    /// <summary> グリッド一辺サイズ </summary>
    public int Size { get; private set; } = 0;
    /// <summary> 現在の各セルの状態 </summary>
    public bool[,] Cells { get; private set; } = new bool[0, 0];
    /// <summary> 更新用のセル状態バッファ(兼以前の各セルの状態) </summary>
    private bool[,] _cells { get; set; } = new bool[0, 0];

    /// <summary> 初期化します。 </summary>
    public void Init(int size, int seed) {
        Size = size;
        Cells = new bool[Size, Size];
        _cells = new bool[Size, Size];
        var rnd = new Random(seed);
        for (var x = 0; x < Size; x++) {
            for (var y = 0; y < Size; y++) {
                // 乱数で初期状態設定
                Cells[x, y] = rnd.Next(0, 100) % 2 == 1;
            }
        }
    }

    /// <summary> 終端の状態か。 </summary>
    public bool IsTerminal() {
        for (var x = 0; x < Size; x++) {
            for (var y = 0; y < Size; y++) {
                var diff = Cells[x, y] != _cells[x, y];
                if (diff)
                    return false; // 差分あれば非終端
            }
        }
        return true; // 差分なしで終端
    }

    /// <summary> 更新します。 </summary>
    public void Update() {
        for (var x = 0; x < Size; x++) {
            for (var y = 0; y < Size; y++) {
                _cells[x, y] = GetNextState(x, y); // 次状態取得
            }
        }
        (Cells, _cells) = (_cells, Cells); // 更新した状態と現在の状態を交換
    }

    /// <summary> 次の状態を取得します。 </summary>
    private bool GetNextState(int x, int y) {
        var isAlive = Cells[x, y];
        var countAroundAlive = CountAroundAliveCells(x, y);
        return (isAlive, countAroundAlive) switch {
            (false, 3) => true, // 誕生
            (false, _) => false, // (死亡維持)
            (true, <= 1) => false, // 過疎
            (true, 2 or 3) => true, // 生存
            (true, >= 4) => false, // 過密
        };
    }

    /// <summary> グリッド走査用 x座標オフセット </summary>
    private static int[] Dx { get; } = { -1, 0, 1, -1, 1, -1, 0, 1 };
    /// <summary> グリッド走査用 y座標オフセット </summary>
    private static int[] Dy { get; } = { -1, -1, -1, 0, 0, 1, 1, 1 };

    /// <summary> 指定した中心セルの周囲の生存セルを数えます。 </summary>
    private int CountAroundAliveCells(int cx, int cy) {
        var count = 0;
        foreach (var (dx, dy) in Dx.Zip(Dy)) {
            // 中心座標+オフセットを正規化して座標算出
            var (x, y) = (ValidateIndex(cx + dx), ValidateIndex(cy + dy));
            if (Cells[x, y])
                count++; // 生存ならカウントアップ
        }
        return count;
    }

    /// <summary> セルのインデックスを正規化します。端と端が繋がるように。 </summary>
    private int ValidateIndex(int index) => index switch {
        < 0 => Size - 1, // 左の外側なら右端に
        _ when Size <= index => 0, // 右の外側なら左端に
        _ => index // 範囲内はそのまま
    };
}

/// <summary> ビュークラス </summary>
internal class View {

    /// <summary> 初期化します。 </summary>
    public void Init(Model model) {
        Console.Clear();
        Console.CursorVisible = false;
        Console.ForegroundColor = ConsoleColor.Green;
        Draw(model);
    }

    /// <summary> 終了します。 </summary>
    public void Term() {
        Console.ForegroundColor = ConsoleColor.Gray;
        Console.CursorVisible = true;
        Console.WriteLine("Done.");
    }

    /// <summary> 描画します。 </summary>
    public void Draw(Model model) {
        Console.SetCursorPosition(0, 0); // コンソール上書きのためカーソルを左上に
        var sb = new StringBuilder();
        for (var x = 0; x < model.Size; x++) {
            for (var y = 0; y < model.Size; y++) {
                var state = model.Cells[x, y] ? "■" : "_"; // 状態を文字化
                sb.Append(state);
            }
            sb.AppendLine();
        }
        Console.Write(sb.ToString()); // 出力
    }
}

/// <summary> 制御クラス </summary>
internal class Controller {
    /// <summary> モデル </summary>
    private Model Model { get; } = new();
    /// <summary> ビュー </summary>
    private View View { get; } = new();

    /// <summary> シミュレーションを実行します。 </summary>
    public void Simulate(int size, int seed) {
        var sw = new Stopwatch();
        Model.Init(size, seed);
        View.Init(Model);
        const int frameMs = 50; // 1フレーム50ms (20FPS)
        while (!Model.IsTerminal()) {
            // 更新所要時間を1フレーム時間から引いた上で待機
            Thread.Sleep(Math.Max(1, frameMs - (int)sw.ElapsedMilliseconds));
            sw.Restart();
            Model.Update(); // モデル更新
            View.Draw(Model); // 描画更新
        }
        View.Term();
    }
}

C#13.0(.NET 9.0)のLockクラスとC#のlock構文について

C#のlockについて少し調べた際にたまたま知ったのですが、
C#13.0(.NET 9.0, 2024/11リリース)にてLockクラスが追加されたようです。
Lock クラス (System.Threading) | Microsoft Learn


// 上記記事からの引用となります
public sealed class ExampleDataStructure {
    private readonly Lock _lockObj = new();

    public void Modify() {
        lock (_lockObj) {
            // Critical section associated with _lockObj
        }

        using (_lockObj.EnterScope()) {
            // Critical section associated with _lockObj
        }

        _lockObj.Enter();
        try {
            // Critical section associated with _lockObj
        }
        finally { _lockObj.Exit(); }

        if (_lockObj.TryEnter()) {
            try {
                // Critical section associated with _lockObj
            }
            finally { _lockObj.Exit(); }
        }
    }
}

このLockクラスは従来のlock構文のロック対象objectのように宣言し、
従来のlock構文で使用できるほか、using構文でも使用でき、
またロック試行など細かな制御も可能なようです。

lock構文に対して、
不満(謎のobjectを用意する必要がある、構文が特殊、細かい制御が不可)があったので、
便利そうで良いなと思いました。

しかし、このLockクラスが追加されたのはどうも利便性向上のためではないらしく、
以下の2記事によるとパフォーマンス向上のためらしいです。
Lock クラス | ++C++; // 未確認飛行 C ブログ
【.NET 9.0】System.Threading.Lock のパフォーマンス #C# – Qiita

任意のオブジェクトに対してlockできるのはオーバーヘッドが大きい
とのことで、Qiitaの記事によると5%ほどパフォーマンス向上するとのことです。

そこで知ったのですが、そもそもC#には排他制御を司るMonitorクラスが存在し、
lock構文はMonitorクラスにより排他制御の糖衣構文のようです。
(以前からやろうと思えば細かい制御もできたんですね。。。)
マルチスレッド – C# によるプログラミング入門 | ++C++; // 未確認飛行 C

その糖衣構文がLockクラスの追加により、
ロック対象に今回のLockクラスのインスタンスに指定した場合に
Monitor使用のコードの代わりにLockによるコードへの置換に変更されるとのことです。

なので、C#13.0では基本的にはlock構文にLockクラスを併用するのが良いかと思われます。
以上、Lockクラスとlock構文についてでした。

※それともう一つ、数値型の単純な演算をアトミックに行えるInterlockedクラスの存在も
 上記のQiita記事内で初めて知りました。
 色々と便利なクラスがあったんですね。。。

Visual Studio Setup Projectにてexeファイル追加によりビルドが失敗する際の対処

今回は題の通りで、
ソフトが使用する別ソフトのexeファイルをインストーラーに同梱する必要が生じたため、
ソフトのインストーラーを作成するSetup Projectにexeファイルを追加したのですが、
それによりSetup Projectのビルドが失敗するようになりました。
その対処として、exeファイルをzipに圧縮して追加するようにしました。

以下経緯です。

失敗の原因は、追加した覚えのないdllファイルでした。
どうもSetup Projectにexeファイルを追加すると、
そのexeの依存するdllがすべて自動で追加されるようです。

追加されたファイルはプロパティウィンドウにて除外の設定が可能なのですが、
除外しても失敗は解決できませんでした。
(正確には、VS上でのビルドは成功するようになったのですが、
 運用としてはスクリプトから言語設定の構成を切り替えて2回ビルドを行う必要があり、
 そのスクリプトでは依然として失敗しました。
 StackOverFlowの投稿で見かけたのですが、
 構成の切り替えにより除外した依存関係が自動で有効化されるらしいです。)

dllが追加されないように(※)exeをzipに圧縮して追加し、
ソフトの起動時にzipを解凍する処理を実装しました。
※拡張子の変更も試しましたが、
 変更してもしっかり実行ファイルだと認識しdllが追加されてしまいました。

結局、dllの追加により何故失敗するかはエラーメッセージからもはっきりわからなかったのですが、
ソフト自身の依存するdllと同名の異なるバージョンのdllが追加されていたので、
バッティングが原因だったのではないかと推測しています。

Setupプロジェクトの動作が重いのも相まって解決に少し手間取ってしまったのですが、
リリース作業の運用は変更する必要なく解決できたので良かったです。

C#にて内部クラスからprivateなメンバへアクセス

C#にてprivateなメンバを別のクラスにのみ公開したいことがあります。
この1つの実現方法として、
特定のクラスが自身と密接に関係する場合以外は推奨されないと思いますが、
特定のクラスを内部クラスとして定義すれば可能だと知りました。
内部クラスはごくまれにしか定義しないので、今更知りました。
Stateパターンってどう実装するのが良いのだろうと思っていたのですが、
Stateのクラスを外部に公開しないなら内部クラスで良かったのですね。
地味に疑問だったので晴れて良かったです。

/// <summary> partialテストクラス </summary>
internal class SampleClass {
  /// <summary> 状態インスタンス </summary>
    private IState State { get; set; }
    /// <summary> 何かの数 </summary>
    private int Count { get; set; }
    
    /// <summary> 具象状態クラス </summary>
    internal class ConcreteState : IState {
        /// <summary> 何かする </summary>
        public void Do(SampleClass parent) {
            tester.Count++;
        }
    }
}

/// <summary> 状態インタフェース </summary>
internal interface IState {
    /// <summary> 何かする </summary>
    void Do(SampleClass tester);
}

※詳しくないのですが、
 Javaだとprotectedがサブクラスだけでなく同一package内ならアクセス可能らしいので、
 packageをちゃんと定義すれば可能なのかもしれません。

OpenVino2021にてモデルをIR形式に変換すると推論精度が低下

もう一月も半ばですが、皆様明けましておめでとうございます。
本年もどうぞよろしくお願いいたします。

内容としては題の通りなのですが、詳細には、
OpenVINO2021にてONNX形式のモデルをOpenVINOの中間表現IR形式(.xml, .bin)に変換し推論を行うと、
ONNXでの推論結果よりもIRでの推論結果は推論精度が目視で10-20%ほど低下することがわかった、
という話となります。

経緯としましては、案件にてPyTorchの学習済みのモデルファイル(.pth)をOpenVINOを使用して推論することとなったのですが、
既存システムで使用しているOpenVINOは2021と少し古いためPyTorch形式の推論に対応しておらず、
対応している別の形式に変換する必要がありました。
既存システムではIR形式にて推論しているため、ひとまずIR形式への変換と推論を試してみたのですが、
その推論結果はPyTorchでの結果よりも精度が低下しました。
そこでONNXへの変換と推論も試してみたところ、ONNXでは精度の低下は見られず、
IRの場合に低下することがわかったという次第になります。

調査したところIR形式はONNX形式よりも若干高速とのこと(※)なので、
推測ですがONNXから変換時にその分ネットワークがそぎ落とされてしまうことがあるのかもしれません。
速度を限界まで最適化したい場合でなければOpenVINOではONNXで推論するのが良さそうですね。


目視では推論速度に大して差があるように見受けられなかったのですが、
以下の記事によると、Yolov5でONNXとIRで推論処理時間に10msほど差があるようです。

【やってみた】ONNX・OpenVinoでYOLOv5の高速化! – 神戸のデータ活用塾!KDL Data Blog

以上です。

ジョークアルゴリズムのスターリンソートについて

皆様寒くなって参りましたが如何お過ごしでしょうか?
自分は先日、自宅にて天井から食卓にゴキブリが降ってくるという恐怖体験に遭遇し、
大の大人が悲鳴を上げました。

それはさておき今回はジョークアルゴリズムのスターリンソートについてです。
ちまちま勉強しているデザインパターンの練習問題でStrategyパターンで
ソートアルゴリズムを実装してくださいと言われ、
数年前に話題になったアルゴリズムがあったなあ、、、と存在を思い出したため記事にしました。

スターリンソート(別名、粛清ソート)と呼ばれるこのアルゴリズムは、
ソ連のスターリンの大粛清になぞらえたもので、
O(N)で動作するソート(?)アルゴリズムです。

配列の要素が先頭から正しく順序通りに並んでいるかを確認し、
順序に反している要素は取り除く(粛清する)ことによって、
配列を順序付けます。
例えば次のような要素を持った配列を昇順でスターリンソートすると、
1, 2, 4, 3, 6, 8, 0, 8, 9, 5, 7

1, 2, 4, 8, 8, 9
となります。

Gitリポジトリにもなっていて有志により各種言語で実装されているようです。
(海外発祥だったんですね)
GitHub – gustavo-depaula/stalin-sort

試しに自分でもC#で実装してみました。


/// <summary> スターリンソートを行います。 </summary>
public static IEnumerable<T> StalinSort<T>(IEnumerable<T> items, bool descending = false) where T : IComparable<T> {
    var prev = items.FirstOrDefault();
    var order = descending ? -1 : 1;
    return items.Where(x => order * x.CompareTo(prev) >= 0).Select(x => prev = x);
}

一時変数とLinqの併用により、すっきり書けました。

ジョークアルゴリズムですが実務で使えるタイミングを捻り出すとするなら、
例えば、正常なら右肩上がりとなる時系列データにおいて
外れ値として直前の値よりも減少したデータが記録されてしまうことがあり、
その外れ値を除去したい、などと言ったケースでしょうか。
純粋なソートとして使うことはないですが、データの正規化では使う機会があるかもしれませんね。

以上です。
皆様お身体にお気を付けてお過ごしください。

URLからhtmlのリンクタグを作成するpythonスクリプト

11月になりようやく肌寒くなってまいりましたね。
個人的にはもう少し暖かい気候が続いてほしかったです。
暑くも寒くもない方が過ごしやすいので。
(先週末記事を書いたのですが、今週は暖かそうですね)

それはさておき今回は題の通り、
URLを与えてhtmlのリンクタグを作成するプログラムを作成した話となります。
ただそれだけのプログラムなのですが、
毎度参考のリンクのタグを手作業で作るのが面倒だったのと、
検索してみたところurlのタイトルを自動で取得してリンクタグを作成してくれるサービスは
特に見当たらなかったため、自前で作成した次第です。

今回は実行のしやすさを考えてpythonのスクリプトで作成しました。

必要なpip

pip install requests beautifulsoup4 pyperclip

作成したコード

import requests
from bs4 import BeautifulSoup
import pyperclip
import sys

# URLからhtmlのタイトルを取得
def get_html_title_from_url(url: str) -> str:
    try:
        response = requests.get(url)
        response.raise_for_status()  # HTTPエラーが発生した場合は例外を発生させる
        # バイナリのcontentを渡してエンコードをよしなにしてもらう
        soup = BeautifulSoup(response.content, 'html.parser') 
        # ページのタイトルを取得
        title = soup.title.string if soup.title else 'No Title' 
        return title
    except requests.RequestException as e:
        print(f"HTTPリクエスト中にエラーが発生しました: {e}")
        return ""

# URLとタイトルからhtmlのリンクタグを作成
def create_html_link(url: str, title: str) -> str:
    return f'<a href="{url}" title="{title}">{title}</a>'

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("使用法: python create_link_tag.py URL")
    else:
        url = sys.argv[1]
        title = get_html_title_from_url(url)
        html_link = create_html_link(url, title)
        print(html_link)
        pyperclip.copy(html_link) # クリップボードにコピー
        print("リンクがクリップボードにコピーされました。")

urlをコマンドライン引数で与えることでリンクタグが出力されます。
無精のためクリップボードにコピーまで行われます。

試しに前回の私の記事のURLを与えてみると、、、

Microsoft IMEの不具合によるSendKeysの動作不良 | 計測制御ソフト受託開発 インフォテック ブログ

<a href="https://www.infortec.co.jp/blog/archives/Item_3080" title="Microsoft IMEの不具合によるSendKeysの動作不良 | 計測制御ソフト受託開発 インフォテック ブログ">Microsoft IMEの不具合によるSendKeysの動作不良 | 計測制御ソフト受託開発 インフォテック ブログ</a>

と出力され、問題なく動作します。
これでチョット楽できます✌

ちなみに今回のコードはほとんどMicrosoft Copilotくんに指示を与えて書いてもらいました。
普段使っておらず不慣れな言語の小規模なコードを作成してもらうのに、
チャットAIは本当に便利ですね。

おかげで、面倒なちょっとした手作業をpythonやbatで自動化するのも楽に行えます。
便利な時代になりました。もっと便利になって欲しいですね!以上です。

Microsoft IMEの不具合によるSendKeysの動作不良

1. はじめに
つい先日、Windows10の特定モデルのPCにて、
プログラムからキー送信を行うのAPIであるSendKeysを使用した機能が動作しない、
という不具合が報告されてきました。
その原因及び回避方法が特定できましたので、その共有となります。

2. 原因
原因はタイトルに記載していますが、おそらくMicrosoft IMEの不具合で、
IMEが半角の場合にSendKeysが動作しないことがあるようです。
(IMEが全角の場合は動作することに気付き、原因に辿り着けました)
Windows 10 version 2004 以降の新しいIMEにはどうも不具合があり、
PCによっては入力時などに問題が発生することがあるそうです。

3. 回避方法
問題を回避するには、
IMEの設定にて「以前のバージョンの Microsoft IME を使う」をオンにすることで回避可能となります。
これによりIMEが半角時でもSendKeysが動作しました。

このIMEの設定は以下のレジストリを変更することでも設定可能です。
> レジストリキー:HKCU¥SOFTWARE¥Microsoft¥Input¥TSF¥Tsf3Override¥{03b5835f-f03c-411b-9ce2-aa23e1171e36}
> 名前:NoTsf3Override2
> 種別:DWORD値
> 値:1
次の記事から引用させていただいております。ありがとうございます。
「以前のバージョンのMicrosoft IMEを使う」をコマンドで変更する方法を検証してみた : ITインフラに悩まされてる日常

上記設定のレジストリ値は、
・管理者権限なしでプログラムから編集可能
・PCの再起動なしで反映可能(IMEのプロセスの再起動により反映)
のため、プログラムからも容易に設定変更可能です。
なのでこの回避方法は、人手による設定変更なしでプログラムだけで完結できます。

4. おわりに
最初は全く見当が付かなかったのですが無事解決して良かったです。
調べている過程で知ったのですが、
どうもSendKeysはWindows10以降まともにサポートされていないようです。
PCによっては動作しないことがまれにあり使用は非推奨である旨の書き込みをちらほら見かけました。
そのため、今回の件もMicrosoft IMEだけが原因でなくSendKeys側にも問題があるのかもしれません。
今後はSendKeysの使用には慎重になろうと思います。

参考:
「以前のバージョンの Microsoft IME のご使用について | Microsoft Japan Windows Technology Support Blog
Windows10の「Microsoft IME」を以前のバージョンに戻したら、ソフトの挙動が安定した話。 | メモノローグ

C#のインタフェースでテンプレートメソッドパターン

9月ももう末ですね。
ようやく涼しくなって来てありがたいです。今夏は暑すぎました。。。

最近今更のように少しずつデザインパターンを学習(再学習含む)しています。
学習中にJavaのコードをC#で書き換える写経をしています。

そんな中でAbstractクラスで実装されがちなテンプレートメソッドパターンを
C#の場合はインタフェースのみで実現できることに気が付きました。
(Javaはおそらく完全な実現は無理そうです、
 インタフェースでfinal defaultなメソッドは宣言できないみたいなので)

実現方法は
「C#8.0でインタフェースにデフォルト実装メソッドを定義し、sealedで修飾」
です。

実現例:

/// <summary> 表示インタフェース </summary>
public interface IDisplay {
    /// <summary> オープンします。 </summary>
    void Open();
    /// <summary> プリントします。 </summary>
    void Print();
    /// <summary> クローズします。 </summary>
    void Close();

    /// <summary> 表示します。 </summary>
    sealed void Display() {
        Open();
        Print();
        Close();
    }
}

「C#8.0以降なら似たようなことをできるものの
 C#にはfinal修飾子がないため実現できない」と思っていたのですが、
デフォルト実装したメソッドをsealedで修飾可能なことに今更気付きました。
(というかそもそもメソッドにsealed修飾できることを知りませんでした)

これならデフォルト実装したテンプレートメソッドを
継承先のクラスにより上書きされてしまうことを防げますね。

実際にテンプレートメソッドパターンを実装することはあまりないかもしれませんが、
インタフェースにsealedなデフォルト実装メソッドを宣言可能なことは
知っているといずれ役立ちそうな気がします。

※調べてみると2019年に既に気付いていた方がおりますね。
 もしかして皆さん知っておられるのでしょうか。

Template Method Pattern C# 8.0風味 #デザインパターン – Qiita

以上です。

VSとMacのペアリング時のメモリ不足をキャッシュ削除により解消

MAUIでiPhoneアプリを開発する場合、ビルドのためにVisualStudioとMacをペアリングする必要があります。
アプリ開発の終盤、そのペアリング時やビルド時にMacのメモリが不足している旨の警告が出るないしエラーが出て失敗することが頻発しました。
その解決方法の共有となります。

タイトルの通り、キャッシュの削除により解決できました。
Windows・Macそれぞれで削除が必要です。

・Windowsでは以下の2フォルダがキャッシュなので削除
%localappdata%\Temp\Xamarin\XMA
%localappdata%\Xamarin\MonoTouch(←こちらはMacの接続先情報なので不要かもですが念のため)

・Macでは以下のフォルダがキャッシュなので削除
$HOME/Library/Caches/Xamarin/XMA

これで警告やエラー出ずにペアリング・ビルドできるようになりました。

以下はキャッシュが溜まる要因の予想です。
・ペアリング時にVisualStudioのバージョンないしMAUIのバージョンごとにキャッシュが溜まる
・異なるPCや異なるプロジェクトでペアリングを行うと、バージョンが異なりがちでその度にキャッシュが溜まる
くらいにふんわり考えています。

若干面倒なのであまりキャッシュ溜まらないことを願っています。
今後は1プロジェクトを改造・保守するのが主なので、上記予想が正しければ大丈夫なのではないかと思うのですが。。。

以上です。

[参考]
Clearing the Broker, IDB, Build, and Designer Agents on the Mac – Microsoft Learn
c# – Can't connect VS2022 .Net Maui Project with Mac – Stack Overflow
macos – I can't connect to Mac with Xamarin Mac Agent from Visual Studio 2015 – Stack Overflow