I'm working on my little game project as a way to learn and practice C# and I've encountered a design problem. Let's suppose we have following set of classes:
interface IGameState
{
//Updates the state and returns next active state
//(Probably itself or a new one)
IGameState Tick();
}
class Game
{
public Game(IGameState initialState)
{
activeState = initialState;
}
public void Tick()
{
activeState = activeState.Tick();
}
IGameState activeState;
}
Game is basically a state machine for GameStates. We could have MainMenuState
,LoadingState
or SinglePlayingState
. But adding MultiplayerState
(which would represent playing a multiplayer game) requires a socket to connect to the server:
class MultiplayerState : IGameState, IDisposable
{
public IGameState Tick()
{
//Game logic...
//Communicate with the server using the Socket
//Game logic...
//Render the game
return this;//Or something else if the player quits
}
public void Dispose()
{
server.Dispose();
}
//Long-living, cannot be in method-scope
Socket server;//Or similar network resource
}
Well and here's my problem, I cannot pass it to Game
because it doesn't know it should dispose of it and the calling code cannot easily know when the game doesn't need it anymore. This class design is almost exactly what I have implemented so far and I would be fine with adding IDisposable
to IGameState
but I don't think its a good design choice, after all not all IGameState
s have resources. Furthermore this state machine is meant to be dynamic in a sense that any active IGameState
can return new state. So Game
really doesn't have know which are disposable and which are so it would have to just test-cast everything.
So this got me asking few questions:
- If I have a class that claims the ownership over an argument of non-sealed type (e.g.
initialState
in Game's ctor) should I always assume it can beIDisposable
? (Probably not) - If I have an
IDisposable
instance should I ever give up its ownership by casting to a base not implementingIDisposable
? (Probably no)
I gather from this that IDisposable
feels like a quite unique interface with significant lossy(*) semantics - it cares about its own lifetime. That seems in direct conflict with idea of GC itself that offers guaranteed but non-deterministic memory management. I come from C++ background so it really feels like it tries to implement RAII concept but manually with Dispose(destructor) being called as soon as there are 0 references to it. I don't mean this as a rant on C# at all more like am I missing some language feature? Or perhaps C#-specific pattern for this? I know there's using
but that's method-scope only. Next there are finalizers which can ensure a call to Dispose but are still nondeterministic, is there anything else? Perhaps automatic reference counting like C++' shared_ptr
?
As I've said the above example can be solved (but I don't think it should) by different design but doesn't answer cases where that might not be possible, so please don't focus on it too much. Ideally I would like to see general patterns for solving similar problems.
(*) Sorry, perhaps not a good word. But I mean that a lot of interfaces express a behaviour and that if class implements said interface it just says "Hey, I can also do these things but if you ignore that part of me I still work just fine". Forgetting IDisposable
is not lossless. I've found following question which shows that IDisposable spreads by composition or alternatively it could spread through inheritance. That seems correct to me, requires more typing, but OK. Also that's exactly how MultiplayerState
got infected. But in my example with Game
class it also wants to spread upstream and that doesn't feel right. Last question might be if there should even be any lossy interfaces, like if it's the right tool for the job and in that case what is?
Aucun commentaire:
Enregistrer un commentaire