dimanche 15 juillet 2018

Should I assume that every owned instance implements IDisposable?

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 IGameStates 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 be IDisposable? (Probably not)
  • If I have an IDisposable instance should I ever give up its ownership by casting to a base not implementing IDisposable? (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