前回(【Cells for C#】その1-そもそも...の「セルの世界」)で書いた通り、今回のC++版 "Cell"のC#版 "Cells"への移植では、何度かトラブルがありました。
1.最初のトラブル-「System.ArgumentOutOfRangeException: インデックスが範囲を超えています。」
最初は「単純移植だけでいいや」と軽く考えていたのですが、C++とC#の「似て非なる」性格から、
【C++版"Cell"】
Cell.rc ーリソースファイル(メニュー、ダイアログ、アイコン、ビットマップ)
Cell.h -ウィンドウとダイアログのクラス定義
Cell.cpp -ウィンドウの作成とエントリーポイント
Ccell_world.h-cellクラスとworldクラスの定義とロジック処理
CellProc.h -worldとcellのインスタンスを使ったウィンドウプログラム(UI)及び描画
↓
?
どうするか悩んだ末に、C++版の構成を変更し、
【C#版"Cells"】
Cell_World.cs -CellクラスとWorldクラスの定義とロジックのPictureBox派生コントロール化(描画)
(Cell_World.dll -↑のモジュール化)
Cell.cs ーCell_World.dllを使ったウィンドウプログラム(UI)
にしました。そして先ずC++のプログラムを適宜コピーしてC#用の構成とし、C++でよく使う"m_(小文字変数名)"や"(小文字関数名)()"などをC#作法に変えるとともに、C++ではカプシュール化の為のアクセス関数を廃止してC#のプロパティへ移行したり等一通りすべて終了してコンパイルし(勿論エラーなし)、実行すると、「インデックスが範囲を超えています」というエラーが出ます。
最初は例の「配列の添字が超過したエラー」だと思い、「配列」だからと、配列回りにトラップをかけてデバッグしてみたのですが、埒が明かないのでChat-GPTに相談すると、
「実は配列ではなく List<T> のインデクサ で発生することが非常に多いです。」
とのこと。また、通常配列の場合には
「IndexOutOfRangeException」
となるところ、今回は
「ArgumentOutOfRangeException」
であることから(成程!)、「List廻りが怪しい」という有用な示唆を得て、エラー発生個所を
int val = c.Find_Cell(); //対象セルxの周囲にセルがいるか確認
int dir = val & 0x1111; //dirは周囲のセルの絶対方向(0 - 7)-解説:"0b"が正しい。(注)
int id = val >> 4; //idはそのセルのID(16倍されている)
注:ついでに言えば、C#はC# 6.0まで二進数表記("0b")が出来ず、最初C"0b"と書き直してまたエラーを出してしまいました。現在C# 5.0用に"0xF"としています。
というC++のオリジナルコードに潜んでいたバグと
if(Around[Dir] >= 4) //解説:本来"dir"となるところ、クラスフィールドで宣言された"Dir"に打ち間違えた。
{
id = Around[dir] - 4;
return dir | (id << 4);
}
が発見できました。(【無駄話】旧悪の暴露)
2.次のトラブル("foreach"に潜む罠-System.InvalidOperationException)
私はもともとがC++だったので、"for(int i = 0; i < ..."というループを常用していましたが、C#に慣れるにつれ"foreach"を多用するようになりました。今回もC++のオリジナルコードをC#に移植する再、可能なところでは"foreach"に書き換えていたのですが、
「この例外は foreach 特有の仕組み によるものです。...foreachでは、内部的に Enumerator(列挙子)が作られ...作成時にそのバージョンを記録しています。...その後、Add(...)、Remove(...)、Clear()等が実行されると、列挙子のバージョンが変化し...次の要素へ進む際に"System.InvalidOperationException:"エラーが発生します。」
とのことであり(Listの要素はshiboushitaCellもデータで残すので一切削除はしませんが、交尾して追加することはあるので...)、foreach文をすべて元のfor文に戻しました。
3.乱数関連トラブル
乱数は「偶発的」に「異なる」ものを得たいときに使用し、今回のオリジナルC++コードでもセルを生成する際に種族、位置、性格、能力や方向の決定に以下の通り「ローカルにseedと乱数を使って」
//ハードウェア乱数をシードにして初期化(uint32_t版)
std::random_device seed; //Windowsではrandom_deviceが有効でない可能性がある
std::mt19937 engine(seed());
std::uniform_int_distribution<> dist(10, 100); //10から100までの整数乱数を生成
いましたが、何故か同じ一つの種族、位置、性格、能力、方向に偏る現象が発生し、又Chat-GPTに相談したところ、
「それは非常に典型的な C# 移植時の問題です。」
ということで、ローカルに
Random rand = new Random((int)DateTime.Now.Ticks & 0x0000FFFF);
等と毎回乱数発生器を生成するとほぼ同じような初発乱数が生成されると諭されました。その為、C#版ではWorldクラスにが代表してグローバルで使える静的(static)乱数生成器を「一つ」作り、その
World.rand.Next()
メソッドで異なる乱数を使用することにしました。(注)
注:その後も"(↑の対応を取りました)が、矢張りTribe(種族はピンク、ブルー、グリーンの3種および死亡で4種ですが、生成では前3種しか使わないのでMax_Tribe - 1にしています)がほぼ1種のみとなります。(時々別種が一つくらい)TribeプロパティにはWorld.rand.Next(0, 4)で0-3までの値が発生されるようにしていますが、うまくゆきません。 何か良い知恵がありますか? "
と書きながら、
"↑ と書いたのですが、Cellのコンストラクターに初期化コードが残っていたことを発見し、自己解決しました。foreachもすべてforに書き直し、何らエラーが出なくなりました。ありがとう。"
と恥ずかしいところを見せてしまいました。「デバッグ対応は慌てず、焦らず」ですね。
4.最後のダイアログ表示トラブル
これも既に【Chat-GPT】問答その1で書きましたが、メインのUIプログラムであるCells.csで、タイマーを設定してWorldを回す(Goes_Around()メソッドを呼ぶ)のですが、アバターコントロールダイアログを呼ぶとダイアログが大量発生します。
これもChat-GPTに相談してすぐに原因を特定
「今回の症状は WinForms の ShowDialog() が壊れているのではなく、「ダイアログ表示中も Timer の Tick が発生し続けている」ことが原因です。これは WinForms ではよく遭遇する挙動です。」
してくれたのですが、「じゃぁ、どうすべきなの?(と酔いのまわった頭では考えられず、直ぐに訊いてしまいました!)」と返したら、Chat-GPTが(胸を張って、上から目線で...と感じました!)
「私なら UIは Cells.cs 側、World_Cell.dll 側には一切入れません。」
と自信満々に言うので、(酔いのまわった頭では論議もまともにできないことから...)
「一寸試行錯誤して考えてみます。又相談させてください。」
と伝え、一晩寝てから翌日の早朝起の寝床の中で
「あっ、そうか!単純に呼ぶ際に一旦タイマーを止めて呼び、正常に帰ってきたらタイマーを再開すればいいじゃん!」
ということで自己解決しました。そして私が
「示唆された内容を考えたのですが、Goes_Aroundメソッドの呼び出しexe側でUI処理を専管する、と言う考えは同意するも、処理の内容は本来呼び出されたdll側で行うべきものであり、それをするためには、dll側にはない処理なので本来隠蔽されていたdll側のフィールド、メソッド等を呼び出しexe側に公開しないとなりませんし、exe側の処理が終わった後、dll側の中断されたforループの続きを再開させなければなりませんので、かなり無理があります。(forループの中でフラグを立てて処理を中断するのは、以降のセルの処理が全て未処理になるので、忌避したい所です。) 矢張り本来dll側でやる処理をユーザーにマニュアルでやらせるのであっても、処理はdllサイドでおこなうべきではないでしょうか? その為には、呼び出しexe側でGoes_Around呼び出し前にタイマーを切り(Enabledをfalse)、呼び出し後再スタートさせれば、dll側で処理することができるのではないでしょうか?」
というメッセージを翌朝返すと、Chat-GPTの反応は、
「潔さんの考え方にはかなり説得力があります。...(ループを途中)で抜ける案は確かに不自然です。なぜなら、
Cell 0 → 処理済み
Cell 1 → 処理済み
Cell 2 → 処理済み
Cell 3(MyID) → 中断
Cell 4 → 未処理
Cell 5 → 未処理 ...
となり、「1ターンの整合性」が崩れます。潔さんが嫌がる理由はよく分かります。...
むしろ潔さんが指摘された
呼び出しexe側でCellの処理をするとなると、
dllの処理対象や処理メソッド全てをexe側のダイアログに持たせる
こちらの方が設計的には不自然です。...
結論としては、今の説明を聞く限り、
「アバターの行動決定だけをダイアログで行い、実際のセル処理は従来どおりWorld.dll内で実行する」
という潔さんの方向性は十分合理的です。特に World が PictureBox 派生の WinForms コントロールであるなら、「DLLだからUIを持ってはいけない」という一般論は、あまり気にしなくてよいと思います。むしろ内部状態やセル処理の隠蔽を維持する方が、このプロジェクトでは重要に見えます。」
僕はChat-GPTは自民党衆議院議員になれると思う!
