Covariance and contravariance usage in C#

134 views Asked by At

I'm trying to achieve something that I'm not sure is possible and I'm a bit stuck.

I have some base types called Client and Server defined like this:

public class Client 
{

}
public class Server<ClientTemplate>  
    where ClientTemplate : Client
{
    public virtual void ClientConnected(ClientTemplate client){}
    public virtual void ClientDisconnected(ClientTemplate client){}
    public virtual void MessageReceived(ClientTemplate client, Message message){}
    public virtual void SendMessage(ClientTemplate client, Message message){}
}

These classes are later expanded in a different assembly like this:

public class LobbyClient : Client
{
    string username;
    string passwordHash;
}
public class LobbyServer : Server<LobbyClient>
{
    public override void ClientConnected(LobbyClient client)
    {
        Console.WriteLine("Client connected");
    }
}

From the first assembly I dynamically load the second one where my base classes are derived.

I'm then trying to do this:

Server<Client> server = Activator.CreateInstance(serverTypeInfo);

but without any luck as the conversion is invalid.

I also want to do something like this later in the code:

Client client = Activator.CreateInstance(clientType) as Client;
server.ClientConnected(client);

I tried making further base interfaces IClient and IServer and derive Client and Server from those and setting the template argument as in but that did not work.

Is there any way in which I could achieve my goal here?

I see Player.IO (now Yahoo Games network) managed to do this but I can't figure how the code works looking just at the compiled assembly.

https://gamesnet.yahoo.net/documentation/services/multiplayer/serverside

EDIT:

Here is the version with the interfaces that I tried:

public interface IClient
{

}

public class Client : IClient
{

}

interface IServer<in ClientTemplate> where ClientTemplate : IClient
{
    void ClientConnected(ClientTemplate client);
    void ClientDisconnected(ClientTemplate client);
    void MessageReceived(ClientTemplate client, Message message);
    void SendMessage(ClientTemplate client, Message message);
}

public class Server<ClientTemplate> : IServer<ClientTemplate>
    where ClientTemplate : IClient
{
    public virtual void ClientConnected(ClientTemplate client){}
    public virtual void ClientDisconnected(ClientTemplate client){}
    public virtual void MessageReceived(ClientTemplate client, Message message){}
    public virtual void SendMessage(ClientTemplate client, Message message){}
}

And later in the code:

IServer<IClient> server = Activator.CreateInstance(serverTypeInfo);

Thank you

3

There are 3 answers

0
Jodrell On

If you want a specialised implementation of Server or IServer, like LobbyServer to handle connections to any Client, then you must specify it appropriately. If you do, then you will be able to treat that instance as a generic Server<Client> or IServer<Client>.

e.g.

if LobbyServer were declared thus,

public class LobbyServer : Server<Client>
{
    public override void ClientConnected(Client client)
    {
        // ...
    }
}

then

var instance = (Server<Client>)Activator.CreateInstance(typeof(LobbyServer));

will execute without an InvalidCastException.

If LobbyServer is defined with a more specialised type then Client, like LobbyClient, then it is not a Server<Client> and cannot be cast to that type.


The answer is to make TClient covariant but, as you've discovered, with your interface declared the way it is, TClient needs to be contravariant to maintain input safety. You can overcome that like this.

interface IServer<out TClient> where TClient : IClient
{
    void ClientConnected(
            Action<TClient, IServer<TClient>> connectionAction);
    void ClientDisconnected(
            Action<TClient, IServer<TClient>> disconnectionAction);
    void MessageReceived(
           Action<TClient, IServer<TClient>, Message> recievedAction);
    void SendMessage(
           Action<TClient, IServer<TClient>, Message> sendAction);
}

here, the Action delagate reverses the direction of variance maintaining input safety.

Now LobbyServer can be declared like this,

public class LobbyServer : Server<LobbyClient>
{
}

and

var instance (IServer<IClient>)Activator.CreateInstance(typeof(LobbyServer));

is perfectly valid.

However, the implementation of the actions is delegated till when the types are known.

0
Tom Gillen On

Co- and contra-variance will not work in this case, as you are attempting to supply a less derived parameter as input into the implementation. Also, co- an contra-variance only work via interfaces, which have had their type parameters declared with the in or out keywords.

For example (pseudo types here for illustration):

IServer<Client> server = new Server<LobbyClient>();
Client client = new GameClient();
server.ClientConnected(client);

The Server<LobbyClient> cannot be cast into a IServer<Client>, because it expects as input LobbyClient instances, but the interface could potentially pass in any Client type.

There are two ways that I can think of to work around this, but both involve subverting the type system; so you need to make sure yourself that the types being used are correct, else you will get runtime exceptions.

The first method is to call your server methods via reflection. This can be both quite slow and verbose, however.

The second method would be to write a non-generic interface for your Server class, and have your Sever class explicitly implement it, while delegating each method to the corresponding generic implementation.

public interface IServer
{
    void ClientConnected(Client client){}
    void ClientDisconnected(Client client){}
    void MessageReceived(Client client, Message message){}
    void SendMessage(Client client, Message message){}
}

public class Server<ClientTemplate> : IServer
    where ClientTemplate : Client
{
    void IServer.ClientConnected(Client client)
    {
        ClientConnected((ClientTemplate)client);
    }

    void IServer.ClientDisconnected(Client client)
    {
        ClientDisconnected((ClientTemplate)client);
    }

    void IServer.MessageReceived(Client client, Message message)
    {
        MessageReceived((ClientTemplate)client, message);
    }

    void IServer.SendMessage(Client client, Message message)
    {
        SendMessage((ClientTemplate)client, message);
    }

    public virtual void ClientConnected(ClientTemplate client){}
    public virtual void ClientDisconnected(ClientTemplate client){}
    public virtual void MessageReceived(ClientTemplate client, Message message){}
    public virtual void SendMessage(ClientTemplate client, Message message){}
}

Now, the non-generic methods will not normally show up on your server implementations when accessed directly, but you will be able to assign instances to the IServer interface and call those non-generic methods. You will get runtime exceptions if the types do not match up correctly.

IServer server = Activator.CreateInstance(serverTypeInfo) as IServer;    
Client client = Activator.CreateInstance(clientType) as Client;
server.ClientConnected(client); // works
0
sQuir3l On

I don't think it will be possible,
As if you converted LobbyServer to Server[Client] there would be potential run time errors.

e.g.

Server<Client> server = new LobbyServer();
server.ClientConnected(new Client()); // how can a LobbyServer handle this?

Haven't really given you an answer, sorry.