質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.31%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

解決済

3回答

948閲覧

ジェネリックなプロパティの実装を非ジェネリックなインターフェースで強制させることができるか

leaf21341

総合スコア10

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

0グッド

0クリップ

投稿2023/10/29 04:18

質問に至るバックグラウンド

自身のUnityプロジェクトにおけるFSMで以下のImtStateMachineを採用しています.
https://github.com/Sinoa/ImtStateMachine/blob/develop/Packages/ImtStateMachine/Runtime/StateMachine.cs
実装を進める上で,

「ステートマシンを実装しているクラスをまとめて扱いたい」

という要件がでてきました.
単純に考えると,基底クラス/インターフェースを使えばいいとおもいましたがどうやっても実現できませんでした.

該当のソースコード

ImtStateMachineはジェネリックで以下のように定義されます.

C#

1using IceMilkTea.Core; 2 3public class SampleClass 4{ 5 public enum EventId 6 { 7 event1, 8 event2, 9 event3, 10 } 11 public ImtStateMachine<SampleClass, EventId> StateMachine { get; set; } 12}

私としては,EventIdとStateMachineプロパティの実装を強制し,StateMachineを適切に外部に公開したいのですが,
ImtStateMachineは派生クラスをジェネリックに指定する必要があるので基底側で定義することができません.そこで次のようにしたんですが...

C#

1public interface IState<TContext, TEvent> 2{ 3 public ImtStateMachine<TContext, TEvent> StateMachine { get; set; } 4} 5public class SampleClass : IState<SampleClass, SampleClass.EventId> 6{ 7 public enum EventId 8 { 9 event1, 10 event2, 11 event3, 12 } 13 public ImtStateMachine<SampleClass, EventId> StateMachine { get; set; } 14}

確かにこれで実装を強制できたのですが,

C#

1IState state = new SampleClass(); 2state.StateMachine.SendEvent......

的なことができないのでこのインターフェースは利用側にはほとんど価値がありません.
(例えば,IStateを全部取得して全てのStateMachineをIdle状態にするといったことができない)
なので実際はIStateは非ジェネリックでなければならないことにここで気が付きましたが,そうなるとImtStateMachineが定義できなくなってしまいます.

以上を踏まえての質問

C#でジェネリックなプロパティを非ジェネリックなインターフェースを通して実装の強制,外部への公開は可能でしょうか?

ご回答よろしくおねがいいたします.

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

isimasa

2023/10/29 12:08

実現しようとしているのは、enumで定義した変数をinterfaceでMonoBehaviourと同じく継承する感じですか?
leaf21341

2023/10/29 12:30 編集

>enumで定義した変数をinterfaceでMonoBehaviourと同じく継承する 部分的にはそうです. とにかく私が実現を目指したのは IStateインターフェースを通してStateMahicne(とそのジェネリックに必要なenumクラス)を公開することです. ImtStateMachineはImtStateMachineを使うクラスそのものの型をジェネリックに使うので,IStateをジェネリックにしても抽象化のレベルが全く変わりません. ↓こんな感じにインスタンスを取得して,StateMachineを使いたいが >if (SampleClassObj.TryGetComponent<IState>(out var state)) .... ↓ジェネリックだと結局派生クラスを知らなければならないので抽象化ができていない >if (SampleClassObj.TryGetComponent<IState<SampleClass,SampleClass.EventId>>(out var state)) ....
guest

回答3

0

自己解決

dynamic型を使うことで解決しました.
ただし,派生クラスからのアクセスを制限できないので命名やらコメントアウトやらドキュメントやらで使用を制限する旨をプロジェクト内で周知しなければならないです.
やや無理筋感がありますが,これ以外の方法は思いつかないので仕方がないです.

改めて問題を咀嚼すると,

  1. 派生クラスに依存するプロパティを基底クラスで公開したい
  2. 基底クラスはインスタンス化されるまで派生クラスの実態がわからないのでコンパイルの段階で把握するのは不可能
  3. 動的に型付けするしかないのでdynamicを使う

って感じですかね.
こう考えるとdynamicの真っ当な使い方のような気がしなくもないです.

実装

C#

1// 動的な型付けで公開することを強調するためにIStatefulDynamic等の命名も検討した方がいいかもしれません. 2public interface IStateful 3{ 4 /// <summary> 5 /// Do not use from outside this interface. 6 /// </summary> 7 /// <returns> ImtStateMachine </returns> 8 dynamic DynamicStateMachine{ get; } 9} 10 11public class SampleClass : IStateful 12{ 13 public enum EventId 14 { 15 event1, 16 event2, 17 event3, 18 } 19 private ImtStateMachine<SampleClass, EventId> _stateMachine; 20 public ImtStateMachine<SampleClass, EventId> StateMachine => _stateMachine; 21 public dynamic DynamicStateMachine => _stateMachine; 22}

利用側

C#

1// 非ジェネリックなインターフェースでジェネリックなプロパティにアクセス 2IStateful stateful = new SampleClass(); 3Console.WriteLine(stateful.DynamicStateMachine.CurrentStateName); 4// Enum定義のイベントidなら一応型情報なしでSendEventもできる 5stateful.DynamicStateMachine.SendEvent(0);

このようなインターフェースを提供することで,例えばデバッグにおいてプロジェクトのすべてのIStatefulを取得してそれぞれの現在のステートを表示するといった要求は当然出てくると思いますが,それを簡潔に記述できます.

【追記】
タイトルへの回答としては「dynamicで公開すれば可能」ということになりますが,
他回答者様のご指摘通り,「そもそもそんなことが必要なのか?」という点を十分に検討する必要があります.
また,実際に利用する際に派生クラスの情報を得ているわけではないので全ての操作が行えるわけではありません.

投稿2023/10/29 11:28

編集2023/11/03 09:39
leaf21341

総合スコア10

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2023/10/29 15:43

CurrentStateじゃなくてCurrentStateNameでは? あとIStateは誤解を招く気がします 全体的にUnity前提ではなくC#として書いて欲しかったかも
leaf21341

2023/10/29 22:29

dameoさんコメントありがとうございます. >CurrentStateじゃなくてCurrentStateNameでは? これはそうでした.CurrentStateは私自身で拡張したプロパティなのでピュアなImtStateMachineには存在しないです. >あとIStateは誤解を招く気がします あまりしっくりくるものがなくて適当に決めました.今考えたらIStatefulとかの方がいいですかね. >全体的にUnity前提ではなくC#として書いて欲しかったかも タグがC#なのでそちらの方が適切ですね.諸々修正しておきました.
退会済みユーザー

退会済みユーザー

2023/10/30 04:39

あとプロパティを式で定義する場合に{get;}は指定できないみたいですよ。
退会済みユーザー

退会済みユーザー

2023/10/30 04:46

一応他の人のためにサンプル的なコードを付けておきます。 public class Program { public interface IStateful { /// <summary> /// Do not use from outside this interface. /// </summary> /// <returns> ImtStateMachine </returns> dynamic DynamicStateMachine{ get; } } public class SampleClass1 : IStateful { public enum EventId { event1, event2, event3, } private class State1: ImtStateMachine<SampleClass1, EventId>.State {} private class State2: ImtStateMachine<SampleClass1, EventId>.State {} private ImtStateMachine<SampleClass1, EventId> _stateMachine; public ImtStateMachine<SampleClass1, EventId> StateMachine => _stateMachine; public dynamic DynamicStateMachine => _stateMachine; public SampleClass1() { _stateMachine = new ImtStateMachine<SampleClass1, EventId>(this); _stateMachine.AddTransition<State1, State2>(EventId.event1); _stateMachine.AddTransition<State1, State2>(EventId.event2); _stateMachine.AddTransition<State2, State1>(EventId.event3); _stateMachine.SetStartState<State1>(); _stateMachine.Update(); } } public class SampleClass2 : IStateful { public enum EventId { event1, event2, event3, } private class State1: ImtStateMachine<SampleClass2, EventId>.State {} private class State2: ImtStateMachine<SampleClass2, EventId>.State {} private ImtStateMachine<SampleClass2, EventId> _stateMachine; public ImtStateMachine<SampleClass2, EventId> StateMachine => _stateMachine; public dynamic DynamicStateMachine => _stateMachine; public SampleClass2() { _stateMachine = new ImtStateMachine<SampleClass2, EventId>(this); _stateMachine.AddTransition<State1, State2>(EventId.event1); _stateMachine.AddTransition<State1, State2>(EventId.event2); _stateMachine.AddTransition<State2, State1>(EventId.event3); _stateMachine.SetStartState<State1>(); _stateMachine.Update(); } } public static void Main(string[] args) { var list = new List<IStateful>(){new SampleClass1(), new SampleClass2()}; list[1].DynamicStateMachine.SendEvent((int)SampleClass2.EventId.event1); list[1].DynamicStateMachine.Update(); foreach(var e in list) Console.WriteLine(e.DynamicStateMachine.CurrentStateName); } }
leaf21341

2023/11/03 09:40

指摘&補足ありがとうございます. 該当箇所は修正しておきました.
guest

0

本当にジェネリックじゃないといけないのか?という所から考えた方がいいような気がします。
共通の処理させたいだけなら、その処理だけ非ジェネリックなインターフェースにしたらどうですか?

cs

1using System; 2using System.Runtime.CompilerServices; 3 4public interface IMachine 5{ 6 string CurrentStateName { get; } 7 void SendEvent(int evt); 8} 9 10public interface IStateful 11{ 12 IMachine StateMachine { get; } 13} 14 15public class ImtStateMachine<TEvent> : IMachine where TEvent : Enum 16{ 17 public void SendEvent(TEvent evt) => Console.WriteLine($"SendEvent:{evt}"); 18 public void SendEvent(int evt) => SendEvent(Unsafe.As<int, TEvent>(ref evt)); //Enumのベースがintである前提 19 public string CurrentStateName => "hogeStatus"; 20} 21 22public class SampleClass : IStateful 23{ 24 public enum EventId 25 { 26 event1, 27 event2, 28 event3, 29 } 30 private ImtStateMachine<EventId> _stateMachine = new ImtStateMachine<EventId>(); 31 public IMachine StateMachine => _stateMachine; 32} 33 34public class Program 35{ 36 public static void Main(string[] args) 37 { 38 var stateful = new SampleClass(); 39 var machine = stateful.StateMachine; 40 Console.WriteLine(machine.CurrentStateName); 41 machine.SendEvent(0); 42 } 43}

投稿2023/10/30 02:51

編集2023/10/30 04:58
nururi

総合スコア167

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

leaf21341

2023/11/03 09:28 編集

回答ありがとうございます. >本当にジェネリックじゃないといけないのか?という所から考えた方がいいような気がします。 共通の処理させたいだけなら、その処理だけ非ジェネリックなインターフェースにしたらどうですか? これはその通りだと思いました. そこで色々要件を調査したところ,結果としてdynamicなステートマシンも型に依存しない処理も両方ともインターフェースに定義しました. (ちなみに,dynamicなステートマシンは主にデバッグ機能の実装に必要でした. デバッガーに公開するだけの目的でIStatefulにあれこれと定義するとIStatefulを実装するクラスが本番環境では無関係なコードで溢れかえってしまうため,ステートマシン自体を公開するのが一番スッキリします.) public interface IStateful { /// <summary> /// Do not use from outside this interface /// </summary> /// <returns> ImtStateMachine </returns> dynamic StateMachineDynamic { get; } public void SendIdle(); public void SendExit(); public bool IsExit { get; } public bool IsIdle { get; } }
guest

0

素人なので話自体が良くわかっていない気もするのですが…

ImtStateMachine<TContext, TEvent> というのを利用側で直接(そのままこの型で)扱わねばならないのでしょうか?
なんとなく,

  • 利用側は各ステートマシンの TContext が具体的に何なのかを知りたくないし,知りようもない.というか,TContext とかいう何かが存在すること自体を意識したくないのでは.
  • 複数のステートマシンで共通の TEvent を用いるのであれば,それらは個々のステートマシンが定義するものではなく,もっと広範な範囲で共用する存在なのでは.

とか思うのですが.

要は,ImtStateMachine<TContext, TEvent> なるものを使っているのは個々の型の内部実装の話なのであって,それを外側にダイレクトに公開しなければ良いのではないか? と.
各型を まとめて/同じように 操作するための全く別のインタフェースを定義するのではダメなのでしょうか?

C#

1//全てのステートマシンが共通で用いる(必ずサポートする)イベント 2enum CommonEventID{ ... }; 3 4//インタフェース 5//ここには TContext は出てこないし, 6//CommonEventID を扱う処理だけがあればよいハズ.(どのみち個々のステートマシン固有のイベントに関する処理を共通インタフェースを介して扱うのは無理なのだろうし) 7interface ICommonStateMachine 8{ 9 bool SendCommonEvent( CommonEventID Event ); 10}

投稿2023/10/30 02:01

fana

総合スコア12151

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

leaf21341

2023/11/03 09:28

回答ありがとうございます >ImtStateMachine<TContext, TEvent> というのを利用側で直接(そのままこの型で)扱わねばならないのでしょうか? >要は,ImtStateMachine<TContext, TEvent> なるものを使っているのは個々の型の内部実装の話なのであって,それを外側にダイレクトに公開しなければ良いのではないか? 上の回答者様とおよそ似た話だと思いますが,やはりdynamicなステートマシンも主にデバッグ機能を実装する上で必要でした.なのでdynamicなステーマシンも定義しつつ,型情報が必要ない汎用的な処理も提供するようにしました. ↓ public interface IStateful { /// <summary> /// Do not use from outside this interface /// </summary> /// <returns> ImtStateMachine </returns> dynamic StateMachineDynamic { get; } public void SendIdle(); public void SendExit(); public bool IsExit { get; } public bool IsIdle { get; } }
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.31%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問