ちゃこブログ

お絵かきとUnityとBlenderと日記

【Unity制作日記、C#】いつも悩むよC#

少しでもかっこよくコードを書きたいと思うとすぐに悩んでしまいます。
今回、久々に作成したEditorツールの中で躓いてしまったところをメモしてみようと思いました。
charcotte.hateblo.jp

継承をするクラス構造

GUIStyleAndTextureViewerは、Unity既存のGUIStyleとTextureの名前を保存・GUIをプレビュー・クリップボードにコピーできるツールとして制作を試みました。
最初はあまり時間をかけずに制作をしようと考えていたのでクラス設計も簡素なものでした。
このツールにおいて保存すべきデータは文字列とお気に入りだけなので、stringとboolのみを持つ簡素なクラスを作成しました。Windowのタブで処理を分岐してGUIの描画処理を実装しました。

f:id:charcotte:20180814003952p:plain

アイテムのデータはScriptableobjectにListを作って保存するようにしています。

f:id:charcotte:20180814125729p:plain

一旦完成したものの描画の際に毎回new GUIStyle()したりEditorGUIUtility.Load()したりするのは好ましくないです。Initialize処理等でそれぞれをキャッシュして描画するという手がありましたが、なんだかスマートさに欠けている気がしました。
また、UIも利便性を高めたかったのでTreeViewを利用することを決め、アイテムクラスを変更することにしました。

失敗

f:id:charcotte:20180814125740p:plain


アイテムのジェネリック型基底クラスを作り、GUIStyleとTextureで派生させるように作ってみました。各要素を利用する前に一度Initialize()を呼ぶことでスマートにGUIの実装が出来るようになりました。また、Unity公式サンプルのTreeViewのクラスを利用するために、基底クラスにTreeElementクラスを継承させます。

TreeViewサンプル
https://forum.unity.com/threads/new-treeview-api.447169/

f:id:charcotte:20180814170919p:plain

ところが、このような設計にしているとTreeViewを実装するときにコードを書く量が増えてしまうことになってしまいました。TreeViewWithTreeModel< GUIElement < GUIStyle > >とTreeViewWithTreeModel< GUIElement < Texture2D > >の二つのクラスを実装する必要があるからです。せっかくスマートに実装したかったのにこれでは本末転倒な気がしてきました。
基底クラスが非ジェネリック型クラスでないと、柔軟に利用しづらい面もあるようです。

最終的なクラス構造

TreeViewWithTreeModelを二つも書きたくなかったので、以下のように利用する基底クラスと派生させたいクラスの間にジェネリック型クラスを挟むということをしてみました。

f:id:charcotte:20180814171009p:plain
f:id:charcotte:20180814220130p:plain

以上の変更を加えたことにより、TreeViewWIthTreeModel< GUIElement >のみを実装すれば良くなりました。ScriptableObjectに保存した派生クラスを基底クラスにキャストすることでTreeViewのクラス内でGUIを実装しやすくなりました。TreeViewの詳しい実装方法など別記事で紹介出来たらします。
クラスの構造を深く考えることは細かい実装をする際に大きな助けになることを深く実感する今日この頃でした!

(-.-) < いやいやクラス構造を考えすぎていつもプログラム書くの遅いじゃーーん

List<派生クラス>からList<基底クラス>へのキャスト

クラス構造のところで、派生クラスから基底クラスへキャストする、とありましたが正確にはScriptableObjectに保存しているList<基底クラス>からList<派生クラス>へのキャストです。
私の頭だと一見以下のようなキャストができてしまうような気がしました。

List<ParentClass> ParentList = (List<ChildClass>)ChildList;

上記のキャストは不可能です。
できないならば、for文などで派生クラスから基底クラスにキャストしながら新しいListを作成するという方法が取れます。しかしこの方法はなんか負けた気分になったので他の方法をググってみました。
Linqを利用するとよりかっこよくListの要素ををキャストすることが出来ました。

ConvertAll
List(T).ConvertAll(TOutput) メソッド (Converter(T, TOutput)) (System.Collections.Generic)

List<ParentClass> ParentList = ChildList.ConvertAll<ParentClass>( e => e );

for文でキャスト処理を書かなくても一行で記述できるのはスマートですね。

List<派生クラス>からList<基底クラス>へキャストすると参照はどうなるか

ScriptableObjectは派生クラスで保存していますが、TreeViewでは基底クラスにキャストされたデータを利用してGUIを表示します。ちゃんとScriptableObjectで保存されてるデータへ参照されるのかイメージが掴めなかったが、GUIで値を操作するとScirptableObjectの方もちゃんと変更されていました。
ところが一つ問題が発生しました。TreeViewは、正確にはTreeElementが継承されたクラスのListを利用してGUIを表示します。要素の一つをドラッグすることで順番を入れえることが可能で、順番を変更するとListのIndexも更新されるようになっています。List<派生クラス>からList<基底クラス>へキャストする際、派生クラスへのデータの更新は可能でしたが、ListのIndexは更新されなかったのです。つまり、Listへの参照は無くなってしまったのです。(そういう事なんでしょうか・・?)Listの参照を考えたことがあまりなかったので勉強になりました。
これをどう解決したかというと、TreeView側のListのIndexが更新されたらScriptableObjectのListへデータを再度派生クラスへキャストし、データを上書きするという方法を取りました。非常にスマートさに欠けてしまいました。

まとめ

継承、キャスト、参照について学ぶことが多かったです。
これらはC#やってるなら当たり前のように理解していなきゃいけない部分ですが、実際に手を動かすときについ悩んでしまうことが多いので早くC#に馴染んでいきたいです。